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,699 changes: 794 additions & 905 deletions lean_client/Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions lean_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,9 @@ features = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6
hex = "0.4.3"
http_api_utils = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" }
k256 = "0.13"
rec_aggregation = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" }
rec_aggregation = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "2eb4b9d983171139af36749f127dd9890c9109e6" }
leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" }
leansig_wrapper = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" }
leansig_wrapper = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "2eb4b9d983171139af36749f127dd9890c9109e6" }
libp2p = { version = "0.56.0", default-features = false, features = [
'dns',
'gossipsub',
Expand Down
15 changes: 10 additions & 5 deletions lean_client/containers/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,23 @@ impl SignedBlock {
})
.collect::<Result<Vec<_>>>()?;

Ok((public_keys, attestation_data_root, slot, aggregated_signature))
Ok((
public_keys,
attestation_data_root,
slot,
aggregated_signature,
))
})
.collect::<Result<Vec<_>>>()?;

// Phase 2: verify all proofs in parallel (CPU-intensive XMSS verification)
verification_tasks
.into_par_iter()
.try_for_each(|(public_keys, attestation_data_root, slot, aggregated_signature)| {
verification_tasks.into_par_iter().try_for_each(
|(public_keys, attestation_data_root, slot, aggregated_signature)| {
aggregated_signature
.verify(public_keys, attestation_data_root, slot)
.context("attestation aggregated signature verification failed")
})?;
},
)?;

// Verify the proposer's XMSS signature over the block root
let proposer_index = block.proposer_index;
Expand Down
15 changes: 9 additions & 6 deletions lean_client/containers/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -866,8 +866,10 @@ impl State {
.map(|proof| {
let proof_validators: HashSet<u64> =
proof.get_participant_indices().into_iter().collect();
let intersection: HashSet<u64> =
remaining.intersection(&proof_validators).copied().collect();
let intersection: HashSet<u64> = remaining
.intersection(&proof_validators)
.copied()
.collect();
(proof, intersection)
})
.max_by_key(|(_, intersection)| intersection.len())
Expand Down Expand Up @@ -966,8 +968,10 @@ impl State {
// AND a Phase 2 (children) proof for the same AttestationData.
// Per spec, each AttestationData must appear at most once in the block body —
// merge any such pairs into a single recursive proof via aggregate_with_children.
let mut proof_groups: HashMap<H256, Vec<(AggregatedAttestation, AggregatedSignatureProof)>> =
HashMap::new();
let mut proof_groups: HashMap<
H256,
Vec<(AggregatedAttestation, AggregatedSignatureProof)>,
> = HashMap::new();
for (att, proof) in results {
proof_groups
.entry(att.data.hash_tree_root())
Expand Down Expand Up @@ -1014,8 +1018,7 @@ impl State {
.collect();
all_validator_ids.sort();
all_validator_ids.dedup();
let all_participants =
AggregationBits::from_validator_indices(&all_validator_ids);
let all_participants = AggregationBits::from_validator_indices(&all_validator_ids);

match AggregatedSignatureProof::aggregate_with_children(
all_participants.clone(),
Expand Down
4 changes: 3 additions & 1 deletion lean_client/containers/tests/unit_tests/state_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ fn test_process_block_header_valid() {
let genesis_header_root = state_at_slot_1.latest_block_header.hash_tree_root();

let signed_block = create_block(1, &mut state_at_slot_1.latest_block_header, None);
let new_state = state_at_slot_1.process_block_header(&signed_block.block).unwrap();
let new_state = state_at_slot_1
.process_block_header(&signed_block.block)
.unwrap();

assert_eq!(new_state.latest_finalized.root, genesis_header_root);
assert_eq!(new_state.latest_justified.root, genesis_header_root);
Expand Down
10 changes: 6 additions & 4 deletions lean_client/fork_choice/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,10 +636,12 @@ fn process_block_internal(
adr.get(&key.data_root)
.map_or(true, |data| data.target.slot.0 > finalized_slot)
});
store.latest_known_aggregated_payloads.retain(|data_root, _| {
adr.get(data_root)
.map_or(true, |data| data.target.slot.0 > finalized_slot)
});
store
.latest_known_aggregated_payloads
.retain(|data_root, _| {
adr.get(data_root)
.map_or(true, |data| data.target.slot.0 > finalized_slot)
});
store.latest_new_aggregated_payloads.retain(|data_root, _| {
adr.get(data_root)
.map_or(true, |data| data.target.slot.0 > finalized_slot)
Expand Down
31 changes: 13 additions & 18 deletions lean_client/fork_choice/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use std::collections::{HashMap, HashSet};
use anyhow::{Result, anyhow, ensure};
use containers::{
AggregatedSignatureProof, Attestation, AttestationData, Block, BlockHeader, Checkpoint, Config,
SignatureKey, SignedAggregatedAttestation, SignedAttestation, SignedBlock, Slot,
State,
SignatureKey, SignedAggregatedAttestation, SignedAttestation, SignedBlock, Slot, State,
};
use metrics::{METRICS, set_gauge_u64};
use ssz::{H256, SszHash};
Expand Down Expand Up @@ -119,25 +118,11 @@ impl Store {

let target_checkpoint = self.get_attestation_target();

let head_state = self
.states
.get(&self.head)
.ok_or_else(|| anyhow!("head state not found"))?;

let source = if head_state.latest_justified.root.is_zero() {
Checkpoint {
root: self.head,
slot: head_state.latest_justified.slot,
}
} else {
head_state.latest_justified.clone()
};

Ok(AttestationData {
slot,
head: head_checkpoint,
target: target_checkpoint,
source,
source: self.latest_justified.clone(),
})
}

Expand Down Expand Up @@ -579,6 +564,7 @@ pub struct BlockProductionInputs {
pub gossip_signatures: HashMap<SignatureKey, Signature>,
pub aggregated_payloads: HashMap<H256, Vec<AggregatedSignatureProof>>,
pub log_inv_rate: usize,
pub store_latest_justified: Checkpoint,
}

pub fn prepare_block_production(
Expand Down Expand Up @@ -655,6 +641,7 @@ pub fn prepare_block_production(
gossip_signatures,
aggregated_payloads,
log_inv_rate,
store_latest_justified: store.latest_justified.clone(),
})
}

Expand All @@ -671,9 +658,10 @@ pub fn execute_block_production(
gossip_signatures,
aggregated_payloads,
log_inv_rate,
store_latest_justified,
} = inputs;

let (final_block, _final_post_state, _aggregated_attestations, signatures) = head_state
let (final_block, final_post_state, _aggregated_attestations, signatures) = head_state
.build_block(
slot,
validator_index,
Expand All @@ -686,6 +674,13 @@ pub fn execute_block_production(
log_inv_rate,
)?;

ensure!(
final_post_state.latest_justified.slot >= store_latest_justified.slot,
"Produced block justified={} < store justified={}. Fixed-point attestation loop did not converge.",
final_post_state.latest_justified.slot.0,
store_latest_justified.slot.0,
);

let block_root = final_block.hash_tree_root();

Ok((block_root, final_block, signatures))
Expand Down
10 changes: 6 additions & 4 deletions lean_client/fork_choice/tests/fork_choice_test_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ impl Into<State> for TestAnchorState {
let proposal_pubkey: PublicKey = test_validator
.proposal_pubkey
.as_deref()
.map(|s| s.parse().expect("Failed to parse validator proposal_pubkey"))
.map(|s| {
s.parse()
.expect("Failed to parse validator proposal_pubkey")
})
.unwrap_or_default();
let validator = Validator {
attestation_pubkey,
Expand Down Expand Up @@ -556,9 +559,8 @@ fn forkchoice(spec_file: &str) {

// Advance time to the block's slot to ensure attestations are processable
// SECONDS_PER_SLOT is 4. Convert to milliseconds for devnet-3
let block_time_millis = (store.config.genesis_time
+ (signed_block.block.slot.0 * 4))
* 1000;
let block_time_millis =
(store.config.genesis_time + (signed_block.block.slot.0 * 4)) * 1000;
on_tick(&mut store, block_time_millis, false);

on_block(&mut store, &mut cache, signed_block).unwrap();
Expand Down
5 changes: 3 additions & 2 deletions lean_client/fork_choice/tests/unit_tests/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,9 @@ fn test_produce_block_with_attestations() {
let slot = Slot(2);
let validator_idx = 2;

let (_root, block, signatures) = produce_block_with_signatures(&mut store, slot, validator_idx, 1)
.expect("block production should succeed");
let (_root, block, signatures) =
produce_block_with_signatures(&mut store, slot, validator_idx, 1)
.expect("block production should succeed");

// Block should include the 2 attestations we added (validators 5 and 6).
// Attestations may be aggregated, so check the count matches signatures.
Expand Down
2 changes: 1 addition & 1 deletion lean_client/networking/src/discovery/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use libp2p_identity::{Keypair, PeerId};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};

use crate::enr_ext::{EnrExt, QUIC6_ENR_KEY, QUIC_ENR_KEY};
use crate::enr_ext::{EnrExt, QUIC_ENR_KEY, QUIC6_ENR_KEY};

pub use config::DiscoveryConfig;

Expand Down
3 changes: 1 addition & 2 deletions lean_client/networking/src/gossipsub/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ impl GossipsubMessage {
pub fn decode(topic: &TopicHash, data: &[u8]) -> Result<Self, String> {
match GossipsubTopic::decode(topic)?.kind {
GossipsubKind::Block => Ok(Self::Block(
SignedBlock::from_ssz_default(data)
.map_err(|e| format!("{:?}", e))?,
SignedBlock::from_ssz_default(data).map_err(|e| format!("{:?}", e))?,
)),
GossipsubKind::AttestationSubnet(subnet_id) => Ok(Self::AttestationSubnet {
subnet_id,
Expand Down
36 changes: 20 additions & 16 deletions lean_client/networking/src/gossipsub/tests/topic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,12 @@ fn test_get_subscription_topics_aggregator_with_validator() {
let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect();
assert!(kinds.contains(&GossipsubKind::Block));
assert!(kinds.contains(&GossipsubKind::Aggregation));
assert!(kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id(
0,
TEST_SUBNET_COUNT
))));
assert!(
kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id(
0,
TEST_SUBNET_COUNT
)))
);

for topic in &topics {
assert_eq!(topic.fork, "myfork");
Expand All @@ -254,8 +256,7 @@ fn test_get_subscription_topics_aggregator_with_validator() {
#[test]
fn test_get_subscription_topics_aggregator_no_validator_fallback() {
// Aggregator with no registered validators falls back to subnet 0.
let topics =
get_subscription_topics("myfork".to_string(), &[], true, &[], TEST_SUBNET_COUNT);
let topics = get_subscription_topics("myfork".to_string(), &[], true, &[], TEST_SUBNET_COUNT);

let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect();
assert!(kinds.contains(&GossipsubKind::Block));
Expand All @@ -282,10 +283,12 @@ fn test_get_subscription_topics_aggregator_explicit_subnets() {
let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect();
assert!(kinds.contains(&GossipsubKind::Block));
assert!(kinds.contains(&GossipsubKind::Aggregation));
assert!(kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id(
0,
TEST_SUBNET_COUNT
))));
assert!(
kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id(
0,
TEST_SUBNET_COUNT
)))
);
assert!(kinds.contains(&GossipsubKind::AttestationSubnet(1)));
assert!(kinds.contains(&GossipsubKind::AttestationSubnet(2)));

Expand All @@ -312,10 +315,12 @@ fn test_get_subscription_topics_non_aggregator_validator() {
let kinds: Vec<_> = topics.iter().map(|t| t.kind.clone()).collect();
assert!(kinds.contains(&GossipsubKind::Block));
assert!(kinds.contains(&GossipsubKind::Aggregation));
assert!(kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id(
validator_id,
TEST_SUBNET_COUNT
))));
assert!(
kinds.contains(&GossipsubKind::AttestationSubnet(compute_subnet_id(
validator_id,
TEST_SUBNET_COUNT
)))
);

for topic in &topics {
assert_eq!(topic.fork, "myfork");
Expand Down Expand Up @@ -348,8 +353,7 @@ fn test_get_subscription_topics_non_aggregator_multi_validator() {
fn test_get_subscription_topics_non_validator_skips_attestation() {
// Non-validator, non-aggregator node subscribes to NO attestation topics (saves bandwidth).
// This aligns with leanSpec PR #482: subnet filtering at the p2p subscription layer.
let topics =
get_subscription_topics("myfork".to_string(), &[], false, &[], TEST_SUBNET_COUNT);
let topics = get_subscription_topics("myfork".to_string(), &[], false, &[], TEST_SUBNET_COUNT);

// Block + Aggregation only — no attestation subnets
assert_eq!(topics.len(), 2);
Expand Down
13 changes: 6 additions & 7 deletions lean_client/networking/src/req_resp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,13 +394,12 @@ impl LeanCodec {
continue;
}

let block =
SignedBlock::from_ssz_default(&ssz_bytes).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("SSZ decode Block failed: {e:?}"),
)
})?;
let block = SignedBlock::from_ssz_default(&ssz_bytes).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("SSZ decode Block failed: {e:?}"),
)
})?;
blocks.push(block);
}
Ok(LeanResponse::BlocksByRoot(blocks))
Expand Down
5 changes: 1 addition & 4 deletions lean_client/networking/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,7 @@ impl ChainMessage {
impl Display for ChainMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChainMessage::ProcessBlock {
signed_block,
..
} => {
ChainMessage::ProcessBlock { signed_block, .. } => {
write!(f, "ProcessBlock(slot={})", signed_block.block.slot.0)
}
ChainMessage::ProcessAttestation {
Expand Down
Loading
Loading