From ba7639a4570d33d0c958fb4e27908c383e06a84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:10:58 -0300 Subject: [PATCH 1/4] docs: slots and intervals --- docs/slots_and_intervals.md | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/slots_and_intervals.md diff --git a/docs/slots_and_intervals.md b/docs/slots_and_intervals.md new file mode 100644 index 00000000..74b3c398 --- /dev/null +++ b/docs/slots_and_intervals.md @@ -0,0 +1,39 @@ +# Slots and Intervals + +A Lean Chain slot has a duration of 4 seconds and is divided in 5 intervals: + +1. Block proposal +2. Vote propagation +3. Vote aggregation +4. Safe target computation +5. Head update + +```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 │ + └────────────┴────────────┴────────────┴────────────┴────────────┘ +``` + +Block proposal is the first interval of a slot. During this interval, a block proposer, selected in a round-robin fashion, 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. + +Vote propagation is the second interval of a slot. During this interval, 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). These votes are in aggregation subnets and are imported by aggregators. Aggregators verify the votes in their subnet and store them for later aggregation. + +Vote aggregation is the third interval of a slot. During this interval, 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". + +Safe target computation is the fourth interval of a slot. During this interval, 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. + +Head update is the fifth and final interval of a slot. During this interval, validators merge the aggregated attestations they have in their "new attestations buffer" into their fork-choice view, and recompute their head. + +> **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`. 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. From ba5431c750ca9de775dd169b65f55107ee097d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:11:29 -0300 Subject: [PATCH 2/4] docs: link new doc and update lmd-ghost --- docs/SUMMARY.md | 1 + docs/introduction.md | 11 +++++---- docs/lmd_ghost.md | 56 ++++++++++++++++++++++++-------------------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 2c059f13..a7ac16fa 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -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) diff --git a/docs/introduction.md b/docs/introduction.md index 06e5bc7c..4ffe4caf 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -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 diff --git a/docs/lmd_ghost.md b/docs/lmd_ghost.md index 980fd3c1..38cca6cd 100644 --- a/docs/lmd_ghost.md +++ b/docs/lmd_ghost.md @@ -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? @@ -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:** @@ -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() From f56be4205f614fe135bb8a2f25bddc0ba9ac5c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:29:42 -0300 Subject: [PATCH 3/4] docs: do an improvement pass --- docs/slots_and_intervals.md | 111 +++++++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 20 deletions(-) diff --git a/docs/slots_and_intervals.md b/docs/slots_and_intervals.md index 74b3c398..4a6f1ecb 100644 --- a/docs/slots_and_intervals.md +++ b/docs/slots_and_intervals.md @@ -1,12 +1,15 @@ # Slots and Intervals -A Lean Chain slot has a duration of 4 seconds and is divided in 5 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: -1. Block proposal -2. Vote propagation -3. Vote aggregation -4. Safe target computation -5. Head update +| 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) @@ -14,26 +17,94 @@ A Lean Chain slot has a duration of 4 seconds and is divided in 5 intervals: │ 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 │ + │ block │ vote │ vote │safe target │ head │ │ proposal │propagation │aggregation │computation │ update │ └────────────┴────────────┴────────────┴────────────┴────────────┘ + ◄───────────── gossiped ─────────────▶ ◄───── local only ───────▶ ``` -Block proposal is the first interval of a slot. During this interval, a block proposer, selected in a round-robin fashion, 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. +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. -Vote propagation is the second interval of a slot. During this interval, 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). 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:** 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`. -Vote aggregation is the third interval of a slot. During this interval, 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". +## Interval 0: Block proposal -Safe target computation is the fourth interval of a slot. During this interval, 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. +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. -Head update is the fifth and final interval of a slot. During this interval, validators merge the aggregated attestations they have in their "new attestations buffer" into their fork-choice view, and recompute their head. +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:** 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`. 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. +> **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 it is the heaviest recurring computation in the +client: collapsing a subnet's worth of them +into one proof is heavy CPU work. 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 run with a two-thirds weight threshold instead of a plain majority, so it +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. Between promotions a vote sits in the buffer without weight, which is +what keeps a validator's fork-choice view from shifting under it mid-slot. See +[why staged promotion](./lmd_ghost.md#why-staged-promotion) for the reasoning. From 3d452f5513f698ced50395dc8ea437d08750bb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:42:47 -0300 Subject: [PATCH 4/4] docs: second round of improvements --- docs/slots_and_intervals.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/slots_and_intervals.md b/docs/slots_and_intervals.md index 4a6f1ecb..93b2bcee 100644 --- a/docs/slots_and_intervals.md +++ b/docs/slots_and_intervals.md @@ -45,9 +45,9 @@ After importing a block, all validators [recompute their head](./lmd_ghost.md), 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. +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. @@ -77,10 +77,10 @@ Aggregators aggregate the votes they have received and gossip the resulting aggr 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 it is the heaviest recurring computation in the -client: collapsing a subnet's worth of them -into one proof is heavy CPU work. 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. +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` @@ -94,7 +94,8 @@ Validators compute the [safe target](./lmd_ghost.md#safe-target-selection) they' 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 run with a two-thirds weight threshold instead of a plain majority, so it +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. @@ -105,6 +106,8 @@ Validators merge the aggregated attestations they have in their "new attestation 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. Between promotions a vote sits in the buffer without weight, which is -what keeps a validator's fork-choice view from shifting under it mid-slot. See -[why staged promotion](./lmd_ghost.md#why-staged-promotion) for the reasoning. +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.