Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2f6a6e8
Add chain and container specs
unnawut Aug 13, 2025
1dca1e0
docs: add networking specs draft
unnawut Aug 13, 2025
b2bb6be
docs: add validator ID and proposer selection
unnawut Aug 13, 2025
4872e53
fix: remove obvious wordings
unnawut Aug 13, 2025
f7b237b
fix: small textual fixes
unnawut Aug 13, 2025
eb8e1d5
fix: missing python colon
unnawut Aug 13, 2025
7203fb5
fix: HISTORICAL_ROOTS_LIMIT
unnawut Aug 13, 2025
36db569
docs: add SignedBlock and SignedVote
unnawut Aug 13, 2025
e6b93ff
fix: small networking specs textual changes
unnawut Aug 13, 2025
6f81d11
refactor: move client specs into docs/ folder
unnawut Aug 13, 2025
20f5237
fix: res/resp id
unnawut Aug 13, 2025
c5d936d
remove redundant specs
unnawut Aug 13, 2025
9883d92
add remarks on pre-gen keys
unnawut Aug 13, 2025
acccf3e
removing clause on unused secp256k1 assuming we still sign gossip mes…
unnawut Aug 13, 2025
63717aa
Use signed data in gossips
unnawut Aug 14, 2025
d4881e1
fix: separate node and validator identification
unnawut Aug 14, 2025
a599d14
fix: ignore codespelling
unnawut Aug 14, 2025
c67e397
fix: gossipsub protocol id to v1.0
unnawut Aug 14, 2025
e38593e
feat: add Status req/resp
unnawut Aug 19, 2025
4c3099c
Add comment to config.num_validators
unnawut Aug 19, 2025
ebaf501
Add slot to State
unnawut Aug 19, 2025
d84d5da
lean_block -> block
unnawut Aug 19, 2025
1b469e8
add proposer_index to Block
unnawut Aug 19, 2025
a236de3
signed_block.data -> message
unnawut Aug 19, 2025
ad9bc63
move votes below state_root
unnawut Aug 19, 2025
dd98282
parent -> parent_root
unnawut Aug 19, 2025
ab3943a
add latest_block_header to State
unnawut Aug 19, 2025
29913a2
add snappy message domains
unnawut Aug 19, 2025
65daac2
fix: remove noise encryption
unnawut Aug 19, 2025
52e8e9f
add genesis_time to State config
unnawut Aug 20, 2025
e600336
refactor: merge bps into INTERVALS_PER_SLOT
unnawut Aug 22, 2025
6ef1f10
fix: move votes inside BlockBody
unnawut Aug 22, 2025
cdcf9ae
add modified ENR specs
unnawut Aug 22, 2025
d19d93d
feat: add MAX_REQUEST_BLOCKS
unnawut Aug 25, 2025
0788d88
Comment on block_body.votes
unnawut Aug 25, 2025
14d5ac2
generalize devnet{N}
unnawut Aug 25, 2025
72909be
lean_vote -> vote
unnawut Aug 25, 2025
64acd24
change ed25519 to secp256k1
unnawut Aug 25, 2025
c551aa2
fix: remove lean_ prefixes
unnawut Aug 25, 2025
884cc1d
fix: 32 -> 33 bytes for secp256k1
unnawut Aug 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .codespell-ignore-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ keccak
blake
merkle
trie
ream
rlp
ssz
zeam
19 changes: 19 additions & 0 deletions docs/client/chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Lean Consensus Experimental Chain

## Configuration

### Time parameters

| Name | Value | Unit | Duration |
| ------------------------------------- | ------------------------- | :----------: | :-----------: |
| `SLOT_DURATION_MS` | `uint64(4000)` | milliseconds | 4 seconds |
| `INTERVALS_PER_SLOT` | `uint64(4)` | intervals | 1 second |

## Presets

### State list lengths

| Name | Value | Unit | Duration |
| ------------------------------ | ------------------------------------- | :--------------: | :-----------: |
| `HISTORICAL_ROOTS_LIMIT` | `uint64(2**18)` (= 262,144) | historical roots | 12.1 days |
| `VALIDATOR_REGISTRY_LIMIT` | `uint64(2**12)` (= 4,096) | validators | |
93 changes: 93 additions & 0 deletions docs/client/containers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Containers

## `Config`

```python
class Config(Container):
// temporary property to support simplified round robin block production in absence of randao & deposit mechanisms
num_validators: uint64
genesis_time: uint64
```

## `Checkpoint`

```python
class Checkpoint(Container):
root: Bytes32
slot: uint64
```

## `State`

```python
class State(Container):
config: Config
slot: uint64
latest_block_header: BlockHeader

latest_justified: Checkpoint
latest_finalized: Checkpoint

historical_block_hashes: List[Bytes32, HISTORICAL_ROOTS_LIMIT]
justified_slots: List[bool, HISTORICAL_ROOTS_LIMIT]

# Diverged from 3SF-mini.py:
# Flattened `justifications: Dict[str, List[bool]]` for SSZ compatibility
justifications_roots: List[Bytes32, HISTORICAL_ROOTS_LIMIT]
justifications_validators: Bitlist[
HISTORICAL_ROOTS_LIMIT * VALIDATOR_REGISTRY_LIMIT
]
```

## `Block`

```python
class Block(Container):
slot: uint64
proposer_index: uin64
parent_root: Bytes32
state_root: Bytes32
body: BlockBody
```

## `BlockBody`


```python
class BlockBody(Container):
votes: List[Vote, VALIDATOR_REGISTRY_LIMIT]
```

Remark: `votes` will be replaced by aggregated attestations.

## `SignedBlock`

```python
class SignedBlock(Container):
message: Block,
signature: Bytes32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we put the correct signature size here even if its

Suggested change
signature: Bytes32,
signature: Bytes32,

we should choose the correct size even if its zero bytes to converge to the params we are going for

image https://github.com/b-wagn/hashsig-parameters

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To update to exact size (~3KiB) based on researchers' input

```

## `Vote`

```python
class Vote(Container):
validator_id: uint64
slot: uint64
head: Checkpoint
target: Checkpoint
source: Checkpoint
```

## `SignedVote`

```python
class SignedVote(Container):
data: Vote,
signature: Bytes32,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To update to exact size (~3KiB) based on researchers' input

```

## Remarks

- The signature type is still to be determined so `Bytes32` is used in the
interim. The actual signature size is expected to be a lot larger (~3 KiB).
228 changes: 228 additions & 0 deletions docs/client/networking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# Networking

## Setup

- Transport: QUIC on IPv4, secured by TLS 1.3 with `secp256k1` identities
- Protocol negotiation: [multistream-select 1.0](https://github.com/multiformats/multistream-select/)
- Multiplexing: Native support by QUIC
- Gossip: [gossipsub v1](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.0.md)

## Node identification

Nodes are defined as a list of their ENRs in a yaml file at [`src/lean_spec/client/nodes.yaml`](../../src/lean_spec/client/nodes.yaml).
For example:

```yaml
- enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg
- enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg
- enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg
```

## ENR structure (modified)

The Ethereum Node Record (ENR) for an Ethereum consensus client MUST contain the
following entries (exclusive of the sequence number and signature, which MUST be
present in an ENR):

- The compressed secp256k1 publickey, 33 bytes (`secp256k1` field).

The ENR MAY contain the following entries:

- An IPv4 address (`ip` field).
- An IPv4 QUIC port (`quic` field) representing the local libp2p QUIC (UDP)
listening port.

Specifications of these parameters can be found in the
[ENR Specification](http://eips.ethereum.org/EIPS/eip-778).

## Configuration

This section outlines configurations that are used in this spec.

| Name | Value | Description |
| ------------------------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------- |
| `MAX_REQUEST_BLOCKS` | `2**10` (= 1024) | Maximum number of blocks in a single request |
| `MESSAGE_DOMAIN_INVALID_SNAPPY` | `DomainType('0x00000000')` | 4-byte domain for gossip message-id isolation of *invalid* snappy messages |
| `MESSAGE_DOMAIN_VALID_SNAPPY` | `DomainType('0x01000000')` | 4-byte domain for gossip message-id isolation of *valid* snappy messages |

## Gossip domain

**Protocol ID:** `/meshsub/1.0.0`

**Gossipsub Parameters**

The following gossipsub
[parameters](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.0.md#parameters)
will be used:

- `D` (topic stable mesh target count): 8
- `D_low` (topic stable mesh low watermark): 6
- `D_high` (topic stable mesh high watermark): 12
- `D_lazy` (gossip target): 6
- `heartbeat_interval` (frequency of heartbeat, seconds): 0.7
- `fanout_ttl` (ttl for fanout maps for topics we are not subscribed to but have
published to, seconds): 60
- `mcache_len` (number of windows to retain full messages in cache for `IWANT`
responses): 6
- `mcache_gossip` (number of windows to gossip about): 3
- `seen_ttl` (expiry time for cache of seen message ids, seconds):
SECONDS_PER_SLOT * SLOTS_PER_EPOCH * 2

#### Topics and messages

Topics are plain UTF-8 strings and are encoded on the wire as determined by
protobuf (gossipsub messages are enveloped in protobuf messages). Topic strings
have form: `/leanconsensus/devnet{N}/Name/Encoding`. This defines both the type of
data being sent on the topic and how the data field of the message is encoded.

- `Name` - see table below
- `Encoding` - the encoding strategy describes a specific representation of
bytes that will be transmitted over the wire. See the [Encodings](#Encodings)
section for further details.

The optional `from` (1), `seqno` (3), `signature` (5) and `key` (6) protobuf
fields are omitted from the message, since messages are identified by content,
anonymous, and signed where necessary in the application layer.

The `message-id` of a gossipsub message MUST be the following 20 byte value
computed from the message data:

- If `message.data` has a valid snappy decompression, set `message-id` to the
first 20 bytes of the `SHA256` hash of the concatenation of
`MESSAGE_DOMAIN_VALID_SNAPPY` with the snappy decompressed message data, i.e.
`SHA256(MESSAGE_DOMAIN_VALID_SNAPPY + snappy_decompress(message.data))[:20]`.
- Otherwise, set `message-id` to the first 20 bytes of the `SHA256` hash of the
concatenation of `MESSAGE_DOMAIN_INVALID_SNAPPY` with the raw message data,
i.e. `SHA256(MESSAGE_DOMAIN_INVALID_SNAPPY + message.data)[:20]`.

Where relevant, clients MUST reject messages with `message-id` sizes other than
20 bytes.

*Note*: The above logic handles two exceptional cases: (1) multiple snappy
`data` can decompress to the same value, and (2) some message `data` can fail to
snappy decompress altogether.

The payload is carried in the `data` field of a gossipsub message, and varies
depending on the topic:

| Name | Message Type |
| -------------------------------- | ------------------------- |
| `block` | `SignedBlock` |
| `vote` | `SignedVote` |

Clients MUST reject (fail validation) messages containing an incorrect type, or
invalid payload.

#### Encodings

Topics are post-fixed with an encoding. Encodings define how the payload of a
gossipsub message is encoded.

- `ssz_snappy` - All objects are SSZ-encoded and then compressed with
[Snappy](https://github.com/google/snappy) block compression. Example: The
lean block topic string is `/leanconsensus/devnet{N}/block/ssz_snappy`,
and the data field of a gossipsub message is an `Block` that has been
SSZ-encoded and then compressed with Snappy.

### The Req/Resp domain

#### Encoding strategies

The token of the negotiated protocol ID specifies the type of encoding to be
used for the req/resp interaction. Only one value is possible at this time:

- `ssz_snappy`: The contents are first
[SSZ-encoded](../../ssz/simple-serialize.md) and then compressed with
[Snappy](https://github.com/google/snappy) frames compression. For objects
containing a single field, only the field is SSZ-encoded not a container with
a single field. For example, the `BlocksByRoot` request is an SSZ-encoded
list of `Root`'s.

#### Messages

##### Status v1

**Protocol ID:** `/leanconsensus/req/status/1/`

Request, Response Content:

```
(
finalized_root: Bytes32
finalized_slot: uint64
head_root: Bytes32
head_slot: uint64
)
```

The fields are, as seen by the client at the time of sending the message:

- `finalized_root`: `store.finalized_checkpoint.root` according to
[3SF-mini](https://github.com/ethereum/research/tree/master/3sf-mini).
(Note this defaults to `Root(b'\x00' * 32)` for the genesis finalized
checkpoint).
- `finalized_epoch`: `store.finalized_checkpoint.epoch` according to
[3SF-mini](https://github.com/ethereum/research/tree/master/3sf-mini).
- `head_root`: The `hash_tree_root` root of the current head block
(`Block`).
- `head_slot`: The slot of the block corresponding to the `head_root`.

The dialing client MUST send a `Status` request upon connection.

The request/response MUST be encoded as an SSZ-container.

The response MUST consist of a single `response_chunk`.

Clients SHOULD immediately disconnect from one another following the handshake
above under the following conditions:

1. If the (`finalized_root`, `finalized_epoch`) shared by the peer is not in the
client's chain at the expected epoch. For example, if Peer 1 sends (root,
epoch) of (A, 5) and Peer 2 sends (B, 3) but Peer 1 has root C at epoch 3,
then Peer 1 would disconnect because it knows that their chains are
irreparably disjoint.

Once the handshake completes, the client with the lower `finalized_epoch` or
`head_slot` (if the clients have equal `finalized_epoch`s) SHOULD request blocks
from its counterparty via the `BlocksByRoot` request.

*Note*: Under abnormal network condition or after some rounds of
`BlocksByRoot` requests, the client might need to send `Status` request
again to learn if the peer has a higher head. Implementers are free to implement
such behavior in their own way.

##### BlocksByRoot v1

**Protocol ID:** `/leanconsensus/req/blocks_by_root/1/`

Request Content:

```
(
List[Root, MAX_REQUEST_BLOCKS]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we define MAX_REQUEST_BLOCKS here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied over 2*10 (= 1024) from beacon specs: d19d93

This is 1024 * 4 / 60 = 68 minutes worth of block under 4s block time

)
```

Response Content:

```
(
List[SignedBlock, MAX_REQUEST_BLOCKS]
)
```

Requests blocks by block root (= `hash_tree_root(SignedBlock.message)`).
The response is a list of `SignedBlock` whose length is less than or equal
to the number of requested blocks. It may be less in the case that the
responding peer is missing blocks.

`BlocksByRoot` is primarily used to recover recent blocks (e.g. when
receiving a block or attestation whose parent is unknown).

The request MUST be encoded as an SSZ-field.

The response MUST consist of zero or more `response_chunk`. Each _successful_
`response_chunk` MUST contain a single `SignedBlock` payload.

Clients MUST respond with at least one block, if they have it. Clients MAY limit
the number of blocks in the response.
31 changes: 31 additions & 0 deletions docs/client/validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Validator

## Validator identification

To ensure a good distribution of block proposer duties in a round-robin manner
and avoid clashing IDs, validator IDs are pre-assigned to each client
implementation in a yaml file at
[`src/lean_spec/client/validators.yaml`](../../src/lean_spec/client/validators.yaml).
For example:

```yaml
ream: [0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
zeam: [1, 4, 7, 10, 13, 16, 19, 22, 25, 28]
quadrivium: [2, 5, 8, 11, 14, 17, 20, 23, 26, 29]
```

## Block proposer selection

The block proposer shall be determined by the modulo of the current slot number
by the total number of validators, such that block proposers are determined in
a round-robin manner by the validator IDs.

```py
def is_proposer(state: BeaconState, validator_index: ValidatorIndex) -> bool:
return get_current_slot() % state.config.num_validators == validator_index
```

## Remarks

- This spec is still missing the file format for the centralized, pre-generated
OTS keys (if any)
5 changes: 5 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ The core protocol specifications are located in `src/lean_spec/`.

Supporting cryptographic primitives are located in `src/lean_spec/subspecs/`.

### Client Subspecifications

Client specifications are located in `docs/client/`. The specs are in markdown
format for the time being and are subject to change.

## Design Principles

1. **Clarity over Performance**: Readable reference implementations
Expand Down
3 changes: 3 additions & 0 deletions src/lean_spec/client/nodes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg
- enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg
- enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg
Loading
Loading