diff --git a/Cargo.lock b/Cargo.lock index d63819fb..6872e2b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2468,12 +2468,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "dyn-eq" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388" - [[package]] name = "ecdsa" version = "0.16.9" @@ -5288,8 +5282,6 @@ dependencies = [ "chrono", "clap", "crossbeam", - "dyn-clone", - "dyn-eq", "ethereum_ssz", "ethereum_ssz_derive", "futures", diff --git a/Cargo.toml b/Cargo.toml index 3040902d..74f593e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,6 @@ cancellation = "0.1.0" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.5", features = ["derive", "env", "cargo"] } crossbeam = "0.8.4" -dyn-clone = "1.0" -dyn-eq = "0.1.3" either = "1.13" eventsource-stream = "0.2" futures = "0.3" diff --git a/crates/app/src/node/wire.rs b/crates/app/src/node/wire.rs index d35bb45f..13632a10 100644 --- a/crates/app/src/node/wire.rs +++ b/crates/app/src/node/wire.rs @@ -550,7 +550,7 @@ pub async fn wire_core_workflow( Arc::new(move |duty: Duty, pubkey: PubKey| { let aggsigdb = aggsigdb.clone(); Box::pin(async move { - let signed: Box = aggsigdb.wait_for(duty, pubkey).await?; + let signed: SignedData = aggsigdb.wait_for(duty, pubkey).await?; Ok(signed) }) }) diff --git a/crates/app/tests/wiring.rs b/crates/app/tests/wiring.rs index b94d44da..96b6d631 100644 --- a/crates/app/tests/wiring.rs +++ b/crates/app/tests/wiring.rs @@ -324,8 +324,7 @@ async fn wiring_exercises_fetcher_back_edges() { // Store the RANDAO into the *same* wired AggSigDB; the back-edge must // unblock. let randao: phase0::BLSSignature = [7u8; 96]; - let randao_set: SignedDataSet = - HashMap::from([(pubkey, Box::new(randao) as Box)]); + let randao_set: SignedDataSet = HashMap::from([(pubkey, SignedData::from(randao))]); tokio::time::timeout( GUARD, aggsigdb.store(Duty::new_randao_duty(SlotNumber::new(SLOT)), randao_set), diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8feede2e..db1ff2ed 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -15,8 +15,6 @@ axum.workspace = true cancellation.workspace = true chrono.workspace = true crossbeam.workspace = true -dyn-clone.workspace = true -dyn-eq.workspace = true futures.workspace = true hex.workspace = true vise.workspace = true diff --git a/crates/core/src/aggsigdb/memory.rs b/crates/core/src/aggsigdb/memory.rs index c414a803..398364e1 100644 --- a/crates/core/src/aggsigdb/memory.rs +++ b/crates/core/src/aggsigdb/memory.rs @@ -7,11 +7,10 @@ use std::collections::{HashMap, hash_map::Entry}; use tokio::sync; use tokio_util::sync::CancellationToken; -type Waiters = - HashMap<(types::Duty, types::PubKey), Vec>>>; +type Waiters = HashMap<(types::Duty, types::PubKey), Vec>>; struct MemoryDBActor { - entries: HashMap>>, + entries: HashMap>, waiters: Waiters, deadliner: deadline::DeadlinerHandle, } @@ -101,11 +100,7 @@ impl MemoryDBActor { Ok(()) } - fn get( - &self, - duty: &types::Duty, - pub_key: &types::PubKey, - ) -> Option> { + fn get(&self, duty: &types::Duty, pub_key: &types::PubKey) -> Option { self.entries .get(duty) .and_then(|for_duty| for_duty.get(pub_key)) @@ -134,7 +129,7 @@ enum Message { WaitFor { duty: types::Duty, pub_key: types::PubKey, - response: sync::oneshot::Sender>, + response: sync::oneshot::Sender, }, } @@ -186,7 +181,7 @@ impl AggSigDB for MemoryDBHandle { &self, duty: types::Duty, pub_key: types::PubKey, - ) -> Result, Error> { + ) -> Result { let (response_tx, response_rx) = sync::oneshot::channel(); let msg = Message::WaitFor { duty, @@ -206,48 +201,25 @@ mod tests { types::{AggSigDB, Error}, }, deadline, - signeddata::SignedDataError, - types::{Duty, PubKey, Signature, SignedData, SignedDataSet, SlotNumber}, + signeddata::MockSignedData, + types::{Duty, PubKey, SignedData, SignedDataSet, SlotNumber}, }; - use pluto_ssz::HashRoot; use tokio::sync; use tokio_util::sync::CancellationToken; - /// Some mock signed data type for testing. - #[derive(Debug, Clone, PartialEq, Eq)] - struct MockSignedData(u8); - - impl SignedData for MockSignedData { - fn signature(&self) -> Result { - Ok([self.0; 96]) - } - - fn set_signature(&self, _signature: Signature) -> Result { - Ok(self.clone()) - } - - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { - Ok([self.0; 32]) - } + /// Builds mock signed data whose signature and message root are derived + /// from `byte`, so distinct values are distinguishable. + fn mock_signed_data(byte: u8) -> SignedData { + MockSignedData::new([byte; 96]) + .with_message_root([byte; 32]) + .into() } - impl MockSignedData { - fn singleton(&self, pub_key: PubKey) -> SignedDataSet { - let mut set = SignedDataSet::new(); - set.insert(pub_key, self.boxed()); - set - } - - fn boxed(&self) -> Box { - Box::new(self.clone()) - } + /// Wraps mock signed data for `pub_key` in a single-entry set. + fn singleton(pub_key: PubKey, data: SignedData) -> SignedDataSet { + let mut set = SignedDataSet::new(); + set.insert(pub_key, data); + set } /// Create a test deadline handle and an expiration channel. @@ -269,15 +241,15 @@ mod tests { let duty = Duty::new_proposer_duty(SlotNumber::new(10)); let pub_key = PubKey::new([7u8; 48]); - let signed_data = MockSignedData(42); + let signed_data = mock_signed_data(42); store - .store(duty.clone(), signed_data.singleton(pub_key)) + .store(duty.clone(), singleton(pub_key, signed_data.clone())) .await .unwrap(); let result = store.wait_for(duty, pub_key).await.unwrap(); - assert_eq!(result, signed_data.boxed()); + assert_eq!(result, signed_data.clone()); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -287,7 +259,7 @@ mod tests { let duty = Duty::new_attester_duty(SlotNumber::new(1)); let pub_key = PubKey::new([7u8; 48]); - let signed_data = MockSignedData(0); + let signed_data = mock_signed_data(0); let reader = { let store = store.clone(); @@ -302,11 +274,13 @@ mod tests { tokio::task::yield_now().await; assert!(!reader.is_finished(), "wait_for should block until store"); - let write = store.store(duty, signed_data.singleton(pub_key)).await; + let write = store + .store(duty, singleton(pub_key, signed_data.clone())) + .await; let read = reader.await.unwrap().unwrap(); assert!(write.is_ok()); - assert_eq!(read, signed_data.boxed()); + assert_eq!(read, signed_data.clone()); } #[tokio::test] @@ -318,11 +292,13 @@ mod tests { let duty = Duty::new_proposer_duty(SlotNumber::new(10)); let pub_key = PubKey::new([7u8; 48]); - let signed_data = MockSignedData(42); + let signed_data = mock_signed_data(42); ct.cancel(); - let res = store.store(duty, signed_data.singleton(pub_key)).await; + let res = store + .store(duty, singleton(pub_key, signed_data.clone())) + .await; assert!(matches!(res, Err(Error::Terminated))); } @@ -333,16 +309,16 @@ mod tests { let duty = Duty::new_proposer_duty(SlotNumber::new(10)); let pub_key = PubKey::new([7u8; 48]); - let first = MockSignedData(1); - let second = MockSignedData(2); + let first = mock_signed_data(1); + let second = mock_signed_data(2); store - .store(duty.clone(), first.singleton(pub_key)) + .store(duty.clone(), singleton(pub_key, first.clone())) .await .unwrap(); let err = store - .store(duty, second.singleton(pub_key)) + .store(duty, singleton(pub_key, second.clone())) .await .expect_err("storing mismatching data should fail"); assert!(matches!(err, super::Error::MismatchingData)); @@ -355,19 +331,19 @@ mod tests { let duty = Duty::new_proposer_duty(SlotNumber::new(10)); let pub_key = PubKey::new([7u8; 48]); - let signed_data = MockSignedData(42); + let signed_data = mock_signed_data(42); store - .store(duty.clone(), signed_data.singleton(pub_key)) + .store(duty.clone(), singleton(pub_key, signed_data.clone())) .await .unwrap(); store - .store(duty.clone(), signed_data.singleton(pub_key)) + .store(duty.clone(), singleton(pub_key, signed_data.clone())) .await .unwrap(); let result = store.wait_for(duty, pub_key).await.unwrap(); - assert_eq!(result, signed_data.boxed()); + assert_eq!(result, signed_data.clone()); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -378,11 +354,11 @@ mod tests { let duty = Duty::new_attester_duty(SlotNumber::new(1)); let pub_key = PubKey::new([7u8; 48]); - let first = MockSignedData(1); - let second = MockSignedData(2); + let first = mock_signed_data(1); + let second = mock_signed_data(2); store - .store(duty.clone(), first.singleton(pub_key)) + .store(duty.clone(), singleton(pub_key, first.clone())) .await .unwrap(); @@ -392,7 +368,7 @@ mod tests { { let dummy = Duty::new_attester_duty(SlotNumber::new(u64::MAX)); store - .store(dummy, MockSignedData(0).singleton(pub_key)) + .store(dummy, singleton(pub_key, mock_signed_data(0))) .await .unwrap(); } @@ -411,11 +387,14 @@ mod tests { // Store new data for the same duty and pubkey. The reader should wake // up and return the new data, not the evicted data. - store.store(duty, second.singleton(pub_key)).await.unwrap(); + store + .store(duty, singleton(pub_key, second.clone())) + .await + .unwrap(); let read = reader.await.unwrap().unwrap(); - assert_eq!(read, second.boxed()); - assert_ne!(read, first.boxed()); + assert_eq!(read, second.clone()); + assert_ne!(read, first.clone()); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] @@ -426,7 +405,7 @@ mod tests { let store = MemoryDBHandle::new(deadliner, expiration_rx, CancellationToken::new()); let duty = Duty::new_proposer_duty(SlotNumber::new(10)); let pub_key = PubKey::new([7u8; 48]); - let signed_data = MockSignedData(42); + let signed_data = mock_signed_data(42); let readers: Vec<_> = (0..N) .map(|_| { @@ -447,13 +426,13 @@ mod tests { // A single store unblocks all readers. store - .store(duty, signed_data.singleton(pub_key)) + .store(duty, singleton(pub_key, signed_data.clone())) .await .unwrap(); for reader in readers { let read = reader.await.unwrap().unwrap(); - assert_eq!(read, signed_data.boxed()); + assert_eq!(read, signed_data.clone()); } } @@ -463,10 +442,10 @@ mod tests { let store = MemoryDBHandle::new(deadliner, expiration_rx, CancellationToken::new()); let duty_a = Duty::new_proposer_duty(SlotNumber::new(10)); - let data_a = MockSignedData(1); + let data_a = mock_signed_data(1); let duty_b = Duty::new_attester_duty(SlotNumber::new(20)); - let data_b = MockSignedData(2); + let data_b = mock_signed_data(2); let pub_key = PubKey::new([7u8; 48]); @@ -481,7 +460,7 @@ mod tests { // Storing an unrelated key does not affect readers. store - .store(duty_b, data_b.singleton(pub_key)) + .store(duty_b, singleton(pub_key, data_b.clone())) .await .unwrap(); @@ -493,12 +472,12 @@ mod tests { // Storing the actual key unblocks the reader. store - .store(duty_a, data_a.singleton(pub_key)) + .store(duty_a, singleton(pub_key, data_a.clone())) .await .unwrap(); let read = reader.await.unwrap().unwrap(); - assert_eq!(read, data_a.boxed()); - assert_ne!(read, data_b.boxed()); + assert_eq!(read, data_a.clone()); + assert_ne!(read, data_b.clone()); } } diff --git a/crates/core/src/aggsigdb/types.rs b/crates/core/src/aggsigdb/types.rs index 8eef10e5..27c3753b 100644 --- a/crates/core/src/aggsigdb/types.rs +++ b/crates/core/src/aggsigdb/types.rs @@ -34,5 +34,5 @@ pub trait AggSigDB { &self, duty: types::Duty, pub_key: types::PubKey, - ) -> Result, Error>; + ) -> Result; } diff --git a/crates/core/src/bcast/mod.rs b/crates/core/src/bcast/mod.rs index f5f1f14e..49be2cd1 100644 --- a/crates/core/src/bcast/mod.rs +++ b/crates/core/src/bcast/mod.rs @@ -3,8 +3,6 @@ mod metrics; mod recast; -use std::any::Any; - use chrono::{DateTime, Duration, Utc}; use pluto_crypto::tbls; use pluto_eth2api::{ @@ -18,14 +16,7 @@ use tree_hash::TreeHash; pub use recast::Recaster; -use crate::{ - signeddata::{ - SignedSyncContributionAndProof, SignedSyncMessage, SignedVoluntaryExit, - VersionedAttestation, VersionedSignedAggregateAndProof, VersionedSignedProposal, - VersionedSignedValidatorRegistration, - }, - types::{Duty, DutyType, PubKey, SignedData, SignedDataSet}, -}; +use crate::types::{Duty, DutyType, PubKey, SignedData, SignedDataSet}; /// Broadcaster result. pub type Result = std::result::Result; @@ -330,15 +321,19 @@ impl Broadcaster { /// submit via the blinded endpoint, otherwise submit the full proposal. async fn broadcast_proposer(&self, duty: &Duty, set: &SignedDataSet) -> Result<()> { let (pubkey, agg_data) = set_to_one(set)?; - let block = - downcast_signed_data::(agg_data, Error::InvalidProposal)?; + let SignedData::VersionedSignedProposal(block) = agg_data else { + return Err(Error::InvalidProposal); + }; let blinded = block.0.blinded; if blinded { - let proposal = block.to_blinded().map_err(|source| Error::SignedData { - context: "cannot broadcast, expected blinded proposal", - source, - })?; + let proposal = block + .clone() + .to_blinded() + .map_err(|source| Error::SignedData { + context: "cannot broadcast, expected blinded proposal", + source, + })?; self.client .publish_blinded_block_v2(&proposal, None) .await?; @@ -478,15 +473,7 @@ impl Broadcaster { } } -fn downcast_signed_data(data: &dyn SignedData, error: Error) -> Result -where - T: SignedData + Clone + 'static, -{ - let any = data as &dyn Any; - any.downcast_ref::().cloned().ok_or(error) -} - -fn set_to_one(set: &SignedDataSet) -> Result<(PubKey, &dyn SignedData)> { +fn set_to_one(set: &SignedDataSet) -> Result<(PubKey, &SignedData)> { if set.len() != 1 { return Err(Error::ExpectedOneItemInSet); } @@ -495,20 +482,18 @@ fn set_to_one(set: &SignedDataSet) -> Result<(PubKey, &dyn SignedData)> { unreachable!("set length checked") }; - Ok((*pubkey, data.as_ref())) + Ok((*pubkey, data)) } -fn set_values_to(set: &SignedDataSet, error: E, map: M) -> Result> +/// Maps every entry of the set through `map`, which selects the payload the +/// duty expects; entries of any other [`SignedData`] variant yield `error()`. +fn set_values_to(set: &SignedDataSet, error: E, map: M) -> Result> where - T: SignedData + Clone + 'static, E: Fn() -> Error, - M: Fn(T) -> U, + M: Fn(&SignedData) -> Option, { set.values() - .map(|data| { - let value = downcast_signed_data::(data.as_ref(), error())?; - Ok(map(value)) - }) + .map(|data| map(data).ok_or_else(&error)) .collect() } @@ -516,7 +501,10 @@ fn set_to_attestations(set: &SignedDataSet) -> Result Some(attestation.0.clone()), + _ => None, + }, ) } @@ -526,15 +514,20 @@ fn set_to_registrations( set_values_to( set, || Error::InvalidRegistration, - |registration: VersionedSignedValidatorRegistration| registration.0, + |data| match data { + SignedData::VersionedSignedValidatorRegistration(registration) => { + Some(registration.0.clone()) + } + _ => None, + }, ) } fn set_to_exits(set: &SignedDataSet) -> Result> { set.iter() - .map(|(pubkey, data)| { - downcast_signed_data::(data.as_ref(), Error::InvalidExit) - .map(|exit| (*pubkey, exit.0)) + .map(|(pubkey, data)| match data { + SignedData::SignedVoluntaryExit(exit) => Ok((*pubkey, exit.0.clone())), + _ => Err(Error::InvalidExit), }) .collect() } @@ -545,7 +538,12 @@ fn set_to_agg_and_proof( set_values_to( set, || Error::InvalidAggregateAndProof, - |aggregate_and_proof: VersionedSignedAggregateAndProof| aggregate_and_proof.0, + |data| match data { + SignedData::VersionedSignedAggregateAndProof(aggregate_and_proof) => { + Some(aggregate_and_proof.0.clone()) + } + _ => None, + }, ) } @@ -553,7 +551,10 @@ fn set_to_sync_messages(set: &SignedDataSet) -> Result Some(message.0.clone()), + _ => None, + }, ) } @@ -563,7 +564,12 @@ fn set_to_sync_contributions( set_values_to( set, || Error::InvalidSyncCommitteeContribution, - |contribution: SignedSyncContributionAndProof| contribution.0, + |data| match data { + SignedData::SignedSyncContributionAndProof(contribution) => { + Some(contribution.0.clone()) + } + _ => None, + }, ) } @@ -771,8 +777,8 @@ mod tests { PubKey::from([byte; 48]) } - fn signed_set(pubkey: PubKey, data: impl SignedData + 'static) -> SignedDataSet { - HashMap::from([(pubkey, Box::new(data) as Box)]) + fn signed_set(pubkey: PubKey, data: impl Into) -> SignedDataSet { + HashMap::from([(pubkey, data.into())]) } fn hex0x(bytes: impl AsRef<[u8]>) -> String { diff --git a/crates/core/src/bcast/recast.rs b/crates/core/src/bcast/recast.rs index 889d01b5..b3bb56df 100644 --- a/crates/core/src/bcast/recast.rs +++ b/crates/core/src/bcast/recast.rs @@ -21,7 +21,7 @@ type RecastSubscriber = Arc RecastFuture + Send + #[derive(Clone)] struct RecastTuple { duty: Duty, - agg_data: Box, + agg_data: SignedData, } #[derive(Default)] @@ -66,13 +66,13 @@ impl Recaster { } for (pubkey, agg_data) in set { - self.store_one(duty.clone(), *pubkey, agg_data.as_ref())?; + self.store_one(duty.clone(), *pubkey, agg_data)?; } Ok(()) } - fn store_one(&self, duty: Duty, pubkey: PubKey, agg_data: &dyn SignedData) -> Result<()> { + fn store_one(&self, duty: Duty, pubkey: PubKey, agg_data: &SignedData) -> Result<()> { let mut state = self .state .lock() @@ -84,7 +84,7 @@ impl Recaster { return Ok(()); } - let agg_data = dyn_clone::clone_box(agg_data); + let agg_data = agg_data.clone(); state.tuples.insert(pubkey, RecastTuple { duty, agg_data }); instrument_recast_registration(pubkey); diff --git a/crates/core/src/eth2signeddata.rs b/crates/core/src/eth2signeddata.rs index 22e663ab..2c88b051 100644 --- a/crates/core/src/eth2signeddata.rs +++ b/crates/core/src/eth2signeddata.rs @@ -1,28 +1,27 @@ //! Eth2 signed-data verification. //! -//! Extends `SignedData` types that carry beacon-chain signatures with the -//! metadata needed to verify them: the signing `DomainName` and the signing -//! `Epoch`. `verify_eth2_signed_data` ties the two together with the -//! upstream beacon-node domain lookup and BLS verification. +//! Extends [`SignedData`](crate::signeddata::SignedData) variants that carry +//! beacon-chain signatures with the metadata needed to verify them: the signing +//! `DomainName` and the signing `Epoch`. `verify_eth2_signed_data` ties the two +//! together with the upstream beacon-node domain lookup and BLS verification. -use std::any::Any; - -use async_trait::async_trait; use pluto_crypto::types::PublicKey; use pluto_eth2api::{client::EthBeaconNodeApiClient, spec::phase0::Epoch}; use pluto_eth2util::{ helpers::{self, HelperError}, signing::{self, DomainName, SigningError}, }; +use pluto_ssz::HashRoot; use crate::{ signeddata::{ Attestation, BeaconCommitteeSelection, SignedAggregateAndProof, SignedDataError, SignedRandao, SignedSyncContributionAndProof, SignedSyncMessage, SignedVoluntaryExit, - SyncCommitteeSelection, VersionedAttestation, VersionedSignedAggregateAndProof, - VersionedSignedProposal, VersionedSignedValidatorRegistration, + SyncCommitteeSelection, SyncContributionAndProof, VersionedAttestation, + VersionedSignedAggregateAndProof, VersionedSignedProposal, + VersionedSignedValidatorRegistration, }, - types::SignedData, + types::{Signature, SignedData}, }; /// Error returned while resolving the signing epoch for, or verifying, an @@ -42,24 +41,205 @@ pub enum Eth2SignedDataError { Helper(#[from] HelperError), } -/// Signed duty data that carries an eth2 beacon-chain signature. +/// A [`SignedData`] payload that carries an eth2 beacon-chain signature — +/// the enum equivalent of Go's `core.Eth2SignedData` interface. +/// +/// Obtained from [`SignedData::as_eth2_signed_data`], the port of Go's +/// `data.(core.Eth2SignedData)` type assertion, so the variants below are +/// exactly the payloads with a beacon-chain signing domain. /// -/// The signing root is the payload's [`SignedData::message_root`] wrapped with -/// the domain identified by [`Self::domain_name`] at the epoch returned by +/// The signing root is the payload's [`Self::message_root`] wrapped with the +/// domain identified by [`Self::domain_name`] at the epoch returned by /// [`Self::epoch`]. -#[async_trait] -pub trait Eth2SignedData: SignedData { +#[derive(Debug, Clone, Copy)] +pub enum Eth2SignedData<'a> { + /// Signed beacon block proposal. + VersionedSignedProposal(&'a VersionedSignedProposal), + /// Non-versioned (phase0) attestation. + Attestation(&'a Attestation), + /// Versioned attestation. + VersionedAttestation(&'a VersionedAttestation), + /// Signed voluntary exit. + SignedVoluntaryExit(&'a SignedVoluntaryExit), + /// Signed validator registration. + VersionedSignedValidatorRegistration(&'a VersionedSignedValidatorRegistration), + /// Signed randao reveal. + SignedRandao(&'a SignedRandao), + /// Beacon committee selection proof. + BeaconCommitteeSelection(&'a BeaconCommitteeSelection), + /// Non-versioned (phase0) signed aggregate-and-proof. + SignedAggregateAndProof(&'a SignedAggregateAndProof), + /// Versioned signed aggregate-and-proof. + VersionedSignedAggregateAndProof(&'a VersionedSignedAggregateAndProof), + /// Signed sync committee message. + SignedSyncMessage(&'a SignedSyncMessage), + /// Signed sync contribution-and-proof. + SignedSyncContributionAndProof(&'a SignedSyncContributionAndProof), + /// Sync committee selection proof. + SyncCommitteeSelection(&'a SyncCommitteeSelection), + /// Sync contribution-and-proof (signed over its selection proof). + SyncContributionAndProof(&'a SyncContributionAndProof), +} + +impl Eth2SignedData<'_> { /// Returns the eth2 signing domain for this data. - fn domain_name(&self) -> DomainName; + pub fn domain_name(&self) -> DomainName { + match self { + Self::VersionedSignedProposal(_) => DomainName::BeaconProposer, + Self::Attestation(_) | Self::VersionedAttestation(_) => DomainName::BeaconAttester, + Self::SignedVoluntaryExit(_) => DomainName::VoluntaryExit, + Self::VersionedSignedValidatorRegistration(_) => DomainName::ApplicationBuilder, + Self::SignedRandao(_) => DomainName::Randao, + Self::BeaconCommitteeSelection(_) => DomainName::SelectionProof, + Self::SignedAggregateAndProof(_) | Self::VersionedSignedAggregateAndProof(_) => { + DomainName::AggregateAndProof + } + Self::SignedSyncMessage(_) => DomainName::SyncCommittee, + Self::SignedSyncContributionAndProof(_) => DomainName::ContributionAndProof, + Self::SyncCommitteeSelection(_) | Self::SyncContributionAndProof(_) => { + DomainName::SyncCommitteeSelectionProof + } + } + } /// Returns the epoch at which the signing domain is resolved. - async fn epoch(&self, client: &EthBeaconNodeApiClient) -> Result; + pub async fn epoch( + &self, + client: &EthBeaconNodeApiClient, + ) -> Result { + match self { + Self::VersionedSignedProposal(data) => { + if data.0.version == pluto_eth2api::versioned::DataVersion::Unknown { + return Err(SignedDataError::UnknownVersion.into()); + } + + Ok(helpers::epoch_from_slot(client, data.0.block.slot()).await?) + } + Self::Attestation(data) => Ok(data.0.data.target.epoch), + Self::VersionedAttestation(data) => { + let version = data.0.version; + if version == pluto_eth2api::versioned::DataVersion::Unknown { + return Err(SignedDataError::UnknownVersion.into()); + } + + let inner = data + .0 + .attestation + .as_ref() + .ok_or(SignedDataError::MissingAttestation(version))? + .data(); + + Ok(inner.target.epoch) + } + Self::SignedVoluntaryExit(data) => Ok(data.0.message.epoch), + // Always use epoch 0 for DomainApplicationBuilder. + Self::VersionedSignedValidatorRegistration(_) => Ok(0), + Self::SignedRandao(data) => Ok(data.0.epoch), + Self::BeaconCommitteeSelection(data) => { + Ok(helpers::epoch_from_slot(client, data.0.slot).await?) + } + Self::SignedAggregateAndProof(data) => { + Ok(helpers::epoch_from_slot(client, data.0.message.aggregate.data.slot).await?) + } + Self::VersionedSignedAggregateAndProof(data) => { + let slot = data.0.slot().ok_or(SignedDataError::UnknownVersion)?; + + Ok(helpers::epoch_from_slot(client, slot).await?) + } + Self::SignedSyncMessage(data) => { + Ok(helpers::epoch_from_slot(client, data.0.slot).await?) + } + Self::SignedSyncContributionAndProof(data) => { + Ok(helpers::epoch_from_slot(client, data.0.message.contribution.slot).await?) + } + Self::SyncCommitteeSelection(data) => { + Ok(helpers::epoch_from_slot(client, data.0.slot).await?) + } + Self::SyncContributionAndProof(data) => { + Ok(helpers::epoch_from_slot(client, data.0.contribution.slot).await?) + } + } + } + + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { + match self { + Self::VersionedSignedProposal(data) => data.signature(), + Self::Attestation(data) => data.signature(), + Self::VersionedAttestation(data) => data.signature(), + Self::SignedVoluntaryExit(data) => data.signature(), + Self::VersionedSignedValidatorRegistration(data) => data.signature(), + Self::SignedRandao(data) => data.signature(), + Self::BeaconCommitteeSelection(data) => data.signature(), + Self::SignedAggregateAndProof(data) => data.signature(), + Self::VersionedSignedAggregateAndProof(data) => data.signature(), + Self::SignedSyncMessage(data) => data.signature(), + Self::SignedSyncContributionAndProof(data) => data.signature(), + Self::SyncCommitteeSelection(data) => data.signature(), + Self::SyncContributionAndProof(data) => data.signature(), + } + } + + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { + match self { + Self::VersionedSignedProposal(data) => data.message_root(), + Self::Attestation(data) => data.message_root(), + Self::VersionedAttestation(data) => data.message_root(), + Self::SignedVoluntaryExit(data) => data.message_root(), + Self::VersionedSignedValidatorRegistration(data) => data.message_root(), + Self::SignedRandao(data) => data.message_root(), + Self::BeaconCommitteeSelection(data) => data.message_root(), + Self::SignedAggregateAndProof(data) => data.message_root(), + Self::VersionedSignedAggregateAndProof(data) => data.message_root(), + Self::SignedSyncMessage(data) => data.message_root(), + Self::SignedSyncContributionAndProof(data) => data.message_root(), + Self::SyncCommitteeSelection(data) => data.message_root(), + Self::SyncContributionAndProof(data) => data.message_root(), + } + } +} + +impl SignedData { + /// Views this payload as an [`Eth2SignedData`], mirroring Go's + /// `data.(core.Eth2SignedData)` type assertion. Returns `None` for + /// variants without a beacon-chain signing domain (e.g. a raw + /// [`Signature`]). + pub fn as_eth2_signed_data(&self) -> Option> { + Some(match self { + Self::Signature(_) => return None, + Self::VersionedSignedProposal(data) => Eth2SignedData::VersionedSignedProposal(data), + Self::Attestation(data) => Eth2SignedData::Attestation(data), + Self::VersionedAttestation(data) => Eth2SignedData::VersionedAttestation(data), + Self::SignedVoluntaryExit(data) => Eth2SignedData::SignedVoluntaryExit(data), + Self::VersionedSignedValidatorRegistration(data) => { + Eth2SignedData::VersionedSignedValidatorRegistration(data) + } + Self::SignedRandao(data) => Eth2SignedData::SignedRandao(data), + Self::BeaconCommitteeSelection(data) => Eth2SignedData::BeaconCommitteeSelection(data), + Self::SyncCommitteeSelection(data) => Eth2SignedData::SyncCommitteeSelection(data), + Self::SignedAggregateAndProof(data) => Eth2SignedData::SignedAggregateAndProof(data), + Self::VersionedSignedAggregateAndProof(data) => { + Eth2SignedData::VersionedSignedAggregateAndProof(data) + } + Self::SignedSyncMessage(data) => Eth2SignedData::SignedSyncMessage(data), + Self::SignedSyncContributionAndProof(data) => { + Eth2SignedData::SignedSyncContributionAndProof(data) + } + // Go's `SyncContributionAndProof` also carries `DomainName`/ + // `Epoch` (`charon/core/signeddata.go`), so its type assertion + // succeeds there too. + Self::SyncContributionAndProof(data) => Eth2SignedData::SyncContributionAndProof(data), + #[cfg(test)] + Self::Mock(_) => return None, + }) + } } /// Verifies the eth2 signature associated with the given [`Eth2SignedData`]. pub async fn verify_eth2_signed_data( client: &EthBeaconNodeApiClient, - data: &dyn Eth2SignedData, + data: Eth2SignedData<'_>, pubkey: &PublicKey, ) -> Result<(), Eth2SignedDataError> { let sig_root = data.message_root()?; @@ -79,205 +259,6 @@ pub async fn verify_eth2_signed_data( Ok(()) } -/// Attempts to view a [`SignedData`] as an [`Eth2SignedData`], mirroring Go's -/// `data.(core.Eth2SignedData)` type assertion. Returns `None` for signed-data -/// variants without a beacon-chain signing domain (e.g. raw [`Signature`]). -/// -/// [`Signature`]: crate::types::Signature -pub fn as_eth2_signed_data(data: &dyn SignedData) -> Option<&dyn Eth2SignedData> { - let any = data as &dyn Any; - - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - if let Some(v) = any.downcast_ref::() { - return Some(v); - } - - None -} - -#[async_trait] -impl Eth2SignedData for VersionedSignedProposal { - fn domain_name(&self) -> DomainName { - DomainName::BeaconProposer - } - - async fn epoch(&self, client: &EthBeaconNodeApiClient) -> Result { - if self.0.version == pluto_eth2api::versioned::DataVersion::Unknown { - return Err(SignedDataError::UnknownVersion.into()); - } - - Ok(helpers::epoch_from_slot(client, self.0.block.slot()).await?) - } -} - -#[async_trait] -impl Eth2SignedData for Attestation { - fn domain_name(&self) -> DomainName { - DomainName::BeaconAttester - } - - async fn epoch(&self, _client: &EthBeaconNodeApiClient) -> Result { - Ok(self.0.data.target.epoch) - } -} - -#[async_trait] -impl Eth2SignedData for VersionedAttestation { - fn domain_name(&self) -> DomainName { - DomainName::BeaconAttester - } - - async fn epoch(&self, _client: &EthBeaconNodeApiClient) -> Result { - let version = self.0.version; - if version == pluto_eth2api::versioned::DataVersion::Unknown { - return Err(SignedDataError::UnknownVersion.into()); - } - - let data = self - .0 - .attestation - .as_ref() - .ok_or(SignedDataError::MissingAttestation(version))? - .data(); - - Ok(data.target.epoch) - } -} - -#[async_trait] -impl Eth2SignedData for SignedVoluntaryExit { - fn domain_name(&self) -> DomainName { - DomainName::VoluntaryExit - } - - async fn epoch(&self, _client: &EthBeaconNodeApiClient) -> Result { - Ok(self.0.message.epoch) - } -} - -#[async_trait] -impl Eth2SignedData for VersionedSignedValidatorRegistration { - fn domain_name(&self) -> DomainName { - DomainName::ApplicationBuilder - } - - async fn epoch(&self, _client: &EthBeaconNodeApiClient) -> Result { - // Always use epoch 0 for DomainApplicationBuilder. - Ok(0) - } -} - -#[async_trait] -impl Eth2SignedData for SignedRandao { - fn domain_name(&self) -> DomainName { - DomainName::Randao - } - - async fn epoch(&self, _client: &EthBeaconNodeApiClient) -> Result { - Ok(self.0.epoch) - } -} - -#[async_trait] -impl Eth2SignedData for BeaconCommitteeSelection { - fn domain_name(&self) -> DomainName { - DomainName::SelectionProof - } - - async fn epoch(&self, client: &EthBeaconNodeApiClient) -> Result { - Ok(helpers::epoch_from_slot(client, self.0.slot).await?) - } -} - -#[async_trait] -impl Eth2SignedData for SignedAggregateAndProof { - fn domain_name(&self) -> DomainName { - DomainName::AggregateAndProof - } - - async fn epoch(&self, client: &EthBeaconNodeApiClient) -> Result { - Ok(helpers::epoch_from_slot(client, self.0.message.aggregate.data.slot).await?) - } -} - -#[async_trait] -impl Eth2SignedData for VersionedSignedAggregateAndProof { - fn domain_name(&self) -> DomainName { - DomainName::AggregateAndProof - } - - async fn epoch(&self, client: &EthBeaconNodeApiClient) -> Result { - let slot = self.0.slot().ok_or(SignedDataError::UnknownVersion)?; - - Ok(helpers::epoch_from_slot(client, slot).await?) - } -} - -#[async_trait] -impl Eth2SignedData for SignedSyncMessage { - fn domain_name(&self) -> DomainName { - DomainName::SyncCommittee - } - - async fn epoch(&self, client: &EthBeaconNodeApiClient) -> Result { - Ok(helpers::epoch_from_slot(client, self.0.slot).await?) - } -} - -#[async_trait] -impl Eth2SignedData for SignedSyncContributionAndProof { - fn domain_name(&self) -> DomainName { - DomainName::ContributionAndProof - } - - async fn epoch(&self, client: &EthBeaconNodeApiClient) -> Result { - Ok(helpers::epoch_from_slot(client, self.0.message.contribution.slot).await?) - } -} - -#[async_trait] -impl Eth2SignedData for SyncCommitteeSelection { - fn domain_name(&self) -> DomainName { - DomainName::SyncCommitteeSelectionProof - } - - async fn epoch(&self, client: &EthBeaconNodeApiClient) -> Result { - Ok(helpers::epoch_from_slot(client, self.0.slot).await?) - } -} - #[cfg(test)] mod tests { use std::{fs, path::PathBuf}; @@ -331,27 +312,30 @@ mod tests { /// Mirrors Go's `TestVerifyEth2SignedData`: resolve the epoch and message /// root, BLS-sign the signing-domain data root, inject the signature, and /// assert verification succeeds. - async fn assert_verifies(client: &EthBeaconNodeApiClient, data: T) - where - T: Eth2SignedData + Clone, - { - let epoch = data.epoch(client).await.unwrap(); - let root = data.message_root().unwrap(); + async fn assert_verifies(client: &EthBeaconNodeApiClient, data: impl Into) { + let data: SignedData = data.into(); + let eth2 = data.as_eth2_signed_data().expect("eth2 signed data"); + let epoch = eth2.epoch(client).await.unwrap(); + let root = eth2.message_root().unwrap(); let mut rng = rand::thread_rng(); let secret = tbls::generate_secret_key(&mut rng).unwrap(); let pubkey = tbls::secret_to_public_key(&secret).unwrap(); - let sig_data = signing::get_data_root(client, data.domain_name(), epoch, root) + let sig_data = signing::get_data_root(client, eth2.domain_name(), epoch, root) .await .unwrap(); let sig: Signature = tbls::sign(&secret, &sig_data).unwrap(); let signed = data.set_signature(sig).unwrap(); - verify_eth2_signed_data(client, &signed, &pubkey) - .await - .unwrap(); + verify_eth2_signed_data( + client, + signed.as_eth2_signed_data().expect("eth2 signed data"), + &pubkey, + ) + .await + .unwrap(); } #[tokio::test] @@ -457,25 +441,28 @@ mod tests { async fn verify_rejects_wrong_pubkey() { let mock = BeaconMock::builder().build().await.unwrap(); let client = mock.client(); - let data: SignedRandao = load("TestJSONSerialisation_SignedRandao.json.golden"); + let data: SignedData = + load::("TestJSONSerialisation_SignedRandao.json.golden").into(); + let eth2 = data.as_eth2_signed_data().unwrap(); - let epoch = data.epoch(client).await.unwrap(); - let root = data.message_root().unwrap(); + let epoch = eth2.epoch(client).await.unwrap(); + let root = eth2.message_root().unwrap(); let mut rng = rand::thread_rng(); let secret = tbls::generate_secret_key(&mut rng).unwrap(); let wrong_secret = tbls::generate_secret_key(&mut rng).unwrap(); let wrong_pubkey = tbls::secret_to_public_key(&wrong_secret).unwrap(); - let sig_data = signing::get_data_root(client, data.domain_name(), epoch, root) + let sig_data = signing::get_data_root(client, eth2.domain_name(), epoch, root) .await .unwrap(); let sig: Signature = tbls::sign(&secret, &sig_data).unwrap(); let signed = data.set_signature(sig).unwrap(); - let err = verify_eth2_signed_data(client, &signed, &wrong_pubkey) - .await - .unwrap_err(); + let err = + verify_eth2_signed_data(client, signed.as_eth2_signed_data().unwrap(), &wrong_pubkey) + .await + .unwrap_err(); assert!(matches!(err, Eth2SignedDataError::Signing(_))); } @@ -484,12 +471,13 @@ mod tests { async fn verify_rejects_zero_signature() { let mock = BeaconMock::builder().build().await.unwrap(); let client = mock.client(); - let data: SignedRandao = load("TestJSONSerialisation_SignedRandao.json.golden"); + let data: SignedData = + load::("TestJSONSerialisation_SignedRandao.json.golden").into(); let pubkey = [0x11; 48]; let signed = data.set_signature([0; SIGNATURE_LENGTH]).unwrap(); - let err = verify_eth2_signed_data(client, &signed, &pubkey) + let err = verify_eth2_signed_data(client, signed.as_eth2_signed_data().unwrap(), &pubkey) .await .unwrap_err(); @@ -503,9 +491,14 @@ mod tests { fn registration_always_uses_epoch_zero() { // VersionedSignedValidatorRegistration uses DomainApplicationBuilder, // which is fixed at epoch 0 regardless of the beacon client. - let data: VersionedSignedValidatorRegistration = - load("VersionedSignedValidatorRegistration.v1.json"); - assert_eq!(data.domain_name(), DomainName::ApplicationBuilder); + let data: SignedData = load::( + "VersionedSignedValidatorRegistration.v1.json", + ) + .into(); + assert_eq!( + data.as_eth2_signed_data().unwrap().domain_name(), + DomainName::ApplicationBuilder + ); } #[test] @@ -513,11 +506,11 @@ mod tests { let randao: SignedRandao = load("TestJSONSerialisation_SignedRandao.json.golden"); // A typed payload is viewable as Eth2SignedData... - let boxed: Box = Box::new(randao); - assert!(as_eth2_signed_data(boxed.as_ref()).is_some()); + let data = SignedData::from(randao); + assert!(data.as_eth2_signed_data().is_some()); // ...while a raw signature is not. - let sig: Box = Box::new([0u8; SIGNATURE_LENGTH] as Signature); - assert!(as_eth2_signed_data(sig.as_ref()).is_none()); + let sig = SignedData::from([0u8; SIGNATURE_LENGTH] as Signature); + assert!(sig.as_eth2_signed_data().is_none()); } } diff --git a/crates/core/src/fetcher/mod.rs b/crates/core/src/fetcher/mod.rs index 15a7fe60..be7ac2d7 100644 --- a/crates/core/src/fetcher/mod.rs +++ b/crates/core/src/fetcher/mod.rs @@ -19,8 +19,7 @@ use tree_hash::TreeHash; use crate::{ signeddata::{ - AttestationData, BeaconCommitteeSelection, ProposalBlock, SignedDataError, - SignedSyncMessage, SyncCommitteeSelection, SyncContribution, + AttestationData, ProposalBlock, SignedDataError, SyncContribution, VersionedAggregatedAttestation, VersionedProposal, }, types::{Duty, DutyDefinition, DutyDefinitionSet, DutyType, PubKey, SignedData}, @@ -37,8 +36,7 @@ type CallbackFuture = Pin CallbackFuture<()> + Send + Sync>; /// AggSigDB callback: resolves aggregated signed data for a duty/pubkey. -pub type AggSigDbFunc = - Arc CallbackFuture> + Send + Sync>; +pub type AggSigDbFunc = Arc CallbackFuture + Send + Sync>; /// DutyDB callback: resolves attestation data for a `(slot, committee index)`. pub type AwaitAttDataFunc = @@ -263,8 +261,9 @@ impl Fetcher { let prep_agg_data = self .query_agg_sig_db(Duty::new_prepare_aggregator_duty(slot.into()), *pubkey) .await?; - let selection = downcast::(prep_agg_data.as_ref()) - .ok_or(FetcherError::InvalidBeaconCommitteeSelection)?; + let SignedData::BeaconCommitteeSelection(selection) = &prep_agg_data else { + return Err(FetcherError::InvalidBeaconCommitteeSelection); + }; let is_aggregator = eth2exp::is_att_aggregator( &self.eth2_cl, @@ -374,8 +373,9 @@ impl Fetcher { *pubkey, ) .await?; - let selection = downcast::(selection_data.as_ref()) - .ok_or(FetcherError::InvalidSyncCommitteeSelection)?; + let SignedData::SyncCommitteeSelection(selection) = &selection_data else { + return Err(FetcherError::InvalidSyncCommitteeSelection); + }; let subcomm_idx = selection.0.subcommittee_index; @@ -392,8 +392,9 @@ impl Fetcher { let sync_msg_data = self .query_agg_sig_db(Duty::new_sync_message_duty(slot.into()), *pubkey) .await?; - let msg = downcast::(sync_msg_data.as_ref()) - .ok_or(FetcherError::InvalidSyncCommitteeMessage)?; + let SignedData::SignedSyncMessage(msg) = &sync_msg_data else { + return Err(FetcherError::InvalidSyncCommitteeMessage); + }; let block_root = msg.0.beacon_block_root; @@ -463,7 +464,7 @@ impl Fetcher { } /// Invokes the AggSigDB resolver. - async fn query_agg_sig_db(&self, duty: Duty, pubkey: PubKey) -> Result> { + async fn query_agg_sig_db(&self, duty: Duty, pubkey: PubKey) -> Result { (self.agg_sig_db)(duty, pubkey) .await .map_err(FetcherError::Callback) @@ -486,11 +487,6 @@ fn wrap(context: &'static str) -> impl Fn(FetcherError) -> FetcherError { } } -/// Downcasts a `&dyn SignedData` to a concrete signed-data type. -fn downcast(data: &dyn SignedData) -> Option<&T> { - (data as &dyn std::any::Any).downcast_ref::() -} - /// Logs a warning when the fee recipient is not correctly populated in the /// proposal. Fee recipient is unavailable in forks earlier than Bellatrix. fn verify_fee_recipient(proposal: &VersionedProposal, fee_recipient_address: &ExecutionAddress) { @@ -596,7 +592,10 @@ mod tests { use pluto_testutil::BeaconMock; use super::*; - use crate::types::SlotNumber; + use crate::{ + signeddata::{BeaconCommitteeSelection, SignedSyncMessage, SyncCommitteeSelection}, + types::SlotNumber, + }; /// 48-byte BLS public key length used to build distinct test pubkeys. const PK_LEN: usize = 48; @@ -914,7 +913,7 @@ mod tests { let agg_sig_db: AggSigDbFunc = Arc::new(move |_duty: Duty, pubkey: PubKey| { let sig = randaos[&pubkey]; Box::pin(async move { - let data: Box = Box::new(sig); + let data = SignedData::from(sig); Ok(data) }) }); @@ -1143,7 +1142,7 @@ mod tests { validator_index: 0, selection_proof: [0u8; 96], }); - let data: Box = Box::new(selection); + let data = SignedData::from(selection); Ok(data) }) }); @@ -1477,9 +1476,9 @@ mod tests { let sels = sels.clone(); let msgs = msgs.clone(); Box::pin(async move { - let data: Box = match duty.duty_type { - DutyType::PrepareSyncContribution => Box::new(sels[&pubkey].clone()), - DutyType::SyncMessage => Box::new(msgs[&pubkey].clone()), + let data: SignedData = match duty.duty_type { + DutyType::PrepareSyncContribution => sels[&pubkey].clone().into(), + DutyType::SyncMessage => msgs[&pubkey].clone().into(), _ => return Err("unsupported duty".into()), }; Ok(data) @@ -1552,23 +1551,23 @@ mod tests { let agg_sig_db: AggSigDbFunc = Arc::new(move |duty: Duty, _pubkey: PubKey| { Box::pin(async move { - let data: Box = match duty.duty_type { + let data: SignedData = match duty.duty_type { DutyType::PrepareSyncContribution => { - Box::new(SyncCommitteeSelection::new(v1::SyncCommitteeSelection { + SyncCommitteeSelection::new(v1::SyncCommitteeSelection { slot: SLOT, validator_index: 2, subcommittee_index: 4, selection_proof: bls_sig(SYNC_AGG_SIG_A), - })) - } - DutyType::SyncMessage => { - Box::new(SignedSyncMessage::new(altair::SyncCommitteeMessage { - slot: SLOT, - beacon_block_root: [10u8; 32], - validator_index: 2, - signature: [0u8; 96], - })) + }) + .into() } + DutyType::SyncMessage => SignedSyncMessage::new(altair::SyncCommitteeMessage { + slot: SLOT, + beacon_block_root: [10u8; 32], + validator_index: 2, + signature: [0u8; 96], + }) + .into(), _ => return Err("unsupported duty".into()), }; Ok(data) @@ -1634,7 +1633,7 @@ mod tests { subcommittee_index: 0, selection_proof: bls_sig(SYNC_NON_AGG_SIG), }); - let data: Box = Box::new(selection); + let data = SignedData::from(selection); return Ok(data); } Err("unsupported duty".into()) diff --git a/crates/core/src/parsigex_codec.rs b/crates/core/src/parsigex_codec.rs index 5d258c42..c4bd8350 100644 --- a/crates/core/src/parsigex_codec.rs +++ b/crates/core/src/parsigex_codec.rs @@ -7,8 +7,6 @@ //! JSON (a `{` prefix) — matching charon's `unmarshal` (`core/proto.go`). The //! `{` prefix is never used to skip SSZ, since valid SSZ can begin with `0x7B`. -use std::any::Any; - use base64::Engine as _; use crate::{ @@ -83,84 +81,51 @@ fn serialize_signature(sig: &Signature) -> Result, ParSigExCodecError> { Ok(serde_json::to_vec(&encoded)?) } -fn deserialize_signature(bytes: &[u8]) -> Result, ParSigExCodecError> { +fn deserialize_signature(bytes: &[u8]) -> Result { let encoded: String = serde_json::from_slice(bytes)?; let raw = base64::engine::general_purpose::STANDARD .decode(encoded) .map_err(|e| ParSigExCodecError::SignedData(format!("invalid base64: {e}")))?; let sig: Signature = pluto_crypto::types::signature_from_bytes(&raw) .map_err(|e| ParSigExCodecError::InvalidSignature(e.to_string()))?; - Ok(Box::new(sig)) + Ok(SignedData::Signature(sig)) } -pub(crate) fn serialize_signed_data(data: &dyn SignedData) -> Result, ParSigExCodecError> { - let any = data as &dyn Any; - - // --------------------------------------------------------------- - // SSZ-capable types — encode as SSZ binary (matching Go `marshal`) - // --------------------------------------------------------------- - - // phase0::Attestation (non-versioned, raw SSZ) - if let Some(value) = any.downcast_ref::() { - return Ok(ssz_codec::encode_phase0_attestation(&value.0)?); - } - - // VersionedAttestation (versioned header + inner SSZ) - if let Some(value) = any.downcast_ref::() { - return Ok(ssz_codec::encode_versioned_attestation(&value.0)?); - } - - // phase0::SignedAggregateAndProof (non-versioned, raw SSZ) - if let Some(value) = any.downcast_ref::() { - return Ok(ssz_codec::encode_phase0_signed_aggregate_and_proof( - &value.0, - )?); - } - - // VersionedSignedAggregateAndProof (versioned header + inner SSZ) - if let Some(value) = any.downcast_ref::() { - return Ok(ssz_codec::encode_versioned_signed_aggregate_and_proof( - &value.0, - )?); - } - - // altair::SyncCommitteeMessage (non-versioned, all fixed) - if let Some(value) = any.downcast_ref::() { - return Ok(ssz_codec::encode_sync_committee_message(&value.0)?); - } - - // altair::SignedContributionAndProof (non-versioned, all fixed) - if let Some(value) = any.downcast_ref::() { - return Ok(ssz_codec::encode_signed_contribution_and_proof(&value.0)?); - } - - // --------------------------------------------------------------- - // JSON-only types - // --------------------------------------------------------------- - - macro_rules! serialize_json { - ($ty:ty) => { - if let Some(value) = any.downcast_ref::<$ty>() { - return Ok(serde_json::to_vec(value)?); - } - }; - } - - // VersionedSignedProposal (versioned header + inner SSZ) - if let Some(value) = any.downcast_ref::() { - return Ok(ssz_codec::encode_versioned_signed_proposal(&value.0)?); - } +pub(crate) fn serialize_signed_data(data: &SignedData) -> Result, ParSigExCodecError> { + match data { + SignedData::Attestation(value) => Ok(ssz_codec::encode_phase0_attestation(&value.0)?), + SignedData::VersionedAttestation(value) => { + Ok(ssz_codec::encode_versioned_attestation(&value.0)?) + } + SignedData::SignedAggregateAndProof(value) => Ok( + ssz_codec::encode_phase0_signed_aggregate_and_proof(&value.0)?, + ), + SignedData::VersionedSignedAggregateAndProof(value) => Ok( + ssz_codec::encode_versioned_signed_aggregate_and_proof(&value.0)?, + ), + SignedData::SignedSyncMessage(value) => { + Ok(ssz_codec::encode_sync_committee_message(&value.0)?) + } + SignedData::SignedSyncContributionAndProof(value) => { + Ok(ssz_codec::encode_signed_contribution_and_proof(&value.0)?) + } + SignedData::VersionedSignedProposal(value) => { + Ok(ssz_codec::encode_versioned_signed_proposal(&value.0)?) + } - serialize_json!(VersionedSignedValidatorRegistration); - serialize_json!(SignedVoluntaryExit); - serialize_json!(SignedRandao); - if let Some(value) = any.downcast_ref::() { - return serialize_signature(value); + SignedData::VersionedSignedValidatorRegistration(value) => Ok(serde_json::to_vec(value)?), + SignedData::SignedVoluntaryExit(value) => Ok(serde_json::to_vec(value)?), + SignedData::SignedRandao(value) => Ok(serde_json::to_vec(value)?), + SignedData::Signature(value) => serialize_signature(value), + SignedData::BeaconCommitteeSelection(value) => Ok(serde_json::to_vec(value)?), + SignedData::SyncCommitteeSelection(value) => Ok(serde_json::to_vec(value)?), + + // Never exchanged on the wire: the unsigned contribution-and-proof is only signed + // locally (charon exchanges the *signed* variant), so it has no `marshal` counterpart. + SignedData::SyncContributionAndProof(_) => Err(ParSigExCodecError::UnsupportedDutyType), + #[cfg(test)] + SignedData::Mock(_) => Err(ParSigExCodecError::UnsupportedDutyType), } - serialize_json!(BeaconCommitteeSelection); - serialize_json!(SyncCommitteeSelection); - - Err(ParSigExCodecError::UnsupportedDutyType) } /// Returns `true` when the first non-whitespace byte is `{`, indicating JSON @@ -176,11 +141,11 @@ pub(crate) fn looks_like_json(bytes: &[u8]) -> bool { pub(crate) fn deserialize_signed_data( duty_type: &DutyType, bytes: &[u8], -) -> Result, ParSigExCodecError> { +) -> Result { macro_rules! deserialize_json { ($ty:ty) => { serde_json::from_slice::<$ty>(bytes) - .map(|value| Box::new(value) as Box) + .map(SignedData::from) .map_err(ParSigExCodecError::from) }; } @@ -190,13 +155,13 @@ pub(crate) fn deserialize_signed_data( DutyType::Attester => { // Try SSZ non-versioned Attestation first. if let Ok(att) = ssz_codec::decode_phase0_attestation(bytes) { - return Ok(Box::new(Attestation::new(att))); + return Ok(Attestation::new(att).into()); } // Try SSZ versioned Attestation. if let Ok(va) = ssz_codec::decode_versioned_attestation(bytes) { let wrapped = VersionedAttestation::new(va) .map_err(|e| ParSigExCodecError::SignedData(e.to_string()))?; - return Ok(Box::new(wrapped)); + return Ok(wrapped.into()); } if looks_like_json(bytes) { return deserialize_json!(Attestation) @@ -210,7 +175,7 @@ pub(crate) fn deserialize_signed_data( if let Ok(vp) = ssz_codec::decode_versioned_signed_proposal(bytes) { let wrapped = VersionedSignedProposal::new(vp) .map_err(|e| ParSigExCodecError::SignedData(e.to_string()))?; - return Ok(Box::new(wrapped)); + return Ok(wrapped.into()); } if looks_like_json(bytes) { return deserialize_json!(VersionedSignedProposal); @@ -239,11 +204,11 @@ pub(crate) fn deserialize_signed_data( DutyType::Aggregator => { // Try SSZ non-versioned SignedAggregateAndProof first. if let Ok(sap) = ssz_codec::decode_phase0_signed_aggregate_and_proof(bytes) { - return Ok(Box::new(SignedAggregateAndProof::new(sap))); + return Ok(SignedAggregateAndProof::new(sap).into()); } // Try SSZ versioned. if let Ok(va) = ssz_codec::decode_versioned_signed_aggregate_and_proof(bytes) { - return Ok(Box::new(VersionedSignedAggregateAndProof::new(va))); + return Ok(VersionedSignedAggregateAndProof::new(va).into()); } if looks_like_json(bytes) { return deserialize_json!(SignedAggregateAndProof) @@ -255,7 +220,7 @@ pub(crate) fn deserialize_signed_data( // -- SyncMessage: SSZ-capable -- DutyType::SyncMessage => { if let Ok(msg) = ssz_codec::decode_sync_committee_message(bytes) { - return Ok(Box::new(SignedSyncMessage::new(msg))); + return Ok(SignedSyncMessage::new(msg).into()); } if looks_like_json(bytes) { return deserialize_json!(SignedSyncMessage); @@ -269,7 +234,7 @@ pub(crate) fn deserialize_signed_data( // -- SyncContribution: SSZ-capable -- DutyType::SyncContribution => { if let Ok(scp) = ssz_codec::decode_signed_contribution_and_proof(bytes) { - return Ok(Box::new(SignedSyncContributionAndProof::new(scp))); + return Ok(SignedSyncContributionAndProof::new(scp).into()); } if looks_like_json(bytes) { return deserialize_json!(SignedSyncContributionAndProof); @@ -309,12 +274,6 @@ mod tests { } } - /// Helper: downcast a `Box` to a concrete type. - fn downcast(boxed: Box) -> T { - let any = boxed as Box; - *any.downcast::().expect("type mismatch in downcast") - } - /// SSZ-capable types serialize as SSZ binary and can be deserialized back. #[test] fn marshal_unmarshal_ssz_attestation() { @@ -323,12 +282,11 @@ mod tests { data: sample_attestation_data(), signature: [0x11; 96], }); - let bytes = serialize_signed_data(&att).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(att.clone())).unwrap(); // SSZ bytes should NOT start with '{'. assert_ne!(bytes.first(), Some(&b'{')); - let decoded: Attestation = - downcast(deserialize_signed_data(&DutyType::Attester, &bytes).unwrap()); - assert_eq!(att, decoded); + let decoded = deserialize_signed_data(&DutyType::Attester, &bytes).unwrap(); + assert_eq!(SignedData::from(att), decoded); } /// SSZ-capable types: versioned attestation round-trip. @@ -344,11 +302,10 @@ mod tests { })), }; let va = VersionedAttestation::new(inner).unwrap(); - let bytes = serialize_signed_data(&va).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(va.clone())).unwrap(); assert_ne!(bytes.first(), Some(&b'{')); - let decoded: VersionedAttestation = - downcast(deserialize_signed_data(&DutyType::Attester, &bytes).unwrap()); - assert_eq!(va, decoded); + let decoded = deserialize_signed_data(&DutyType::Attester, &bytes).unwrap(); + assert_eq!(SignedData::from(va), decoded); } /// SSZ-capable types: SyncMessage round-trip. @@ -360,11 +317,10 @@ mod tests { validator_index: 50, signature: [0xee; 96], }); - let bytes = serialize_signed_data(&msg).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(msg.clone())).unwrap(); assert_ne!(bytes.first(), Some(&b'{')); - let decoded: SignedSyncMessage = - downcast(deserialize_signed_data(&DutyType::SyncMessage, &bytes).unwrap()); - assert_eq!(msg, decoded); + let decoded = deserialize_signed_data(&DutyType::SyncMessage, &bytes).unwrap(); + assert_eq!(SignedData::from(msg), decoded); } /// SSZ-capable types: SignedSyncContributionAndProof round-trip. @@ -384,11 +340,10 @@ mod tests { }, signature: [0xfa; 96], }); - let bytes = serialize_signed_data(&scp).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(scp.clone())).unwrap(); assert_ne!(bytes.first(), Some(&b'{')); - let decoded: SignedSyncContributionAndProof = - downcast(deserialize_signed_data(&DutyType::SyncContribution, &bytes).unwrap()); - assert_eq!(scp, decoded); + let decoded = deserialize_signed_data(&DutyType::SyncContribution, &bytes).unwrap(); + assert_eq!(SignedData::from(scp), decoded); } /// Regression: `SyncCommitteeMessage`'s leading `u64` slot makes its SSZ @@ -402,15 +357,14 @@ mod tests { validator_index: 50, signature: [0xee; 96], }); - let bytes = serialize_signed_data(&msg).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(msg.clone())).unwrap(); assert_eq!( bytes.first(), Some(&b'{'), "leading SSZ byte should be 0x7B" ); - let decoded: SignedSyncMessage = - downcast(deserialize_signed_data(&DutyType::SyncMessage, &bytes).unwrap()); - assert_eq!(msg, decoded); + let decoded = deserialize_signed_data(&DutyType::SyncMessage, &bytes).unwrap(); + assert_eq!(SignedData::from(msg), decoded); } /// Regression: `SignedContributionAndProof`'s leading `u64` aggregator @@ -432,15 +386,14 @@ mod tests { }, signature: [0xfa; 96], }); - let bytes = serialize_signed_data(&scp).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(scp.clone())).unwrap(); assert_eq!( bytes.first(), Some(&b'{'), "leading SSZ byte should be 0x7B" ); - let decoded: SignedSyncContributionAndProof = - downcast(deserialize_signed_data(&DutyType::SyncContribution, &bytes).unwrap()); - assert_eq!(scp, decoded); + let decoded = deserialize_signed_data(&DutyType::SyncContribution, &bytes).unwrap(); + assert_eq!(SignedData::from(scp), decoded); } /// SSZ-capable types: SignedAggregateAndProof round-trip. @@ -458,23 +411,21 @@ mod tests { }, signature: [0x55; 96], }); - let bytes = serialize_signed_data(&sap).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(sap.clone())).unwrap(); assert_ne!(bytes.first(), Some(&b'{')); - let decoded: SignedAggregateAndProof = - downcast(deserialize_signed_data(&DutyType::Aggregator, &bytes).unwrap()); - assert_eq!(sap, decoded); + let decoded = deserialize_signed_data(&DutyType::Aggregator, &bytes).unwrap(); + assert_eq!(SignedData::from(sap), decoded); } /// JSON-only types still serialize as JSON. #[test] fn marshal_unmarshal_json_randao() { let randao = SignedRandao::new(10, [0x99; 96]); - let bytes = serialize_signed_data(&randao).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(randao.clone())).unwrap(); // JSON bytes should start with '{'. assert_eq!(bytes.first(), Some(&b'{')); - let decoded: SignedRandao = - downcast(deserialize_signed_data(&DutyType::Randao, &bytes).unwrap()); - assert_eq!(randao, decoded); + let decoded = deserialize_signed_data(&DutyType::Randao, &bytes).unwrap(); + assert_eq!(SignedData::from(randao), decoded); } /// JSON data can still be deserialized for SSZ-capable types (fallback). @@ -489,9 +440,8 @@ mod tests { let json_bytes = serde_json::to_vec(&att).unwrap(); assert_eq!(json_bytes.first(), Some(&b'{')); // Deserialize should fall back to JSON and succeed. - let decoded: Attestation = - downcast(deserialize_signed_data(&DutyType::Attester, &json_bytes).unwrap()); - assert_eq!(att, decoded); + let decoded = deserialize_signed_data(&DutyType::Attester, &json_bytes).unwrap(); + assert_eq!(SignedData::from(att), decoded); } /// JSON data can still be deserialized for SSZ-capable SyncMessage @@ -505,9 +455,8 @@ mod tests { signature: [0xbb; 96], }); let json_bytes = serde_json::to_vec(&msg).unwrap(); - let decoded: SignedSyncMessage = - downcast(deserialize_signed_data(&DutyType::SyncMessage, &json_bytes).unwrap()); - assert_eq!(msg, decoded); + let decoded = deserialize_signed_data(&DutyType::SyncMessage, &json_bytes).unwrap(); + assert_eq!(SignedData::from(msg), decoded); } /// JSON data can still be deserialized for SSZ-capable Aggregator @@ -528,24 +477,22 @@ mod tests { }); let json_bytes = serde_json::to_vec(&sap).unwrap(); assert_eq!(json_bytes.first(), Some(&b'{')); - let decoded: SignedAggregateAndProof = - downcast(deserialize_signed_data(&DutyType::Aggregator, &json_bytes).unwrap()); - assert_eq!(sap, decoded); + let decoded = deserialize_signed_data(&DutyType::Aggregator, &json_bytes).unwrap(); + assert_eq!(SignedData::from(sap), decoded); } #[test] fn marshal_unmarshal_signature() { let sig: Signature = [0xab; SIGNATURE_LENGTH]; - let bytes = serialize_signed_data(&sig).unwrap(); + let bytes = serialize_signed_data(&SignedData::from(sig)).unwrap(); // Snapshot: Signature serializes as a base64-encoded JSON string. // Changing this breaks wire compatibility with Charon. const EXPECTED: &str = "\"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur\""; assert_eq!(bytes, EXPECTED.as_bytes()); - let decoded: Signature = - downcast(deserialize_signed_data(&DutyType::Signature, &bytes).unwrap()); - assert_eq!(sig, decoded); + let decoded = deserialize_signed_data(&DutyType::Signature, &bytes).unwrap(); + assert_eq!(SignedData::from(sig), decoded); } #[test] diff --git a/crates/core/src/sigagg.rs b/crates/core/src/sigagg.rs index 1d0fa5a6..ce4c6e60 100644 --- a/crates/core/src/sigagg.rs +++ b/crates/core/src/sigagg.rs @@ -8,8 +8,8 @@ use pluto_eth2api::client::EthBeaconNodeApiClient; use tracing::{debug, error, info_span}; use crate::{ - eth2signeddata::{Eth2SignedDataError, as_eth2_signed_data, verify_eth2_signed_data}, - signeddata::{SignedDataError, VersionedAttestation}, + eth2signeddata::{Eth2SignedDataError, verify_eth2_signed_data}, + signeddata::SignedDataError, types::{Duty, ParSignedData, PubKey, Signature, SignedData}, }; @@ -91,7 +91,7 @@ pub enum SigAggError { pub type Result = std::result::Result; /// Per-duty output: one aggregated [`SignedData`] per validator public key. -pub type AggSignedDataSet = HashMap>; +pub type AggSignedDataSet = HashMap; /// Callback invoked after a successful threshold aggregation for a duty. pub type AggSub = Arc< @@ -103,7 +103,7 @@ pub type AggSub = Arc< /// Verify callback — checks the aggregated signature against the beacon chain. pub type VerifyFn = Arc< - dyn Fn(&PubKey, &dyn SignedData) -> Pin> + Send>> + dyn Fn(&PubKey, &SignedData) -> Pin> + Send>> + Send + Sync + 'static, @@ -173,7 +173,7 @@ impl Aggregator { &self, pubkey: &PubKey, par_sigs: &[ParSignedData], - ) -> Result> { + ) -> Result { if (par_sigs.len() as u64) < self.threshold { return Err(SigAggError::RequireThresholdSignatures { pubkey: *pubkey }); } @@ -218,23 +218,19 @@ impl Aggregator { // All parSigs for one (pubkey, duty) share the same concrete type and // unsigned payload (guaranteed by consensus), so the non-attestation // slice is homogeneous and parSigs[0] is a valid template. - let mut full_sig: Option<&dyn SignedData> = None; + let mut full_sig: Option<&SignedData> = None; for ps in par_sigs { - let Some(att) = ps - .signed_data - .as_any() - .downcast_ref::() - else { + let SignedData::VersionedAttestation(att) = &ps.signed_data else { break; // first non-attestation aborts the scan, matching Go }; if att.0.validator_index.is_some() { - full_sig = Some(ps.signed_data.as_ref()); + full_sig = Some(&ps.signed_data); break; } } - let template = full_sig.unwrap_or_else(|| par_sigs[0].signed_data.as_ref()); + let template = full_sig.unwrap_or(&par_sigs[0].signed_data); - let agg_signed = template.set_signature_boxed(agg_bytes).map_err(|e| { + let agg_signed = template.set_signature(agg_bytes).map_err(|e| { error!(parent: &span, error = %e, "set_signature failed"); SigAggError::SetSignature { pubkey: *pubkey, @@ -242,12 +238,10 @@ impl Aggregator { } })?; - (self.verify_fn)(pubkey, agg_signed.as_ref()) - .await - .map_err(|e| { - error!(parent: &span, error = %e, "verify failed"); - e - })?; + (self.verify_fn)(pubkey, &agg_signed).await.map_err(|e| { + error!(parent: &span, error = %e, "verify failed"); + e + })?; Ok(agg_signed) } @@ -256,19 +250,20 @@ impl Aggregator { /// Returns a [`VerifyFn`] that verifies the aggregated signature against the /// beacon chain. pub fn new_verifier(eth2_cl: EthBeaconNodeApiClient) -> VerifyFn { - Arc::new(move |pubkey: &PubKey, data: &dyn SignedData| { + Arc::new(move |pubkey: &PubKey, data: &SignedData| { let eth2_cl = eth2_cl.clone(); // The future must be `'static`, so clone the borrowed inputs out of the // call frame before entering the async block. let tbls_pubkey = PublicKey::try_from(pubkey.as_ref()); - let owned: Box = dyn_clone::clone_box(data); + let owned = data.clone(); Box::pin(async move { let tbls_pubkey = tbls_pubkey.map_err(|source| SigAggError::PubkeyFromCore { source })?; - let eth2_signed = - as_eth2_signed_data(owned.as_ref()).ok_or(SigAggError::InvalidEth2SignedData)?; + let eth2_signed = owned + .as_eth2_signed_data() + .ok_or(SigAggError::InvalidEth2SignedData)?; verify_eth2_signed_data(ð2_cl, eth2_signed, &tbls_pubkey).await?; @@ -281,13 +276,11 @@ pub fn new_verifier(eth2_cl: EthBeaconNodeApiClient) -> VerifyFn { mod tests { use std::{fs, sync::Mutex}; - use pluto_ssz::HashRoot; - use super::*; use crate::{ signeddata::{ - SignedDataError, SignedRandao, SignedVoluntaryExit, VersionedSignedProposal, - VersionedSignedValidatorRegistration, + MockSignedData, SignedRandao, SignedVoluntaryExit, VersionedAttestation, + VersionedSignedProposal, VersionedSignedValidatorRegistration, }, types::{SIGNATURE_LENGTH, Signature}, }; @@ -302,109 +295,15 @@ mod tests { let eth2_cl = mock.client().clone(); let verify = new_verifier(eth2_cl); - let data = MockSignedData { - sig: [0u8; SIGNATURE_LENGTH], - }; + let data = SignedData::from(MockSignedData::new([0u8; SIGNATURE_LENGTH])); let err = verify(&PubKey::new([0x11; 48]), &data).await.unwrap_err(); assert!(matches!(err, SigAggError::InvalidEth2SignedData)); } - #[derive(Debug, Clone, PartialEq, Eq)] - struct MockSignedData { - sig: [u8; SIGNATURE_LENGTH], - } - - impl SignedData for MockSignedData { - fn signature(&self) -> std::result::Result { - Ok(self.sig) - } - - fn set_signature(&self, sig: Signature) -> std::result::Result - where - Self: Sized, - { - Ok(Self { sig }) - } - - fn set_signature_boxed( - &self, - signature: Signature, - ) -> std::result::Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> std::result::Result { - Ok([0u8; 32]) - } - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct FailSignatureMock; - - impl SignedData for FailSignatureMock { - fn signature(&self) -> std::result::Result { - Err(SignedDataError::UnknownType) - } - - fn set_signature(&self, _: Signature) -> std::result::Result - where - Self: Sized, - { - Ok(Self) - } - - fn set_signature_boxed( - &self, - sig: Signature, - ) -> std::result::Result, SignedDataError> { - Ok(Box::new(self.set_signature(sig)?)) - } - - fn message_root(&self) -> std::result::Result { - Ok([0u8; 32]) - } - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct FailSetSignatureMock { - sig: [u8; SIGNATURE_LENGTH], - } - - impl SignedData for FailSetSignatureMock { - fn signature(&self) -> std::result::Result { - Ok(self.sig) - } - - fn set_signature(&self, _: Signature) -> std::result::Result - where - Self: Sized, - { - Err(SignedDataError::UnknownType) - } - - fn set_signature_boxed( - &self, - _: Signature, - ) -> std::result::Result, SignedDataError> { - Err(SignedDataError::UnknownType) - } - - fn message_root(&self) -> std::result::Result { - Ok([0u8; 32]) - } - } - fn mock_par_sigs(count: usize, share_idx: u64) -> Vec { (0..count) - .map(|_| { - ParSignedData::new( - MockSignedData { - sig: [0u8; SIGNATURE_LENGTH], - }, - share_idx, - ) - }) + .map(|_| ParSignedData::new(MockSignedData::new([0u8; SIGNATURE_LENGTH]), share_idx)) .collect() } @@ -466,14 +365,15 @@ mod tests { assert_eq!(received_sig, expected_agg); } - async fn run_aggregation_test(template: &dyn SignedData, duty: &Duty) { + async fn run_aggregation_test(template: impl Into, duty: &Duty) { + let template: SignedData = template.into(); let ctx = make_bls_context(); let par_sigs = ctx .sigs .iter() .map(|(idx, sig)| { - let signed = template.set_signature_boxed(*sig).unwrap(); - ParSignedData::new_boxed(signed, *idx) + let signed = template.set_signature(*sig).unwrap(); + ParSignedData::new(signed, *idx) }) .collect(); assert_aggregates(ctx.pubkey, par_sigs, ctx.expected_agg, duty).await; @@ -517,7 +417,7 @@ mod tests { let par_sigs = ctx .sigs .iter() - .map(|(idx, sig)| ParSignedData::new(MockSignedData { sig: *sig }, *idx)) + .map(|(idx, sig)| ParSignedData::new(MockSignedData::new(*sig), *idx)) .collect(); assert_aggregates( ctx.pubkey, @@ -578,7 +478,7 @@ mod tests { let mut par_sigs = Vec::new(); for (share_idx, share) in &shares { let sig = tbls::sign(share, &msg).unwrap(); - par_sigs.push(ParSignedData::new(MockSignedData { sig }, *share_idx)); + par_sigs.push(ParSignedData::new(MockSignedData::new(sig), *share_idx)); } let mut agg = Aggregator::new(THRESHOLD, noop_verify()).unwrap(); @@ -613,14 +513,14 @@ mod tests { let mut par_sigs: Vec = ctx .sigs .iter() - .map(|(idx, sig)| ParSignedData::new(MockSignedData { sig: *sig }, *idx)) + .map(|(idx, sig)| ParSignedData::new(MockSignedData::new(*sig), *idx)) .collect(); // Add a duplicate of the first share — last writer wins, same sig so // result identical. let (first_idx, first_sig) = ctx.sigs[0]; par_sigs.push(ParSignedData::new( - MockSignedData { sig: first_sig }, + MockSignedData::new(first_sig), first_idx, )); @@ -647,7 +547,7 @@ mod tests { )) .unwrap(); let template: SignedRandao = serde_json::from_str(&json).unwrap(); - run_aggregation_test(&template, &Duty::new_randao_duty(1.into())).await; + run_aggregation_test(template, &Duty::new_randao_duty(1.into())).await; } #[tokio::test] @@ -657,7 +557,7 @@ mod tests { )) .unwrap(); let template: SignedVoluntaryExit = serde_json::from_str(&json).unwrap(); - run_aggregation_test(&template, &Duty::new_voluntary_exit_duty(1.into())).await; + run_aggregation_test(template, &Duty::new_voluntary_exit_duty(1.into())).await; } #[tokio::test] @@ -667,7 +567,7 @@ mod tests { )) .unwrap(); let template: VersionedSignedProposal = serde_json::from_str(&json).unwrap(); - run_aggregation_test(&template, &Duty::new_proposer_duty(1.into())).await; + run_aggregation_test(template, &Duty::new_proposer_duty(1.into())).await; } #[tokio::test] @@ -677,7 +577,7 @@ mod tests { )) .unwrap(); let template: VersionedSignedProposal = serde_json::from_str(&json).unwrap(); - run_aggregation_test(&template, &Duty::new_builder_proposer_duty(1.into())).await; + run_aggregation_test(template, &Duty::new_builder_proposer_duty(1.into())).await; } #[tokio::test] @@ -685,7 +585,7 @@ mod tests { let json = fs::read_to_string(fixture_path("VersionedSignedValidatorRegistration.v1.json")) .unwrap(); let template: VersionedSignedValidatorRegistration = serde_json::from_str(&json).unwrap(); - run_aggregation_test(&template, &Duty::new_builder_registration_duty(1.into())).await; + run_aggregation_test(template, &Duty::new_builder_registration_duty(1.into())).await; } #[tokio::test] @@ -710,7 +610,7 @@ mod tests { for (share_idx, share) in &shares { let sig = tbls::sign(share, &msg).unwrap(); bls_map.insert(*share_idx, sig); - par_sigs.push(ParSignedData::new(MockSignedData { sig }, *share_idx)); + par_sigs.push(ParSignedData::new(MockSignedData::new(sig), *share_idx)); } let agg_sig = tbls::threshold_aggregate(&bls_map).unwrap(); @@ -753,7 +653,7 @@ mod tests { let par_sigs: Vec = ctx .sigs .iter() - .map(|(idx, sig)| ParSignedData::new(MockSignedData { sig: *sig }, *idx)) + .map(|(idx, sig)| ParSignedData::new(MockSignedData::new(*sig), *idx)) .collect(); let fail_verify: VerifyFn = @@ -774,7 +674,7 @@ mod tests { let par_sigs: Vec = ctx .sigs .iter() - .map(|(idx, sig)| ParSignedData::new(MockSignedData { sig: *sig }, *idx)) + .map(|(idx, sig)| ParSignedData::new(MockSignedData::new(*sig), *idx)) .collect(); let mut agg = Aggregator::new(3, noop_verify()).unwrap(); @@ -794,7 +694,7 @@ mod tests { async fn signature_from_core_error() { let agg = Aggregator::new(3, noop_verify()).unwrap(); let par_sigs: Vec = (0..3u64) - .map(|i| ParSignedData::new(FailSignatureMock, i)) + .map(|i| ParSignedData::new(MockSignedData::failing_signature(), i)) .collect(); let mut set = HashMap::new(); set.insert(PubKey::new([1u8; 48]), par_sigs); @@ -811,7 +711,9 @@ mod tests { let par_sigs: Vec = ctx .sigs .iter() - .map(|(idx, sig)| ParSignedData::new(FailSetSignatureMock { sig: *sig }, *idx)) + .map(|(idx, sig)| { + ParSignedData::new(MockSignedData::new(*sig).with_failing_set_signature(), *idx) + }) .collect(); let agg = Aggregator::new(3, noop_verify()).unwrap(); @@ -848,13 +750,17 @@ mod tests { .iter() .enumerate() .map(|(i, (idx, sig))| { - let template: &dyn SignedData = if i == 0 { &without_idx } else { &with_idx }; - let signed = template.set_signature_boxed(*sig).unwrap(); - ParSignedData::new_boxed(signed, *idx) + let template = if i == 0 { + SignedData::from(without_idx.clone()) + } else { + SignedData::from(with_idx.clone()) + }; + let signed = template.set_signature(*sig).unwrap(); + ParSignedData::new(signed, *idx) }) .collect(); - let captured: Arc>>> = Arc::new(Mutex::new(None)); + let captured: Arc>> = Arc::new(Mutex::new(None)); let captured_clone = captured.clone(); let mut agg = Aggregator::new(3, noop_verify()).unwrap(); @@ -874,10 +780,9 @@ mod tests { .unwrap(); let output = captured.lock().unwrap().take().unwrap(); - let att = output - .as_any() - .downcast_ref::() - .expect("output must be VersionedAttestation"); + let SignedData::VersionedAttestation(att) = &output else { + panic!("output must be VersionedAttestation"); + }; assert!( att.0.validator_index.is_some(), "output must preserve validator_index from template" @@ -894,10 +799,10 @@ mod tests { let par_sigs: Vec = ctx .sigs .iter() - .map(|(idx, sig)| ParSignedData::new(MockSignedData { sig: *sig }, *idx)) + .map(|(idx, sig)| ParSignedData::new(MockSignedData::new(*sig), *idx)) .collect(); - let captured: Arc>>> = Arc::new(Mutex::new(None)); + let captured: Arc>> = Arc::new(Mutex::new(None)); let captured_clone = captured.clone(); let mut agg = Aggregator::new(3, noop_verify()).unwrap(); @@ -918,7 +823,7 @@ mod tests { let output = captured.lock().unwrap().take().unwrap(); assert!( - output.as_any().downcast_ref::().is_some(), + matches!(output, SignedData::Mock(_)), "output must keep the non-attestation template type (par_sigs[0])" ); } diff --git a/crates/core/src/signeddata.rs b/crates/core/src/signeddata.rs index ed97a2fd..9eebd859 100644 --- a/crates/core/src/signeddata.rs +++ b/crates/core/src/signeddata.rs @@ -13,7 +13,7 @@ use pluto_eth2api::{ use pluto_eth2util::types::SignedEpoch; use pluto_ssz::HashRoot; -use crate::types::{ParSignedData, Signature, SignedData}; +use crate::types::{ParSignedData, Signature}; /// Error type for signed data operations. #[derive(Debug, thiserror::Error)] @@ -94,24 +94,301 @@ pub fn sig_from_eth2(sig: phase0::BLSSignature) -> Signature { sig } -impl SignedData for Signature { - fn signature(&self) -> Result { - Ok(*self) +/// Signed duty data variant — the enum equivalent of Go's `core.SignedData` +/// interface, closed over every payload pluto signs and exchanges. +/// +/// Mirrors [`UnsignedDutyData`](crate::unsigneddata::UnsignedDutyData) for the +/// sibling unsigned concept: dispatch is an exhaustive match, so adding a +/// payload type is a compile error at every site that has to handle it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SignedData { + /// Raw BLS signature (`DutyType::Signature`). + Signature(Signature), + /// Signed beacon block proposal (`DutyType::Proposer`). + VersionedSignedProposal(Box), + /// Non-versioned (phase0) attestation (`DutyType::Attester`). + Attestation(Attestation), + /// Versioned attestation (`DutyType::Attester`). + VersionedAttestation(VersionedAttestation), + /// Signed voluntary exit (`DutyType::Exit`). + SignedVoluntaryExit(SignedVoluntaryExit), + /// Signed validator registration (`DutyType::BuilderRegistration`). + VersionedSignedValidatorRegistration(VersionedSignedValidatorRegistration), + /// Signed randao reveal (`DutyType::Randao`). + SignedRandao(SignedRandao), + /// Beacon committee selection proof (`DutyType::PrepareAggregator`). + BeaconCommitteeSelection(BeaconCommitteeSelection), + /// Sync committee selection proof (`DutyType::PrepareSyncContribution`). + SyncCommitteeSelection(SyncCommitteeSelection), + /// Non-versioned (phase0) signed aggregate-and-proof + /// (`DutyType::Aggregator`). + SignedAggregateAndProof(Box), + /// Versioned signed aggregate-and-proof (`DutyType::Aggregator`). + VersionedSignedAggregateAndProof(Box), + /// Signed sync committee message (`DutyType::SyncMessage`). + SignedSyncMessage(SignedSyncMessage), + /// Sync contribution-and-proof (`DutyType::SyncContribution`). + SyncContributionAndProof(Box), + /// Signed sync contribution-and-proof (`DutyType::SyncContribution`). + SignedSyncContributionAndProof(Box), + /// Test-only payload, used by unit tests that need a signed-data value + /// without a real beacon-chain payload behind it. + #[cfg(test)] + Mock(MockSignedData), +} + +impl SignedData { + /// Returns the signed duty data's signature. + pub fn signature(&self) -> Result { + match self { + Self::Signature(sig) => Ok(*sig), + Self::VersionedSignedProposal(inner) => inner.signature(), + Self::Attestation(inner) => inner.signature(), + Self::VersionedAttestation(inner) => inner.signature(), + Self::SignedVoluntaryExit(inner) => inner.signature(), + Self::VersionedSignedValidatorRegistration(inner) => inner.signature(), + Self::SignedRandao(inner) => inner.signature(), + Self::BeaconCommitteeSelection(inner) => inner.signature(), + Self::SyncCommitteeSelection(inner) => inner.signature(), + Self::SignedAggregateAndProof(inner) => inner.signature(), + Self::VersionedSignedAggregateAndProof(inner) => inner.signature(), + Self::SignedSyncMessage(inner) => inner.signature(), + Self::SyncContributionAndProof(inner) => inner.signature(), + Self::SignedSyncContributionAndProof(inner) => inner.signature(), + #[cfg(test)] + Self::Mock(inner) => inner.signature(), + } + } + + /// Returns a copy of the signed duty data with the signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { + Ok(match self { + Self::Signature(_) => Self::Signature(signature), + Self::VersionedSignedProposal(inner) => { + Self::VersionedSignedProposal(Box::new(inner.set_signature(signature)?)) + } + Self::Attestation(inner) => Self::Attestation(inner.set_signature(signature)?), + Self::VersionedAttestation(inner) => { + Self::VersionedAttestation(inner.set_signature(signature)?) + } + Self::SignedVoluntaryExit(inner) => { + Self::SignedVoluntaryExit(inner.set_signature(signature)?) + } + Self::VersionedSignedValidatorRegistration(inner) => { + Self::VersionedSignedValidatorRegistration(inner.set_signature(signature)?) + } + Self::SignedRandao(inner) => Self::SignedRandao(inner.set_signature(signature)?), + Self::BeaconCommitteeSelection(inner) => { + Self::BeaconCommitteeSelection(inner.set_signature(signature)?) + } + Self::SyncCommitteeSelection(inner) => { + Self::SyncCommitteeSelection(inner.set_signature(signature)?) + } + Self::SignedAggregateAndProof(inner) => { + Self::SignedAggregateAndProof(Box::new(inner.set_signature(signature)?)) + } + Self::VersionedSignedAggregateAndProof(inner) => { + Self::VersionedSignedAggregateAndProof(Box::new(inner.set_signature(signature)?)) + } + Self::SignedSyncMessage(inner) => { + Self::SignedSyncMessage(inner.set_signature(signature)?) + } + Self::SyncContributionAndProof(inner) => { + Self::SyncContributionAndProof(Box::new(inner.set_signature(signature)?)) + } + Self::SignedSyncContributionAndProof(inner) => { + Self::SignedSyncContributionAndProof(Box::new(inner.set_signature(signature)?)) + } + #[cfg(test)] + Self::Mock(inner) => Self::Mock(inner.set_signature(signature)?), + }) + } + + /// Returns the message root of the signed duty data's unsigned message. + pub fn message_root(&self) -> Result { + match self { + Self::Signature(_) => Err(SignedDataError::UnsupportedSignatureMessageRoot), + Self::VersionedSignedProposal(inner) => inner.message_root(), + Self::Attestation(inner) => inner.message_root(), + Self::VersionedAttestation(inner) => inner.message_root(), + Self::SignedVoluntaryExit(inner) => inner.message_root(), + Self::VersionedSignedValidatorRegistration(inner) => inner.message_root(), + Self::SignedRandao(inner) => inner.message_root(), + Self::BeaconCommitteeSelection(inner) => inner.message_root(), + Self::SyncCommitteeSelection(inner) => inner.message_root(), + Self::SignedAggregateAndProof(inner) => inner.message_root(), + Self::VersionedSignedAggregateAndProof(inner) => inner.message_root(), + Self::SignedSyncMessage(inner) => inner.message_root(), + Self::SyncContributionAndProof(inner) => inner.message_root(), + Self::SignedSyncContributionAndProof(inner) => inner.message_root(), + #[cfg(test)] + Self::Mock(inner) => inner.message_root(), + } + } +} + +impl From for SignedData { + fn from(value: Signature) -> Self { + Self::Signature(value) + } +} + +impl From for SignedData { + fn from(value: VersionedSignedProposal) -> Self { + Self::VersionedSignedProposal(Box::new(value)) + } +} + +impl From for SignedData { + fn from(value: Attestation) -> Self { + Self::Attestation(value) + } +} + +impl From for SignedData { + fn from(value: VersionedAttestation) -> Self { + Self::VersionedAttestation(value) + } +} + +impl From for SignedData { + fn from(value: SignedVoluntaryExit) -> Self { + Self::SignedVoluntaryExit(value) + } +} + +impl From for SignedData { + fn from(value: VersionedSignedValidatorRegistration) -> Self { + Self::VersionedSignedValidatorRegistration(value) + } +} + +impl From for SignedData { + fn from(value: SignedRandao) -> Self { + Self::SignedRandao(value) + } +} + +impl From for SignedData { + fn from(value: BeaconCommitteeSelection) -> Self { + Self::BeaconCommitteeSelection(value) + } +} + +impl From for SignedData { + fn from(value: SyncCommitteeSelection) -> Self { + Self::SyncCommitteeSelection(value) + } +} + +impl From for SignedData { + fn from(value: SignedAggregateAndProof) -> Self { + Self::SignedAggregateAndProof(Box::new(value)) + } +} + +impl From for SignedData { + fn from(value: VersionedSignedAggregateAndProof) -> Self { + Self::VersionedSignedAggregateAndProof(Box::new(value)) + } +} + +impl From for SignedData { + fn from(value: SignedSyncMessage) -> Self { + Self::SignedSyncMessage(value) + } +} + +impl From for SignedData { + fn from(value: SyncContributionAndProof) -> Self { + Self::SyncContributionAndProof(Box::new(value)) + } +} + +impl From for SignedData { + fn from(value: SignedSyncContributionAndProof) -> Self { + Self::SignedSyncContributionAndProof(Box::new(value)) + } +} + +/// Test-only signed-data payload, standing in for a real beacon-chain payload +/// in unit tests that only care about the signature and message root. +#[cfg(test)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MockSignedData { + /// Signature returned by [`Self::signature`]. + pub sig: Signature, + /// Message root returned by [`Self::message_root`]. + pub message_root: HashRoot, + /// When set, [`Self::signature`] fails with + /// [`SignedDataError::UnknownType`]. + pub fail_signature: bool, + /// When set, [`Self::set_signature`] fails with + /// [`SignedDataError::UnknownType`]. + pub fail_set_signature: bool, +} + +#[cfg(test)] +impl MockSignedData { + /// Creates a mock payload carrying `sig` and a zero message root. + pub fn new(sig: Signature) -> Self { + Self { + sig, + message_root: [0u8; 32], + fail_signature: false, + fail_set_signature: false, + } + } + + /// Returns the mock with its message root replaced. + pub fn with_message_root(mut self, message_root: HashRoot) -> Self { + self.message_root = message_root; + self + } + + /// Returns a mock whose [`Self::signature`] always fails. + pub fn failing_signature() -> Self { + Self { + fail_signature: true, + ..Self::new([0u8; crate::types::SIGNATURE_LENGTH]) + } } - fn set_signature(&self, signature: Signature) -> Result { - Ok(signature) + /// Returns the mock with a failing [`Self::set_signature`]. + pub fn with_failing_set_signature(mut self) -> Self { + self.fail_set_signature = true; + self } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { + if self.fail_signature { + return Err(SignedDataError::UnknownType); + } + Ok(self.sig) } - fn message_root(&self) -> Result { - Err(SignedDataError::UnsupportedSignatureMessageRoot) + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, sig: Signature) -> Result { + if self.fail_set_signature { + return Err(SignedDataError::UnknownType); + } + Ok(Self { + sig, + ..self.clone() + }) + } + + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { + Ok(self.message_root) + } +} + +#[cfg(test)] +impl From for SignedData { + fn from(value: MockSignedData) -> Self { + Self::Mock(value) } } @@ -197,8 +474,9 @@ impl VersionedSignedProposal { } } -impl SignedData for VersionedSignedProposal { - fn signature(&self) -> Result { +impl VersionedSignedProposal { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { let proposal = &self.0; if proposal.version == versioned::DataVersion::Unknown { return Err(SignedDataError::UnknownVersion); @@ -206,7 +484,8 @@ impl SignedData for VersionedSignedProposal { Ok(sig_from_eth2(proposal.block.signature())) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); let proposal = &mut out.0; if proposal.version == versioned::DataVersion::Unknown { @@ -218,14 +497,8 @@ impl SignedData for VersionedSignedProposal { Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { let proposal = &self.0; if proposal.version == versioned::DataVersion::Unknown { return Err(SignedDataError::UnknownVersion); @@ -309,25 +582,21 @@ impl Attestation { } } -impl SignedData for Attestation { - fn signature(&self) -> Result { +impl Attestation { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.signature)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.signature = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(hash_root(&self.0.data)) } } @@ -377,8 +646,9 @@ impl VersionedAttestation { } } -impl SignedData for VersionedAttestation { - fn signature(&self) -> Result { +impl VersionedAttestation { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { let version = self.0.version; if version == versioned::DataVersion::Unknown { return Err(SignedDataError::UnknownVersion); @@ -390,7 +660,8 @@ impl SignedData for VersionedAttestation { .ok_or(SignedDataError::MissingAttestation(version)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); let version = out.0.version; if version == versioned::DataVersion::Unknown { @@ -405,14 +676,8 @@ impl SignedData for VersionedAttestation { Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { let version = self.0.version; if version == versioned::DataVersion::Unknown { return Err(SignedDataError::UnknownVersion); @@ -516,25 +781,21 @@ pub struct SignedVoluntaryExit( pub phase0::SignedVoluntaryExit, ); -impl SignedData for SignedVoluntaryExit { - fn signature(&self) -> Result { +impl SignedVoluntaryExit { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.signature)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.signature = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(self.0.message_root()) } } @@ -586,8 +847,9 @@ impl VersionedSignedValidatorRegistration { } } -impl SignedData for VersionedSignedValidatorRegistration { - fn signature(&self) -> Result { +impl VersionedSignedValidatorRegistration { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { match self.0.version { versioned::BuilderVersion::V1 => self .0 @@ -599,7 +861,8 @@ impl SignedData for VersionedSignedValidatorRegistration { } } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); match out.0.version { versioned::BuilderVersion::V1 => { @@ -616,14 +879,8 @@ impl SignedData for VersionedSignedValidatorRegistration { Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { match self.0.version { versioned::BuilderVersion::V1 => self .0 @@ -688,25 +945,21 @@ pub struct SignedRandao( pub SignedEpoch, ); -impl SignedData for SignedRandao { - fn signature(&self) -> Result { +impl SignedRandao { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.signature)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.signature = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(self.0.message_root()) } } @@ -738,25 +991,21 @@ pub struct BeaconCommitteeSelection( pub v1::BeaconCommitteeSelection, ); -impl SignedData for BeaconCommitteeSelection { - fn signature(&self) -> Result { +impl BeaconCommitteeSelection { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.selection_proof)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.selection_proof = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(self.0.message_root()) } } @@ -781,25 +1030,21 @@ pub struct SyncCommitteeSelection( pub v1::SyncCommitteeSelection, ); -impl SignedData for SyncCommitteeSelection { - fn signature(&self) -> Result { +impl SyncCommitteeSelection { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.selection_proof)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.selection_proof = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(self.0.message_root()) } } @@ -824,25 +1069,21 @@ pub struct SignedAggregateAndProof( pub phase0::SignedAggregateAndProof, ); -impl SignedData for SignedAggregateAndProof { - fn signature(&self) -> Result { +impl SignedAggregateAndProof { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.signature)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.signature = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(hash_root(&self.0.message)) } } @@ -899,8 +1140,9 @@ impl VersionedSignedAggregateAndProof { } } -impl SignedData for VersionedSignedAggregateAndProof { - fn signature(&self) -> Result { +impl VersionedSignedAggregateAndProof { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { let version = self.0.version; if version == versioned::DataVersion::Unknown { return Err(SignedDataError::UnknownVersion); @@ -909,7 +1151,8 @@ impl SignedData for VersionedSignedAggregateAndProof { Ok(sig_from_eth2(self.0.aggregate_and_proof.signature())) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); let version = out.0.version; if version == versioned::DataVersion::Unknown { @@ -922,14 +1165,8 @@ impl SignedData for VersionedSignedAggregateAndProof { Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { let version = self.0.version; if version == versioned::DataVersion::Unknown { return Err(SignedDataError::UnknownVersion); @@ -1004,25 +1241,21 @@ pub struct SignedSyncMessage( pub altair::SyncCommitteeMessage, ); -impl SignedData for SignedSyncMessage { - fn signature(&self) -> Result { +impl SignedSyncMessage { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.signature)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.signature = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(self.0.message_root()) } } @@ -1047,25 +1280,21 @@ pub struct SyncContributionAndProof( pub altair::ContributionAndProof, ); -impl SignedData for SyncContributionAndProof { - fn signature(&self) -> Result { +impl SyncContributionAndProof { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.selection_proof)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.selection_proof = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(self.0.selection_proof_message_root()) } } @@ -1090,25 +1319,21 @@ pub struct SignedSyncContributionAndProof( pub altair::SignedContributionAndProof, ); -impl SignedData for SignedSyncContributionAndProof { - fn signature(&self) -> Result { +impl SignedSyncContributionAndProof { + /// Returns the payload's BLS signature. + pub fn signature(&self) -> Result { Ok(sig_from_eth2(self.0.signature)) } - fn set_signature(&self, signature: Signature) -> Result { + /// Returns a copy of the payload with its signature replaced. + pub fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); out.0.signature = types::sig_to_eth2(signature); Ok(out) } - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { + /// Returns the hash-tree-root of the signed message. + pub fn message_root(&self) -> Result { Ok(self.0.message_root()) } } @@ -2031,10 +2256,8 @@ mod tests { } } - fn assert_set_signature(data: T) - where - T: SignedData + std::fmt::Debug + PartialEq, - { + fn assert_set_signature(data: impl Into) { + let data: SignedData = data.into(); let clone = data.set_signature(sample_signature(0xAB)).unwrap(); let clone_sig = clone.signature().unwrap(); let data_sig = data.signature().unwrap(); @@ -2247,7 +2470,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_BeaconCommitteeSelection.json.golden", "76090e708e9b20aa000000000000000000000000000000000000000000000000", - SignedData::message_root, + BeaconCommitteeSelection::message_root, ); } @@ -2256,7 +2479,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_SignedRandao.json.golden", "1e34c5f04204cb9a000000000000000000000000000000000000000000000000", - SignedData::message_root, + SignedRandao::message_root, ); } @@ -2265,7 +2488,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_SignedSyncContributionAndProof.json.golden", "a9114ab23ddeca5729536b5f7132b0845653b235f11e10195659cd8b88ca48e4", - SignedData::message_root, + SignedSyncContributionAndProof::message_root, ); } @@ -2274,7 +2497,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_SignedSyncMessage.json.golden", "0272908d45b0164a1ed1fe5f6c6bb64a52fa1a95e2bdff2aea5190ce067ad5d2", - SignedData::message_root, + SignedSyncMessage::message_root, ); } @@ -2283,7 +2506,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_SignedVoluntaryExit.json.golden", "d5fe7392cad0d8cd8cf3a3b29e14f6e687bc2e64141973099c60d3097d26629b", - SignedData::message_root, + SignedVoluntaryExit::message_root, ); } @@ -2292,7 +2515,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_SyncCommitteeSelection.json.golden", "af587f1aea1ba20c11450b28da5905c861bdce697ea67d3ba23f62f8ffcffd25", - SignedData::message_root, + SyncCommitteeSelection::message_root, ); } @@ -2310,7 +2533,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_SyncContributionAndProof.json.golden", "7d175134bb90ae74308d78c559b8ae6e5280fee44de77209361305f8cc56e5df", - SignedData::message_root, + SyncContributionAndProof::message_root, ); } @@ -2328,7 +2551,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_VersionedAttestation.json.golden", "a36b13159845b8afc947ea7f9ffd74ceb1178e9882533ee767b6a6578501771c", - SignedData::message_root, + VersionedAttestation::message_root, ); } @@ -2373,7 +2596,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_VersionedSignedAggregateAndProof.json.golden", "b583185cb9587e89300afca09c2052bd6e75b885fbdccdcea9d7bcdaa80646f0", - SignedData::message_root, + VersionedSignedAggregateAndProof::message_root, ); } @@ -2400,7 +2623,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_VersionedSignedProposal.json#01.golden", "4bf04729550ce290f32088070ae5dead2940c4350620a4bb85e04b0b1f3c2177", - SignedData::message_root, + VersionedSignedProposal::message_root, ); } @@ -2409,7 +2632,7 @@ mod tests { assert_golden_fixture::( "TestJSONSerialisation_VersionedSignedProposal.json.golden", "cd3d0d0abc5d9ba7a85b8c3388a6d4ebe2ee6367e20bf3c77a8d4977c657c0e1", - SignedData::message_root, + VersionedSignedProposal::message_root, ); } @@ -2418,21 +2641,21 @@ mod tests { assert_golden_fixture::( "VersionedSignedValidatorRegistration.v1.json", "e342f29f5f6bb692ec8fae5ab27854afbb2a40296497001b31987a8587e70b8e", - SignedData::message_root, + VersionedSignedValidatorRegistration::message_root, ); } #[test] fn signature() { - let sig1 = sample_signature(0x22); - let sig2 = sig1; + let sig1 = SignedData::Signature(sample_signature(0x22)); + let sig2 = sig1.clone(); assert!(matches!( sig1.message_root(), Err(SignedDataError::UnsupportedSignatureMessageRoot) )); - assert_eq!(sig1, sig1.signature().unwrap()); - assert_eq!(sig1, sig2.signature().unwrap()); + assert_eq!(sample_signature(0x22), sig1.signature().unwrap()); + assert_eq!(sample_signature(0x22), sig2.signature().unwrap()); let ss = sig1.set_signature(sig2.signature().unwrap()).unwrap(); assert_eq!(sig2, ss); diff --git a/crates/core/src/tracker/analysis.rs b/crates/core/src/tracker/analysis.rs index 05ddd6a4..ef5dad50 100644 --- a/crates/core/src/tracker/analysis.rs +++ b/crates/core/src/tracker/analysis.rs @@ -588,11 +588,11 @@ fn string_error(s: &str) -> StepError { mod tests { use std::sync::Arc; - use pluto_crypto::types::{SIGNATURE_LENGTH, Signature}; + use pluto_crypto::types::SIGNATURE_LENGTH; use super::*; use crate::{ - signeddata::SignedDataError, + signeddata::MockSignedData, types::{ParSignedData, SignedData, SlotNumber}, }; @@ -673,43 +673,12 @@ mod tests { ))) } - #[derive(Debug, Clone, PartialEq, Eq)] - struct TestSignedData { - id: HashRoot, - sig: [u8; SIGNATURE_LENGTH], - } - - impl TestSignedData { - fn new(id_byte: u8) -> Self { - Self { - id: [id_byte; 32], - sig: [0u8; SIGNATURE_LENGTH], - } - } - } - - impl SignedData for TestSignedData { - fn signature(&self) -> Result { - Ok(self.sig) - } - - fn set_signature(&self, sig: Signature) -> Result - where - Self: Sized, - { - Ok(Self { id: self.id, sig }) - } - - fn set_signature_boxed( - &self, - sig: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(sig)?)) - } - - fn message_root(&self) -> Result { - Ok(self.id) - } + /// Builds mock signed data whose message root is derived from `id_byte`, + /// so distinct values group under distinct roots. + fn test_signed_data(id_byte: u8) -> SignedData { + MockSignedData::new([0u8; SIGNATURE_LENGTH]) + .with_message_root([id_byte; 32]) + .into() } #[test] @@ -1380,7 +1349,7 @@ mod tests { let mut next_idx: u64 = 0; // pk_a, root=A, 4 sigs. - let data_a = TestSignedData::new(0xAA); + let data_a = test_signed_data(0xAA); for _ in 0..4 { events.push(Event { duty: att.clone(), @@ -1393,7 +1362,7 @@ mod tests { } // pk_a, root=B, 2 sigs. - let data_b = TestSignedData::new(0xBB); + let data_b = test_signed_data(0xBB); for _ in 0..2 { events.push(Event { duty: att.clone(), @@ -1406,7 +1375,7 @@ mod tests { } // pk_b, root=C, 6 sigs. - let data_c = TestSignedData::new(0xCC); + let data_c = test_signed_data(0xCC); for _ in 0..6 { events.push(Event { duty: att.clone(), @@ -1444,7 +1413,7 @@ mod tests { // entry, regardless of differing signature content. let att = Duty::new_attester_duty(SlotNumber::new(0)); let pk = pubkey(1); - let data = TestSignedData::new(0xAA); + let data = test_signed_data(0xAA); let events = vec![ Event { diff --git a/crates/core/src/tracker/inclusion.rs b/crates/core/src/tracker/inclusion.rs index 4f614b72..9ac17c09 100644 --- a/crates/core/src/tracker/inclusion.rs +++ b/crates/core/src/tracker/inclusion.rs @@ -11,7 +11,6 @@ //! builds the `Block` inputs is layered on top separately. use std::{ - any::Any, collections::HashMap, sync::{Arc, Mutex}, time::Duration, @@ -25,10 +24,7 @@ use tokio_util::sync::CancellationToken; use tree_hash::TreeHash; use crate::{ - signeddata::{ - Attestation, SignedAggregateAndProof, SignedDataError, VersionedAttestation, - VersionedSignedAggregateAndProof, VersionedSignedProposal, - }, + signeddata::SignedDataError, tracker::{StepError, analysis, metrics::TRACKER_METRICS}, types::{Duty, DutyType, PubKey, SignedData, SignedDataSet}, }; @@ -121,7 +117,7 @@ pub struct Submission { /// The validator the duty belongs to. pub pubkey: PubKey, /// The signed data broadcast to the beacon node. - pub data: Box, + pub data: SignedData, /// Hash-tree-root of the attestation data (zero for proposals). pub att_data_root: HashRoot, /// Delay between slot start and broadcast. @@ -201,7 +197,7 @@ impl InclusionCore { &mut self, duty: Duty, pubkey: PubKey, - data: Box, + data: SignedData, delay: Duration, ) -> Result<(), InclusionError> { if !analysis::incl_supported(self.feature_set).contains(&duty.duty_type) { @@ -211,38 +207,39 @@ impl InclusionCore { let mut att_data_root = [0u8; 32]; if duty.duty_type == DutyType::Attester { - let any = &*data as &dyn Any; - if let Some(att) = any.downcast_ref::() { - let payload = att - .0 - .attestation - .as_ref() - .ok_or(InclusionError::MissingAttestation)?; - att_data_root = payload.data().tree_hash_root().0; - } else if let Some(att) = any.downcast_ref::() { - att_data_root = att.0.data.tree_hash_root().0; - } else { - return Err(InclusionError::InvalidAttestation); + match &data { + SignedData::VersionedAttestation(att) => { + let payload = att + .0 + .attestation + .as_ref() + .ok_or(InclusionError::MissingAttestation)?; + att_data_root = payload.data().tree_hash_root().0; + } + SignedData::Attestation(att) => { + att_data_root = att.0.data.tree_hash_root().0; + } + _ => return Err(InclusionError::InvalidAttestation), } } if duty.duty_type == DutyType::Aggregator { - let any = &*data as &dyn Any; - if let Some(agg) = any.downcast_ref::() { - let data = agg.data().ok_or(InclusionError::InvalidAggregateAndProof)?; - att_data_root = data.tree_hash_root().0; - } else if let Some(agg) = any.downcast_ref::() { - att_data_root = agg.0.message.aggregate.data.tree_hash_root().0; - } else { - return Err(InclusionError::InvalidAggregateAndProof); + match &data { + SignedData::VersionedSignedAggregateAndProof(agg) => { + let data = agg.data().ok_or(InclusionError::InvalidAggregateAndProof)?; + att_data_root = data.tree_hash_root().0; + } + SignedData::SignedAggregateAndProof(agg) => { + att_data_root = agg.0.message.aggregate.data.tree_hash_root().0; + } + _ => return Err(InclusionError::InvalidAggregateAndProof), } } if duty.duty_type == DutyType::Proposer { - let any = &*data as &dyn Any; - let proposal = any - .downcast_ref::() - .ok_or(InclusionError::InvalidBlock)?; + let SignedData::VersionedSignedProposal(proposal) = &data else { + return Err(InclusionError::InvalidBlock); + }; if proposal.0.is_synthetic() { // Synthetic blocks are already on-chain; report inclusion now. (self.tracker_incl_fn)(&duty, pubkey, None); @@ -313,18 +310,16 @@ impl InclusionCore { for key in matched { let blinded = match self.submissions.get(&key) { - Some(sub) => { - match (&*sub.data as &dyn Any).downcast_ref::() { - Some(proposal) => proposal.0.blinded, - None => { - tracing::error!( - duty = %sub.duty, - "Submission data has wrong type", - ); - continue; - } + Some(sub) => match &sub.data { + SignedData::VersionedSignedProposal(proposal) => proposal.0.blinded, + _ => { + tracing::error!( + duty = %sub.duty, + "Submission data has wrong type", + ); + continue; } - } + }, None => continue, }; @@ -371,14 +366,14 @@ impl InclusionCore { if sub.duty.slot.inner() != block.slot { continue; } - match (&*sub.data as &dyn Any).downcast_ref::() { - Some(proposal) => acts.push(( + match &sub.data { + SignedData::VersionedSignedProposal(proposal) => acts.push(( key.clone(), Act::ProposerInclude { blinded: proposal.0.blinded, }, )), - None => { + _ => { tracing::error!(duty = %sub.duty, "Submission data has wrong type"); } } @@ -432,10 +427,9 @@ fn electra_committee_index(payload: &versioned::AttestationPayload) -> Result Result { - let any = &*sub.data as &dyn Any; - let sub_att = any - .downcast_ref::() - .ok_or(InclusionError::NotAnAttestation)?; + let SignedData::VersionedAttestation(sub_att) = &sub.data else { + return Err(InclusionError::NotAnAttestation); + }; let Some(att) = block.attestations_by_data_root.get(&sub.att_data_root) else { return Ok(false); @@ -499,10 +493,9 @@ fn check_aggregation_inclusion(sub: &Submission, block: &Block) -> Result() - .ok_or(InclusionError::ParseVersionedAggregate)?; + let SignedData::VersionedSignedAggregateAndProof(agg) = &sub.data else { + return Err(InclusionError::ParseVersionedAggregate); + }; let sub_bits = AggBits::from_ssz_bytes( agg.aggregation_bits() .ok_or(InclusionError::ParseVersionedAggregate)?, @@ -530,24 +523,22 @@ fn report_missed(sub: &Submission) { "{msg}", ); } - DutyType::Proposer => { - match (&*sub.data as &dyn Any).downcast_ref::() { - Some(proposal) => { - let msg = if proposal.0.blinded { - "Broadcasted blinded block never included on-chain" - } else { - "Broadcasted block never included on-chain" - }; - tracing::warn!( - pubkey = %sub.pubkey, - block_slot = sub.duty.slot.inner(), - broadcast_delay = ?sub.delay, - "{msg}", - ); - } - None => tracing::error!(duty = %sub.duty, "Submission data has wrong type"), + DutyType::Proposer => match &sub.data { + SignedData::VersionedSignedProposal(proposal) => { + let msg = if proposal.0.blinded { + "Broadcasted blinded block never included on-chain" + } else { + "Broadcasted block never included on-chain" + }; + tracing::warn!( + pubkey = %sub.pubkey, + block_slot = sub.duty.slot.inner(), + broadcast_delay = ?sub.delay, + "{msg}", + ); } - } + _ => tracing::error!(duty = %sub.duty, "Submission data has wrong type"), + }, _ => unreachable!("bug: unexpected type"), } } @@ -757,7 +748,12 @@ mod tests { use pluto_featureset::{Config, Feature}; use super::*; - use crate::types::SlotNumber; + use crate::{ + signeddata::{ + Attestation, SignedAggregateAndProof, VersionedAttestation, VersionedSignedProposal, + }, + types::SlotNumber, + }; /// Shared recorder of duties passed to a callback. type Rec = Arc>>; @@ -826,7 +822,7 @@ mod tests { .expect("golden proposal deserialises") } - fn submission(duty: Duty, data: Box, att_data_root: HashRoot) -> Submission { + fn submission(duty: Duty, data: SignedData, att_data_root: HashRoot) -> Submission { Submission { duty, pubkey: pubkey(), @@ -875,28 +871,28 @@ mod tests { core.submitted( Duty::new_attester_duty(SlotNumber::new(1)), pubkey(), - Box::new(att1), + att1.clone().into(), Duration::ZERO, ) .expect("submit attester 1"); core.submitted( Duty::new_aggregator_duty(SlotNumber::new(2)), pubkey(), - Box::new(agg2), + agg2.clone().into(), Duration::ZERO, ) .expect("submit aggregator 2"); core.submitted( Duty::new_attester_duty(SlotNumber::new(3)), pubkey(), - Box::new(att3), + att3.clone().into(), Duration::ZERO, ) .expect("submit attester 3"); core.submitted( Duty::new_proposer_duty(SlotNumber::new(100)), pubkey(), - Box::new(block4), + block4.clone().into(), Duration::ZERO, ) .expect("submit proposer 100"); @@ -943,7 +939,7 @@ mod tests { core.submitted( Duty::new_proposer_duty(SlotNumber::new(slot)), pubkey(), - Box::new(proposal()), + proposal().into(), Duration::ZERO, ) .expect("submit proposal"); @@ -974,7 +970,7 @@ mod tests { core.submitted( Duty::new_attester_duty(SlotNumber::new(7)), pubkey(), - Box::new(Attestation::new(phase0_attestation(7))), + Attestation::new(phase0_attestation(7)).into(), Duration::ZERO, ) .expect("submit attester"); @@ -1000,7 +996,7 @@ mod tests { core.submitted( Duty::new_proposer_duty(SlotNumber::new(5)), pubkey(), - Box::new(proposal()), + proposal().into(), Duration::ZERO, ) .expect("submit proposal"); @@ -1056,7 +1052,7 @@ mod tests { }; let sub = submission( Duty::new_attester_duty(SlotNumber::new(slot)), - Box::new(VersionedAttestation::new(sub_att).unwrap()), + VersionedAttestation::new(sub_att).unwrap().into(), data_root, ); let block = Block { @@ -1120,7 +1116,7 @@ mod tests { }; let sub = submission( Duty::new_attester_duty(SlotNumber::new(slot)), - Box::new(VersionedAttestation::new(sub_att).unwrap()), + VersionedAttestation::new(sub_att).unwrap().into(), data_root, ); let block = Block { diff --git a/crates/core/src/tracker/mod.rs b/crates/core/src/tracker/mod.rs index 81f3cf5c..02e5532b 100644 --- a/crates/core/src/tracker/mod.rs +++ b/crates/core/src/tracker/mod.rs @@ -524,13 +524,12 @@ mod tests { use std::{collections::HashMap, sync::Mutex, time::Duration}; use chrono::{DateTime, Utc}; - use pluto_ssz::HashRoot; use tokio_util::sync::CancellationToken; use super::*; use crate::{ deadline::{DeadlineCalculator, DeadlinerTask, NeverExpiringCalculator}, - signeddata::SignedDataError, + signeddata::MockSignedData, tracker::{ reason::Reason, reporters::{DutyResultReporter, ParticipationReporter}, @@ -667,37 +666,14 @@ mod tests { /// Minimal [`crate::types::SignedData`] for constructing [`ParSignedData`] /// in tests without needing real ETH2 attestation data. - #[derive(Debug, Clone, PartialEq, Eq)] - struct SimpleSignedData; - - impl crate::types::SignedData for SimpleSignedData { - fn signature(&self) -> Result { - Ok([0u8; 96]) - } - - fn set_signature( - &self, - _sig: pluto_crypto::types::Signature, - ) -> Result { - Ok(Self) - } - - fn set_signature_boxed( - &self, - sig: pluto_crypto::types::Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(sig)?)) - } - - fn message_root(&self) -> Result { - Ok([0u8; 32]) - } + fn simple_signed_data() -> crate::types::SignedData { + MockSignedData::new([0u8; 96]).into() } fn par_sig_set(pubkeys: &[PubKey], share_idx: u64) -> ParSignedDataSet { let mut set = ParSignedDataSet::new(); for pk in pubkeys { - set.insert(*pk, ParSignedData::new(SimpleSignedData, share_idx)); + set.insert(*pk, ParSignedData::new(simple_signed_data(), share_idx)); } set } diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index a747d3b5..9bb81a30 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -1,22 +1,21 @@ //! Types for the Charon core. -use std::{any::Any, collections::HashMap, fmt::Display, iter}; +use std::{collections::HashMap, fmt::Display, iter}; use chrono::{DateTime, Duration, Utc}; -use dyn_clone::DynClone; -use dyn_eq::DynEq; use pluto_eth2api::v1; -use pluto_ssz::HashRoot; use serde::{Deserialize, Serialize}; -use std::fmt::Debug as StdDebug; use crate::{ ParSigExCodecError, corepb::v1::core as pbcore, parsigex_codec::{deserialize_signed_data, serialize_signed_data}, - signeddata::SignedDataError, }; +/// Signed duty data, re-exported from [`crate::signeddata`] where the closed +/// enum over every signed payload lives. +pub use crate::signeddata::SignedData; + /// The type of duty. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -524,70 +523,22 @@ pub enum DutyDefinition { /// public key. pub type DutyDefinitionSet = HashMap; -/// Signed data type -pub trait SignedData: Any + DynClone + DynEq + StdDebug + Send + Sync { - /// signature returns the signed duty data's signature. - fn signature(&self) -> Result; - - /// Returns a copy of signed duty data with the signature replaced. - fn set_signature(&self, signature: Signature) -> Result - where - Self: Sized; - - /// Object-safe equivalent of [`SignedData::set_signature`]. - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError>; - - /// message_root returns the message root for the unsigned data. - fn message_root(&self) -> Result; -} - -dyn_eq::eq_trait_object!(SignedData); -dyn_clone::clone_trait_object!(SignedData); - /// ParSignedData is a partially signed duty data only signed by a single /// threshold BLS share. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ParSignedData { /// Partially signed duty data. - pub signed_data: Box, + pub signed_data: SignedData, /// Threshold BLS share index. pub share_idx: u64, } -impl Clone for ParSignedData { - fn clone(&self) -> Self { - Self { - signed_data: self.signed_data.clone(), - share_idx: self.share_idx, - } - } -} - -impl PartialEq for ParSignedData { - fn eq(&self, other: &Self) -> bool { - self.share_idx == other.share_idx && self.signed_data == other.signed_data - } -} - -impl Eq for ParSignedData {} - impl ParSignedData { /// Create a new partially signed data. - pub fn new(partially_signed_data: T, share_idx: u64) -> Self { + pub fn new(partially_signed_data: impl Into, share_idx: u64) -> Self { Self { - signed_data: Box::new(partially_signed_data), - share_idx, - } - } - - /// Create a new partially signed data from a boxed signed data. - pub fn new_boxed(partially_signed_data: Box, share_idx: u64) -> Self { - Self { - signed_data: partially_signed_data, + signed_data: partially_signed_data.into(), share_idx, } } @@ -597,7 +548,7 @@ impl TryFrom<&ParSignedData> for pbcore::ParSignedData { type Error = ParSigExCodecError; fn try_from(data: &ParSignedData) -> Result { - let encoded = serialize_signed_data(data.signed_data.as_ref())?; + let encoded = serialize_signed_data(&data.signed_data)?; let share_idx = i32::try_from(data.share_idx).map_err(|_| ParSigExCodecError::InvalidShareIndex)?; let signature = data @@ -621,7 +572,7 @@ impl TryFrom<(&DutyType, &pbcore::ParSignedData)> for ParSignedData { let share_idx = u64::try_from(data.share_idx).map_err(|_| ParSigExCodecError::InvalidShareIndex)?; let signed_data = deserialize_signed_data(duty_type, &data.data)?; - Ok(Self::new_boxed(signed_data, share_idx)) + Ok(Self::new(signed_data, share_idx)) } } @@ -696,7 +647,7 @@ impl TryFrom<(&DutyType, &pbcore::ParSignedDataSet)> for ParSignedDataSet { } /// A set of signed duty data. -pub type SignedDataSet = HashMap>; +pub type SignedDataSet = HashMap; /// Slot struct #[derive(Debug, Clone, PartialEq, Eq)] @@ -766,6 +717,7 @@ impl Slot { #[cfg(test)] mod tests { use super::*; + use crate::signeddata::MockSignedData; #[test] fn pub_key_to_string() { @@ -1149,40 +1101,16 @@ mod tests { assert_eq!(pk.abbreviated(), "2a2_a2a"); } - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - struct MockSignedData; - - impl MockSignedData { - fn boxed(&self) -> Box { - Box::new(self.clone()) - } - } - - impl SignedData for MockSignedData { - fn signature(&self) -> Result { - Ok([42u8; SIGNATURE_LENGTH]) - } - - fn set_signature(&self, _signature: Signature) -> Result { - Ok(self.clone()) - } - - fn set_signature_boxed( - &self, - signature: Signature, - ) -> Result, SignedDataError> { - Ok(Box::new(self.set_signature(signature)?)) - } - - fn message_root(&self) -> Result { - Ok([42u8; 32]) - } + fn mock_signed_data() -> SignedData { + MockSignedData::new([42u8; SIGNATURE_LENGTH]) + .with_message_root([42u8; 32]) + .into() } #[test] fn partially_signed_data_set() { let mut partially_signed_data_set = ParSignedDataSet::new(); - let par_signed = ParSignedData::new(MockSignedData, 0); + let par_signed = ParSignedData::new(mock_signed_data(), 0); partially_signed_data_set.insert(PubKey::new([42u8; PK_LEN]), par_signed.clone()); let retrieved = partially_signed_data_set.get(&PubKey::new([42u8; PK_LEN])); assert!(retrieved.is_some()); @@ -1197,8 +1125,8 @@ mod tests { #[test] fn signed_data_set() { let mut signed_data_set = SignedDataSet::new(); - signed_data_set.insert(PubKey::new([42u8; PK_LEN]), MockSignedData.boxed()); - let expected = MockSignedData.boxed(); + signed_data_set.insert(PubKey::new([42u8; PK_LEN]), mock_signed_data()); + let expected = mock_signed_data(); assert_eq!( signed_data_set.get(&PubKey::new([42u8; PK_LEN])), Some(&expected) diff --git a/crates/core/src/validatorapi/component.rs b/crates/core/src/validatorapi/component.rs index b6340bfd..89e5d40e 100644 --- a/crates/core/src/validatorapi/component.rs +++ b/crates/core/src/validatorapi/component.rs @@ -95,7 +95,7 @@ pub type AwaitSyncContributionFn = Arc< /// Looks up aggregated signed data from the AggSigDB for a `(duty, pubkey)`. pub type AwaitAggSigDbFn = Arc< - dyn Fn(Duty, PubKey) -> BoxFuture<'static, Result, CallbackError>> + dyn Fn(Duty, PubKey) -> BoxFuture<'static, Result> + Send + Sync + 'static, @@ -321,7 +321,7 @@ impl Component { pub fn register_await_agg_sig_db(&mut self, f: F) where F: Fn(Duty, PubKey) -> Fut + Send + Sync + 'static, - Fut: Future, CallbackError>> + Send + 'static, + Fut: Future> + Send + 'static, { self.await_agg_sig_db_fn = Some(Arc::new(move |duty, pubkey| Box::pin(f(duty, pubkey)))); } @@ -364,17 +364,16 @@ impl Component { // The domain choice is hard-wired to the signed-data wrapper passed // in. Each handler picks the right wrapper and we map here. - let signed: &dyn SignedData = par_sig.signed_data.as_ref(); - let any_signed = signed as &dyn Any; - let domain_name = if any_signed.is::() { - DomainName::SyncCommittee - } else if any_signed.is::() { - DomainName::ContributionAndProof - } else { - return Err(ApiError::new( - StatusCode::INTERNAL_SERVER_ERROR, - "unsupported signed-data wrapper for verify_partial_sig_for", - )); + let signed = &par_sig.signed_data; + let domain_name = match signed { + SignedData::SignedSyncMessage(_) => DomainName::SyncCommittee, + SignedData::SignedSyncContributionAndProof(_) => DomainName::ContributionAndProof, + _ => { + return Err(ApiError::new( + StatusCode::INTERNAL_SERVER_ERROR, + "unsupported signed-data wrapper for verify_partial_sig_for", + )); + } }; let epoch = epoch_from_slot(&self.eth2_cl, slot).await.map_err(|err| { @@ -1348,7 +1347,7 @@ impl Handler for Component { .with_boxed_source(err) })?; - let selection = downcast_beacon_committee_selection(signed.as_ref())?; + let selection = expect_beacon_committee_selection(&signed)?; resp.push(selection.0.clone()); } } @@ -1441,7 +1440,7 @@ impl Handler for Component { .with_boxed_source(err) })?; - let selection = downcast_sync_committee_selection(signed.as_ref())?; + let selection = expect_sync_committee_selection(&signed)?; resp.push(selection.0.clone()); } } @@ -2072,38 +2071,34 @@ fn invert_pub_share_map( .collect() } -/// Downcasts the aggregated signed data from the AggSigDB to a -/// `BeaconCommitteeSelection`. A mismatch indicates a wiring bug — the cluster -/// stored the wrong duty type under the `PrepareAggregator` duty — so it -/// surfaces as 500 rather than 4xx. -fn downcast_beacon_committee_selection( - signed: &dyn SignedData, +/// Selects the `BeaconCommitteeSelection` payload of the aggregated signed +/// data from the AggSigDB. Any other variant indicates a wiring bug — the +/// cluster stored the wrong duty type under the `PrepareAggregator` duty — so +/// it surfaces as 500 rather than 4xx. +fn expect_beacon_committee_selection( + signed: &SignedData, ) -> Result<&signeddata::BeaconCommitteeSelection, ApiError> { - signed - .as_any() - .downcast_ref::() - .ok_or_else(|| { - ApiError::new( - StatusCode::INTERNAL_SERVER_ERROR, - "invalid beacon committee selection", - ) - }) + match signed { + SignedData::BeaconCommitteeSelection(selection) => Ok(selection), + _ => Err(ApiError::new( + StatusCode::INTERNAL_SERVER_ERROR, + "invalid beacon committee selection", + )), + } } /// Sync committee selections counterpart of -/// [`downcast_beacon_committee_selection`]. -fn downcast_sync_committee_selection( - signed: &dyn SignedData, +/// [`expect_beacon_committee_selection`]. +fn expect_sync_committee_selection( + signed: &SignedData, ) -> Result<&signeddata::SyncCommitteeSelection, ApiError> { - signed - .as_any() - .downcast_ref::() - .ok_or_else(|| { - ApiError::new( - StatusCode::INTERNAL_SERVER_ERROR, - "invalid sync committee selection", - ) - }) + match signed { + SignedData::SyncCommitteeSelection(selection) => Ok(selection), + _ => Err(ApiError::new( + StatusCode::INTERNAL_SERVER_ERROR, + "invalid sync committee selection", + )), + } } /// Re-interprets a Pluto [`PubKey`] as the [`BLSPubKey`] byte-array used by @@ -3301,12 +3296,10 @@ mod tests { .unwrap_err(); assert_eq!(err.to_string(), "s2"); - component.register_await_agg_sig_db(|_duty, _pk| async { - Err::, _>("d1".into()) - }); - component.register_await_agg_sig_db(|_duty, _pk| async { - Err::, _>("d2".into()) - }); + component + .register_await_agg_sig_db(|_duty, _pk| async { Err::("d1".into()) }); + component + .register_await_agg_sig_db(|_duty, _pk| async { Err::("d2".into()) }); let err = (component.await_agg_sig_db_fn.as_ref().unwrap())( Duty::new(SlotNumber::new(0), DutyType::Attester), core_pubkey(0), @@ -3758,7 +3751,7 @@ mod tests { component.register_await_agg_sig_db(move |_duty, _pk| { let agg = agg_clone.clone(); async move { - Ok::, CallbackError>(Box::new( + Ok::(SignedData::from( SignedBeaconCommitteeSelection::new(agg), )) } @@ -3821,7 +3814,7 @@ mod tests { _ => 999, }; async move { - Ok::, CallbackError>(Box::new( + Ok::(SignedData::from( SignedBeaconCommitteeSelection::new(V1BeaconCommitteeSelection { slot, validator_index: val_idx, @@ -3941,7 +3934,7 @@ mod tests { component.register_await_agg_sig_db(move |_duty, _pk| { let agg = agg_clone.clone(); async move { - Ok::, CallbackError>(Box::new( + Ok::(SignedData::from( SignedSyncCommitteeSelection::new(agg), )) } @@ -3999,7 +3992,7 @@ mod tests { _ => 999, }; async move { - Ok::, CallbackError>(Box::new( + Ok::(SignedData::from( SignedSyncCommitteeSelection::new(V1SyncCommitteeSelection { slot, validator_index: val_idx, @@ -4143,7 +4136,7 @@ mod tests { // `PrepareAggregator` duty, which `downcast_beacon_committee_selection` // cannot satisfy. component.register_await_agg_sig_db(|_duty, _pk| async { - Ok::, CallbackError>(Box::new(SignedSyncCommitteeSelection::new( + Ok::(SignedData::from(SignedSyncCommitteeSelection::new( V1SyncCommitteeSelection { slot: 1, validator_index: 1, @@ -4173,7 +4166,7 @@ mod tests { make_selections_component_insecure(HashMap::from([(1u64, dv_root)])).await; component.register_await_agg_sig_db(|_duty, _pk| async { - Ok::, CallbackError>(Box::new(SignedBeaconCommitteeSelection::new( + Ok::(SignedData::from(SignedBeaconCommitteeSelection::new( V1BeaconCommitteeSelection { slot: 1, validator_index: 1, @@ -6742,8 +6735,9 @@ mod tests { duty, Duty::new_prepare_aggregator_duty(SlotNumber::new(SLOT)) ); - Ok(Box::new(SignedBeaconCommitteeSelection::new(aggregated)) - as Box) + Ok(SignedData::from(SignedBeaconCommitteeSelection::new( + aggregated, + ))) } }); @@ -6776,7 +6770,7 @@ mod tests { // The AggSigDB hook is checked first; register it so the test reaches // the validator-not-found path. component.register_await_agg_sig_db(|_duty, _pk| async { - Err::, _>("unused".into()) + Err::("unused".into()) }); let selection = Eth2BeaconCommitteeSelection { slot: 9, @@ -6802,13 +6796,13 @@ mod tests { // Echo back a selection keyed by the requested pubkey's slot is not // available here; return a fixed aggregated selection. let _ = pk; - Ok(Box::new(SignedBeaconCommitteeSelection::new( + Ok(SignedData::from(SignedBeaconCommitteeSelection::new( Eth2BeaconCommitteeSelection { slot: 0, validator_index: 0, selection_proof: [0xCD; 96], }, - )) as Box) + ))) }); let selections = vec![ diff --git a/crates/dkg/src/aggregate.rs b/crates/dkg/src/aggregate.rs index 6baa3bfd..f3d07687 100644 --- a/crates/dkg/src/aggregate.rs +++ b/crates/dkg/src/aggregate.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use pluto_core::{ signeddata::{SignedDataError, VersionedSignedValidatorRegistration}, - types::{ParSignedData, PubKey, SignedData}, + types::{ParSignedData, PubKey}, }; use pluto_crypto::{ tbls, diff --git a/crates/dkg/src/validators.rs b/crates/dkg/src/validators.rs index 176a7330..082a478e 100644 --- a/crates/dkg/src/validators.rs +++ b/crates/dkg/src/validators.rs @@ -5,10 +5,7 @@ use pluto_cluster::{ distvalidator::DistValidator, registration::{BuilderRegistration, Registration}, }; -use pluto_core::{ - signeddata::{SignedDataError, VersionedSignedValidatorRegistration}, - types::SignedData, -}; +use pluto_core::signeddata::{SignedDataError, VersionedSignedValidatorRegistration}; use pluto_eth2api::{spec::phase0, v1, versioned}; use crate::share::{Share, ShareMsg}; diff --git a/crates/parsigex/src/behaviour.rs b/crates/parsigex/src/behaviour.rs index caa9590a..b593aee8 100644 --- a/crates/parsigex/src/behaviour.rs +++ b/crates/parsigex/src/behaviour.rs @@ -79,13 +79,14 @@ pub fn new_eth2_verifier( .get(&par_signed_data.share_idx) .ok_or(VerifyError::InvalidShareIndex)?; - // `verify_eth2_signed_data` takes an already-upcast - // `&dyn Eth2SignedData`; the upcast failure (Charon's + // `verify_eth2_signed_data` takes an already-narrowed + // `Eth2SignedData`; the narrowing failure (Charon's // `data.(core.Eth2SignedData)` type assertion) maps to the // "invalid signed data family" error. - let eth2_data = - eth2signeddata::as_eth2_signed_data(par_signed_data.signed_data.as_ref()) - .ok_or(VerifyError::InvalidSignedDataFamily)?; + let eth2_data = par_signed_data + .signed_data + .as_eth2_signed_data() + .ok_or(VerifyError::InvalidSignedDataFamily)?; eth2signeddata::verify_eth2_signed_data(ð2_cl, eth2_data, pubshare) .await @@ -639,16 +640,14 @@ mod eth2_verifier_tests { /// Signs the eth2 signing root of `data` for the given domain/epoch with /// `secret`, returning a copy of `data` carrying that signature. - async fn sign( + async fn sign( client: &EthBeaconNodeApiClient, secret: &PrivateKey, - data: &T, + data: impl Into, domain: DomainName, epoch: phase0::Epoch, - ) -> T - where - T: SignedData + Sized, - { + ) -> SignedData { + let data: SignedData = data.into(); let message_root = data.message_root().unwrap(); let signing_root = get_data_root(client, domain, epoch, message_root) .await @@ -687,7 +686,7 @@ mod eth2_verifier_tests { let signed = sign( client, &shares[&share_idx], - &att, + att, DomainName::BeaconAttester, 4, ) @@ -715,7 +714,7 @@ mod eth2_verifier_tests { // Sign with share 2's secret but claim share index 3, so the verifier // looks up share 3's public key and the signature fails to verify. let att = sample_attestation(4); - let signed = sign(client, &shares[&2], &att, DomainName::BeaconAttester, 4).await; + let signed = sign(client, &shares[&2], att, DomainName::BeaconAttester, 4).await; let par = ParSignedData::new(signed, 3); let mut pub_shares_by_key = HashMap::new(); @@ -739,7 +738,7 @@ mod eth2_verifier_tests { let (shares, _pub_shares) = split_shares(&secret); let att = sample_attestation(4); - let signed = sign(client, &shares[&1], &att, DomainName::BeaconAttester, 4).await; + let signed = sign(client, &shares[&1], att, DomainName::BeaconAttester, 4).await; let par = ParSignedData::new(signed, 1); // Empty map: the validator public key is not part of the cluster lock. @@ -763,7 +762,7 @@ mod eth2_verifier_tests { let (shares, pub_shares) = split_shares(&secret); let att = sample_attestation(4); - let signed = sign(client, &shares[&1], &att, DomainName::BeaconAttester, 4).await; + let signed = sign(client, &shares[&1], att, DomainName::BeaconAttester, 4).await; // Claim a share index that was never produced by the split. let par = ParSignedData::new(signed, TOTAL_SHARES + 1); diff --git a/deny.toml b/deny.toml index 5a520514..bf132eb8 100644 --- a/deny.toml +++ b/deny.toml @@ -66,7 +66,6 @@ allow = [ "Xnet", "Zlib", ] -exceptions = [{ crate = "dyn-eq", allow = ["MPL-2.0"] }] confidence-threshold = 0.8 unused-allowed-license = "allow"