Skip to content

Commit 66053d8

Browse files
committed
Merge branch 'main' into devnet4
2 parents 2d5b273 + fd2cc63 commit 66053d8

3 files changed

Lines changed: 72 additions & 42 deletions

File tree

crates/blockchain/src/lib.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,16 @@ impl BlockChainServer {
213213
info!(%slot, %validator_id, "We are the proposer for this slot");
214214

215215
// Build the block with attestation signatures
216-
let Ok((block, attestation_signatures)) =
216+
let Ok((block, attestation_signatures, post_checkpoints)) =
217217
store::produce_block_with_signatures(&mut self.store, slot, validator_id)
218218
.inspect_err(|err| error!(%slot, %validator_id, %err, "Failed to build block"))
219219
else {
220220
return;
221221
};
222222

223-
// Create proposer's attestation (attests to the new block)
223+
// Create proposer's attestation using post-block checkpoints because
224+
// the block's attestations may have advanced justification/finalization
225+
// but the block hasn't been imported into the store yet.
224226
let proposer_attestation = Attestation {
225227
validator_id,
226228
data: AttestationData {
@@ -229,8 +231,12 @@ impl BlockChainServer {
229231
root: block.hash_tree_root(),
230232
slot: block.slot,
231233
},
232-
target: store::get_attestation_target(&self.store),
233-
source: self.store.latest_justified(),
234+
target: store::get_attestation_target_with_checkpoints(
235+
&self.store,
236+
post_checkpoints.justified,
237+
post_checkpoints.finalized,
238+
),
239+
source: post_checkpoints.justified,
234240
},
235241
};
236242

crates/blockchain/src/store.rs

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3;
3535
/// See: https://github.com/lambdaclass/ethlambda/issues/259
3636
const MAX_ATTESTATION_PROOF_BYTES: usize = 9 * 1024 * 1024;
3737

38+
/// Post-block checkpoints extracted from the state transition in `build_block`.
39+
///
40+
/// When building a block, the state transition processes attestations that may
41+
/// advance justification/finalization. These checkpoints reflect the post-state
42+
/// values, which the proposer needs for its attestation (since the block hasn't
43+
/// been imported into the store yet).
44+
pub struct PostBlockCheckpoints {
45+
pub justified: Checkpoint,
46+
pub finalized: Checkpoint,
47+
}
48+
3849
/// Accept new aggregated payloads, promoting them to known for fork choice.
3950
fn accept_new_attestations(store: &mut Store, log_tree: bool) {
4051
store.promote_new_aggregated_payloads();
@@ -633,6 +644,28 @@ fn on_block_core(
633644
///
634645
/// NOTE: this assumes that we have all the blocks from the head back to the latest finalized.
635646
pub fn get_attestation_target(store: &Store) -> Checkpoint {
647+
get_attestation_target_with_checkpoints(
648+
store,
649+
store.latest_justified(),
650+
store.latest_finalized(),
651+
)
652+
}
653+
654+
/// Calculate target checkpoint using the provided justified and finalized checkpoints
655+
/// instead of reading them from the store.
656+
///
657+
/// This is needed by the proposer, whose block hasn't been imported yet but whose
658+
/// state transition may have advanced justification/finalization.
659+
///
660+
/// Note: the walk-back still starts from `store.head()` (the pre-import head), not
661+
/// the new block. This is correct because the new block is only 1 slot ahead with less than 2/3 of votes — the
662+
/// walk-back immediately reaches the same chain. The important fix is using the
663+
/// post-state justified/finalized for the justifiability check and clamping guard.
664+
pub fn get_attestation_target_with_checkpoints(
665+
store: &Store,
666+
justified: Checkpoint,
667+
finalized: Checkpoint,
668+
) -> Checkpoint {
636669
// Start from current head
637670
let mut target_block_root = store.head();
638671
let mut target_header = store
@@ -659,7 +692,7 @@ pub fn get_attestation_target(store: &Store) -> Checkpoint {
659692
}
660693
}
661694

662-
let finalized_slot = store.latest_finalized().slot;
695+
let finalized_slot = finalized.slot;
663696

664697
// Ensure target is in justifiable slot range
665698
//
@@ -673,7 +706,7 @@ pub fn get_attestation_target(store: &Store) -> Checkpoint {
673706
.get_block_header(&target_block_root)
674707
.expect("parent block exists");
675708
}
676-
// Guard: clamp target to latest_justified (not in the spec).
709+
// Guard: clamp target to justified (not in the spec).
677710
//
678711
// The spec's walk-back has no lower bound, so it can produce attestations
679712
// where target.slot < source.slot (source = latest_justified). These would
@@ -685,14 +718,13 @@ pub fn get_attestation_target(store: &Store) -> Checkpoint {
685718
// justified checkpoint.
686719
//
687720
// See https://github.com/blockblaz/zeam/blob/697c293879e922942965cdb1da3c6044187ae00e/pkgs/node/src/forkchoice.zig#L654-L659
688-
let latest_justified = store.latest_justified();
689-
if target_header.slot < latest_justified.slot {
721+
if target_header.slot < justified.slot {
690722
warn!(
691723
target_slot = target_header.slot,
692-
justified_slot = latest_justified.slot,
724+
justified_slot = justified.slot,
693725
"Attestation target walked behind justified source, clamping to justified"
694726
);
695-
return latest_justified;
727+
return justified;
696728
}
697729

698730
Checkpoint {
@@ -769,7 +801,7 @@ pub fn produce_block_with_signatures(
769801
store: &mut Store,
770802
slot: u64,
771803
validator_index: u64,
772-
) -> Result<(Block, Vec<AggregatedSignatureProof>), StoreError> {
804+
) -> Result<(Block, Vec<AggregatedSignatureProof>, PostBlockCheckpoints), StoreError> {
773805
// Get parent block and state to build upon
774806
let head_root = get_proposal_head(store, slot);
775807
let head_state = store
@@ -794,7 +826,7 @@ pub fn produce_block_with_signatures(
794826

795827
let known_block_roots = store.get_block_roots();
796828

797-
let (block, signatures) = build_block(
829+
let (block, signatures, post_checkpoints) = build_block(
798830
&head_state,
799831
slot,
800832
validator_index,
@@ -803,7 +835,7 @@ pub fn produce_block_with_signatures(
803835
&aggregated_payloads,
804836
)?;
805837

806-
Ok((block, signatures))
838+
Ok((block, signatures, post_checkpoints))
807839
}
808840

809841
/// Errors that can occur during Store operations.
@@ -1102,7 +1134,7 @@ fn build_block(
11021134
parent_root: H256,
11031135
known_block_roots: &HashSet<H256>,
11041136
aggregated_payloads: &HashMap<H256, (AttestationData, Vec<AggregatedSignatureProof>)>,
1105-
) -> Result<(Block, Vec<AggregatedSignatureProof>), StoreError> {
1137+
) -> Result<(Block, Vec<AggregatedSignatureProof>, PostBlockCheckpoints), StoreError> {
11061138
let mut aggregated_attestations: Vec<AggregatedAttestation> = Vec::new();
11071139
let mut aggregated_signatures: Vec<AggregatedSignatureProof> = Vec::new();
11081140
let mut accumulated_proof_bytes: usize = 0;
@@ -1206,7 +1238,12 @@ fn build_block(
12061238
process_block(&mut post_state, &final_block)?;
12071239
final_block.state_root = post_state.hash_tree_root();
12081240

1209-
Ok((final_block, aggregated_signatures))
1241+
let post_checkpoints = PostBlockCheckpoints {
1242+
justified: post_state.latest_justified,
1243+
finalized: post_state.latest_finalized,
1244+
};
1245+
1246+
Ok((final_block, aggregated_signatures, post_checkpoints))
12101247
}
12111248

12121249
/// Verify all signatures in a signed block.
@@ -1522,7 +1559,7 @@ mod tests {
15221559
}
15231560

15241561
// Build the block; this should succeed (the bug: no size guard)
1525-
let (block, signatures) = build_block(
1562+
let (block, signatures, _post_checkpoints) = build_block(
15261563
&head_state,
15271564
slot,
15281565
proposer_index,

crates/storage/src/store.rs

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::{HashMap, HashSet, VecDeque};
1+
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
22
use std::sync::{Arc, LazyLock, Mutex};
33

44
/// The tree hash root of an empty block body.
@@ -205,17 +205,15 @@ impl PayloadBuffer {
205205
}
206206
}
207207

208-
/// Individual validator signature received via gossip.
209-
#[derive(Clone)]
210-
struct GossipSignatureEntry {
211-
validator_id: u64,
212-
signature: ValidatorSignature,
213-
}
214-
215208
/// Gossip signatures grouped by attestation data.
209+
///
210+
/// Signatures are stored in a `BTreeMap` keyed by validator_id to guarantee
211+
/// ascending iteration order. XMSS aggregate proofs are order-dependent:
212+
/// verification reconstructs pubkeys from the participation bitfield (low-to-high),
213+
/// so aggregation must produce them in the same ascending order.
216214
struct GossipDataEntry {
217215
data: AttestationData,
218-
signatures: Vec<GossipSignatureEntry>,
216+
signatures: BTreeMap<u64, ValidatorSignature>,
219217
}
220218

221219
/// Gossip signatures grouped by attestation data (via data_root).
@@ -260,8 +258,7 @@ pub struct Store {
260258
backend: Arc<dyn StorageBackend>,
261259
new_payloads: Arc<Mutex<PayloadBuffer>>,
262260
known_payloads: Arc<Mutex<PayloadBuffer>>,
263-
/// In-memory gossip signatures: data_root → (AttestationData, Vec<GossipSignatureEntry>).
264-
/// Transient data consumed at interval 2 aggregation.
261+
/// In-memory gossip signatures, consumed at interval 2 aggregation.
265262
gossip_signatures: Arc<Mutex<GossipSignatureMap>>,
266263
}
267264

@@ -947,7 +944,7 @@ impl Store {
947944
// Gossip signatures are individual validator signatures received via P2P.
948945
// They're transient (consumed at interval 2 aggregation) so stored in-memory.
949946
// Keyed by AttestationData (via data_root) matching the leanSpec structure:
950-
// gossip_signatures: dict[AttestationData, set[GossipSignatureEntry]]
947+
// gossip_signatures: dict[AttestationData, set[GossipSignature]]
951948

952949
/// Delete gossip entries for the given (validator_id, data_root) pairs.
953950
pub fn delete_gossip_signatures(&mut self, keys: &[(u64, H256)]) {
@@ -957,7 +954,7 @@ impl Store {
957954
let mut gossip = self.gossip_signatures.lock().unwrap();
958955
for &(vid, data_root) in keys {
959956
if let Some(entry) = gossip.get_mut(&data_root) {
960-
entry.signatures.retain(|e| e.validator_id != vid);
957+
entry.signatures.remove(&vid);
961958
if entry.signatures.is_empty() {
962959
gossip.remove(&data_root);
963960
}
@@ -974,7 +971,7 @@ impl Store {
974971
let sigs: Vec<_> = entry
975972
.signatures
976973
.iter()
977-
.map(|e| (e.validator_id, e.signature.clone()))
974+
.map(|(&vid, sig)| (vid, sig.clone()))
978975
.collect();
979976
(HashedAttestationData::new(entry.data.clone()), sigs)
980977
})
@@ -992,19 +989,9 @@ impl Store {
992989
let mut gossip = self.gossip_signatures.lock().unwrap();
993990
let entry = gossip.entry(data_root).or_insert_with(|| GossipDataEntry {
994991
data: att_data,
995-
signatures: Vec::new(),
992+
signatures: BTreeMap::new(),
996993
});
997-
// Avoid duplicates for same validator
998-
if !entry
999-
.signatures
1000-
.iter()
1001-
.any(|e| e.validator_id == validator_id)
1002-
{
1003-
entry.signatures.push(GossipSignatureEntry {
1004-
validator_id,
1005-
signature,
1006-
});
1007-
}
994+
entry.signatures.entry(validator_id).or_insert(signature);
1008995
}
1009996

1010997
// ============ Derived Accessors ============

0 commit comments

Comments
 (0)