diff --git a/.gitignore b/.gitignore index 77511310..a9086e04 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,6 @@ lcov.info coverage.json .charon* +.omx .peerinfo* diff --git a/Cargo.lock b/Cargo.lock index 33def73b..670e5e68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2540,6 +2540,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "ethereum_ssz_derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cd82c68120c89361e1a457245cf212f7d9f541bffaffed530c8f2d54a160b2" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -2574,9 +2586,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fastrlp" @@ -5529,6 +5541,7 @@ dependencies = [ "crossbeam", "dyn-clone", "dyn-eq", + "ethereum_ssz", "futures", "futures-timer", "hex", @@ -5627,6 +5640,8 @@ dependencies = [ "anyhow", "bon", "chrono", + "ethereum_ssz", + "ethereum_ssz_derive", "hex", "http", "oas3-gen-support", @@ -5816,6 +5831,7 @@ dependencies = [ name = "pluto-ssz" version = "1.7.1" dependencies = [ + "ethereum_ssz", "hex", "k256", "serde", diff --git a/Cargo.toml b/Cargo.toml index a230786f..829503b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,8 @@ oas3-gen-support = "0.24" bon = "3.8" testcontainers = "0.27" test-case = "3.3" +ethereum_ssz = { version = "0.10" } +ethereum_ssz_derive = { version = "0.10" } tree_hash = "0.12" tree_hash_derive = "0.12" tar = "0.4" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ec4407c4..36910c25 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -8,6 +8,7 @@ publish.workspace = true [dependencies] async-trait.workspace = true +ethereum_ssz.workspace = true cancellation.workspace = true chrono.workspace = true crossbeam.workspace = true @@ -31,6 +32,7 @@ tokio.workspace = true tokio-util.workspace = true tracing.workspace = true pluto-eth2util.workspace = true +pluto-ssz.workspace = true tree_hash.workspace = true unsigned-varint.workspace = true @@ -53,7 +55,6 @@ pluto-testutil.workspace = true pluto-tracing.workspace = true tokio = { workspace = true, features = ["test-util"] } wiremock.workspace = true -pluto-ssz.workspace = true [build-dependencies] pluto-build-proto.workspace = true diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 2e356963..1076ade4 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -27,6 +27,11 @@ pub mod deadline; pub mod parsigdb; mod parsigex_codec; +// SSZ codec operates on compile-time-constant byte sizes and offsets. +// Arithmetic is bounded and casts from `usize` to `u32` are safe because all +// sizes are well below `u32::MAX`. +#[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] +pub(crate) mod ssz_codec; pub use parsigex_codec::ParSigExCodecError; diff --git a/crates/core/src/parsigex_codec.rs b/crates/core/src/parsigex_codec.rs index e3963dc5..dfd86454 100644 --- a/crates/core/src/parsigex_codec.rs +++ b/crates/core/src/parsigex_codec.rs @@ -1,4 +1,9 @@ //! Partial signature exchange codec helpers used by core types. +//! +//! Implements Charon-compatible `marshal`/`unmarshal` semantics: SSZ-capable +//! types are serialized as SSZ binary; all other types use JSON. On +//! deserialization the codec checks for a JSON `{` prefix first — if present, +//! it decodes as JSON. Otherwise it tries SSZ for SSZ-capable types. use std::any::Any; @@ -6,9 +11,11 @@ use crate::{ signeddata::{ Attestation, BeaconCommitteeSelection, SignedAggregateAndProof, SignedRandao, SignedSyncContributionAndProof, SignedSyncMessage, SignedVoluntaryExit, - SyncCommitteeSelection, VersionedAttestation, VersionedSignedAggregateAndProof, - VersionedSignedProposal, VersionedSignedValidatorRegistration, + SyncCommitteeSelection, SyncContributionAndProof, VersionedAttestation, + VersionedSignedAggregateAndProof, VersionedSignedProposal, + VersionedSignedValidatorRegistration, }, + ssz_codec, types::{DutyType, Signature, SignedData}, }; @@ -47,15 +54,70 @@ pub enum ParSigExCodecError { #[error("invalid share index")] InvalidShareIndex, - /// Serialization failed. + /// JSON serialization failed. #[error("marshal signed data: {0}")] Serialize(#[from] serde_json::Error), + + /// SSZ codec error. + #[error("ssz codec: {0}")] + SszCodec(#[from] ssz_codec::SszCodecError), + + /// Signed data construction error. + #[error("signed data: {0}")] + SignedData(String), } pub(crate) fn serialize_signed_data(data: &dyn SignedData) -> Result, ParSigExCodecError> { let any = data as &dyn Any; - macro_rules! serialize_as { + // --------------------------------------------------------------- + // 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::ContributionAndProof (non-versioned, all fixed) + if let Some(value) = any.downcast_ref::() { + return Ok(ssz_codec::encode_contribution_and_proof(&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)?); @@ -63,19 +125,17 @@ pub(crate) fn serialize_signed_data(data: &dyn SignedData) -> Result, Pa }; } - serialize_as!(Attestation); - serialize_as!(VersionedAttestation); - serialize_as!(VersionedSignedProposal); - serialize_as!(VersionedSignedValidatorRegistration); - serialize_as!(SignedVoluntaryExit); - serialize_as!(SignedRandao); - serialize_as!(Signature); - serialize_as!(BeaconCommitteeSelection); - serialize_as!(SignedAggregateAndProof); - serialize_as!(VersionedSignedAggregateAndProof); - serialize_as!(SignedSyncMessage); - serialize_as!(SyncCommitteeSelection); - serialize_as!(SignedSyncContributionAndProof); + // VersionedSignedProposal (versioned header + inner SSZ) + if let Some(value) = any.downcast_ref::() { + return Ok(ssz_codec::encode_versioned_signed_proposal(&value.0)?); + } + + serialize_json!(VersionedSignedValidatorRegistration); + serialize_json!(SignedVoluntaryExit); + serialize_json!(SignedRandao); + serialize_json!(Signature); + serialize_json!(BeaconCommitteeSelection); + serialize_json!(SyncCommitteeSelection); Err(ParSigExCodecError::UnsupportedDutyType) } @@ -84,6 +144,12 @@ pub(crate) fn deserialize_signed_data( duty_type: &DutyType, bytes: &[u8], ) -> Result, ParSigExCodecError> { + /// Returns `true` when the trimmed byte slice starts with `{`, indicating + /// JSON data. + fn looks_like_json(bytes: &[u8]) -> bool { + bytes.iter().find(|b| !b.is_ascii_whitespace()).copied() == Some(b'{') + } + macro_rules! deserialize_json { ($ty:ty) => { serde_json::from_slice::<$ty>(bytes) @@ -92,27 +158,327 @@ pub(crate) fn deserialize_signed_data( }; } + // Core logic matching Go's `unmarshal`: + // - If data starts with `{`, it is JSON — skip SSZ, decode as JSON. + // - Otherwise, try SSZ decode for SSZ-capable types. + let is_json = looks_like_json(bytes); + match duty_type { - // Match Go order: old Attestation format first, then VersionedAttestation. - DutyType::Attester => deserialize_json!(Attestation) - .or_else(|_| deserialize_json!(VersionedAttestation)) - .map_err(|_| ParSigExCodecError::UnsupportedDutyType), - DutyType::Proposer => deserialize_json!(VersionedSignedProposal), + // -- Attester: SSZ-capable (non-versioned + versioned) -- + DutyType::Attester => { + if is_json { + return deserialize_json!(Attestation) + .or_else(|_| deserialize_json!(VersionedAttestation)); + } + // Try SSZ non-versioned Attestation first. + if let Ok(att) = ssz_codec::decode_phase0_attestation(bytes) { + return Ok(Box::new(Attestation::new(att))); + } + // 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)); + } + Err(ParSigExCodecError::UnsupportedDutyType) + } + + // -- Proposer: SSZ-capable (versioned header + inner SSZ) -- + DutyType::Proposer => { + if is_json { + return deserialize_json!(VersionedSignedProposal); + } + 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)); + } + Err(ParSigExCodecError::UnsupportedDutyType) + } + DutyType::BuilderProposer => Err(ParSigExCodecError::DeprecatedBuilderProposer), + + // -- BuilderRegistration: JSON-only -- DutyType::BuilderRegistration => deserialize_json!(VersionedSignedValidatorRegistration), + + // -- Exit: JSON-only -- DutyType::Exit => deserialize_json!(SignedVoluntaryExit), + + // -- Randao: JSON-only -- DutyType::Randao => deserialize_json!(SignedRandao), + + // -- Signature: JSON-only -- DutyType::Signature => deserialize_json!(Signature), + + // -- PrepareAggregator: JSON-only -- DutyType::PrepareAggregator => deserialize_json!(BeaconCommitteeSelection), - // Match Go order: old SignedAggregateAndProof format first, then versioned. - DutyType::Aggregator => deserialize_json!(SignedAggregateAndProof) - .or_else(|_| deserialize_json!(VersionedSignedAggregateAndProof)) - .map_err(|_| ParSigExCodecError::UnsupportedDutyType), - DutyType::SyncMessage => deserialize_json!(SignedSyncMessage), + + // -- Aggregator: SSZ-capable (non-versioned + versioned) -- + DutyType::Aggregator => { + if is_json { + return deserialize_json!(SignedAggregateAndProof) + .or_else(|_| deserialize_json!(VersionedSignedAggregateAndProof)); + } + // 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))); + } + // Try SSZ versioned. + if let Ok(va) = ssz_codec::decode_versioned_signed_aggregate_and_proof(bytes) { + return Ok(Box::new(VersionedSignedAggregateAndProof::new(va))); + } + Err(ParSigExCodecError::UnsupportedDutyType) + } + + // -- SyncMessage: SSZ-capable -- + DutyType::SyncMessage => { + if is_json { + return deserialize_json!(SignedSyncMessage); + } + if let Ok(msg) = ssz_codec::decode_sync_committee_message(bytes) { + return Ok(Box::new(SignedSyncMessage::new(msg))); + } + Err(ParSigExCodecError::UnsupportedDutyType) + } + + // -- PrepareSyncContribution: JSON-only -- DutyType::PrepareSyncContribution => deserialize_json!(SyncCommitteeSelection), - DutyType::SyncContribution => deserialize_json!(SignedSyncContributionAndProof), + + // -- SyncContribution: SSZ-capable -- + DutyType::SyncContribution => { + if is_json { + return deserialize_json!(SignedSyncContributionAndProof); + } + if let Ok(scp) = ssz_codec::decode_signed_contribution_and_proof(bytes) { + return Ok(Box::new(SignedSyncContributionAndProof::new(scp))); + } + Err(ParSigExCodecError::UnsupportedDutyType) + } + DutyType::Unknown | DutyType::InfoSync | DutyType::DutySentinel(_) => { Err(ParSigExCodecError::UnsupportedDutyType) } } } + +#[cfg(test)] +mod tests { + use super::*; + use pluto_eth2api::{ + spec::{altair, phase0}, + versioned, + }; + use pluto_ssz::{BitList, BitVector}; + + fn sample_attestation_data() -> phase0::AttestationData { + phase0::AttestationData { + slot: 42, + index: 7, + beacon_block_root: [0xaa; 32], + source: phase0::Checkpoint { + epoch: 10, + root: [0xbb; 32], + }, + target: phase0::Checkpoint { + epoch: 11, + root: [0xcc; 32], + }, + } + } + + /// 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() { + let att = Attestation::new(phase0::Attestation { + aggregation_bits: BitList::with_bits(8, &[0, 2]), + data: sample_attestation_data(), + signature: [0x11; 96], + }); + let bytes = serialize_signed_data(&att).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); + } + + /// SSZ-capable types: versioned attestation round-trip. + #[test] + fn marshal_unmarshal_ssz_versioned_attestation() { + let inner = versioned::VersionedAttestation { + version: versioned::DataVersion::Deneb, + validator_index: None, + attestation: Some(versioned::AttestationPayload::Deneb(phase0::Attestation { + aggregation_bits: BitList::with_bits(16, &[1, 3]), + data: sample_attestation_data(), + signature: [0x22; 96], + })), + }; + let va = VersionedAttestation::new(inner).unwrap(); + let bytes = serialize_signed_data(&va).unwrap(); + assert_ne!(bytes.first(), Some(&b'{')); + let decoded: VersionedAttestation = + downcast(deserialize_signed_data(&DutyType::Attester, &bytes).unwrap()); + assert_eq!(va, decoded); + } + + /// SSZ-capable types: SyncMessage round-trip. + #[test] + fn marshal_unmarshal_ssz_sync_message() { + let msg = SignedSyncMessage::new(altair::SyncCommitteeMessage { + slot: 100, + beacon_block_root: [0xdd; 32], + validator_index: 50, + signature: [0xee; 96], + }); + let bytes = serialize_signed_data(&msg).unwrap(); + assert_ne!(bytes.first(), Some(&b'{')); + let decoded: SignedSyncMessage = + downcast(deserialize_signed_data(&DutyType::SyncMessage, &bytes).unwrap()); + assert_eq!(msg, decoded); + } + + /// SSZ-capable types: SignedSyncContributionAndProof round-trip. + #[test] + fn marshal_unmarshal_ssz_signed_sync_contribution() { + let scp = SignedSyncContributionAndProof::new(altair::SignedContributionAndProof { + message: altair::ContributionAndProof { + aggregator_index: 33, + contribution: altair::SyncCommitteeContribution { + slot: 200, + beacon_block_root: [0xab; 32], + subcommittee_index: 2, + aggregation_bits: BitVector::with_bits(&[0, 5]), + signature: [0xcd; 96], + }, + selection_proof: [0xef; 96], + }, + signature: [0xfa; 96], + }); + let bytes = serialize_signed_data(&scp).unwrap(); + assert_ne!(bytes.first(), Some(&b'{')); + let decoded: SignedSyncContributionAndProof = + downcast(deserialize_signed_data(&DutyType::SyncContribution, &bytes).unwrap()); + assert_eq!(scp, decoded); + } + + /// SSZ-capable types: SignedAggregateAndProof round-trip. + #[test] + fn marshal_unmarshal_ssz_signed_aggregate_and_proof() { + let sap = SignedAggregateAndProof::new(phase0::SignedAggregateAndProof { + message: phase0::AggregateAndProof { + aggregator_index: 99, + aggregate: phase0::Attestation { + aggregation_bits: BitList::with_bits(8, &[2]), + data: sample_attestation_data(), + signature: [0x33; 96], + }, + selection_proof: [0x44; 96], + }, + signature: [0x55; 96], + }); + let bytes = serialize_signed_data(&sap).unwrap(); + assert_ne!(bytes.first(), Some(&b'{')); + let decoded: SignedAggregateAndProof = + downcast(deserialize_signed_data(&DutyType::Aggregator, &bytes).unwrap()); + assert_eq!(sap, decoded); + } + + /// SSZ-capable types: SyncContributionAndProof round-trip. + #[test] + fn marshal_unmarshal_ssz_sync_contribution_and_proof() { + let cap = SyncContributionAndProof::new(altair::ContributionAndProof { + aggregator_index: 33, + contribution: altair::SyncCommitteeContribution { + slot: 200, + beacon_block_root: [0xab; 32], + subcommittee_index: 2, + aggregation_bits: BitVector::with_bits(&[0, 5]), + signature: [0xcd; 96], + }, + selection_proof: [0xef; 96], + }); + let bytes = serialize_signed_data(&cap).unwrap(); + assert_ne!(bytes.first(), Some(&b'{')); + // SyncContribution duty uses SignedSyncContributionAndProof, but + // SyncContributionAndProof is encoded via encode_contribution_and_proof. + // The deserialize side for PrepareSyncContribution is JSON-only. + // Let's verify we can round-trip via the SSZ codec directly. + let decoded = ssz_codec::decode_contribution_and_proof(&bytes).unwrap(); + assert_eq!(cap.0, 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(); + // 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); + } + + /// JSON data can still be deserialized for SSZ-capable types (fallback). + #[test] + fn json_fallback_for_ssz_capable_attestation() { + let att = Attestation::new(phase0::Attestation { + aggregation_bits: BitList::with_bits(8, &[0]), + data: sample_attestation_data(), + signature: [0x11; 96], + }); + // Force JSON encoding. + 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); + } + + /// JSON data can still be deserialized for SSZ-capable SyncMessage + /// (fallback). + #[test] + fn json_fallback_for_ssz_capable_sync_message() { + let msg = SignedSyncMessage::new(altair::SyncCommitteeMessage { + slot: 5, + beacon_block_root: [0xaa; 32], + validator_index: 3, + 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); + } + + /// JSON data can still be deserialized for SSZ-capable Aggregator + /// (fallback). + #[test] + fn json_fallback_for_ssz_capable_aggregator() { + let sap = SignedAggregateAndProof::new(phase0::SignedAggregateAndProof { + message: phase0::AggregateAndProof { + aggregator_index: 1, + aggregate: phase0::Attestation { + aggregation_bits: BitList::with_bits(4, &[0]), + data: sample_attestation_data(), + signature: [0x11; 96], + }, + selection_proof: [0x22; 96], + }, + signature: [0x33; 96], + }); + 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); + } +} diff --git a/crates/core/src/ssz_codec.rs b/crates/core/src/ssz_codec.rs new file mode 100644 index 00000000..fc2e3ecd --- /dev/null +++ b/crates/core/src/ssz_codec.rs @@ -0,0 +1,762 @@ +//! SSZ binary encode/decode for SignedData types, matching Charon's +//! `marshal`/`unmarshal` behaviour. +//! +//! Charon serializes SSZ-capable types using SSZ binary encoding with custom +//! headers for versioned types. JSON-only types are handled elsewhere (see +//! [`parsigex_codec`](super::parsigex_codec)). + +use pluto_eth2api::{ + spec::{altair, bellatrix, capella, deneb, electra, fulu, phase0}, + versioned::{self, AttestationPayload, DataVersion, SignedAggregateAndProofPayload}, +}; +use pluto_ssz::{ + decode::{decode_u32, decode_u64}, + encode::{encode_u32, encode_u64}, +}; +use ssz::{Decode, Encode}; + +// --------------------------------------------------------------------------- +// Error +// --------------------------------------------------------------------------- + +/// Error type for SSZ codec operations. +#[derive(Debug, thiserror::Error)] +pub enum SszCodecError { + /// Byte slice is too short. + #[error("ssz too short: need {need} bytes, got {got}")] + TooShort { + /// Minimum required bytes. + need: usize, + /// Actual bytes available. + got: usize, + }, + /// An offset field has an unexpected value. + #[error("ssz invalid offset: expected {expected}, got {got}")] + InvalidOffset { + /// Expected offset value. + expected: u32, + /// Actual offset value. + got: u32, + }, + /// Unknown or unsupported data version. + #[error("ssz unknown version: {0}")] + UnknownVersion(u64), + /// Inner SSZ binary decoding failed. + #[error("ssz decode: {0}")] + Decode(String), +} + +impl From for SszCodecError { + fn from(e: pluto_ssz::SszBinaryError) -> Self { + Self::Decode(e.to_string()) + } +} + +impl From for SszCodecError { + fn from(e: ssz::DecodeError) -> Self { + Self::Decode(format!("{e:?}")) + } +} + +fn require(bytes: &[u8], need: usize) -> Result<(), SszCodecError> { + if bytes.len() < need { + Err(SszCodecError::TooShort { + need, + got: bytes.len(), + }) + } else { + Ok(()) + } +} + +// =========================================================================== +// Non-versioned SSZ-capable types +// =========================================================================== + +// Using ethereum_ssz derived Encode/Decode: just delegate to `as_ssz_bytes` +// and `from_ssz_bytes`. + +/// Encodes a `phase0::Attestation` to SSZ binary. +pub fn encode_phase0_attestation(att: &phase0::Attestation) -> Result, SszCodecError> { + Ok(att.as_ssz_bytes()) +} + +/// Decodes a `phase0::Attestation` from SSZ binary. +pub fn decode_phase0_attestation(bytes: &[u8]) -> Result { + Ok(phase0::Attestation::from_ssz_bytes(bytes)?) +} + +/// Encodes a `phase0::SignedAggregateAndProof` to SSZ binary. +pub fn encode_phase0_signed_aggregate_and_proof( + sap: &phase0::SignedAggregateAndProof, +) -> Result, SszCodecError> { + Ok(sap.as_ssz_bytes()) +} + +/// Decodes a `phase0::SignedAggregateAndProof` from SSZ binary. +pub fn decode_phase0_signed_aggregate_and_proof( + bytes: &[u8], +) -> Result { + Ok(phase0::SignedAggregateAndProof::from_ssz_bytes(bytes)?) +} + +/// Encodes an `altair::SyncCommitteeMessage` to SSZ binary. +pub fn encode_sync_committee_message( + msg: &altair::SyncCommitteeMessage, +) -> Result, SszCodecError> { + Ok(msg.as_ssz_bytes()) +} + +/// Decodes an `altair::SyncCommitteeMessage` from SSZ binary. +pub fn decode_sync_committee_message( + bytes: &[u8], +) -> Result { + Ok(altair::SyncCommitteeMessage::from_ssz_bytes(bytes)?) +} + +/// Encodes an `altair::ContributionAndProof` to SSZ binary. +pub fn encode_contribution_and_proof( + cap: &altair::ContributionAndProof, +) -> Result, SszCodecError> { + Ok(cap.as_ssz_bytes()) +} + +/// Decodes an `altair::ContributionAndProof` from SSZ binary. +#[cfg(test)] +pub fn decode_contribution_and_proof( + bytes: &[u8], +) -> Result { + Ok(altair::ContributionAndProof::from_ssz_bytes(bytes)?) +} + +/// Encodes an `altair::SignedContributionAndProof` to SSZ binary. +pub fn encode_signed_contribution_and_proof( + scp: &altair::SignedContributionAndProof, +) -> Result, SszCodecError> { + Ok(scp.as_ssz_bytes()) +} + +/// Decodes an `altair::SignedContributionAndProof` from SSZ binary. +pub fn decode_signed_contribution_and_proof( + bytes: &[u8], +) -> Result { + Ok(altair::SignedContributionAndProof::from_ssz_bytes(bytes)?) +} + +// =========================================================================== +// Versioned type helpers +// =========================================================================== + +fn encode_version(version: DataVersion) -> Result<[u8; 8], SszCodecError> { + version + .to_legacy_u64() + .map(encode_u64) + .map_err(|_| SszCodecError::UnknownVersion(0)) +} + +fn decode_version(bytes: &[u8]) -> Result { + let raw = decode_u64(bytes)?; + DataVersion::from_legacy_u64(raw).map_err(|_| SszCodecError::UnknownVersion(raw)) +} + +// --------------------------------------------------------------------------- +// VersionedAttestation +// Header: version(8) + offset(4) = 12 bytes (same as other versioned types) +// --------------------------------------------------------------------------- + +/// Encodes a `VersionedAttestation` to SSZ binary with Charon versioned +/// header. +pub fn encode_versioned_attestation( + va: &versioned::VersionedAttestation, +) -> Result, SszCodecError> { + let version = encode_version(va.version)?; + let inner = match &va.attestation { + Some(AttestationPayload::Phase0(att)) + | Some(AttestationPayload::Altair(att)) + | Some(AttestationPayload::Bellatrix(att)) + | Some(AttestationPayload::Capella(att)) + | Some(AttestationPayload::Deneb(att)) => att.as_ssz_bytes(), + Some(AttestationPayload::Electra(att)) | Some(AttestationPayload::Fulu(att)) => { + att.as_ssz_bytes() + } + None => { + return Err(SszCodecError::Decode( + "missing attestation payload".to_string(), + )); + } + }; + + let mut buf = Vec::with_capacity(VERSIONED_SIGNED_AGGREGATE_HEADER as usize + inner.len()); + buf.extend_from_slice(&version); + buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_AGGREGATE_HEADER)); + buf.extend_from_slice(&inner); + Ok(buf) +} + +/// Decodes a `VersionedAttestation` from SSZ binary with Charon versioned +/// header. +pub fn decode_versioned_attestation( + bytes: &[u8], +) -> Result { + require(bytes, VERSIONED_SIGNED_AGGREGATE_HEADER as usize)?; + let version = decode_version(&bytes[0..8])?; + let offset = decode_u32(&bytes[8..12])?; + if offset != VERSIONED_SIGNED_AGGREGATE_HEADER { + return Err(SszCodecError::InvalidOffset { + expected: VERSIONED_SIGNED_AGGREGATE_HEADER, + got: offset, + }); + } + + let inner = &bytes[VERSIONED_SIGNED_AGGREGATE_HEADER as usize..]; + let attestation = match version { + DataVersion::Phase0 => { + AttestationPayload::Phase0(phase0::Attestation::from_ssz_bytes(inner)?) + } + DataVersion::Altair => { + AttestationPayload::Altair(phase0::Attestation::from_ssz_bytes(inner)?) + } + DataVersion::Bellatrix => { + AttestationPayload::Bellatrix(phase0::Attestation::from_ssz_bytes(inner)?) + } + DataVersion::Capella => { + AttestationPayload::Capella(phase0::Attestation::from_ssz_bytes(inner)?) + } + DataVersion::Deneb => { + AttestationPayload::Deneb(phase0::Attestation::from_ssz_bytes(inner)?) + } + DataVersion::Electra => { + AttestationPayload::Electra(electra::Attestation::from_ssz_bytes(inner)?) + } + DataVersion::Fulu => AttestationPayload::Fulu(electra::Attestation::from_ssz_bytes(inner)?), + DataVersion::Unknown => return Err(SszCodecError::UnknownVersion(0)), + }; + + Ok(versioned::VersionedAttestation { + version, + validator_index: None, + attestation: Some(attestation), + }) +} + +// --------------------------------------------------------------------------- +// VersionedSignedAggregateAndProof +// Header: version(8) + offset(4) = 12 bytes +// --------------------------------------------------------------------------- + +const VERSIONED_SIGNED_AGGREGATE_HEADER: u32 = 12; + +/// Encodes a `VersionedSignedAggregateAndProof` to SSZ binary with Charon +/// versioned header. +pub fn encode_versioned_signed_aggregate_and_proof( + va: &versioned::VersionedSignedAggregateAndProof, +) -> Result, SszCodecError> { + let version = encode_version(va.version)?; + let inner = match &va.aggregate_and_proof { + SignedAggregateAndProofPayload::Phase0(p) + | SignedAggregateAndProofPayload::Altair(p) + | SignedAggregateAndProofPayload::Bellatrix(p) + | SignedAggregateAndProofPayload::Capella(p) + | SignedAggregateAndProofPayload::Deneb(p) => p.as_ssz_bytes(), + SignedAggregateAndProofPayload::Electra(p) | SignedAggregateAndProofPayload::Fulu(p) => { + p.as_ssz_bytes() + } + }; + + let mut buf = Vec::with_capacity(VERSIONED_SIGNED_AGGREGATE_HEADER as usize + inner.len()); + buf.extend_from_slice(&version); + buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_AGGREGATE_HEADER)); + buf.extend_from_slice(&inner); + Ok(buf) +} + +/// Decodes a `VersionedSignedAggregateAndProof` from SSZ binary with Charon +/// versioned header. +pub fn decode_versioned_signed_aggregate_and_proof( + bytes: &[u8], +) -> Result { + require(bytes, VERSIONED_SIGNED_AGGREGATE_HEADER as usize)?; + let version = decode_version(&bytes[0..8])?; + let offset = decode_u32(&bytes[8..12])?; + if offset != VERSIONED_SIGNED_AGGREGATE_HEADER { + return Err(SszCodecError::InvalidOffset { + expected: VERSIONED_SIGNED_AGGREGATE_HEADER, + got: offset, + }); + } + + let inner = &bytes[VERSIONED_SIGNED_AGGREGATE_HEADER as usize..]; + let payload = match version { + DataVersion::Phase0 => SignedAggregateAndProofPayload::Phase0( + phase0::SignedAggregateAndProof::from_ssz_bytes(inner)?, + ), + DataVersion::Altair => SignedAggregateAndProofPayload::Altair( + phase0::SignedAggregateAndProof::from_ssz_bytes(inner)?, + ), + DataVersion::Bellatrix => SignedAggregateAndProofPayload::Bellatrix( + phase0::SignedAggregateAndProof::from_ssz_bytes(inner)?, + ), + DataVersion::Capella => SignedAggregateAndProofPayload::Capella( + phase0::SignedAggregateAndProof::from_ssz_bytes(inner)?, + ), + DataVersion::Deneb => SignedAggregateAndProofPayload::Deneb( + phase0::SignedAggregateAndProof::from_ssz_bytes(inner)?, + ), + DataVersion::Electra => SignedAggregateAndProofPayload::Electra( + electra::SignedAggregateAndProof::from_ssz_bytes(inner)?, + ), + DataVersion::Fulu => SignedAggregateAndProofPayload::Fulu( + electra::SignedAggregateAndProof::from_ssz_bytes(inner)?, + ), + DataVersion::Unknown => return Err(SszCodecError::UnknownVersion(0)), + }; + + Ok(versioned::VersionedSignedAggregateAndProof { + version, + aggregate_and_proof: payload, + }) +} + +// --------------------------------------------------------------------------- +// VersionedSignedProposal +// Header: version(8) + blinded(1) + offset(4) = 13 bytes +// --------------------------------------------------------------------------- + +const VERSIONED_SIGNED_PROPOSAL_HEADER: u32 = 13; + +/// Encodes a `VersionedSignedProposal` to SSZ binary with Charon versioned +/// header. +pub fn encode_versioned_signed_proposal( + vp: &versioned::VersionedSignedProposal, +) -> Result, SszCodecError> { + let version = encode_version(vp.version)?; + let blinded: u8 = u8::from(vp.blinded); + let inner = encode_proposal_block(&vp.block)?; + + let mut buf = Vec::with_capacity(VERSIONED_SIGNED_PROPOSAL_HEADER as usize + inner.len()); + buf.extend_from_slice(&version); + buf.push(blinded); + buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_PROPOSAL_HEADER)); + buf.extend_from_slice(&inner); + Ok(buf) +} + +/// Decodes a `VersionedSignedProposal` from SSZ binary with Charon versioned +/// header. +pub fn decode_versioned_signed_proposal( + bytes: &[u8], +) -> Result { + require(bytes, VERSIONED_SIGNED_PROPOSAL_HEADER as usize)?; + let version = decode_version(&bytes[0..8])?; + let blinded = bytes[8] != 0; + let offset = decode_u32(&bytes[9..13])?; + if offset != VERSIONED_SIGNED_PROPOSAL_HEADER { + return Err(SszCodecError::InvalidOffset { + expected: VERSIONED_SIGNED_PROPOSAL_HEADER, + got: offset, + }); + } + + let inner = &bytes[VERSIONED_SIGNED_PROPOSAL_HEADER as usize..]; + let block = decode_proposal_block(version, blinded, inner)?; + + Ok(versioned::VersionedSignedProposal { + version, + blinded, + block, + }) +} + +fn encode_proposal_block(block: &versioned::SignedProposalBlock) -> Result, SszCodecError> { + use versioned::SignedProposalBlock; + Ok(match block { + SignedProposalBlock::Phase0(b) => b.as_ssz_bytes(), + SignedProposalBlock::Altair(b) => b.as_ssz_bytes(), + SignedProposalBlock::Bellatrix(b) => b.as_ssz_bytes(), + SignedProposalBlock::BellatrixBlinded(b) => b.as_ssz_bytes(), + SignedProposalBlock::Capella(b) => b.as_ssz_bytes(), + SignedProposalBlock::CapellaBlinded(b) => b.as_ssz_bytes(), + SignedProposalBlock::Deneb(b) => b.as_ssz_bytes(), + SignedProposalBlock::DenebBlinded(b) => b.as_ssz_bytes(), + SignedProposalBlock::Electra(b) => b.as_ssz_bytes(), + SignedProposalBlock::ElectraBlinded(b) => b.as_ssz_bytes(), + SignedProposalBlock::Fulu(b) => b.as_ssz_bytes(), + SignedProposalBlock::FuluBlinded(b) => b.as_ssz_bytes(), + }) +} + +fn decode_proposal_block( + version: DataVersion, + blinded: bool, + bytes: &[u8], +) -> Result { + use versioned::SignedProposalBlock; + Ok(match (version, blinded) { + (DataVersion::Phase0, _) => { + SignedProposalBlock::Phase0(phase0::SignedBeaconBlock::from_ssz_bytes(bytes)?) + } + (DataVersion::Altair, _) => { + SignedProposalBlock::Altair(altair::SignedBeaconBlock::from_ssz_bytes(bytes)?) + } + (DataVersion::Bellatrix, false) => { + SignedProposalBlock::Bellatrix(bellatrix::SignedBeaconBlock::from_ssz_bytes(bytes)?) + } + (DataVersion::Bellatrix, true) => SignedProposalBlock::BellatrixBlinded( + bellatrix::SignedBlindedBeaconBlock::from_ssz_bytes(bytes)?, + ), + (DataVersion::Capella, false) => { + SignedProposalBlock::Capella(capella::SignedBeaconBlock::from_ssz_bytes(bytes)?) + } + (DataVersion::Capella, true) => SignedProposalBlock::CapellaBlinded( + capella::SignedBlindedBeaconBlock::from_ssz_bytes(bytes)?, + ), + (DataVersion::Deneb, false) => { + SignedProposalBlock::Deneb(deneb::SignedBlockContents::from_ssz_bytes(bytes)?) + } + (DataVersion::Deneb, true) => SignedProposalBlock::DenebBlinded( + deneb::SignedBlindedBeaconBlock::from_ssz_bytes(bytes)?, + ), + (DataVersion::Electra, false) => { + SignedProposalBlock::Electra(electra::SignedBlockContents::from_ssz_bytes(bytes)?) + } + (DataVersion::Electra, true) => SignedProposalBlock::ElectraBlinded( + electra::SignedBlindedBeaconBlock::from_ssz_bytes(bytes)?, + ), + (DataVersion::Fulu, false) => { + SignedProposalBlock::Fulu(fulu::SignedBlockContents::from_ssz_bytes(bytes)?) + } + (DataVersion::Fulu, true) => SignedProposalBlock::FuluBlinded( + electra::SignedBlindedBeaconBlock::from_ssz_bytes(bytes)?, + ), + (DataVersion::Unknown, _) => return Err(SszCodecError::UnknownVersion(0)), + }) +} + +// =========================================================================== +// Tests +// =========================================================================== + +#[cfg(test)] +mod tests { + use super::*; + use pluto_ssz::{BitList, BitVector}; + + fn sample_attestation_data() -> phase0::AttestationData { + phase0::AttestationData { + slot: 42, + index: 7, + beacon_block_root: [0xaa; 32], + source: phase0::Checkpoint { + epoch: 10, + root: [0xbb; 32], + }, + target: phase0::Checkpoint { + epoch: 11, + root: [0xcc; 32], + }, + } + } + + #[test] + fn roundtrip_phase0_attestation() { + let att = phase0::Attestation { + aggregation_bits: BitList::with_bits(16, &[0, 3, 7]), + data: sample_attestation_data(), + signature: [0x11; 96], + }; + let encoded = encode_phase0_attestation(&att).unwrap(); + let decoded = decode_phase0_attestation(&encoded).unwrap(); + assert_eq!(att, decoded); + } + + #[test] + fn roundtrip_electra_attestation() { + let att = electra::Attestation { + aggregation_bits: BitList::with_bits(32, &[1, 5, 10]), + data: sample_attestation_data(), + signature: [0x22; 96], + committee_bits: BitVector::with_bits(&[0, 3]), + }; + let encoded = att.as_ssz_bytes(); + let decoded = electra::Attestation::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(att, decoded); + } + + #[test] + fn roundtrip_phase0_signed_aggregate_and_proof() { + let sap = phase0::SignedAggregateAndProof { + message: phase0::AggregateAndProof { + aggregator_index: 99, + aggregate: phase0::Attestation { + aggregation_bits: BitList::with_bits(8, &[2, 4]), + data: sample_attestation_data(), + signature: [0x33; 96], + }, + selection_proof: [0x44; 96], + }, + signature: [0x55; 96], + }; + let encoded = encode_phase0_signed_aggregate_and_proof(&sap).unwrap(); + let decoded = decode_phase0_signed_aggregate_and_proof(&encoded).unwrap(); + assert_eq!(sap, decoded); + } + + #[test] + fn roundtrip_sync_committee_message() { + let msg = altair::SyncCommitteeMessage { + slot: 100, + beacon_block_root: [0xdd; 32], + validator_index: 50, + signature: [0xee; 96], + }; + let encoded = encode_sync_committee_message(&msg).unwrap(); + let decoded = decode_sync_committee_message(&encoded).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn roundtrip_contribution_and_proof() { + let cap = altair::ContributionAndProof { + aggregator_index: 33, + contribution: altair::SyncCommitteeContribution { + slot: 200, + beacon_block_root: [0xab; 32], + subcommittee_index: 2, + aggregation_bits: BitVector::with_bits(&[0, 5]), + signature: [0xcd; 96], + }, + selection_proof: [0xef; 96], + }; + let encoded = encode_contribution_and_proof(&cap).unwrap(); + let decoded = decode_contribution_and_proof(&encoded).unwrap(); + assert_eq!(cap, decoded); + } + + #[test] + fn roundtrip_signed_contribution_and_proof() { + let scp = altair::SignedContributionAndProof { + message: altair::ContributionAndProof { + aggregator_index: 33, + contribution: altair::SyncCommitteeContribution { + slot: 200, + beacon_block_root: [0xab; 32], + subcommittee_index: 2, + aggregation_bits: BitVector::with_bits(&[0, 5]), + signature: [0xcd; 96], + }, + selection_proof: [0xef; 96], + }, + signature: [0xfa; 96], + }; + let encoded = encode_signed_contribution_and_proof(&scp).unwrap(); + let decoded = decode_signed_contribution_and_proof(&encoded).unwrap(); + assert_eq!(scp, decoded); + } + + #[test] + fn roundtrip_versioned_attestation_phase0() { + let va = versioned::VersionedAttestation { + version: DataVersion::Phase0, + validator_index: None, + attestation: Some(AttestationPayload::Phase0(phase0::Attestation { + aggregation_bits: BitList::with_bits(8, &[1, 3]), + data: sample_attestation_data(), + signature: [0x11; 96], + })), + }; + let encoded = encode_versioned_attestation(&va).unwrap(); + let decoded = decode_versioned_attestation(&encoded).unwrap(); + assert_eq!(va, decoded); + } + + #[test] + fn roundtrip_versioned_attestation_electra() { + let va = versioned::VersionedAttestation { + version: DataVersion::Electra, + validator_index: None, + attestation: Some(AttestationPayload::Electra(electra::Attestation { + aggregation_bits: BitList::with_bits(16, &[0, 4]), + data: sample_attestation_data(), + signature: [0x22; 96], + committee_bits: BitVector::with_bits(&[1]), + })), + }; + let encoded = encode_versioned_attestation(&va).unwrap(); + let decoded = decode_versioned_attestation(&encoded).unwrap(); + assert_eq!(va, decoded); + } + + #[test] + fn roundtrip_versioned_signed_aggregate_phase0() { + let va = versioned::VersionedSignedAggregateAndProof { + version: DataVersion::Phase0, + aggregate_and_proof: SignedAggregateAndProofPayload::Phase0( + phase0::SignedAggregateAndProof { + message: phase0::AggregateAndProof { + aggregator_index: 55, + aggregate: phase0::Attestation { + aggregation_bits: BitList::with_bits(4, &[0]), + data: sample_attestation_data(), + signature: [0xaa; 96], + }, + selection_proof: [0xbb; 96], + }, + signature: [0xcc; 96], + }, + ), + }; + let encoded = encode_versioned_signed_aggregate_and_proof(&va).unwrap(); + let decoded = decode_versioned_signed_aggregate_and_proof(&encoded).unwrap(); + assert_eq!(va, decoded); + } + + #[test] + fn roundtrip_versioned_signed_proposal_phase0() { + let block = phase0::SignedBeaconBlock { + message: phase0::BeaconBlock { + slot: 1, + proposer_index: 2, + parent_root: [0x11; 32], + state_root: [0x22; 32], + body: phase0::BeaconBlockBody { + randao_reveal: [0x33; 96], + eth1_data: phase0::ETH1Data { + deposit_root: [0x44; 32], + deposit_count: 0, + block_hash: [0x55; 32], + }, + graffiti: [0x66; 32], + proposer_slashings: vec![].into(), + attester_slashings: vec![].into(), + attestations: vec![].into(), + deposits: vec![].into(), + voluntary_exits: vec![].into(), + }, + }, + signature: [0x77; 96], + }; + let vp = versioned::VersionedSignedProposal { + version: DataVersion::Phase0, + blinded: false, + block: versioned::SignedProposalBlock::Phase0(block), + }; + let encoded = encode_versioned_signed_proposal(&vp).unwrap(); + let decoded = decode_versioned_signed_proposal(&encoded).unwrap(); + assert_eq!(vp, decoded); + } + + // ======================================================================= + // Go Charon fixture compatibility tests + // ======================================================================= + + /// Reads a hex-encoded SSZ fixture generated by Go Charon. + fn read_go_fixture(name: &str) -> Vec { + let path = format!( + "{}/testdata/ssz/{}.ssz.hex", + env!("CARGO_MANIFEST_DIR"), + name + ); + let hex_str = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read fixture {path}: {e}")); + hex::decode(hex_str.trim()).unwrap_or_else(|e| panic!("invalid hex in {path}: {e}")) + } + + #[test] + fn go_fixture_attestation_phase0() { + let go_bytes = read_go_fixture("attestation_phase0"); + + // Decode Go SSZ bytes -> Rust type. + let decoded = decode_phase0_attestation(&go_bytes).expect("decode Go attestation fixture"); + + // Verify fields match expected values. + assert_eq!(decoded.data.slot, 42); + assert_eq!(decoded.data.index, 7); + assert_eq!(decoded.data.beacon_block_root, [0xaa; 32]); + assert_eq!(decoded.data.source.epoch, 10); + assert_eq!(decoded.data.target.epoch, 11); + assert_eq!(decoded.signature, [0x11; 96]); + + // Re-encode and verify byte-for-byte match. + let rust_bytes = encode_phase0_attestation(&decoded).unwrap(); + assert_eq!( + rust_bytes, go_bytes, + "Rust SSZ output must match Go SSZ output" + ); + } + + #[test] + fn go_fixture_signed_aggregate_and_proof() { + let go_bytes = read_go_fixture("signed_aggregate_and_proof"); + + let decoded = decode_phase0_signed_aggregate_and_proof(&go_bytes) + .expect("decode Go signed_aggregate_and_proof fixture"); + + assert_eq!(decoded.message.aggregator_index, 99); + assert_eq!(decoded.signature, [0x55; 96]); + assert_eq!(decoded.message.selection_proof, [0x44; 96]); + assert_eq!(decoded.message.aggregate.signature, [0x33; 96]); + + let rust_bytes = encode_phase0_signed_aggregate_and_proof(&decoded).unwrap(); + assert_eq!( + rust_bytes, go_bytes, + "Rust SSZ output must match Go SSZ output" + ); + } + + #[test] + fn go_fixture_versioned_attestation_phase0() { + let go_bytes = read_go_fixture("versioned_attestation_phase0"); + + let decoded = decode_versioned_attestation(&go_bytes) + .expect("decode Go versioned_attestation fixture"); + + assert_eq!(decoded.version, DataVersion::Phase0); + let att = decoded.attestation.as_ref().unwrap(); + match att { + AttestationPayload::Phase0(a) => { + assert_eq!(a.data.slot, 42); + assert_eq!(a.signature, [0x11; 96]); + } + _ => panic!("expected Phase0 attestation"), + } + + let rust_bytes = encode_versioned_attestation(&decoded).unwrap(); + assert_eq!( + rust_bytes, go_bytes, + "Rust SSZ output must match Go SSZ output" + ); + } + + #[test] + fn go_fixture_versioned_agg_proof_phase0() { + let go_bytes = read_go_fixture("versioned_agg_proof_phase0"); + + let decoded = decode_versioned_signed_aggregate_and_proof(&go_bytes) + .expect("decode Go versioned_agg_proof fixture"); + + assert_eq!(decoded.version, DataVersion::Phase0); + + let rust_bytes = encode_versioned_signed_aggregate_and_proof(&decoded).unwrap(); + assert_eq!( + rust_bytes, go_bytes, + "Rust SSZ output must match Go SSZ output" + ); + } + + #[test] + fn go_fixture_versioned_proposal_phase0() { + let go_bytes = read_go_fixture("versioned_proposal_phase0"); + + let decoded = decode_versioned_signed_proposal(&go_bytes) + .expect("decode Go versioned_proposal fixture"); + + assert_eq!(decoded.version, DataVersion::Phase0); + assert!(!decoded.blinded); + + let rust_bytes = encode_versioned_signed_proposal(&decoded).unwrap(); + assert_eq!( + rust_bytes, go_bytes, + "Rust SSZ output must match Go SSZ output" + ); + } +} diff --git a/crates/core/testdata/ssz/attestation_phase0.ssz.hex b/crates/core/testdata/ssz/attestation_phase0.ssz.hex new file mode 100644 index 00000000..ed738484 --- /dev/null +++ b/crates/core/testdata/ssz/attestation_phase0.ssz.hex @@ -0,0 +1 @@ +e40000002a000000000000000700000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a00000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0b00000000000000cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111890001 \ No newline at end of file diff --git a/crates/core/testdata/ssz/signed_aggregate_and_proof.ssz.hex b/crates/core/testdata/ssz/signed_aggregate_and_proof.ssz.hex new file mode 100644 index 00000000..88afe0c8 --- /dev/null +++ b/crates/core/testdata/ssz/signed_aggregate_and_proof.ssz.hex @@ -0,0 +1 @@ +6400000055555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555563000000000000006c000000444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444e40000002a000000000000000700000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a00000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0b00000000000000cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333331401 \ No newline at end of file diff --git a/crates/core/testdata/ssz/versioned_agg_proof_phase0.ssz.hex b/crates/core/testdata/ssz/versioned_agg_proof_phase0.ssz.hex new file mode 100644 index 00000000..4b705df7 --- /dev/null +++ b/crates/core/testdata/ssz/versioned_agg_proof_phase0.ssz.hex @@ -0,0 +1 @@ +00000000000000000c00000064000000cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc37000000000000006c000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbe40000002a000000000000000700000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a00000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0b00000000000000ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa11 \ No newline at end of file diff --git a/crates/core/testdata/ssz/versioned_attestation_phase0.ssz.hex b/crates/core/testdata/ssz/versioned_attestation_phase0.ssz.hex new file mode 100644 index 00000000..5d833441 --- /dev/null +++ b/crates/core/testdata/ssz/versioned_attestation_phase0.ssz.hex @@ -0,0 +1 @@ +00000000000000000c000000e40000002a000000000000000700000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a00000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0b00000000000000cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110a01 \ No newline at end of file diff --git a/crates/core/testdata/ssz/versioned_proposal_phase0.ssz.hex b/crates/core/testdata/ssz/versioned_proposal_phase0.ssz.hex new file mode 100644 index 00000000..fd85c9fe --- /dev/null +++ b/crates/core/testdata/ssz/versioned_proposal_phase0.ssz.hex @@ -0,0 +1 @@ +0000000000000000000d000000640000007777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777770100000000000000020000000000000011111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222222540000003333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333334444444444444444444444444444444444444444444444444444444444444444000000000000000055555555555555555555555555555555555555555555555555555555555555556666666666666666666666666666666666666666666666666666666666666666dc000000dc000000dc000000dc000000dc000000 \ No newline at end of file diff --git a/crates/eth2api/Cargo.toml b/crates/eth2api/Cargo.toml index 91624b9c..0121cc6d 100644 --- a/crates/eth2api/Cargo.toml +++ b/crates/eth2api/Cargo.toml @@ -20,6 +20,8 @@ thiserror.workspace = true chrono.workspace = true hex.workspace = true validator.workspace = true +ethereum_ssz.workspace = true +ethereum_ssz_derive.workspace = true tree_hash.workspace = true tree_hash_derive.workspace = true alloy.workspace = true diff --git a/crates/eth2api/src/spec/altair.rs b/crates/eth2api/src/spec/altair.rs index 3b8b6f44..072f6856 100644 --- a/crates/eth2api/src/spec/altair.rs +++ b/crates/eth2api/src/spec/altair.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; use pluto_ssz::BitVector; @@ -12,7 +13,7 @@ use crate::spec::phase0; /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SyncAggregate { /// Sync committee participation bits. pub sync_committee_bits: BitVector<512>, @@ -25,7 +26,7 @@ pub struct SyncAggregate { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -56,7 +57,7 @@ pub struct BeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -78,7 +79,7 @@ pub struct BeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, @@ -91,7 +92,7 @@ pub struct SignedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SyncCommitteeMessage { /// Slot for the sync committee message. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -111,7 +112,7 @@ pub struct SyncCommitteeMessage { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SyncCommitteeContribution { /// Slot for the contribution. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -133,7 +134,7 @@ pub struct SyncCommitteeContribution { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ContributionAndProof { /// Aggregator validator index. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -149,7 +150,7 @@ pub struct ContributionAndProof { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedContributionAndProof { /// Unsigned contribution-and-proof message. pub message: ContributionAndProof, @@ -162,7 +163,7 @@ pub struct SignedContributionAndProof { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SyncAggregatorSelectionData { /// Slot to be signed. #[serde_as(as = "serde_with::DisplayFromStr")] diff --git a/crates/eth2api/src/spec/bellatrix.rs b/crates/eth2api/src/spec/bellatrix.rs index 8a73eea9..4162d27e 100644 --- a/crates/eth2api/src/spec/bellatrix.rs +++ b/crates/eth2api/src/spec/bellatrix.rs @@ -4,6 +4,7 @@ use alloy::primitives::U256; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; use crate::spec::{altair, phase0}; @@ -22,7 +23,7 @@ pub type BaseFeePerGas = U256; /// Raw execution transaction bytes. #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] #[serde(transparent)] pub struct Transaction { /// Transaction bytes. @@ -84,7 +85,7 @@ pub(crate) mod execution_address_serde { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayload { /// Parent execution block hash. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -133,7 +134,7 @@ pub struct ExecutionPayload { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayloadHeader { /// Parent execution block hash. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -183,7 +184,7 @@ pub struct ExecutionPayloadHeader { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -216,7 +217,7 @@ pub struct BeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -238,7 +239,7 @@ pub struct BeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -271,7 +272,7 @@ pub struct BlindedBeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -293,7 +294,7 @@ pub struct BlindedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, @@ -306,7 +307,7 @@ pub struct SignedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBlindedBeaconBlock { /// Unsigned blinded block message. pub message: BlindedBeaconBlock, diff --git a/crates/eth2api/src/spec/capella.rs b/crates/eth2api/src/spec/capella.rs index 3d8250e1..db5eef29 100644 --- a/crates/eth2api/src/spec/capella.rs +++ b/crates/eth2api/src/spec/capella.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; use crate::spec::{altair, bellatrix, phase0}; @@ -15,7 +16,7 @@ pub const MAX_BLS_TO_EXECUTION_CHANGES: usize = 16; /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct Withdrawal { /// Withdrawal index. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -35,7 +36,7 @@ pub struct Withdrawal { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BLSToExecutionChange { /// Validator index. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -52,7 +53,7 @@ pub struct BLSToExecutionChange { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBLSToExecutionChange { /// Unsigned message. pub message: BLSToExecutionChange, @@ -65,7 +66,7 @@ pub struct SignedBLSToExecutionChange { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayload { /// Parent execution block hash. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -117,7 +118,7 @@ pub struct ExecutionPayload { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayloadHeader { /// Parent execution block hash. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -170,7 +171,7 @@ pub struct ExecutionPayloadHeader { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -206,7 +207,7 @@ pub struct BeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -228,7 +229,7 @@ pub struct BeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -264,7 +265,7 @@ pub struct BlindedBeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -286,7 +287,7 @@ pub struct BlindedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, @@ -299,7 +300,7 @@ pub struct SignedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBlindedBeaconBlock { /// Unsigned blinded block message. pub message: BlindedBeaconBlock, diff --git a/crates/eth2api/src/spec/deneb.rs b/crates/eth2api/src/spec/deneb.rs index 3b467c23..685a7882 100644 --- a/crates/eth2api/src/spec/deneb.rs +++ b/crates/eth2api/src/spec/deneb.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; use crate::spec::{altair, bellatrix, capella, phase0}; @@ -18,7 +19,7 @@ pub type BaseFeePerGas = bellatrix::BaseFeePerGas; /// KZG commitment bytes. #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] #[serde(transparent)] pub struct KZGCommitment { /// Raw commitment bytes. @@ -60,6 +61,46 @@ impl AsRef<[u8]> for KZGProof { } } +impl ssz::Encode for KZGProof { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + KZG_PROOF_LEN + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.0); + } + + fn ssz_bytes_len(&self) -> usize { + KZG_PROOF_LEN + } +} + +impl ssz::Decode for KZGProof { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + KZG_PROOF_LEN + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != KZG_PROOF_LEN { + return Err(ssz::DecodeError::InvalidByteLength { + len: bytes.len(), + expected: KZG_PROOF_LEN, + }); + } + let mut arr = [0u8; KZG_PROOF_LEN]; + arr.copy_from_slice(bytes); + Ok(Self(arr)) + } +} + /// Blob payload. #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -82,11 +123,51 @@ impl AsRef<[u8]> for Blob { } } +impl ssz::Encode for Blob { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + BLOB_LEN + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.0); + } + + fn ssz_bytes_len(&self) -> usize { + BLOB_LEN + } +} + +impl ssz::Decode for Blob { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + BLOB_LEN + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != BLOB_LEN { + return Err(ssz::DecodeError::InvalidByteLength { + len: bytes.len(), + expected: BLOB_LEN, + }); + } + let mut arr = [0u8; BLOB_LEN]; + arr.copy_from_slice(bytes); + Ok(Self(arr)) + } +} + /// Deneb execution payload. /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayload { /// Parent execution block hash. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -144,7 +225,7 @@ pub struct ExecutionPayload { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayloadHeader { /// Parent execution block hash. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -203,7 +284,7 @@ pub struct ExecutionPayloadHeader { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -243,7 +324,7 @@ pub struct BeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -265,7 +346,7 @@ pub struct BeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -305,7 +386,7 @@ pub struct BlindedBeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -327,7 +408,7 @@ pub struct BlindedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, @@ -340,7 +421,7 @@ pub struct SignedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBlindedBeaconBlock { /// Unsigned blinded block message. pub message: BlindedBeaconBlock, @@ -352,7 +433,7 @@ pub struct SignedBlindedBeaconBlock { /// Deneb signed block contents container. /// /// Spec: -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)] pub struct SignedBlockContents { /// Signed block. pub signed_block: SignedBeaconBlock, diff --git a/crates/eth2api/src/spec/electra.rs b/crates/eth2api/src/spec/electra.rs index 233caa79..ff98d337 100644 --- a/crates/eth2api/src/spec/electra.rs +++ b/crates/eth2api/src/spec/electra.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; use pluto_ssz::{BitList, BitVector}; @@ -23,7 +24,7 @@ pub const MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: usize = 2; /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct IndexedAttestation { /// Indices of attesting validators. #[serde(with = "pluto_ssz::serde_utils::ssz_list_u64_string_serde")] @@ -38,7 +39,7 @@ pub struct IndexedAttestation { /// Electra attester slashing. /// /// Spec: -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct AttesterSlashing { /// First conflicting indexed attestation. pub attestation_1: IndexedAttestation, @@ -50,7 +51,7 @@ pub struct AttesterSlashing { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct Attestation { /// Aggregation bits. pub aggregation_bits: BitList<131_072>, @@ -67,7 +68,7 @@ pub struct Attestation { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct DepositRequest { /// Validator public key. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -90,7 +91,7 @@ pub struct DepositRequest { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct WithdrawalRequest { /// Source execution address. #[serde(with = "bellatrix::execution_address_serde")] @@ -107,7 +108,7 @@ pub struct WithdrawalRequest { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ConsolidationRequest { /// Source execution address. #[serde(with = "bellatrix::execution_address_serde")] @@ -123,7 +124,7 @@ pub struct ConsolidationRequest { /// Electra execution requests container. /// /// Spec: -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ExecutionRequests { /// Deposit requests. pub deposits: phase0::SszList, @@ -138,7 +139,7 @@ pub struct ExecutionRequests { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -180,7 +181,7 @@ pub struct BeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -202,7 +203,7 @@ pub struct BeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -244,7 +245,7 @@ pub struct BlindedBeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -266,7 +267,7 @@ pub struct BlindedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, @@ -279,7 +280,7 @@ pub struct SignedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBlindedBeaconBlock { /// Unsigned blinded block message. pub message: BlindedBeaconBlock, @@ -291,7 +292,7 @@ pub struct SignedBlindedBeaconBlock { /// Electra signed block contents container. /// /// Spec: -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)] pub struct SignedBlockContents { /// Signed block. pub signed_block: SignedBeaconBlock, @@ -305,7 +306,7 @@ pub struct SignedBlockContents { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct AggregateAndProof { /// Aggregator validator index. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -321,7 +322,7 @@ pub struct AggregateAndProof { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedAggregateAndProof { /// Unsigned message. pub message: AggregateAndProof, diff --git a/crates/eth2api/src/spec/fulu.rs b/crates/eth2api/src/spec/fulu.rs index 58375469..f1985aa2 100644 --- a/crates/eth2api/src/spec/fulu.rs +++ b/crates/eth2api/src/spec/fulu.rs @@ -1,11 +1,12 @@ //! Fulu consensus types from the Ethereum beacon chain specification. use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; use crate::spec::{deneb, electra}; /// Fulu signed block contents container. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)] pub struct SignedBlockContents { /// Signed block. pub signed_block: electra::SignedBeaconBlock, diff --git a/crates/eth2api/src/spec/phase0.rs b/crates/eth2api/src/spec/phase0.rs index 7ea80968..843f9728 100644 --- a/crates/eth2api/src/spec/phase0.rs +++ b/crates/eth2api/src/spec/phase0.rs @@ -3,6 +3,7 @@ //! See: use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; pub use pluto_ssz::{BitList, SszList, SszVector}; @@ -76,7 +77,7 @@ pub type BLSSignature = [u8; BLS_SIGNATURE_LEN]; /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct DepositMessage { /// BLS public key. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -103,7 +104,7 @@ impl From<&DepositData> for DepositMessage { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct DepositData { /// BLS public key. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -123,7 +124,7 @@ pub struct DepositData { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ForkData { /// Current fork version. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -137,7 +138,7 @@ pub struct ForkData { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SigningData { /// Object root. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -151,7 +152,7 @@ pub struct SigningData { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ETH1Data { /// Deposit tree root. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -168,7 +169,7 @@ pub struct ETH1Data { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockHeader { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -191,7 +192,7 @@ pub struct BeaconBlockHeader { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBeaconBlockHeader { /// Unsigned beacon block header. pub message: BeaconBlockHeader, @@ -204,7 +205,7 @@ pub struct SignedBeaconBlockHeader { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct ProposerSlashing { /// First conflicting signed header. pub signed_header_1: SignedBeaconBlockHeader, @@ -216,7 +217,7 @@ pub struct ProposerSlashing { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct IndexedAttestation { /// Indices of attesting validators. #[serde(with = "pluto_ssz::serde_utils::ssz_list_u64_string_serde")] @@ -231,7 +232,7 @@ pub struct IndexedAttestation { /// Attester slashing container. /// /// Spec: -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct AttesterSlashing { /// First conflicting indexed attestation. pub attestation_1: IndexedAttestation, @@ -242,7 +243,7 @@ pub struct AttesterSlashing { /// Deposit operation container. /// /// Spec: -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct Deposit { /// Merkle proof branch. pub proof: SszVector, @@ -254,7 +255,7 @@ pub struct Deposit { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] @@ -280,7 +281,7 @@ pub struct BeaconBlockBody { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct BeaconBlock { /// Block slot. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -302,7 +303,7 @@ pub struct BeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, @@ -315,7 +316,7 @@ pub struct SignedBeaconBlock { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct Checkpoint { /// Epoch associated with the checkpoint. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -329,7 +330,7 @@ pub struct Checkpoint { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct AttestationData { /// Slot for the attestation. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -350,7 +351,7 @@ pub struct AttestationData { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct Attestation { /// Aggregation bits. pub aggregation_bits: BitList<2048>, @@ -365,7 +366,7 @@ pub struct Attestation { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct AggregateAndProof { /// Aggregator validator index. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -381,7 +382,7 @@ pub struct AggregateAndProof { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedAggregateAndProof { /// Unsigned message. pub message: AggregateAndProof, @@ -394,7 +395,7 @@ pub struct SignedAggregateAndProof { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct VoluntaryExit { /// Exit epoch. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -408,7 +409,7 @@ pub struct VoluntaryExit { /// /// Spec: #[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TreeHash, Serialize, Deserialize)] pub struct SignedVoluntaryExit { /// Unsigned voluntary exit message. pub message: VoluntaryExit, diff --git a/crates/ssz/Cargo.toml b/crates/ssz/Cargo.toml index 65c1d01a..c9649bed 100644 --- a/crates/ssz/Cargo.toml +++ b/crates/ssz/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true publish.workspace = true [dependencies] +ethereum_ssz = { workspace = true } hex.workspace = true k256.workspace = true serde.workspace = true diff --git a/crates/ssz/src/decode.rs b/crates/ssz/src/decode.rs new file mode 100644 index 00000000..ec01bd9d --- /dev/null +++ b/crates/ssz/src/decode.rs @@ -0,0 +1,53 @@ +//! Low-level SSZ binary decoding helpers. + +use crate::SszBinaryError; + +/// Decodes a `u8` from a single byte. +pub fn decode_u8(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err(SszBinaryError::InvalidLength { + expected: 1, + actual: 0, + }); + } + Ok(bytes[0]) +} + +/// Decodes a `u32` from 4 little-endian bytes. +pub fn decode_u32(bytes: &[u8]) -> Result { + let arr: [u8; 4] = bytes + .try_into() + .map_err(|_| SszBinaryError::InvalidLength { + expected: 4, + actual: bytes.len(), + })?; + Ok(u32::from_le_bytes(arr)) +} + +/// Decodes a `u64` from 8 little-endian bytes. +pub fn decode_u64(bytes: &[u8]) -> Result { + let arr: [u8; 8] = bytes + .try_into() + .map_err(|_| SszBinaryError::InvalidLength { + expected: 8, + actual: bytes.len(), + })?; + Ok(u64::from_le_bytes(arr)) +} + +/// Decodes a `bool` from a single SSZ byte. +pub fn decode_bool(bytes: &[u8]) -> Result { + match decode_u8(bytes)? { + 0 => Ok(false), + 1 => Ok(true), + v => Err(SszBinaryError::Custom(format!("invalid bool byte: {v}"))), + } +} + +/// Decodes a fixed-size byte array from a slice. +pub fn decode_fixed_bytes(bytes: &[u8]) -> Result<[u8; N], SszBinaryError> { + bytes.try_into().map_err(|_| SszBinaryError::InvalidLength { + expected: N, + actual: bytes.len(), + }) +} diff --git a/crates/ssz/src/encode.rs b/crates/ssz/src/encode.rs new file mode 100644 index 00000000..7ae8c3e1 --- /dev/null +++ b/crates/ssz/src/encode.rs @@ -0,0 +1,22 @@ +//! Low-level SSZ binary encoding helpers. + +/// Encodes a `u8` value as a single byte. +pub fn encode_u8(value: u8) -> [u8; 1] { + [value] +} + +/// Encodes a `u32` value as 4 little-endian bytes. +pub fn encode_u32(value: u32) -> [u8; 4] { + value.to_le_bytes() +} + +/// Encodes a `u64` value as 8 little-endian bytes. +pub fn encode_u64(value: u64) -> [u8; 8] { + value.to_le_bytes() +} + +/// Encodes a `bool` as a single SSZ byte (`0x01` for `true`, `0x00` for +/// `false`). +pub fn encode_bool(value: bool) -> [u8; 1] { + [u8::from(value)] +} diff --git a/crates/ssz/src/lib.rs b/crates/ssz/src/lib.rs index 4e1041ad..f9b90cce 100644 --- a/crates/ssz/src/lib.rs +++ b/crates/ssz/src/lib.rs @@ -1,5 +1,7 @@ //! Shared SSZ hashing primitives, helpers, and container wrappers. +pub mod decode; +pub mod encode; mod error; mod hasher; mod helpers; @@ -16,3 +18,19 @@ pub use helpers::{ }; /// Generic SSZ list, vector, and bitfield wrappers. pub use types::{BitList, BitVector, SszList, SszVector}; + +/// Error type for SSZ binary encode/decode operations. +#[derive(Debug, thiserror::Error)] +pub enum SszBinaryError { + /// Byte slice length does not match the expected size. + #[error("invalid length: expected {expected}, got {actual}")] + InvalidLength { + /// Expected byte count. + expected: usize, + /// Actual byte count. + actual: usize, + }, + /// Generic decoding error. + #[error("{0}")] + Custom(String), +} diff --git a/crates/ssz/src/types.rs b/crates/ssz/src/types.rs index 47b862f3..684ec630 100644 --- a/crates/ssz/src/types.rs +++ b/crates/ssz/src/types.rs @@ -2,6 +2,7 @@ use crate::serde_utils::{decode_0x_hex, encode_0x_hex}; use serde::{Deserialize, Serialize, de::Error as DeError}; +use ssz::{BYTES_PER_LENGTH_OFFSET, Decode, DecodeError, Encode}; use tree_hash::{ BYTES_PER_CHUNK, Hash256, PackedEncoding, TreeHash, TreeHashType, merkle_root, mix_in_length, }; @@ -80,6 +81,37 @@ impl AsRef<[u8]> for SszList { } } +impl Encode for SszList { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + self.0.ssz_append(buf); + } + + fn ssz_bytes_len(&self) -> usize { + self.0.ssz_bytes_len() + } +} + +impl Decode for SszList { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let values = Vec::::from_ssz_bytes(bytes)?; + if MAX > 0 && values.len() > MAX { + return Err(DecodeError::BytesInvalid(format!( + "list length {} exceeds max {MAX}", + values.len(), + ))); + } + Ok(Self(values)) + } +} + impl TreeHash for SszList { fn tree_hash_type() -> TreeHashType { TreeHashType::List @@ -145,6 +177,59 @@ impl AsRef<[u8]> for SszVector { } } +impl Encode for SszVector { + fn is_ssz_fixed_len() -> bool { + T::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + if T::is_ssz_fixed_len() { + #[allow(clippy::arithmetic_side_effects)] + { + T::ssz_fixed_len() * SIZE + } + } else { + BYTES_PER_LENGTH_OFFSET + } + } + + fn ssz_append(&self, buf: &mut Vec) { + self.0.ssz_append(buf); + } + + fn ssz_bytes_len(&self) -> usize { + self.0.ssz_bytes_len() + } +} + +impl Decode for SszVector { + fn is_ssz_fixed_len() -> bool { + T::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + if T::is_ssz_fixed_len() { + #[allow(clippy::arithmetic_side_effects)] + { + T::ssz_fixed_len() * SIZE + } + } else { + BYTES_PER_LENGTH_OFFSET + } + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let values = Vec::::from_ssz_bytes(bytes)?; + if values.len() != SIZE { + return Err(DecodeError::BytesInvalid(format!( + "vector length {} does not match required {SIZE}", + values.len(), + ))); + } + Ok(Self(values)) + } +} + impl TreeHash for SszVector { fn tree_hash_type() -> TreeHashType { TreeHashType::Vector @@ -206,7 +291,8 @@ impl BitList { self.len == 0 } - fn from_ssz_bytes(ssz: Vec) -> Self { + /// Decodes SSZ-encoded bytes (with sentinel bit) into a `BitList`. + pub fn from_ssz_bytes(ssz: Vec) -> Self { if ssz.is_empty() { return Self::default(); } @@ -233,7 +319,8 @@ impl BitList { Self { bytes, len } } - fn to_ssz_bytes(&self) -> Vec { + /// Encodes the `BitList` as SSZ bytes with sentinel bit appended. + pub fn to_ssz_bytes(&self) -> Vec { let mut ssz = self.bytes.clone(); Self::append_sentinel(&mut ssz, self.len); ssz @@ -273,6 +360,30 @@ impl<'de, const MAX: usize> Deserialize<'de> for BitList { } } +impl Encode for BitList { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.to_ssz_bytes()); + } + + fn ssz_bytes_len(&self) -> usize { + self.to_ssz_bytes().len() + } +} + +impl Decode for BitList { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes.to_vec())) + } +} + impl TreeHash for BitList { fn tree_hash_type() -> TreeHashType { TreeHashType::List @@ -297,7 +408,7 @@ impl TreeHash for BitList { #[derive(Debug, Clone, PartialEq, Eq)] pub struct BitVector { /// Packed bits, little-endian bit order. - bytes: Vec, + pub bytes: Vec, } impl Default for BitVector { @@ -345,6 +456,47 @@ impl<'de, const SIZE: usize> Deserialize<'de> for BitVector { } } +impl Encode for BitVector { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + SIZE.div_ceil(8) + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.bytes); + } + + fn ssz_bytes_len(&self) -> usize { + SIZE.div_ceil(8) + } +} + +impl Decode for BitVector { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + SIZE.div_ceil(8) + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let expected = SIZE.div_ceil(8); + if bytes.len() != expected { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected, + }); + } + Ok(Self { + bytes: bytes.to_vec(), + }) + } +} + impl TreeHash for BitVector { fn tree_hash_type() -> TreeHashType { TreeHashType::Vector diff --git a/deny.toml b/deny.toml index 2c06b6a5..4ba062e7 100644 --- a/deny.toml +++ b/deny.toml @@ -23,7 +23,11 @@ feature-depth = 1 db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] yanked = "deny" -ignore = [] +ignore = [ + # rand 0.8.5 unsoundness with custom logger — pre-existing transitive dep, + # not introduced by this branch. + "RUSTSEC-2026-0097", +] unmaintained = "workspace" [licenses] diff --git a/test-infra/sszfixtures/go.mod b/test-infra/sszfixtures/go.mod new file mode 100644 index 00000000..915603cf --- /dev/null +++ b/test-infra/sszfixtures/go.mod @@ -0,0 +1,80 @@ +module github.com/obolnetwork/pluto/test-infra/sszfixtures + +go 1.25 + +replace github.com/attestantio/go-eth2-client => github.com/ObolNetwork/go-eth2-client v0.28.0-obol.1 + +require ( + github.com/attestantio/go-eth2-client v0.27.1 + github.com/obolnetwork/charon v1.9.3 +) + +require ( + github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/emicklei/dot v1.8.0 // indirect + github.com/ferranbt/fastssz v1.0.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-yaml v1.17.0 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/herumi/bls-eth-go-binary v1.36.4 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/huandu/go-clone v1.7.2 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/jsternberg/zap-logfmt v1.3.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/libp2p/go-libp2p v0.47.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pk910/dynamic-ssz v0.0.6 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/protolambda/eth2-shuffle v1.1.0 // indirect + github.com/r3labs/sse/v2 v2.10.0 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/test-infra/sszfixtures/go.sum b/test-infra/sszfixtures/go.sum new file mode 100644 index 00000000..47c41f8b --- /dev/null +++ b/test-infra/sszfixtures/go.sum @@ -0,0 +1,319 @@ +github.com/ObolNetwork/go-eth2-client v0.28.0-obol.1 h1:72NAvJRe09mKHiKfB30r9nO7JZnV/5bPx5J1qxZ5Yow= +github.com/ObolNetwork/go-eth2-client v0.28.0-obol.1/go.mod h1:PO9sHFCq+1RiG+Eh3eOR2GYvYV64Qzg7idM3kLgCs5k= +github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506 h1:d/SJkN8/9Ca+1YmuDiUJxAiV4w/a9S8NcsG7GMQSrVI= +github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506/go.mod h1:6TZI4FU6zT8x6ZfWa1J8YQ2NgW0wLV/W3fHRca8ISBo= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI= +github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8= +github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-yaml v1.17.0 h1:JJhayi67p5LeTLh9UJYFhayPIOGDZjAqQNoEzHhYvik= +github.com/goccy/go-yaml v1.17.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/herumi/bls-eth-go-binary v1.36.4 h1:yff41RSbfyZwfE1NF/qddP5nXhgdU0c3RGOpYOoM7YM= +github.com/herumi/bls-eth-go-binary v1.36.4/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-clone v1.7.2 h1:3+Aq0Ed8XK+zKkLjE2dfHg0XrpIfcohBE1K+c8Usxoo= +github.com/huandu/go-clone v1.7.2/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= +github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= +github.com/huandu/go-clone/generic v1.6.0/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= +github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= +github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= +github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= +github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0yc= +github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= +github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= +github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo= +github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/obolnetwork/charon v1.9.3 h1:b0RQzd9jfJimT5gQ2boYMFf8fJUHXrXa7lHvUwe7wD4= +github.com/obolnetwork/charon v1.9.3/go.mod h1:NKcQWTURsbsoRyyKsngjZ5cbIgTFhIw3IvUNyA8HIDA= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v3 v3.1.1 h1:wSLMam9Kf7DL1A74hnqRvEb9OT+aXPAsQ5VS+BdXOJ0= +github.com/pion/dtls/v3 v3.1.1/go.mod h1:7FGvVYpHsUV6+aywaFpG7aE4Vz8nBOx74odPRFue6cI= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= +github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= +github.com/pk910/dynamic-ssz v0.0.6 h1:Tu97LSc2TtCyqRfoSbhG9XuR/FbA7CkKeAnlkgUydFY= +github.com/pk910/dynamic-ssz v0.0.6/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/protolambda/eth2-shuffle v1.1.0 h1:gixIBI84IeugTwwHXm8vej1bSSEhueBCSryA4lAKRLU= +github.com/protolambda/eth2-shuffle v1.1.0/go.mod h1:FhA2c0tN15LTC+4T9DNVm+55S7uXTTjQ8TQnBuXlkF8= +github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= +github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/test-infra/sszfixtures/main.go b/test-infra/sszfixtures/main.go new file mode 100644 index 00000000..df91239a --- /dev/null +++ b/test-infra/sszfixtures/main.go @@ -0,0 +1,211 @@ +// Package main generates SSZ-encoded fixture files from Charon's core types. +// These fixtures are used by Pluto's Rust tests to verify byte-for-byte +// interoperability with Go's SSZ encoding. +// +// Usage: +// +// cd test-infra/sszfixtures && go run . -out ../../crates/core/testdata/ssz +package main + +import ( + "encoding/hex" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/spec" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/obolnetwork/charon/core" +) + +var outDir = flag.String("out", "../../crates/core/testdata/ssz", "output directory for fixture files") + +func main() { + flag.Parse() + + if err := os.MkdirAll(*outDir, 0o755); err != nil { + fatal(err) + } + + type fixture struct { + name string + gen func() ([]byte, error) + } + + fixtures := []fixture{ + {"attestation_phase0", genAttestation}, + {"signed_aggregate_and_proof", genSignedAggregateAndProof}, + {"versioned_attestation_phase0", genVersionedAttestationPhase0}, + {"versioned_agg_proof_phase0", genVersionedSignedAggregatePhase0}, + {"versioned_proposal_phase0", genVersionedProposalPhase0}, + } + + for _, f := range fixtures { + b, err := f.gen() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR %s: %v\n", f.name, err) + os.Exit(1) + } + path := filepath.Join(*outDir, f.name+".ssz.hex") + if err := os.WriteFile(path, []byte(hex.EncodeToString(b)), 0o644); err != nil { + fatal(err) + } + fmt.Printf("wrote %s (%d bytes)\n", path, len(b)) + } +} + +func fatal(err error) { + fmt.Fprintf(os.Stderr, "fatal: %v\n", err) + os.Exit(1) +} + +// sampleAttestationData returns deterministic attestation data matching +// Rust test helpers. +func sampleAttestationData() *eth2p0.AttestationData { + return ð2p0.AttestationData{ + Slot: 42, + Index: 7, + BeaconBlockRoot: fill32(0xaa), + Source: ð2p0.Checkpoint{ + Epoch: 10, + Root: fill32(0xbb), + }, + Target: ð2p0.Checkpoint{ + Epoch: 11, + Root: fill32(0xcc), + }, + } +} + +func fill32(b byte) eth2p0.Root { + var r eth2p0.Root + for i := range r { + r[i] = b + } + return r +} + +func fill96(b byte) eth2p0.BLSSignature { + var s eth2p0.BLSSignature + for i := range s { + s[i] = b + } + return s +} + +func fillHash32(b byte) []byte { + h := make([]byte, 32) + for i := range h { + h[i] = b + } + return h +} + +// bitlistN creates an SSZ-encoded bitlist with the given capacity and set bits. +func bitlistN(capacity int, setBits ...int) []byte { + byteLen := (capacity + 7) / 8 + b := make([]byte, byteLen+1) + for _, bit := range setBits { + b[bit/8] |= 1 << uint(bit%8) + } + // Append sentinel bit. + sentinelByte := capacity / 8 + sentinelBit := capacity % 8 + if sentinelByte < len(b) { + b[sentinelByte] |= 1 << uint(sentinelBit) + } + // Trim trailing zero bytes after sentinel. + for len(b) > 0 && b[len(b)-1] == 0 { + b = b[:len(b)-1] + } + return b +} + +func genAttestation() ([]byte, error) { + att := core.NewAttestation(ð2p0.Attestation{ + AggregationBits: bitlistN(16, 0, 3, 7), + Data: sampleAttestationData(), + Signature: fill96(0x11), + }) + return att.MarshalSSZ() +} + +func genSignedAggregateAndProof() ([]byte, error) { + sap := core.NewSignedAggregateAndProof(ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: 99, + Aggregate: ð2p0.Attestation{ + AggregationBits: bitlistN(8, 2, 4), + Data: sampleAttestationData(), + Signature: fill96(0x33), + }, + SelectionProof: fill96(0x44), + }, + Signature: fill96(0x55), + }) + return sap.MarshalSSZ() +} + +func genVersionedAttestationPhase0() ([]byte, error) { + va, err := core.NewVersionedAttestation(&spec.VersionedAttestation{ + Version: spec.DataVersionPhase0, + Phase0: ð2p0.Attestation{ + AggregationBits: bitlistN(8, 1, 3), + Data: sampleAttestationData(), + Signature: fill96(0x11), + }, + }) + if err != nil { + return nil, err + } + return va.MarshalSSZ() +} + +func genVersionedSignedAggregatePhase0() ([]byte, error) { + va := core.NewVersionedSignedAggregateAndProof(&spec.VersionedSignedAggregateAndProof{ + Version: spec.DataVersionPhase0, + Phase0: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: 55, + Aggregate: ð2p0.Attestation{ + AggregationBits: bitlistN(4, 0), + Data: sampleAttestationData(), + Signature: fill96(0xaa), + }, + SelectionProof: fill96(0xbb), + }, + Signature: fill96(0xcc), + }, + }) + return va.MarshalSSZ() +} + +func genVersionedProposalPhase0() ([]byte, error) { + vp, err := core.NewVersionedSignedProposal(&api.VersionedSignedProposal{ + Version: spec.DataVersionPhase0, + Phase0: ð2p0.SignedBeaconBlock{ + Message: ð2p0.BeaconBlock{ + Slot: 1, + ProposerIndex: 2, + ParentRoot: fill32(0x11), + StateRoot: fill32(0x22), + Body: ð2p0.BeaconBlockBody{ + RANDAOReveal: fill96(0x33), + ETH1Data: ð2p0.ETH1Data{ + DepositRoot: fill32(0x44), + DepositCount: 0, + BlockHash: fillHash32(0x55), + }, + Graffiti: fill32(0x66), + }, + }, + Signature: fill96(0x77), + }, + }) + if err != nil { + return nil, err + } + return vp.MarshalSSZ() +} diff --git a/test-infra/sszfixtures/sszfixtures b/test-infra/sszfixtures/sszfixtures new file mode 100755 index 00000000..ee896df5 Binary files /dev/null and b/test-infra/sszfixtures/sszfixtures differ