Skip to content

Commit ec7fe97

Browse files
authored
refactor: phase metadata service across full slot duration (#781)
Required for committee-based partial signature collection. The full list of attesting and sync committee validators must be available at slot start before produce_selection_proof is called, which was not possible with the previous single-phase metadata approach. - Refactor MetadataService to use a phased approach that spans the full slot duration, rather than computing all metadata at a single point - Split metadata into three distinct phases: VotingAssignments at slot start, VotingContext at 1/3 slot, and AggregationAssignments at 2/3 slot - Add event-driven coordination between MetadataService and DutiesService using watch channels to ensure duties are polled before reading them - Update lighthouse dependencies to a fork that exposes the new duties service polling events - Update produce_selection_proof and produce_sync_selection_proof to use committee-based partial signature collection after the Boole fork - These methods now request signatures from all committee operators rather than just the validator's operators - VotingAssignments provides the expected message count for committee-based batching - Update lighthouse dependencies to a fork that exposes the new duties service polling events Co-Authored-By: shane-moore <skm1790@gmail.com>
1 parent 00404c4 commit ec7fe97

8 files changed

Lines changed: 1055 additions & 101 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

anchor/client/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ impl Client {
542542
spec.clone(),
543543
genesis_validators_root,
544544
config.impostor.is_none().then_some(key),
545+
fork_schedule.clone(),
545546
config.gas_limit,
546547
config.builder_boost_factor,
547548
config.prefer_builder_proposals,

anchor/common/ssv_types/src/consensus.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use types::{
2828
ForkName, Hash256, Slot, SyncCommitteeContribution,
2929
};
3030

31-
use crate::{ValidatorIndex, message::*};
31+
use crate::{CommitteeId, ValidatorIndex, message::*, partial_sig::PartialSignatureKind};
3232
// UnsignedSSVMessage
3333
// ----------------------------------------------
3434
// | |
@@ -861,6 +861,39 @@ impl QbftData for BeaconVote {
861861
}
862862
}
863863

864+
/// Identifies a batch of pre-consensus selection proofs for a committee.
865+
/// All operators compute the same hash for a given `(slot, committee_id)` pair, ensuring
866+
/// consistent batching across the network.
867+
///
868+
/// Unlike `BeaconVote::hash()` which hashes decided consensus data, this hash is an
869+
/// artificial correlation identifier since pre-consensus selection proofs have different
870+
/// signing roots (attestation vs sync committee selection proofs use different domains).
871+
#[derive(Debug, Clone)]
872+
pub struct SelectionProofBatchId {
873+
pub slot: Slot,
874+
pub committee_id: CommitteeId,
875+
}
876+
877+
impl SelectionProofBatchId {
878+
pub fn new(slot: Slot, committee_id: CommitteeId) -> Self {
879+
Self { slot, committee_id }
880+
}
881+
882+
/// Compute deterministic hash for batching correlation.
883+
///
884+
/// The hash includes the SSZ encoding of `PartialSignatureKind::AggregatorCommitteePartialSig`
885+
/// as a domain separator to prevent collision with other hashes in the system.
886+
pub fn hash(&self) -> Hash256 {
887+
let mut hasher = Sha256::new();
888+
// Domain separator: SSZ encoding of the partial signature kind
889+
hasher.update(PartialSignatureKind::AggregatorCommitteePartialSig.as_ssz_bytes());
890+
hasher.update(self.slot.as_u64().to_le_bytes());
891+
hasher.update(self.committee_id.0);
892+
893+
Hash256::from_slice(&hasher.finalize())
894+
}
895+
}
896+
864897
pub struct BeaconVoteValidator<E: EthSpec> {
865898
slot: Slot,
866899
// `None` if slashing protection is disabled via CLI.
@@ -1897,4 +1930,75 @@ mod tests {
18971930
err => panic!("Expected DifferentCheckpoint error, got: {:?}", err),
18981931
}
18991932
}
1933+
1934+
// ═══════════════════════════════════════════════════════════════════════════════
1935+
// SelectionProofBatchId Tests
1936+
// ═══════════════════════════════════════════════════════════════════════════════
1937+
1938+
#[test]
1939+
fn test_selection_proof_batch_id_same_inputs_same_hash() {
1940+
let slot = Slot::new(12345);
1941+
let committee_id = CommitteeId::from([0u8; 32]);
1942+
1943+
// Compute hash twice with same inputs
1944+
let batch_id1 = SelectionProofBatchId::new(slot, committee_id);
1945+
let batch_id2 = SelectionProofBatchId::new(slot, committee_id);
1946+
1947+
// Same inputs should produce same hash
1948+
assert_eq!(batch_id1.hash(), batch_id2.hash());
1949+
}
1950+
1951+
#[test]
1952+
fn test_selection_proof_batch_id_different_slots() {
1953+
let committee_id = CommitteeId::from([1u8; 32]);
1954+
1955+
// Different slots
1956+
let slot1 = Slot::new(12345);
1957+
let slot2 = Slot::new(12346);
1958+
1959+
let batch_id1 = SelectionProofBatchId::new(slot1, committee_id);
1960+
let batch_id2 = SelectionProofBatchId::new(slot2, committee_id);
1961+
1962+
// Different slots should produce different hashes
1963+
assert_ne!(batch_id1.hash(), batch_id2.hash());
1964+
}
1965+
1966+
#[test]
1967+
fn test_selection_proof_batch_id_different_committees() {
1968+
let slot = Slot::new(12345);
1969+
1970+
// Different committee IDs
1971+
let committee_id1 = CommitteeId::from([1u8; 32]);
1972+
let committee_id2 = CommitteeId::from([2u8; 32]);
1973+
1974+
let batch_id1 = SelectionProofBatchId::new(slot, committee_id1);
1975+
let batch_id2 = SelectionProofBatchId::new(slot, committee_id2);
1976+
1977+
// Different committees should produce different hashes
1978+
assert_ne!(batch_id1.hash(), batch_id2.hash());
1979+
}
1980+
1981+
#[test]
1982+
fn test_selection_proof_batch_id_deterministic_across_operators() {
1983+
// This test verifies that the hash is deterministic and identical
1984+
// across different operators for the same inputs
1985+
1986+
let slot = Slot::new(42);
1987+
let committee_bytes = [
1988+
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
1989+
0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x10, 0x20, 0x30, 0x40,
1990+
0x50, 0x60, 0x70, 0x80,
1991+
];
1992+
let committee_id = CommitteeId::from(committee_bytes);
1993+
1994+
let batch_id = SelectionProofBatchId::new(slot, committee_id);
1995+
let expected_hash = batch_id.hash();
1996+
1997+
// Simulate computing on different "operators" (same calculation repeated)
1998+
for _ in 0..3 {
1999+
let batch_id = SelectionProofBatchId::new(slot, committee_id);
2000+
// All operators should get the same hash
2001+
assert_eq!(batch_id.hash(), expected_hash);
2002+
}
2003+
}
19002004
}

anchor/qbft_manager/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ pub trait QbftDecidable: QbftData<Hash = Hash256> + Send + Sync + 'static {
311311
dashmap::Entry::Occupied(entry) => entry.get().clone(),
312312
dashmap::Entry::Vacant(entry) => {
313313
// There is not an instance running yet, store the sender and spawn a new instance
314-
// with the reeiver
314+
// with the receiver
315315
let (tx, rx) = mpsc::unbounded_channel();
316316
let span = debug_span!("qbft_instance", instance_id = ?entry.key());
317317
let tx = entry.insert(tx);

anchor/validator_store/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ bls = { workspace = true }
1010
database = { workspace = true }
1111
eth2 = { workspace = true }
1212
ethereum_ssz = { workspace = true }
13+
fork = { workspace = true }
1314
futures = { workspace = true }
1415
hex = { workspace = true }
1516
lru = { workspace = true }

0 commit comments

Comments
 (0)