Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# Consensus

- [Slots and Intervals](./slots_and_intervals.md)
- [3SF-mini: Justification & Finalization](./3sf_mini.md)
- [LMD-GHOST Fork Choice](./lmd_ghost.md)

Expand Down
11 changes: 6 additions & 5 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ consensus client, written in Rust.
This book collects the design notes and operator-facing references for ethlambda.
It is split into two parts:

- **Consensus** explains the algorithms ethlambda implements: the
[3SF-mini](./3sf_mini.md) justification and finalization rules, and the
[LMD-GHOST](./lmd_ghost.md) fork choice algorithm. Both documents are
implementation-agnostic; ethlambda-specific behaviour is called out in
blockquotes.
- **Consensus** explains how the chain advances: the
[slot and interval structure](./slots_and_intervals.md) that schedules every
validator duty, the [3SF-mini](./3sf_mini.md) justification and finalization
rules, and the [LMD-GHOST](./lmd_ghost.md) fork choice algorithm. These
documents are implementation-agnostic; ethlambda-specific behaviour is called
out in blockquotes.
- **Operations** documents observable surfaces of a running node:
[Prometheus metrics](./metrics.md), [checkpoint sync](./checkpoint_sync.md),
and the [fork choice visualization](./fork_choice_visualization.md) served
Expand Down
56 changes: 31 additions & 25 deletions docs/lmd_ghost.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,9 @@ designated moments. This ensures all validators operate on a consistent view.
fixed points
```

> **In ethlambda:** The two stages are called "new" and "known" attestations, stored
> in `LatestNewAttestations` and `LatestKnownAttestations` tables respectively.
> Promotion happens at tick intervals 0 (if proposing) and 3 (end of slot).
> **In ethlambda:** The two stages are called "new" and "known" attestations, held in
> the in-memory `new_payloads` and `known_payloads` buffers of the `Store` respectively.
> Promotion happens at tick intervals 0 (if proposing) and 4 (end of slot).

### Why Staged Promotion?

Expand Down Expand Up @@ -618,27 +618,28 @@ source code locations, and performance.

### Tick-Based Scheduling

ethlambda divides time into **4-second slots**, each split into **4 intervals** (1 second
each). Fork choice operations are scheduled at specific intervals:
ethlambda divides time into **4-second slots**, each split into **5 intervals** (800 ms
each), as described in [Slots and Intervals](./slots_and_intervals.md). Fork choice
operations are scheduled at specific intervals:

```text
ONE SLOT (4 seconds)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Interval 0 β”‚ Interval 1 β”‚ Interval 2 β”‚ Interval 3 β”‚
β”‚ (t+0s) β”‚ (t+1s) β”‚ (t+2s) β”‚ (t+3s) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ IF PROPOSER: β”‚ NON-PROPOSER:β”‚ update_safe β”‚ accept_new β”‚
β”‚ accept new β”‚ produce β”‚ _target() β”‚ _attestationsβ”‚
β”‚ attestationsβ”‚ attestation β”‚ β”‚ () β”‚
β”‚ + propose β”‚ β”‚ (2/3 vote β”‚ β”‚
β”‚ block β”‚ β”‚ threshold) β”‚ update_head()β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ update_head()β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

◄─────────────── Slot N ──────────────────────────────────────►
ONE SLOT (4000 ms)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Interval 0 β”‚ Interval 1 β”‚ Interval 2 β”‚ Interval 3 β”‚ Interval 4 β”‚
β”‚ t+0 ms β”‚ t+800 ms β”‚ t+1600 ms β”‚ t+2400 ms β”‚ t+3200 ms β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚IF PROPOSER:β”‚ ALL β”‚ aggregatorsβ”‚update_safe β”‚accept_new_ β”‚
β”‚ accept new β”‚ VALIDATORS:β”‚ publish β”‚_target() β”‚attestationsβ”‚
β”‚ attestationβ”‚ produce β”‚ aggregated β”‚ β”‚() β”‚
β”‚ + propose β”‚ attestationβ”‚ attestationβ”‚ (2/3 vote β”‚ β”‚
β”‚ block β”‚ β”‚ β”‚ threshold) β”‚update_head β”‚
β”‚ β”‚ β”‚ β”‚ β”‚() β”‚
β”‚update_head β”‚ β”‚ β”‚ β”‚ β”‚
β”‚() β”‚ β”‚ β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

◄─────────────── Slot N ──────────────────────────────────────────►
```

**Detailed sequence:**
Expand All @@ -655,20 +656,25 @@ each). Fork choice operations are scheduled at specific intervals:
β”‚
Interval 1 ─ Attestation production
β”‚
β”œβ”€β”€ Non-proposers:
β”œβ”€β”€ All validators, proposer included:
β”‚ └── Create attestation with:
β”‚ β€’ head = current fork choice head (newest head)
β”‚ β€’ target = derived from safe_target (for 3SF-mini)
β”‚ β€’ source = latest_justified checkpoint
β”‚ Publish attestation to gossipsub
β”‚
Interval 2 ─ Safe target update
Interval 2 ─ Aggregation
β”‚
β”œβ”€β”€ Aggregators: aggregate their subnet's gossip signatures
β”‚ └── Publish the aggregated attestation to gossipsub
β”‚
Interval 3 ─ Safe target update
β”‚
β”œβ”€β”€ Recalculate safe_target using 2/3 supermajority threshold
β”‚ └── Only blocks with β‰₯ ⌈2V/3βŒ‰ attestation weight qualify
β”‚ (V = total validators)
β”‚
Interval 3 ─ End of slot
Interval 4 ─ End of slot
β”‚
β”œβ”€β”€ Promote new β†’ known attestations
└── Run fork choice β†’ update_head()
Expand Down
113 changes: 113 additions & 0 deletions docs/slots_and_intervals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Slots and Intervals

A Lean Chain slot has a duration of 4 seconds and is divided in 5 intervals of 800 ms.
Every duty a validator owes the chain is due in one of them:

| Interval | Offset | Duty | Who acts | What it publishes |
| --- | --- | --- | --- | --- |
| 0 | t+0 ms | [Block proposal](#interval-0-block-proposal) | the slot's proposer | the block, on the `block` topic |
| 1 | t+800 ms | [Vote propagation](#interval-1-vote-propagation) | every validator | a signed attestation, on its subnet topic |
| 2 | t+1600 ms | [Vote aggregation](#interval-2-vote-aggregation) | aggregators | an aggregated attestation, on the `aggregation` topic |
| 3 | t+2400 ms | [Safe target computation](#interval-3-safe-target-computation) | every validator | nothing: local bookkeeping |
| 4 | t+3200 ms | [Head update](#interval-4-head-update) | every validator | nothing: local bookkeeping |

```text
ONE SLOT (4000 ms)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Interval 0 β”‚ Interval 1 β”‚ Interval 2 β”‚ Interval 3 β”‚ Interval 4 β”‚
β”‚ t+0 ms β”‚ t+800 ms β”‚ t+1600 ms β”‚ t+2400 ms β”‚ t+3200 ms β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ block β”‚ vote β”‚ vote β”‚safe target β”‚ head β”‚
β”‚ proposal β”‚propagation β”‚aggregation β”‚computation β”‚ update β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
◄───────────── gossiped ─────────────▢ ◄───── local only ───────▢
```

The grid comes from a genesis timestamp every node shares, so the schedule needs no
coordination messages: a node reads its clock, works out which interval it is in, and
knows which duty is due. The order is a dependency chain, since each interval consumes
what the previous one produced. A duty that overruns its interval is not rescheduled: it
lands late, and the slot moves on without it.

> **In ethlambda:** the intervals are the `SlotInterval` variants in
> `crates/blockchain/src/lib.rs`, and their length comes from
> `MILLISECONDS_PER_INTERVAL` and `INTERVALS_PER_SLOT` in
> `crates/common/types/src/constants.rs`.

## Interval 0: Block proposal

A block proposer, selected in a round-robin fashion (`slot % num_validators`), proposes a
new block and gossips it to the network. Right before building the block, the proposer
merges their "new attestations buffer" into their fork-choice view. They then include
attestations that the proposer has recently seen into their block. Other validators verify
the block and its contents, and merge the votes it includes into their fork-choice view.
After importing a block, all validators [recompute their head](./lmd_ghost.md), and update
the latest [finalized and justified checkpoints](./3sf_mini.md) according to the block's
post-state.

A block body carries at most `MAX_ATTESTATIONS_DATA` aggregated attestations: distinct
`(slot, head, target, source)` tuples, each paired with a bitfield naming the validators
bound to it.
Genesis occupies slot 0, so proposals start at slot 1, and nothing forces a slot to be
filled: a proposer that is offline or too slow leaves an empty slot, and the next block
simply points its parent root at an older block.

> **In ethlambda:** block proposal is merged into the previous slot's head-update
> interval: the proposer advances its store to the next slot, builds the block there,
> and holds publication until the slot boundary. That buys the build one extra interval
> of headroom and leaves no actor work at the block-proposal tick itself.

## Interval 1: Vote propagation

Validators gossip their votes for the block they consider to be the head of the chain, and
append to it a `(source, target)` [finality vote](./3sf_mini.md#recap-attestation-anatomy).
These votes are in aggregation subnets and are imported by aggregators. Aggregators verify
the votes in their subnet and store them for later aggregation.

> **In ethlambda:** a validator's subnet is `validator_index % attestation_committee_count`,
> and a node only aggregates for subnets it subscribed to at startup. Aggregation is also
> gated on the aggregator role, seeded by `--is-aggregator` and flippable at runtime
> through the admin API. A chain whose validators all decline the role still gossips votes
> and logs them as processed, but no aggregate is ever produced, so every block is empty
> and the chain never justifies.

## Interval 2: Vote aggregation

Aggregators aggregate the votes they have received and gossip the resulting aggregated
attestations to the network. These aggregated attestations are imported by all validators,
who verify and store them in a "new attestations buffer".

Aggregation earns its own interval because collapsing a subnet's worth of XMSS signatures
into one proof is the heaviest recurring computation in the client. It is also what makes a
block affordable, since a block carrying raw votes would need one full XMSS signature per
voter, quickly going over the network bandwidth limit.

> **In ethlambda:** the proofs run on an off-thread worker so the blockchain actor's
> message loop stays responsive, and a session may start up to `EARLY_AGGREGATION_WINDOW`
> before the interval boundary once two thirds of the signatures are in. At the session's
> soft deadline the actor stops handing out new jobs, but a proof already in flight
> finishes and publishes late rather than being discarded.

## Interval 3: Safe target computation

Validators compute the [safe target](./lmd_ghost.md#safe-target-selection) they'll use when
deciding which finality vote to cast on the next slot. The safe target is computed based on
the votes received in the current slot.

It is LMD-GHOST again, but run over just the votes that arrived this slot and with a
two-thirds weight threshold, where head selection applies none. The safe target therefore
sits at or behind the head and advances only once a branch is backed by a supermajority.
Deriving targets from it is what stops [3SF-mini](./3sf_mini.md) from justifying a branch
the network has not visibly converged on.

## Interval 4: Head update

Validators merge the aggregated attestations they have in their "new attestations buffer"
into their fork-choice view, and recompute their head.

This is the slot's second and last promotion point; the first is the proposer's, just
before it builds. Until a vote is promoted it carries no weight in head selection, which is
what keeps a validator's fork-choice view from shifting under it mid-slot. The safe target
is the exception: it reads the unpromoted buffer directly, which is how it stays a view of
this slot alone. See [why staged promotion](./lmd_ghost.md#why-staged-promotion) for the
reasoning.
Loading