diff --git a/crates/common/types/src/beacon/containers/altair.rs b/crates/common/types/src/beacon/containers/altair.rs index d968c113..11da84ce 100644 --- a/crates/common/types/src/beacon/containers/altair.rs +++ b/crates/common/types/src/beacon/containers/altair.rs @@ -70,10 +70,19 @@ pub type SyncSubcommitteeBits = /// needs one regardless of how many attestations or other operations it /// includes. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct SyncAggregate { - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub sync_committee_bits: SyncCommitteeBits, /// The aggregate of every signature from a member set in /// `sync_committee_bits`, over the previous slot's block root. @@ -85,7 +94,7 @@ pub struct SyncAggregate { /// participates. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] pub struct SyncCommittee { - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pubkeys: SyncCommitteePubkeys, /// The aggregate of every key in `pubkeys`, so `process_sync_aggregate` does /// not have to re-aggregate from scratch when the whole committee signs. @@ -109,15 +118,15 @@ pub struct BeaconBlockBody { pub eth1_data: Eth1Data, /// Arbitrary proposer-chosen bytes, which consensus never reads. pub graffiti: Bytes32, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub proposer_slashings: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attester_slashings: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attestations: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub voluntary_exits: SszList, /// The aggregated sync committee signature over the previous slot's block /// root, plus which members contributed. This is what lets a light client @@ -180,16 +189,16 @@ pub struct BeaconState { /// slot advances, since a block cannot commit to the root of the state /// containing it. pub latest_block_header: BeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_roots: HistoricalRoots, // -- Eth1 -- pub eth1_data: Eth1Data, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub eth1_data_votes: Eth1DataVotes, /// How many deposits from the contract have been processed, which is where /// the next one will be read from. @@ -197,17 +206,17 @@ pub struct BeaconState { pub eth1_deposit_index: u64, // -- Registry -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub validators: Validators, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub balances: Balances, // -- Randomness -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub randao_mixes: RandaoMixes, // -- Slashings -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub slashings: Slashings, // -- Participation -- @@ -217,15 +226,15 @@ pub struct BeaconState { /// it is processed instead of deferring to the epoch boundary, so there is /// no longer a backlog of whole attestations to keep around, only a flag /// per validator per epoch. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub previous_epoch_participation: EpochParticipation, /// Flags for the current epoch, which become `previous_epoch_participation` /// at the next epoch boundary. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub current_epoch_participation: EpochParticipation, // -- Finality -- - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub justification_bits: JustificationBits, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, @@ -237,7 +246,7 @@ pub struct BeaconState { /// non-finalizing epoch and falls otherwise, which is what lets the /// inactivity leak single out validators who are actually offline rather /// than penalizing everyone during a stall. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub inactivity_scores: InactivityScores, // -- Sync committees -- @@ -289,7 +298,7 @@ pub struct SyncCommitteeContribution { pub beacon_block_root: Root, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub subcommittee_index: u64, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub aggregation_bits: SyncSubcommitteeBits, /// The aggregate signature of every member set in `aggregation_bits`, over /// `beacon_block_root`. diff --git a/crates/common/types/src/beacon/containers/bellatrix.rs b/crates/common/types/src/beacon/containers/bellatrix.rs index 80993a8b..daafe8ca 100644 --- a/crates/common/types/src/beacon/containers/bellatrix.rs +++ b/crates/common/types/src/beacon/containers/bellatrix.rs @@ -91,7 +91,7 @@ pub struct ExecutionPayload { /// own `state_root`. pub state_root: Bytes32, pub receipts_root: Bytes32, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub logs_bloom: LogsBloom, /// The randomness the beacon chain exposes to the EVM for this block; /// `difficulty` in the yellow paper before the merge repurposed the @@ -105,7 +105,7 @@ pub struct ExecutionPayload { pub gas_used: u64, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub timestamp: u64, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub extra_data: ExtraData, /// This block's EIP-1559 base fee, a `uint256` because the execution /// layer's fee market is not bounded to fit a `uint64`. @@ -113,7 +113,7 @@ pub struct ExecutionPayload { /// This payload's own hash, which the next payload's `parent_hash` must /// equal. pub block_hash: ExecutionBlockHash, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex_seq")] pub transactions: Transactions, } @@ -127,7 +127,7 @@ pub struct ExecutionPayloadHeader { pub fee_recipient: ExecutionAddress, pub state_root: Bytes32, pub receipts_root: Bytes32, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub logs_bloom: LogsBloom, pub prev_randao: Bytes32, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] @@ -138,7 +138,7 @@ pub struct ExecutionPayloadHeader { pub gas_used: u64, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub timestamp: u64, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub extra_data: ExtraData, pub base_fee_per_gas: Uint256, /// The hash of the execution block this header summarizes. @@ -177,15 +177,15 @@ pub struct BeaconBlockBody { pub eth1_data: Eth1Data, /// Arbitrary proposer-chosen bytes, which consensus never reads. pub graffiti: Bytes32, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub proposer_slashings: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attester_slashings: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attestations: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub voluntary_exits: SszList, /// The aggregated sync committee signature over the previous slot's block /// root, plus which members contributed. @@ -292,16 +292,16 @@ pub struct BeaconState { /// slot advances, since a block cannot commit to the root of the state /// containing it. pub latest_block_header: BeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_roots: HistoricalRoots, // -- Eth1 -- pub eth1_data: Eth1Data, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub eth1_data_votes: Eth1DataVotes, /// How many deposits from the contract have been processed, which is where /// the next one will be read from. @@ -309,31 +309,31 @@ pub struct BeaconState { pub eth1_deposit_index: u64, // -- Registry -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub validators: Validators, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub balances: Balances, // -- Randomness -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub randao_mixes: RandaoMixes, // -- Slashings -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub slashings: Slashings, // -- Participation -- /// Per-validator participation flags for the previous epoch, positionally /// parallel to `validators`. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub previous_epoch_participation: EpochParticipation, /// Flags for the current epoch, which become `previous_epoch_participation` /// at the next epoch boundary. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub current_epoch_participation: EpochParticipation, // -- Finality -- - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub justification_bits: JustificationBits, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, @@ -341,7 +341,7 @@ pub struct BeaconState { // -- Inactivity -- /// Per-validator inactivity score, positionally parallel to `validators`. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub inactivity_scores: InactivityScores, // -- Sync committees -- diff --git a/crates/common/types/src/beacon/containers/capella.rs b/crates/common/types/src/beacon/containers/capella.rs index 0ec2e397..c536b1be 100644 --- a/crates/common/types/src/beacon/containers/capella.rs +++ b/crates/common/types/src/beacon/containers/capella.rs @@ -70,7 +70,17 @@ pub type Withdrawals = SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attester_slashings: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attestations: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub voluntary_exits: SszList, /// The aggregated sync committee signature over the previous slot's block /// root, plus which members contributed. @@ -234,7 +264,7 @@ pub struct BeaconBlockBody { /// holds here, so it never affects this same slot's withdrawal sweep: that /// sweep already ran, against whatever credentials were in effect before /// this block. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub bls_to_execution_changes: SszList, } @@ -336,18 +366,18 @@ pub struct BeaconState { /// slot advances, since a block cannot commit to the root of the state /// containing it. pub latest_block_header: BeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, /// Frozen as of this fork: no longer appended to. See /// [`Self::historical_summaries`]. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_roots: HistoricalRoots, // -- Eth1 -- pub eth1_data: Eth1Data, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub eth1_data_votes: Eth1DataVotes, /// How many deposits from the contract have been processed, which is where /// the next one will be read from. @@ -355,31 +385,31 @@ pub struct BeaconState { pub eth1_deposit_index: u64, // -- Registry -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub validators: Validators, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub balances: Balances, // -- Randomness -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub randao_mixes: RandaoMixes, // -- Slashings -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub slashings: Slashings, // -- Participation -- /// Per-validator participation flags for the previous epoch, positionally /// parallel to `validators`. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub previous_epoch_participation: EpochParticipation, /// Flags for the current epoch, which become `previous_epoch_participation` /// at the next epoch boundary. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub current_epoch_participation: EpochParticipation, // -- Finality -- - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub justification_bits: JustificationBits, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, @@ -387,7 +417,7 @@ pub struct BeaconState { // -- Inactivity -- /// Per-validator inactivity score, positionally parallel to `validators`. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub inactivity_scores: InactivityScores, // -- Sync committees -- @@ -418,7 +448,7 @@ pub struct BeaconState { /// root period, replacing the growth of `historical_roots` as of this /// fork. See [`super::shared::HistoricalSummary`] for why the two forms /// are `hash_tree_root`-compatible. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_summaries: HistoricalSummaries, } diff --git a/crates/common/types/src/beacon/containers/deneb.rs b/crates/common/types/src/beacon/containers/deneb.rs index 3092844f..195dbeb2 100644 --- a/crates/common/types/src/beacon/containers/deneb.rs +++ b/crates/common/types/src/beacon/containers/deneb.rs @@ -86,13 +86,23 @@ pub type VersionedHashes = SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attester_slashings: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attestations: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub voluntary_exits: SszList, /// The aggregated sync committee signature over the previous slot's block /// root, plus which members contributed. @@ -201,14 +211,14 @@ pub struct BeaconBlockBody { /// each [`SignedBLSToExecutionChange`] wraps a /// [`super::capella::BLSToExecutionChange`] with a signature proving its /// holder controls the credential being changed. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub bls_to_execution_changes: SszList, /// One KZG commitment per blob this block's proposer chose to include. /// Never the blobs themselves: those are propagated separately as /// [`BlobSidecar`]s, which is what keeps a block's own size independent /// of how much blob data it references. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub blob_kzg_commitments: KzgCommitments, } @@ -307,16 +317,16 @@ pub struct BeaconState { /// slot advances, since a block cannot commit to the root of the state /// containing it. pub latest_block_header: BeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_roots: HistoricalRoots, // -- Eth1 -- pub eth1_data: Eth1Data, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub eth1_data_votes: Eth1DataVotes, /// How many deposits from the contract have been processed, which is /// where the next one will be read from. @@ -324,31 +334,31 @@ pub struct BeaconState { pub eth1_deposit_index: u64, // -- Registry -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub validators: Validators, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub balances: Balances, // -- Randomness -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub randao_mixes: RandaoMixes, // -- Slashings -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub slashings: Slashings, // -- Participation -- /// Per-validator participation flags for the previous epoch, positionally /// parallel to `validators`. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub previous_epoch_participation: EpochParticipation, /// Flags for the current epoch, which become /// `previous_epoch_participation` at the next epoch boundary. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub current_epoch_participation: EpochParticipation, // -- Finality -- - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub justification_bits: JustificationBits, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, @@ -356,7 +366,7 @@ pub struct BeaconState { // -- Inactivity -- /// Per-validator inactivity score, positionally parallel to `validators`. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub inactivity_scores: InactivityScores, // -- Sync committees -- @@ -384,7 +394,7 @@ pub struct BeaconState { // -- History -- /// Capella's replacement for whole [`super::shared::HistoricalBatch`] /// roots: one summary per historical window, appended the same way. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_summaries: HistoricalSummaries, } @@ -430,7 +440,7 @@ pub struct BlobSidecar { /// is also its leaf index for `kzg_commitment_inclusion_proof`. #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub index: BlobIndex, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub blob: Blob, pub kzg_commitment: KzgCommitment, /// A KZG proof that `blob` evaluates to `kzg_commitment`, checkable @@ -439,7 +449,7 @@ pub struct BlobSidecar { /// The header of the block this blob belongs to, signed by its proposer, /// which is what `kzg_commitment_inclusion_proof` terminates at. pub signed_block_header: SignedBeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_commitment_inclusion_proof: BlobKzgCommitmentInclusionProof, } diff --git a/crates/common/types/src/beacon/containers/electra.rs b/crates/common/types/src/beacon/containers/electra.rs index 4610957c..20ed5e7c 100644 --- a/crates/common/types/src/beacon/containers/electra.rs +++ b/crates/common/types/src/beacon/containers/electra.rs @@ -161,11 +161,21 @@ pub struct Attestation { /// the state transition converts an [`Attestation`] into this form before /// checking it. `attesting_indices` now spans every committee an /// [`Attestation`] covers, following the same widening as [`AggregationBits`]. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct IndexedAttestation { /// The attesters, which the specification requires to be sorted and /// unique. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub attesting_indices: AttestingIndices, pub data: AttestationData, pub signature: BlsSignature, @@ -179,7 +189,17 @@ pub struct IndexedAttestation { /// `MAX_ATTESTER_SLASHINGS_ELECTRA` rather than phase0's larger /// `MAX_ATTESTER_SLASHINGS`: evidence spanning a whole slot is proportionally /// more expensive to include. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct AttesterSlashing { pub attestation_1: IndexedAttestation, pub attestation_2: IndexedAttestation, @@ -232,7 +252,17 @@ pub struct SingleAttestation { /// unlike a contract-log deposit, a request arrives already ordered by the /// execution layer, so there is no merkle proof to check, only a position to /// record. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct DepositRequest { pub pubkey: BlsPubkey, pub withdrawal_credentials: Bytes32, @@ -250,7 +280,17 @@ pub struct DepositRequest { /// credentials before honoring the request. An `amount` of /// `FULL_EXIT_REQUEST_AMOUNT` signals a full exit rather than a partial /// withdrawal of that amount. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct WithdrawalRequest { pub source_address: ExecutionAddress, pub validator_pubkey: BlsPubkey, @@ -262,7 +302,17 @@ pub struct WithdrawalRequest { /// to merge `source_pubkey`'s balance into `target_pubkey`'s and exit the /// source, which is how a validator raises its effective balance past /// `MAX_EFFECTIVE_BALANCE` without a fresh deposit. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct ConsolidationRequest { pub source_address: ExecutionAddress, pub source_pubkey: BlsPubkey, @@ -272,14 +322,23 @@ pub struct ConsolidationRequest { /// The execution payload's envelope for every execution-layer-triggered /// request in this block, grouped by kind. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct ExecutionRequests { - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub withdrawals: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub consolidations: SszList, } @@ -294,7 +353,7 @@ pub struct ExecutionRequests { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] pub struct NewPayloadRequest { pub execution_payload: ExecutionPayload, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub versioned_hashes: VersionedHashes, pub parent_beacon_block_root: Root, pub execution_requests: ExecutionRequests, @@ -367,7 +426,17 @@ pub struct PendingConsolidation { /// Unchanged from deneb except for three things: `attester_slashings` and /// `attestations` are now bounded (and, for attestations, shaped) /// differently, and `execution_requests` is appended at the end. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct BeaconBlockBody { /// The proposer's contribution to the chain's randomness, which is a /// signature over the current epoch and so cannot be chosen freely. @@ -376,24 +445,24 @@ pub struct BeaconBlockBody { pub eth1_data: Eth1Data, /// Arbitrary proposer-chosen bytes, which consensus never reads. pub graffiti: Bytes32, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub proposer_slashings: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attester_slashings: AttesterSlashings, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attestations: Attestations, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub voluntary_exits: SszList, /// The aggregated sync committee signature over the previous slot's block /// root, plus which members contributed. pub sync_aggregate: SyncAggregate, pub execution_payload: ExecutionPayload, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub bls_to_execution_changes: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub blob_kzg_commitments: KzgCommitments, /// The execution-layer-triggered deposit, withdrawal, and consolidation /// requests carried by this block's payload. The one field electra adds @@ -449,7 +518,17 @@ impl BeaconBlockBody { } /// A block. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct BeaconBlock { #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub slot: Slot, @@ -462,7 +541,17 @@ pub struct BeaconBlock { pub body: BeaconBlockBody, } -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct SignedBeaconBlock { pub message: BeaconBlock, pub signature: BlsSignature, @@ -508,16 +597,16 @@ pub struct BeaconState { /// slot advances, since a block cannot commit to the root of the state /// containing it. pub latest_block_header: BeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_roots: HistoricalRoots, // -- Eth1 -- pub eth1_data: Eth1Data, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub eth1_data_votes: Eth1DataVotes, /// How many deposits from the contract have been processed, which is /// where the next one will be read from. Superseded for new deposits once @@ -527,34 +616,34 @@ pub struct BeaconState { pub eth1_deposit_index: u64, // -- Registry -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub validators: Validators, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub balances: Balances, // -- Randomness -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub randao_mixes: RandaoMixes, // -- Slashings -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub slashings: Slashings, // -- Participation -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub previous_epoch_participation: EpochParticipation, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub current_epoch_participation: EpochParticipation, // -- Finality -- - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub justification_bits: JustificationBits, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, pub finalized_checkpoint: Checkpoint, // -- Inactivity -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub inactivity_scores: InactivityScores, // -- Sync committees -- @@ -577,7 +666,7 @@ pub struct BeaconState { pub next_withdrawal_validator_index: ValidatorIndex, // -- Deep history -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_summaries: HistoricalSummaries, // -- Deposit requests (EIP-6110) -- @@ -614,15 +703,15 @@ pub struct BeaconState { // -- Pending queues (EIP-7251) -- /// Deposits known but not yet credited to the validator registry, /// drained a bounded amount per epoch by `process_pending_deposits`. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_deposits: PendingDeposits, /// Partial withdrawals known but not yet paid out, drained a bounded /// amount per slot by `get_expected_withdrawals`. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_partial_withdrawals: PendingPartialWithdrawals, /// Consolidations known but not yet applied, drained a bounded amount per /// epoch by `process_pending_consolidations`. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_consolidations: PendingConsolidations, } diff --git a/crates/common/types/src/beacon/containers/fulu.rs b/crates/common/types/src/beacon/containers/fulu.rs index 83167aca..a5f9d7b4 100644 --- a/crates/common/types/src/beacon/containers/fulu.rs +++ b/crates/common/types/src/beacon/containers/fulu.rs @@ -124,17 +124,17 @@ pub struct DataColumnSidecar { /// (which requires each element to already implement `Serialize`) or /// [`ssz_hex`](crate::beacon::serde_helpers::ssz_hex) (which would hex-encode /// the whole list as one string instead of one string per cell). - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex_seq")] pub column: DataColumn, /// The block's full `blob_kzg_commitments`, repeated in every one of that /// block's sidecars rather than fetched separately, which is what lets a /// sidecar be checked on its own. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_commitments: KzgCommitments, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_proofs: KzgProofs, pub signed_block_header: SignedBeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_commitments_inclusion_proof: KzgCommitmentsInclusionProof, } @@ -153,7 +153,7 @@ pub struct MatrixEntry { /// what [`ssz_hex`](crate::beacon::serde_helpers::ssz_hex) exists for, /// unlike [`DataColumnSidecar::column`], which holds a whole list of /// cells. - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub cell: Cell, pub kzg_proof: KzgProof, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] @@ -169,7 +169,7 @@ pub struct MatrixEntry { )] pub struct DataColumnsByRootIdentifier { pub block_root: Root, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub columns: ColumnIndices, } @@ -208,13 +208,13 @@ pub type OptionalPartialDataColumnHeader = SszList; /// from the gossipsub topic's subnet. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] pub struct PartialDataColumnSidecar { - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub cells_present_bitmap: CellsBitList, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex_seq")] pub partial_column: DataColumn, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_proofs: KzgProofs, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub header: OptionalPartialDataColumnHeader, } @@ -226,9 +226,9 @@ pub struct PartialDataColumnSidecar { /// already encodes. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] pub struct PartialDataColumnPartsMetadata { - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub available: CellsBitList, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub requests: CellsBitList, } @@ -243,10 +243,10 @@ pub struct PartialDataColumnPartsMetadata { /// [`DataColumnSidecar::kzg_commitments_inclusion_proof`] uses. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] pub struct PartialDataColumnHeader { - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_commitments: KzgCommitments, pub signed_block_header: SignedBeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_commitments_inclusion_proof: KzgCommitmentsInclusionProof, } @@ -290,20 +290,20 @@ pub struct BeaconState { /// slot advances, since a block cannot commit to the root of the state /// containing it. pub latest_block_header: BeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, /// Frozen since capella: further history is committed to by /// `historical_summaries` instead. Kept rather than removed so a fulu /// state's shape stays hash-tree-root-compatible with every root /// committed while this field was still growing. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_roots: HistoricalRoots, // -- Eth1 -- pub eth1_data: Eth1Data, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub eth1_data_votes: Eth1DataVotes, /// How many deposits from the contract have been processed, which is /// where the next one will be read from. @@ -311,32 +311,32 @@ pub struct BeaconState { pub eth1_deposit_index: u64, // -- Registry -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub validators: Validators, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub balances: Balances, // -- Randomness -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub randao_mixes: RandaoMixes, // -- Slashings -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub slashings: Slashings, // -- Participation -- /// Per-validator participation flags for the previous epoch, positionally /// parallel to `validators`. See altair for why this replaces phase0's /// accumulated attestations. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub previous_epoch_participation: EpochParticipation, /// Flags for the current epoch, which become `previous_epoch_participation` /// at the next epoch boundary. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub current_epoch_participation: EpochParticipation, // -- Finality -- - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub justification_bits: JustificationBits, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, @@ -344,7 +344,7 @@ pub struct BeaconState { // -- Inactivity -- /// Per-validator inactivity score, positionally parallel to `validators`. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub inactivity_scores: InactivityScores, // -- Sync committees -- @@ -373,7 +373,7 @@ pub struct BeaconState { // -- History (capella) -- /// The commitment to history from capella onward, appended to instead of /// `historical_roots` once that field was frozen. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_summaries: HistoricalSummaries, // -- Deposits, exits, and consolidations (electra) -- @@ -404,13 +404,13 @@ pub struct BeaconState { pub earliest_consolidation_epoch: Epoch, /// Deposits credited on the execution layer but not yet applied to the /// registry, drained a bounded number at a time each epoch. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_deposits: PendingDeposits, /// Partial withdrawals requested but not yet paid out. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_partial_withdrawals: PendingPartialWithdrawals, /// Validator consolidations requested but not yet applied. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_consolidations: PendingConsolidations, // -- Proposer lookahead (fulu) -- @@ -419,7 +419,7 @@ pub struct BeaconState { /// epoch boundary by `process_proposer_lookahead` so that /// `get_beacon_proposer_index` becomes a lookup into this vector rather /// than a shuffle computed on demand. - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub proposer_lookahead: ProposerLookahead, } diff --git a/crates/common/types/src/beacon/containers/gloas.rs b/crates/common/types/src/beacon/containers/gloas.rs index 51d236b7..09838fa0 100644 --- a/crates/common/types/src/beacon/containers/gloas.rs +++ b/crates/common/types/src/beacon/containers/gloas.rs @@ -232,7 +232,16 @@ pub struct BuilderPendingWithdrawal { /// An execution-layer-triggered builder deposit (EIP-8282), the builder-side /// counterpart of [`super::electra::DepositRequest`]. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct BuilderDepositRequest { pub pubkey: BlsPubkey, @@ -245,7 +254,16 @@ pub struct BuilderDepositRequest { /// An execution-layer-triggered builder exit (EIP-8282), the builder-side /// counterpart of [`super::electra::WithdrawalRequest`]'s full-exit case. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct BuilderExitRequest { pub source_address: ExecutionAddress, @@ -280,10 +298,20 @@ pub struct PayloadAttestationData { } /// An aggregate payload timeliness attestation, as included in a block. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] #[ssz(progressive_container)] pub struct PayloadAttestation { - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub aggregation_bits: PayloadTimelinessCommitteeBits, pub data: PayloadAttestationData, pub signature: BlsSignature, @@ -311,10 +339,20 @@ pub struct PayloadAttestationMessage { /// A [`PayloadAttestation`] with its attesters named rather than bit-encoded, /// the payload-attestation counterpart of [`IndexedAttestation`]. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] #[ssz(progressive_container)] pub struct IndexedPayloadAttestation { - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub attesting_indices: PayloadTimelinessCommitteeIndices, pub data: PayloadAttestationData, pub signature: BlsSignature, @@ -328,7 +366,16 @@ pub struct IndexedPayloadAttestation { /// proposer chooses among, and signs over, in place of embedding a payload in /// the block directly. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] #[ssz(progressive_container)] pub struct ExecutionPayloadBid { @@ -349,7 +396,7 @@ pub struct ExecutionPayloadBid { /// the payload is never revealed or attested to as available. #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub execution_payment: Gwei, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub blob_kzg_commitments: BlobKzgCommitments, /// The root of the [`ExecutionRequests`] this payload's envelope will /// carry, committed to here so the bid is binding on the requests too, @@ -358,7 +405,16 @@ pub struct ExecutionPayloadBid { } #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct SignedExecutionPayloadBid { pub message: ExecutionPayloadBid, @@ -372,19 +428,28 @@ pub struct SignedExecutionPayloadBid { /// and `builder_exits` (EIP-8282), the execution-layer path [`Builder`]s are /// onboarded and retired through from the fork onward. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] #[ssz(progressive_container)] pub struct ExecutionRequests { - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: DepositRequests, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub withdrawals: WithdrawalRequests, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub consolidations: ConsolidationRequests, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub builder_deposits: BuilderDepositRequests, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub builder_exits: BuilderExitRequests, } @@ -397,14 +462,24 @@ pub struct ExecutionRequests { /// (EIP-7928) and `slot_number` (EIP-8061) are appended. Not /// `#[derive(Default)]`: `logs_bloom` is an [`SszVector`], which, like every /// earlier fork's payload, has no meaningful empty value. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] #[ssz(progressive_container)] pub struct ExecutionPayload { pub parent_hash: ExecutionBlockHash, pub fee_recipient: ExecutionAddress, pub state_root: Bytes32, pub receipts_root: Bytes32, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub logs_bloom: LogsBloom, pub prev_randao: Bytes32, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] @@ -415,13 +490,13 @@ pub struct ExecutionPayload { pub gas_used: u64, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub timestamp: u64, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub extra_data: ExtraData, pub base_fee_per_gas: Uint256, pub block_hash: ExecutionBlockHash, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex_seq")] pub transactions: Transactions, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub withdrawals: Withdrawals, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub blob_gas_used: u64, @@ -429,7 +504,7 @@ pub struct ExecutionPayload { pub excess_blob_gas: u64, /// The serialized block access list this payload's execution produced /// (EIP-7928). - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub block_access_list: BlockAccessList, /// The slot this payload was built for (EIP-8061's gas limit schedule /// reads it against the schedule rather than the enclosing block's own @@ -441,7 +516,17 @@ pub struct ExecutionPayload { /// A builder-signed reveal of the payload it bid for: the payload itself, /// the execution-layer-triggered requests it carries, and enough to bind it /// to the block whose [`ExecutionPayloadBid`] it fulfills. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] #[ssz(progressive_container)] pub struct ExecutionPayloadEnvelope { pub payload: ExecutionPayload, @@ -452,7 +537,17 @@ pub struct ExecutionPayloadEnvelope { pub parent_beacon_block_root: Root, } -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct SignedExecutionPayloadEnvelope { pub message: ExecutionPayloadEnvelope, pub signature: BlsSignature, @@ -469,7 +564,7 @@ pub struct SignedExecutionPayloadEnvelope { #[ssz(progressive_container)] pub struct NewPayloadRequest { pub execution_payload: ExecutionPayload, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub versioned_hashes: VersionedHashes, pub parent_beacon_block_root: Root, pub execution_requests: ExecutionRequests, @@ -555,10 +650,20 @@ impl TryFrom<&Attestation> for super::electra::Attestation { /// An attestation with its attesters named rather than bit-encoded, the same /// role electra's plays, with `attesting_indices` now the unbounded /// [`AttestingIndices`]. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] #[ssz(progressive_container)] pub struct IndexedAttestation { - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub attesting_indices: AttestingIndices, pub data: AttestationData, pub signature: BlsSignature, @@ -567,7 +672,17 @@ pub struct IndexedAttestation { /// Evidence that a set of validators made two conflicting attestations. /// Unchanged in shape from electra; what changed is [`IndexedAttestation`] /// itself. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct AttesterSlashing { pub attestation_1: IndexedAttestation, pub attestation_2: IndexedAttestation, @@ -627,7 +742,16 @@ pub struct SignedAggregateAndProof { /// separate fetch). Every other field is electra's, now progressive /// (EIP-7688). #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] #[ssz(progressive_container)] pub struct BeaconBlockBody { @@ -638,20 +762,20 @@ pub struct BeaconBlockBody { pub eth1_data: Eth1Data, /// Arbitrary proposer-chosen bytes, which consensus never reads. pub graffiti: Bytes32, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub proposer_slashings: ProposerSlashings, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attester_slashings: AttesterSlashings, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attestations: Attestations, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: Deposits, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub voluntary_exits: VoluntaryExits, /// The aggregated sync committee signature over the previous slot's /// block root, plus which members contributed. pub sync_aggregate: SyncAggregate, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub bls_to_execution_changes: BlsToExecutionChanges, /// The proposer's binding commitment to the builder's bid for this /// slot's payload (EIP-7732); the payload itself is revealed afterward @@ -659,7 +783,7 @@ pub struct BeaconBlockBody { pub signed_execution_payload_bid: SignedExecutionPayloadBid, /// The payload timeliness committee's votes on the *previous* slot's /// payload. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub payload_attestations: PayloadAttestations, /// The previous slot's execution-layer-triggered requests, carried /// forward so `process_payload_attestation` and friends do not need a @@ -684,7 +808,16 @@ impl BeaconBlockBody { /// A block. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct BeaconBlock { #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] @@ -699,7 +832,16 @@ pub struct BeaconBlock { } #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct SignedBeaconBlock { pub message: BeaconBlock, @@ -747,18 +889,18 @@ pub struct BeaconState { /// slot advances, since a block cannot commit to the root of the state /// containing it. pub latest_block_header: BeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, /// Frozen since capella: further history is committed to by /// `historical_summaries` instead. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_roots: HistoricalRoots, // -- Eth1 -- pub eth1_data: Eth1Data, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub eth1_data_votes: Eth1DataVotes, /// How many deposits from the contract have been processed, which is /// where the next one will be read from. @@ -768,34 +910,34 @@ pub struct BeaconState { // -- Registry (EIP-7688) -- /// The validator registry, tree-backed and progressively merkleized. See /// [`ProgressiveValidators`]. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub validators: ProgressiveValidators, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub balances: ProgressiveBalances, // -- Randomness -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub randao_mixes: RandaoMixes, // -- Slashings -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub slashings: Slashings, // -- Participation (EIP-7688) -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub previous_epoch_participation: EpochParticipation, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub current_epoch_participation: EpochParticipation, // -- Finality -- - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub justification_bits: JustificationBits, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, pub finalized_checkpoint: Checkpoint, // -- Inactivity (EIP-7688) -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub inactivity_scores: InactivityScores, // -- Sync committees -- @@ -815,7 +957,7 @@ pub struct BeaconState { pub next_withdrawal_validator_index: ValidatorIndex, // -- History (capella) -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_summaries: HistoricalSummaries, // -- Deposits, exits, and consolidations (electra) -- @@ -831,20 +973,20 @@ pub struct BeaconState { pub consolidation_balance_to_consume: Gwei, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub earliest_consolidation_epoch: Epoch, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_deposits: PendingDeposits, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_partial_withdrawals: PendingPartialWithdrawals, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub pending_consolidations: PendingConsolidations, // -- Proposer lookahead (fulu) -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub proposer_lookahead: ProposerLookahead, // -- Builders (EIP-7732) -- /// The builder registry. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub builders: Builders, /// Where the builder withdrawal sweep last stopped, the builder-side /// counterpart of `next_withdrawal_validator_index`. @@ -852,13 +994,13 @@ pub struct BeaconState { pub next_withdrawal_builder_index: BuilderIndex, /// Bits tracking, for each of the last `SLOTS_PER_HISTORICAL_ROOT` slots, /// whether that slot's payload was made available. - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub execution_payload_availability: ExecutionPayloadAvailability, /// Builder payments owed for the previous and current epoch, settled by /// `process_builder_pending_payments`. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub builder_pending_payments: BuilderPendingPayments, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub builder_pending_withdrawals: BuilderPendingWithdrawals, /// The bid the payload currently expected for this slot, if any, was /// bought under. @@ -866,7 +1008,7 @@ pub struct BeaconState { /// The withdrawals `get_expected_withdrawals` computed for the payload /// this slot expects, cached here so a late-revealed payload's /// `process_execution_payload` does not have to recompute the sweep. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub payload_expected_withdrawals: Withdrawals, /// The cached window of payload timeliness committees for the previous, /// current, and lookahead epochs, refreshed each epoch by @@ -896,9 +1038,9 @@ pub struct BeaconState { pub struct DataColumnSidecar { #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub index: ColumnIndex, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex_seq")] pub column: DataColumn, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_proofs: KzgProofs, #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] pub slot: Slot, @@ -931,11 +1073,11 @@ pub struct DataColumnSidecar { Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, )] pub struct PartialDataColumnSidecar { - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub cells_present_bitmap: CellsBitList, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex_seq")] pub partial_column: DataColumn, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub kzg_proofs: KzgProofs, } @@ -946,9 +1088,9 @@ pub struct PartialDataColumnSidecar { Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, )] pub struct PartialDataColumnPartsMetadata { - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub available: CellsBitList, - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub requests: CellsBitList, } diff --git a/crates/common/types/src/beacon/containers/json_tests.rs b/crates/common/types/src/beacon/containers/json_tests.rs new file mode 100644 index 00000000..62c6ca3e --- /dev/null +++ b/crates/common/types/src/beacon/containers/json_tests.rs @@ -0,0 +1,405 @@ +//! JSON round trips of the containers a validator client submits, which the +//! Beacon API's request bodies carry: serialize, deserialize, compare. Each +//! value has every list non-empty and every integer non-zero, so an adapter +//! that dropped or misread a field shows up as a difference. + +use super::{altair, capella, electra, gloas, shared}; +use crate::beacon::primitives::{BlsPubkey, BlsSignature, KzgCommitment, Uint256}; + +fn round_trip(value: &T) +where + T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug, +{ + let json = serde_json::to_string(value).unwrap(); + let back: T = serde_json::from_str(&json).unwrap(); + assert_eq!(&back, value, "{json}"); +} + +fn signature(byte: u8) -> BlsSignature { + let mut signature = BlsSignature::default(); + signature.0[0] = byte; + signature.0[95] = byte; + signature +} + +fn pubkey(byte: u8) -> BlsPubkey { + let mut pubkey = BlsPubkey::default(); + pubkey.0[0] = byte; + pubkey +} + +fn header(slot: u64) -> shared::SignedBeaconBlockHeader { + shared::SignedBeaconBlockHeader { + message: shared::BeaconBlockHeader { + slot, + proposer_index: 3, + parent_root: [1; 32].into(), + state_root: [2; 32].into(), + body_root: [3; 32].into(), + }, + signature: signature(9), + } +} + +fn data() -> shared::AttestationData { + shared::AttestationData { + slot: 12, + index: 0, + beacon_block_root: [4; 32].into(), + source: shared::Checkpoint { + epoch: 1, + root: [5; 32].into(), + }, + target: shared::Checkpoint { + epoch: 2, + root: [6; 32].into(), + }, + } +} + +fn deposit() -> shared::Deposit { + shared::Deposit { + proof: vec![[7u8; 32].into(); crate::beacon::constants::DEPOSIT_CONTRACT_TREE_DEPTH + 1] + .try_into() + .unwrap(), + data: shared::DepositData { + pubkey: pubkey(1), + withdrawal_credentials: [8; 32].into(), + amount: 32_000_000_000, + signature: signature(2), + }, + } +} + +fn exit() -> shared::SignedVoluntaryExit { + shared::SignedVoluntaryExit { + message: shared::VoluntaryExit { + epoch: 5, + validator_index: 6, + }, + signature: signature(3), + } +} + +fn bls_change() -> capella::SignedBLSToExecutionChange { + capella::SignedBLSToExecutionChange { + message: capella::BLSToExecutionChange { + validator_index: 4, + from_bls_pubkey: pubkey(4), + to_execution_address: [0xaa; 20].into(), + }, + signature: signature(4), + } +} + +fn sync_aggregate() -> altair::SyncAggregate { + let mut aggregate = altair::SyncAggregate::default(); + aggregate.sync_committee_bits.set(3, true).unwrap(); + aggregate.sync_committee_signature = signature(5); + aggregate +} + +fn electra_attestation() -> electra::Attestation { + let mut aggregation_bits = electra::AggregationBits::with_length(5).unwrap(); + aggregation_bits.set(1, true).unwrap(); + aggregation_bits.set(4, true).unwrap(); + let mut committee_bits = electra::CommitteeBits::default(); + committee_bits.set(2, true).unwrap(); + electra::Attestation { + aggregation_bits, + data: data(), + signature: signature(6), + committee_bits, + } +} + +fn electra_indexed() -> electra::IndexedAttestation { + electra::IndexedAttestation { + attesting_indices: vec![3, 9, 27].try_into().unwrap(), + data: data(), + signature: signature(7), + } +} + +fn electra_payload() -> super::deneb::ExecutionPayload { + use super::{bellatrix, deneb}; + deneb::ExecutionPayload { + parent_hash: [1; 32].into(), + fee_recipient: [2; 20].into(), + state_root: [3; 32].into(), + receipts_root: [4; 32].into(), + logs_bloom: bellatrix::LogsBloom::try_from(vec![ + 0xab; + crate::beacon::preset::BYTES_PER_LOGS_BLOOM + ]) + .unwrap(), + prev_randao: [5; 32].into(), + block_number: 10, + gas_limit: 30_000_000, + gas_used: 21_000, + timestamp: 1_700_000_000, + extra_data: vec![0xde, 0xad].try_into().unwrap(), + base_fee_per_gas: Uint256::from(7_000_000_000u64), + block_hash: [6; 32].into(), + transactions: vec![ + bellatrix::Transaction::try_from(vec![1, 2, 3]).unwrap(), + bellatrix::Transaction::try_from(vec![4]).unwrap(), + ] + .try_into() + .unwrap(), + withdrawals: vec![capella::Withdrawal { + index: 1, + validator_index: 2, + address: [3; 20].into(), + amount: 4, + }] + .try_into() + .unwrap(), + blob_gas_used: 131_072, + excess_blob_gas: 262_144, + } +} + +fn electra_requests() -> electra::ExecutionRequests { + electra::ExecutionRequests { + deposits: vec![electra::DepositRequest { + pubkey: pubkey(5), + withdrawal_credentials: [1; 32].into(), + amount: 32_000_000_000, + signature: signature(8), + index: 11, + }] + .try_into() + .unwrap(), + withdrawals: vec![electra::WithdrawalRequest { + source_address: [2; 20].into(), + validator_pubkey: pubkey(6), + amount: 5, + }] + .try_into() + .unwrap(), + consolidations: vec![electra::ConsolidationRequest { + source_address: [3; 20].into(), + source_pubkey: pubkey(7), + target_pubkey: pubkey(8), + }] + .try_into() + .unwrap(), + } +} + +fn electra_block() -> electra::SignedBeaconBlock { + let mut body = electra::BeaconBlockBody::empty(); + body.randao_reveal = signature(1); + body.graffiti = [9; 32].into(); + body.proposer_slashings = vec![shared::ProposerSlashing { + signed_header_1: header(7), + signed_header_2: header(7), + }] + .try_into() + .unwrap(); + body.attester_slashings = vec![electra::AttesterSlashing { + attestation_1: electra_indexed(), + attestation_2: electra_indexed(), + }] + .try_into() + .unwrap(); + body.attestations = vec![electra_attestation()].try_into().unwrap(); + body.deposits = vec![deposit()].try_into().unwrap(); + body.voluntary_exits = vec![exit()].try_into().unwrap(); + body.sync_aggregate = sync_aggregate(); + body.execution_payload = electra_payload(); + body.bls_to_execution_changes = vec![bls_change()].try_into().unwrap(); + body.blob_kzg_commitments = vec![KzgCommitment([5; 48])].try_into().unwrap(); + body.execution_requests = electra_requests(); + electra::SignedBeaconBlock { + message: electra::BeaconBlock { + slot: 33, + proposer_index: 4, + parent_root: [1; 32].into(), + state_root: [2; 32].into(), + body, + }, + signature: signature(9), + } +} + +fn gloas_attestation() -> gloas::Attestation { + gloas::Attestation::from(&electra_attestation()) +} + +fn gloas_payload() -> gloas::ExecutionPayload { + let payload = electra_payload(); + gloas::ExecutionPayload { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom, + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data, + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions: payload + .transactions + .iter() + .map(|transaction| gloas::Transaction::from(transaction.to_vec())) + .collect::>() + .into(), + withdrawals: payload + .withdrawals + .iter() + .cloned() + .collect::>() + .into(), + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + block_access_list: vec![0xca, 0xfe].into(), + slot_number: 33, + } +} + +fn gloas_requests() -> gloas::ExecutionRequests { + let requests = electra_requests(); + gloas::ExecutionRequests { + deposits: requests.deposits.iter().cloned().collect::>().into(), + withdrawals: requests + .withdrawals + .iter() + .cloned() + .collect::>() + .into(), + consolidations: requests + .consolidations + .iter() + .cloned() + .collect::>() + .into(), + builder_deposits: vec![gloas::BuilderDepositRequest { + pubkey: pubkey(9), + withdrawal_credentials: [4; 32].into(), + amount: 1, + signature: signature(1), + }] + .into(), + builder_exits: vec![gloas::BuilderExitRequest { + source_address: [5; 20].into(), + pubkey: pubkey(10), + }] + .into(), + } +} + +fn payload_attestation() -> gloas::PayloadAttestation { + let mut aggregation_bits = gloas::PayloadTimelinessCommitteeBits::default(); + aggregation_bits.set(1, true).unwrap(); + gloas::PayloadAttestation { + aggregation_bits, + data: gloas::PayloadAttestationData { + beacon_block_root: [6; 32].into(), + slot: 32, + payload_present: true, + blob_data_available: true, + }, + signature: signature(2), + } +} + +fn gloas_block() -> gloas::SignedBeaconBlock { + let electra = electra_block().message; + let mut body = gloas::BeaconBlockBody::empty(); + body.randao_reveal = electra.body.randao_reveal; + body.graffiti = electra.body.graffiti; + body.proposer_slashings = electra + .body + .proposer_slashings + .iter() + .cloned() + .collect::>() + .into(); + body.attestations = vec![gloas_attestation()].into(); + body.deposits = electra + .body + .deposits + .iter() + .cloned() + .collect::>() + .into(); + body.voluntary_exits = electra + .body + .voluntary_exits + .iter() + .cloned() + .collect::>() + .into(); + body.sync_aggregate = sync_aggregate(); + body.signed_execution_payload_bid = gloas::SignedExecutionPayloadBid { + message: gloas::ExecutionPayloadBid { + parent_block_hash: [1; 32].into(), + parent_block_root: [2; 32].into(), + block_hash: [3; 32].into(), + prev_randao: [4; 32].into(), + fee_recipient: [5; 20].into(), + gas_limit: 30_000_000, + builder_index: 7, + slot: 33, + value: 8, + execution_payment: 9, + execution_requests_root: [7; 32].into(), + blob_kzg_commitments: vec![KzgCommitment([6; 48])].try_into().unwrap(), + }, + signature: signature(3), + }; + body.payload_attestations = vec![payload_attestation()].try_into().unwrap(); + body.parent_execution_requests = gloas_requests(); + gloas::SignedBeaconBlock { + message: gloas::BeaconBlock { + slot: 33, + proposer_index: 4, + parent_root: [1; 32].into(), + state_root: [2; 32].into(), + body, + }, + signature: signature(9), + } +} + +#[test] +fn a_fulu_block_round_trips_through_json() { + round_trip(&electra_block()); +} + +#[test] +fn a_gloas_block_round_trips_through_json() { + round_trip(&gloas_block()); +} + +#[test] +fn a_gloas_envelope_round_trips_through_json() { + round_trip(&gloas::SignedExecutionPayloadEnvelope { + message: gloas::ExecutionPayloadEnvelope { + payload: gloas_payload(), + execution_requests: gloas_requests(), + builder_index: 7, + beacon_block_root: [1; 32].into(), + parent_beacon_block_root: [2; 32].into(), + }, + signature: signature(4), + }); +} + +#[test] +fn an_oversized_list_is_refused_rather_than_truncated() { + let mut json = serde_json::to_value(electra_block()).unwrap(); + let commitment = json["message"]["body"]["blob_kzg_commitments"][0].clone(); + json["message"]["body"]["blob_kzg_commitments"] = serde_json::Value::Array(vec![ + commitment; + crate::beacon::preset::MAX_BLOB_COMMITMENTS_PER_BLOCK + + 1 + ]); + assert!(serde_json::from_value::(json).is_err()); +} diff --git a/crates/common/types/src/beacon/containers/mod.rs b/crates/common/types/src/beacon/containers/mod.rs index 1e8b09e1..367da3c7 100644 --- a/crates/common/types/src/beacon/containers/mod.rs +++ b/crates/common/types/src/beacon/containers/mod.rs @@ -1630,6 +1630,9 @@ impl DataColumnSidecar { } } +#[cfg(test)] +mod json_tests; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/common/types/src/beacon/containers/phase0.rs b/crates/common/types/src/beacon/containers/phase0.rs index 631dbd8e..d937795a 100644 --- a/crates/common/types/src/beacon/containers/phase0.rs +++ b/crates/common/types/src/beacon/containers/phase0.rs @@ -42,7 +42,7 @@ pub type PendingAttestations = SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attester_slashings: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub attestations: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub deposits: SszList, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub voluntary_exits: SszList, } @@ -167,16 +167,16 @@ pub struct BeaconState { /// advances, since a block cannot commit to the root of the state containing /// it. pub latest_block_header: BeaconBlockHeader, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub historical_roots: HistoricalRoots, // -- Eth1 -- pub eth1_data: Eth1Data, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub eth1_data_votes: Eth1DataVotes, /// How many deposits from the contract have been processed, which is where /// the next one will be read from. @@ -184,31 +184,31 @@ pub struct BeaconState { pub eth1_deposit_index: u64, // -- Registry -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub validators: Validators, - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub balances: Balances, // -- Randomness -- - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub randao_mixes: RandaoMixes, // -- Slashings -- - #[serde(serialize_with = "crate::beacon::serde_helpers::quoted_u64_seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::quoted_u64_seq")] pub slashings: Slashings, // -- Attestations -- /// Attestations for the previous epoch, replayed at the epoch boundary. Only /// phase0 has these; altair replaces them with participation flags. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub previous_epoch_attestations: PendingAttestations, /// Attestations for the current epoch, which become /// `previous_epoch_attestations` at the next boundary. - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub current_epoch_attestations: PendingAttestations, // -- Finality -- - #[serde(serialize_with = "crate::beacon::serde_helpers::ssz_hex::serialize")] + #[serde(with = "crate::beacon::serde_helpers::ssz_hex")] pub justification_bits: JustificationBits, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, diff --git a/crates/common/types/src/beacon/containers/shared.rs b/crates/common/types/src/beacon/containers/shared.rs index c7d857f1..b2df9069 100644 --- a/crates/common/types/src/beacon/containers/shared.rs +++ b/crates/common/types/src/beacon/containers/shared.rs @@ -240,7 +240,16 @@ pub struct AttestationData { /// The execution chain's deposit state, as voted on by proposers. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct Eth1Data { /// The deposit contract's merkle root at this point. @@ -272,9 +281,9 @@ pub struct Eth1Block { /// [`HistoricalRoots`] once the window is full. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] pub struct HistoricalBatch { - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub block_roots: BlockRoots, - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub state_roots: StateRoots, } @@ -324,7 +333,16 @@ pub struct DepositMessage { /// A deposit as recorded by the deposit contract. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct DepositData { pub pubkey: BlsPubkey, @@ -339,9 +357,19 @@ pub struct DepositData { } /// A deposit together with its merkle proof against the deposit contract root. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct Deposit { - #[serde(serialize_with = "crate::beacon::serde_helpers::seq::serialize")] + #[serde(with = "crate::beacon::serde_helpers::seq")] pub proof: DepositProof, pub data: DepositData, } @@ -353,7 +381,16 @@ pub struct Deposit { /// A block without its body, which is what the state retains and what proposer /// slashings compare. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct BeaconBlockHeader { #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] @@ -371,7 +408,16 @@ pub struct BeaconBlockHeader { } #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct SignedBeaconBlockHeader { pub message: BeaconBlockHeader, @@ -380,7 +426,16 @@ pub struct SignedBeaconBlockHeader { /// Evidence that a proposer signed two different blocks for the same slot. #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct ProposerSlashing { pub signed_header_1: SignedBeaconBlockHeader, @@ -389,7 +444,17 @@ pub struct ProposerSlashing { /// A validator's request to stop validating. #[derive( - Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct VoluntaryExit { /// The earliest epoch the exit may be processed at. @@ -400,7 +465,16 @@ pub struct VoluntaryExit { } #[derive( - Debug, Clone, Default, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot, + Debug, + Clone, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, )] pub struct SignedVoluntaryExit { pub message: VoluntaryExit, diff --git a/crates/common/types/src/beacon/primitives.rs b/crates/common/types/src/beacon/primitives.rs index 43c2589b..b19309a8 100644 --- a/crates/common/types/src/beacon/primitives.rs +++ b/crates/common/types/src/beacon/primitives.rs @@ -337,6 +337,18 @@ impl serde::Serialize for U256 { } } +impl<'de> serde::Deserialize<'de> for U256 { + /// The inverse of [`serde::Serialize`] above: a decimal string, quoted or + /// bare like every integer [`super::serde_helpers::quoted_or_bare`] reads. + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let text = ::deserialize(deserializer)?; + U256::from_dec_str(text.trim()).map_err(serde::de::Error::custom) + } +} + /// Why a decimal string was not a [`U256`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum ParseU256Error { @@ -490,8 +502,9 @@ impl_hex_serialize!(H160, BlsPubkey, BlsSignature, KzgCommitment, KzgProof); /// The `Deserialize` counterpart of [`impl_hex_serialize`], for the types the /// Beacon API accepts in a request body: a validator's public key (as a -/// validator id), a signature (inside a submitted attestation) and an execution -/// address (a proposer's fee recipient). Hex with or +/// validator id), a signature (inside a submitted attestation), an execution +/// address (a proposer's fee recipient) and a block's KZG commitments and +/// proofs. Hex with or /// without the `0x` prefix, of exactly the type's width. macro_rules! impl_hex_deserialize { ($($ty:ty),* $(,)?) => { @@ -508,7 +521,7 @@ macro_rules! impl_hex_deserialize { }; } -impl_hex_deserialize!(BlsPubkey, BlsSignature, H160); +impl_hex_deserialize!(BlsPubkey, BlsSignature, H160, KzgCommitment, KzgProof); #[cfg(test)] mod tests { diff --git a/crates/common/types/src/beacon/serde_helpers.rs b/crates/common/types/src/beacon/serde_helpers.rs index 976b2890..a20db64a 100644 --- a/crates/common/types/src/beacon/serde_helpers.rs +++ b/crates/common/types/src/beacon/serde_helpers.rs @@ -179,6 +179,25 @@ pub mod quoted_u64_seq { } seq.end() } + + /// The inverse: an array of quoted (or bare) integers, which is how + /// [`quoted_or_bare`](super::quoted_or_bare) reads one. + pub fn deserialize<'de, D, C, T>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + C: TryFrom>, + C::Error: std::fmt::Debug, + T: std::str::FromStr, + T::Err: std::fmt::Display, + { + let texts = as serde::Deserialize>::deserialize(deserializer)?; + let values = texts + .iter() + .map(|text| text.trim().parse().map_err(serde::de::Error::custom)) + .collect::, _>>()?; + C::try_from(values) + .map_err(|err| serde::de::Error::custom(format!("invalid sequence: {err:?}"))) + } } /// A sequence of sequences of integers, each innermost value written quoted. @@ -351,6 +370,31 @@ pub mod ssz_hex_seq { } seq.end() } + + /// The inverse: an array of hex strings, each decoded through the + /// element's `SszDecode`, so a byte list's bound is checked as on the wire. + pub fn deserialize<'de, D, C, T>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + C: TryFrom>, + C::Error: std::fmt::Debug, + T: libssz::SszDecode, + { + let texts = as serde::Deserialize>::deserialize(deserializer)?; + let values = texts + .iter() + .map(|text| { + let digits = text.trim(); + let bytes = hex::decode(digits.strip_prefix("0x").unwrap_or(digits)) + .map_err(serde::de::Error::custom)?; + T::from_ssz_bytes(&bytes).map_err(|err| { + serde::de::Error::custom(format!("invalid SSZ encoding: {err:?}")) + }) + }) + .collect::, _>>()?; + C::try_from(values) + .map_err(|err| serde::de::Error::custom(format!("invalid sequence: {err:?}"))) + } } /// A sequence of values that serialize themselves, from a foreign collection. @@ -375,6 +419,21 @@ pub mod seq { } seq.end() } + + /// The inverse: a JSON array of elements, collected into the foreign + /// collection through its `TryFrom>`, which is where a length bound + /// is checked (a progressive list's conversion cannot fail). + pub fn deserialize<'de, D, C, T>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + C: TryFrom>, + C::Error: std::fmt::Debug, + T: serde::Deserialize<'de>, + { + let values = as serde::Deserialize>::deserialize(deserializer)?; + C::try_from(values) + .map_err(|err| serde::de::Error::custom(format!("invalid sequence: {err:?}"))) + } } #[cfg(test)] diff --git a/crates/net/rpc/src/beacon/config.rs b/crates/net/rpc/src/beacon/config.rs index 10e2279f..f3c06db4 100644 --- a/crates/net/rpc/src/beacon/config.rs +++ b/crates/net/rpc/src/beacon/config.rs @@ -33,7 +33,55 @@ use ethlambda_types::beacon::{config::Config, constants, preset, serde_helpers:: use serde_json::{Map, Value}; pub(crate) fn routes() -> Router { - Router::new().route("/eth/v1/config/spec", get(get_spec)) + Router::new() + .route("/eth/v1/config/spec", get(get_spec)) + .route("/eth/v1/config/fork_schedule", get(get_fork_schedule)) +} + +/// `GET /eth/v1/config/fork_schedule`: every fork of the node's `Config` +/// that is scheduled, oldest first. +/// +/// Validator clients compare this against their own schedule at startup (nimbus +/// refuses a node that cannot answer it). +async fn get_fork_schedule(State(store): State) -> Response { + crate::json_response(serde_json::json!({ "data": fork_schedule(store.config().as_ref()) })) +} + +/// The `Fork` objects (`previous_version`, `current_version`, `epoch`) for +/// phase0 and every later fork whose epoch is not `FAR_FUTURE_EPOCH`. +/// +/// Phase0 is its own predecessor, as in the spec's genesis `Fork`. An +/// unscheduled fork is skipped without breaking the chain: the next scheduled +/// fork's `previous_version` is the last *scheduled* one's version. +fn fork_schedule(config: &Config) -> Vec { + let forks = [ + (config.altair_fork_version, config.altair_fork_epoch), + (config.bellatrix_fork_version, config.bellatrix_fork_epoch), + (config.capella_fork_version, config.capella_fork_epoch), + (config.deneb_fork_version, config.deneb_fork_epoch), + (config.electra_fork_version, config.electra_fork_epoch), + (config.fulu_fork_version, config.fulu_fork_epoch), + (config.gloas_fork_version, config.gloas_fork_epoch), + ]; + let fork = |previous: [u8; 4], current: [u8; 4], epoch: u64| { + serde_json::json!({ + "previous_version": hex_string(previous), + "current_version": hex_string(current), + "epoch": epoch.to_string(), + }) + }; + + let genesis = config.genesis_fork_version; + let mut schedule = vec![fork(genesis, genesis, 0)]; + let mut previous = genesis; + for (version, epoch) in forks { + if epoch == constants::FAR_FUTURE_EPOCH { + continue; + } + schedule.push(fork(previous, version, epoch)); + previous = version; + } + schedule } async fn get_spec(State(store): State) -> Response { @@ -287,6 +335,96 @@ mod tests { ); } + /// What nimbus's validator client (`checkConfig` and + /// `getConsensusForkConfig`, v26.10.0) reads off the spec before it will + /// use a node: a missing or differing key marks the node incompatible. + #[tokio::test] + async fn the_spec_carries_every_key_nimbus_checks() { + let json = get_spec_json().await; + let data = &json["data"]; + + let checked = [ + ( + "MAX_VALIDATORS_PER_COMMITTEE", + preset::MAX_VALIDATORS_PER_COMMITTEE.to_string(), + ), + ("SLOTS_PER_EPOCH", preset::SLOTS_PER_EPOCH.to_string()), + ( + "EPOCHS_PER_ETH1_VOTING_PERIOD", + preset::EPOCHS_PER_ETH1_VOTING_PERIOD.to_string(), + ), + ( + "SLOTS_PER_HISTORICAL_ROOT", + preset::SLOTS_PER_HISTORICAL_ROOT.to_string(), + ), + ( + "EPOCHS_PER_HISTORICAL_VECTOR", + preset::EPOCHS_PER_HISTORICAL_VECTOR.to_string(), + ), + ( + "EPOCHS_PER_SLASHINGS_VECTOR", + preset::EPOCHS_PER_SLASHINGS_VECTOR.to_string(), + ), + ( + "HISTORICAL_ROOTS_LIMIT", + preset::HISTORICAL_ROOTS_LIMIT.to_string(), + ), + ( + "VALIDATOR_REGISTRY_LIMIT", + preset::VALIDATOR_REGISTRY_LIMIT.to_string(), + ), + ( + "MAX_PROPOSER_SLASHINGS", + preset::MAX_PROPOSER_SLASHINGS.to_string(), + ), + ( + "MAX_ATTESTER_SLASHINGS", + preset::MAX_ATTESTER_SLASHINGS.to_string(), + ), + ("MAX_ATTESTATIONS", preset::MAX_ATTESTATIONS.to_string()), + ("MAX_DEPOSITS", preset::MAX_DEPOSITS.to_string()), + ( + "MAX_VOLUNTARY_EXITS", + preset::MAX_VOLUNTARY_EXITS.to_string(), + ), + ]; + for (key, expected) in checked { + assert_eq!(data[key], expected, "{key}"); + } + for domain in [ + "DOMAIN_BEACON_PROPOSER", + "DOMAIN_BEACON_ATTESTER", + "DOMAIN_RANDAO", + "DOMAIN_DEPOSIT", + "DOMAIN_VOLUNTARY_EXIT", + "DOMAIN_SELECTION_PROOF", + "DOMAIN_AGGREGATE_AND_PROOF", + ] { + assert!(data[domain].as_str().unwrap().starts_with("0x"), "{domain}"); + } + // One of the two keys that fix the slot time (it compares the one + // present with its own and refuses a node that has neither, unless + // it too runs the default). + assert!(data["SECONDS_PER_SLOT"].is_string()); + // Each fork's version and epoch, altair's scheduled. + for fork in [ + "ALTAIR", + "BELLATRIX", + "CAPELLA", + "DENEB", + "ELECTRA", + "FULU", + "GLOAS", + ] { + assert!(data[format!("{fork}_FORK_VERSION")].is_string(), "{fork}"); + assert!(data[format!("{fork}_FORK_EPOCH")].is_string(), "{fork}"); + } + assert_ne!( + data["ALTAIR_FORK_EPOCH"], + constants::FAR_FUTURE_EPOCH.to_string() + ); + } + #[tokio::test] async fn the_spec_carries_gloas_preset_and_constant_keys() { let json = get_spec_json().await; @@ -320,6 +458,66 @@ mod tests { assert!(data["MIN_BUILDER_WITHDRAWABILITY_DELAY"].is_string()); } + #[tokio::test] + async fn the_fork_schedule_lists_scheduled_forks_with_quoted_epochs() { + let fixture = beacon_fixture(64); + let config = fixture.store.config(); + let app = routes().with_state(fixture.store); + let response = app + .oneshot( + Request::builder() + .uri("/eth/v1/config/fork_schedule") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + let data = json["data"].as_array().unwrap(); + + // Phase0 comes first and is its own predecessor. + assert_eq!(data[0]["epoch"], "0"); + assert_eq!(data[0]["previous_version"], data[0]["current_version"]); + assert_eq!( + data[0]["current_version"], + hex_string(config.genesis_fork_version) + ); + // Each fork's predecessor is the previous entry's version, and epochs + // never go down. + let epoch = |v: &serde_json::Value| v["epoch"].as_str().unwrap().parse::().unwrap(); + for pair in data.windows(2) { + assert_eq!(pair[1]["previous_version"], pair[0]["current_version"]); + assert!(epoch(&pair[1]) >= epoch(&pair[0])); + } + for fork in data { + assert_ne!(fork["epoch"], constants::FAR_FUTURE_EPOCH.to_string()); + } + } + + #[test] + fn an_unscheduled_fork_is_left_out_of_the_schedule() { + let mut config = Config::mainnet(); + config.gloas_fork_epoch = constants::FAR_FUTURE_EPOCH; + let schedule = fork_schedule(&config); + let last = schedule.last().unwrap(); + assert_eq!( + last["current_version"], + hex_string(config.fulu_fork_version) + ); + + config.fulu_fork_epoch = constants::FAR_FUTURE_EPOCH; + config.gloas_fork_epoch = 100; + let schedule = fork_schedule(&config); + let last = schedule.last().unwrap(); + // Gloas follows the last scheduled fork, electra. + assert_eq!( + last["previous_version"], + hex_string(config.electra_fork_version) + ); + } + #[test] fn no_key_is_reported_twice() { let config = Config::mainnet(); diff --git a/crates/net/rpc/src/beacon/gloas_proposal.rs b/crates/net/rpc/src/beacon/gloas_proposal.rs index fb9218a1..12a3a507 100644 --- a/crates/net/rpc/src/beacon/gloas_proposal.rs +++ b/crates/net/rpc/src/beacon/gloas_proposal.rs @@ -84,13 +84,13 @@ use ethlambda_types::{ }, primitives::H256, }; -use libssz::{SszDecode as _, SszEncode as _}; +use libssz::SszEncode as _; use libssz_derive::{SszDecode, SszEncode}; use serde::Deserialize; use tracing::{debug, info, warn}; use crate::beacon::{ - ApiError, + ApiError, BodyEncoding, proposal::{Blobs, CellKzgProofs, decimal, require_gloas_slot}, validator::{FeeRecipients, head}, }; @@ -140,22 +140,24 @@ impl PayloadCache { /// Gloas's `BlockContents`, the Beacon API's envelope for a self-built block /// with its payload: not a consensus container, so it lives with the API. -#[derive(Debug, Clone, PartialEq, SszEncode, SszDecode, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, SszEncode, SszDecode, serde::Serialize, serde::Deserialize)] pub(crate) struct GloasBlockContents { pub(crate) block: BeaconBlock, pub(crate) execution_payload_envelope: ExecutionPayloadEnvelope, - #[serde(serialize_with = "ethlambda_types::beacon::serde_helpers::seq::serialize")] + #[serde(with = "ethlambda_types::beacon::serde_helpers::seq")] pub(crate) kzg_proofs: CellKzgProofs, - #[serde(serialize_with = "ethlambda_types::beacon::serde_helpers::ssz_hex_seq::serialize")] + #[serde(with = "ethlambda_types::beacon::serde_helpers::ssz_hex_seq")] pub(crate) blobs: Blobs, } /// Gloas's `SignedExecutionPayloadEnvelopeContents`, what the stateless form of /// `publishExecutionPayloadEnvelope` receives. -#[derive(Debug, Clone, PartialEq, SszEncode, SszDecode)] +#[derive(Debug, Clone, PartialEq, SszEncode, SszDecode, serde::Serialize, serde::Deserialize)] pub(crate) struct GloasSignedEnvelopeContents { pub(crate) signed_execution_payload_envelope: SignedExecutionPayloadEnvelope, + #[serde(with = "ethlambda_types::beacon::serde_helpers::seq")] pub(crate) kzg_proofs: CellKzgProofs, + #[serde(with = "ethlambda_types::beacon::serde_helpers::ssz_hex_seq")] pub(crate) blobs: Blobs, } @@ -756,13 +758,10 @@ async fn post_envelope_waiting( if consensus_version(headers) != Some(ForkName::Gloas) { return ApiError::BadRequest("Eth-Consensus-Version must be gloas").into_response(); } - if !is_ssz(headers) { - return ( - StatusCode::UNSUPPORTED_MEDIA_TYPE, - "envelopes are accepted as application/octet-stream only", - ) - .into_response(); - } + let encoding = match BodyEncoding::from_headers(headers) { + Ok(encoding) => encoding, + Err(err) => return err.into_response(), + }; let blob_data_included = match headers .get("eth-blob-data-included") .and_then(|value| value.to_str().ok()) @@ -775,8 +774,8 @@ async fn post_envelope_waiting( } }; let (signed, supplied) = if blob_data_included { - match GloasSignedEnvelopeContents::from_ssz_bytes(body) { - Ok(contents) => ( + match encoding.decode::(body) { + Some(contents) => ( contents.signed_execution_payload_envelope, Some(( contents @@ -787,7 +786,7 @@ async fn post_envelope_waiting( contents.kzg_proofs.to_vec(), )), ), - Err(_) => { + None => { return ApiError::BadRequest( "the body is not a gloas SignedExecutionPayloadEnvelopeContents", ) @@ -795,9 +794,9 @@ async fn post_envelope_waiting( } } } else { - match SignedExecutionPayloadEnvelope::from_ssz_bytes(body) { - Ok(signed) => (signed, None), - Err(_) => { + match encoding.decode::(body) { + Some(signed) => (signed, None), + None => { return ApiError::BadRequest( "the body is not a gloas SignedExecutionPayloadEnvelope", ) @@ -1093,6 +1092,82 @@ mod tests { (headers, body) } + /// The same as [`envelope_request`] with a JSON body, which is what teku's + /// validator client posts. + fn json_envelope_request(included: Option<&str>, body: Vec) -> (HeaderMap, Vec) { + let (mut headers, body) = envelope_request(included, body); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ); + (headers, body) + } + + #[tokio::test] + async fn an_envelope_is_accepted_as_json_exactly_as_it_is_as_ssz() { + let state = state_to_build_on(); + let produced = produced_without_blobs(&state); + let signature = sign_block(&state, &produced.block); + let (store, _root, post) = store_with_block(&state, &produced, signature); + let signed = signed_envelope(&post, &produced); + let wait = Duration::from_millis(100); + + let mut published = Vec::new(); + for json in [false, true] { + let network = Arc::new(RecordingNetwork::default()); + let p2p: RpcToP2PRef = network.clone(); + let (headers, body) = if json { + json_envelope_request(Some("false"), serde_json::to_vec(&signed).unwrap()) + } else { + envelope_request(Some("false"), signed.to_ssz()) + }; + let response = post_envelope_waiting( + &store, + &p2p, + &PayloadCache::default(), + &headers, + &body, + wait, + ) + .await; + assert_eq!(response.status(), StatusCode::OK, "json: {json}"); + published.push(network.envelopes.lock().unwrap().clone()); + } + assert_eq!(published[0].len(), 1); + assert_eq!(published[0], published[1]); + + // The same refusals: a forged signature, and a body that is not an envelope. + let network = Arc::new(RecordingNetwork::default()); + let p2p: RpcToP2PRef = network.clone(); + let mut forged = signed.clone(); + forged.signature = + sign_envelope(&post, produced.block.proposer_index + 1, &produced.envelope); + let (headers, body) = + json_envelope_request(Some("false"), serde_json::to_vec(&forged).unwrap()); + let response = post_envelope_waiting( + &store, + &p2p, + &PayloadCache::default(), + &headers, + &body, + wait, + ) + .await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let (headers, body) = json_envelope_request(Some("false"), b"{\"message\": 1}".to_vec()); + let response = post_envelope_waiting( + &store, + &p2p, + &PayloadCache::default(), + &headers, + &body, + wait, + ) + .await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert!(network.envelopes.lock().unwrap().is_empty()); + } + /// A produced block with no blobs, so the envelope needs no columns. fn produced_without_blobs(state: &BeaconState) -> GloasProduced { use ethlambda_state_transition::beacon::gloas_block_production::{ @@ -1242,15 +1317,21 @@ mod tests { let response = post_envelope_waiting(&store, &p2p, &cache, &headers, &body, wait).await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); - // JSON is not accepted. + // Any other content type is a 415 (JSON is accepted, see + // `an_envelope_is_accepted_as_json_exactly_as_it_is_as_ssz`), and SSZ + // bytes labelled JSON are not a JSON envelope. let signed = signed_envelope(&post, &produced); let (mut headers, body) = envelope_request(Some("false"), signed.to_ssz()); + headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain")); + let response = post_envelope_waiting(&store, &p2p, &cache, &headers, &body, wait).await; + assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); + let (mut headers, body) = envelope_request(Some("false"), signed.to_ssz()); headers.insert( header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); let response = post_envelope_waiting(&store, &p2p, &cache, &headers, &body, wait).await; - assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); // The blob-data header is required. let (headers, body) = envelope_request(None, signed.to_ssz()); @@ -1366,6 +1447,88 @@ mod tests { } } + #[tokio::test] + async fn json_envelope_contents_carry_the_blobs_and_are_checked_like_ssz() { + let WithBlob { + state, + produced, + blob, + proofs, + } = with_blob(); + let signature = sign_block(&state, &produced.block); + let (store, _root, post) = store_with_block(&state, &produced, signature); + let signed = signed_envelope(&post, &produced); + let wait = Duration::from_millis(100); + let contents = |blob: &[u8]| GloasSignedEnvelopeContents { + signed_execution_payload_envelope: signed.clone(), + kzg_proofs: CellKzgProofs::try_from(proofs.clone()).unwrap(), + blobs: blobs_list(vec![blob.to_vec()]).unwrap(), + }; + let send = |network: Arc, contents: GloasSignedEnvelopeContents| { + let store = store.clone(); + async move { + let p2p: RpcToP2PRef = network; + let (headers, body) = + json_envelope_request(Some("true"), serde_json::to_vec(&contents).unwrap()); + post_envelope_waiting( + &store, + &p2p, + &PayloadCache::default(), + &headers, + &body, + wait, + ) + .await + } + }; + + let network = Arc::new(RecordingNetwork::default()); + let response = send(network.clone(), contents(&blob)).await; + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + network.envelopes.lock().unwrap()[0].1.len(), + ethlambda_types::beacon::preset::NUMBER_OF_COLUMNS + ); + + let mut tampered = blob.clone(); + tampered[63] ^= 1; + let rejected = Arc::new(RecordingNetwork::default()); + let response = send(rejected.clone(), contents(&tampered)).await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert!(rejected.envelopes.lock().unwrap().is_empty()); + } + + #[test] + fn the_block_and_envelope_contents_round_trip_through_json() { + let WithBlob { + state, + produced, + blob, + proofs, + } = with_blob(); + let signature = sign_block(&state, &produced.block); + let (_store, _root, post) = store_with_block(&state, &produced, signature); + let signed = signed_envelope(&post, &produced); + let contents = GloasSignedEnvelopeContents { + signed_execution_payload_envelope: signed, + kzg_proofs: CellKzgProofs::try_from(proofs.clone()).unwrap(), + blobs: blobs_list(vec![blob.clone()]).unwrap(), + }; + let back: GloasSignedEnvelopeContents = + serde_json::from_slice(&serde_json::to_vec(&contents).unwrap()).unwrap(); + assert_eq!(back, contents); + + let block_contents = GloasBlockContents { + block: produced.block.clone(), + execution_payload_envelope: produced.envelope.clone(), + kzg_proofs: CellKzgProofs::try_from(proofs).unwrap(), + blobs: blobs_list(vec![blob]).unwrap(), + }; + let back: GloasBlockContents = + serde_json::from_slice(&serde_json::to_vec(&block_contents).unwrap()).unwrap(); + assert_eq!(back, block_contents); + } + #[tokio::test] async fn an_envelope_with_blobs_gossips_every_column_and_a_tampered_blob_is_refused() { let WithBlob { diff --git a/crates/net/rpc/src/beacon/mod.rs b/crates/net/rpc/src/beacon/mod.rs index 3b94fbb1..728a2087 100644 --- a/crates/net/rpc/src/beacon/mod.rs +++ b/crates/net/rpc/src/beacon/mod.rs @@ -72,6 +72,9 @@ pub(crate) enum ApiError { /// A request this node cannot answer right now, typically because its /// execution client did not (the Beacon API's 503). ServiceUnavailable(&'static str), + /// A request body in an encoding this endpoint does not take (415). A + /// client that posted SSZ and gets this one falls back to JSON. + UnsupportedMediaType(&'static str), } impl From for ApiError { @@ -90,6 +93,7 @@ impl IntoResponse for ApiError { ApiError::NotFound(m) => (StatusCode::NOT_FOUND, m), ApiError::Internal(m) => (StatusCode::INTERNAL_SERVER_ERROR, m), ApiError::ServiceUnavailable(m) => (StatusCode::SERVICE_UNAVAILABLE, m), + ApiError::UnsupportedMediaType(m) => (StatusCode::UNSUPPORTED_MEDIA_TYPE, m), }; let body = serde_json::json!({ "code": status.as_u16(), "message": message }); let mut response = crate::json_response(body); @@ -98,6 +102,64 @@ impl IntoResponse for ApiError { } } +/// How a request body is encoded, from its `Content-Type`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BodyEncoding { + Json, + Ssz, +} + +impl BodyEncoding { + /// An absent `Content-Type` is read as JSON, which is what every client + /// that predates SSZ submission sends. Anything else is a 415 rather than a + /// guess, so a client that tries SSZ first (prysm) learns this node wants + /// the other. + pub(crate) fn from_headers(headers: &axum::http::HeaderMap) -> Result { + let content_type = headers + .get(axum::http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .map(|value| value.split(';').next().unwrap_or("").trim()); + match content_type { + None | Some("application/json") => Ok(Self::Json), + Some(crate::SSZ_CONTENT_TYPE) => Ok(Self::Ssz), + Some(_) => Err(ApiError::UnsupportedMediaType( + "Content-Type must be application/json or application/octet-stream", + )), + } + } + + /// Decode one container in this encoding, or `None` when the body is not + /// one. JSON and SSZ go through the same type, so what follows a decode + /// cannot depend on which one the client chose. + pub(crate) fn decode(self, body: &[u8]) -> Option + where + T: serde::de::DeserializeOwned + libssz::SszDecode, + { + match self { + Self::Json => serde_json::from_slice(body).ok(), + Self::Ssz => T::from_ssz_bytes(body).ok(), + } + } +} + +/// Decode the array a batch-submission endpoint takes, as JSON or as the SSZ +/// `List[T, ...]` of the same elements, by the request's `Content-Type`. +pub(crate) fn decode_list( + headers: &axum::http::HeaderMap, + body: &[u8], +) -> Result, ApiError> +where + T: serde::de::DeserializeOwned + libssz::SszDecode, +{ + let invalid = || ApiError::BadRequest("invalid request body"); + match BodyEncoding::from_headers(headers)? { + BodyEncoding::Json => serde_json::from_slice(body).map_err(|_| invalid()), + BodyEncoding::Ssz => { + as libssz::SszDecode>::from_ssz_bytes(body).map_err(|_| invalid()) + } + } +} + /// Every route this surface serves. /// /// Deliberately not a superset of [`crate::build_api_router`]: the `/lean/v0` diff --git a/crates/net/rpc/src/beacon/pool.rs b/crates/net/rpc/src/beacon/pool.rs index 88ad1bab..03bd1429 100644 --- a/crates/net/rpc/src/beacon/pool.rs +++ b/crates/net/rpc/src/beacon/pool.rs @@ -51,7 +51,7 @@ use ethlambda_types::{ use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; -use crate::beacon::{ApiError, validator::head}; +use crate::beacon::{ApiError, decode_list, validator::head}; pub(crate) fn routes() -> Router { Router::new() @@ -96,8 +96,9 @@ async fn post_pool_attestations( Ok(fork) => fork, Err(err) => return err.into_response(), }; - let Ok(attestations) = serde_json::from_slice::>(&body) else { - return ApiError::BadRequest("invalid request body").into_response(); + let attestations = match decode_list::(&headers, &body) { + Ok(attestations) => attestations, + Err(err) => return err.into_response(), }; let (_head_root, state) = match head(&store) { Ok(found) => found, @@ -340,23 +341,25 @@ async fn post_aggregate_and_proofs( Err(err) => return err.into_response(), }; // Gloas's aggregate is its own container, so the header picks the decoder. - let aggregates = if header_fork == ForkName::Gloas { - serde_json::from_slice::>(&body).map(|signed| { - signed - .into_iter() - .map(SignedAggregateAndProof::Gloas) - .collect() - }) - } else { - serde_json::from_slice::>(&body).map(|signed| { - signed - .into_iter() - .map(SignedAggregateAndProof::Electra) - .collect() - }) - }; - let Ok(aggregates): Result, _> = aggregates else { - return ApiError::BadRequest("invalid request body").into_response(); + let aggregates: Result, ApiError> = + if header_fork == ForkName::Gloas { + decode_list::(&headers, &body).map(|signed| { + signed + .into_iter() + .map(SignedAggregateAndProof::Gloas) + .collect() + }) + } else { + decode_list::(&headers, &body).map(|signed| { + signed + .into_iter() + .map(SignedAggregateAndProof::Electra) + .collect() + }) + }; + let aggregates = match aggregates { + Ok(aggregates) => aggregates, + Err(err) => return err.into_response(), }; let now_ms = std::time::SystemTime::now() @@ -1090,4 +1093,149 @@ mod tests { assert_eq!(response.status(), StatusCode::BAD_REQUEST); assert!(fixture.network.aggregates.lock().unwrap().is_empty()); } + + /// POST `body` with an explicit content type, as a client that does not + /// send JSON would. + async fn post_raw( + fixture: &Fixture, + uri: &str, + content_type: Option<&str>, + version: &str, + body: Vec, + ) -> (StatusCode, serde_json::Value) { + let network: RpcToP2PRef = fixture.network.clone(); + let app = routes() + .with_state(fixture.store.clone()) + .layer(Extension(network)) + .layer(Extension(fixture.pool.clone())); + let mut request = Request::post(uri).header("eth-consensus-version", version); + if let Some(content_type) = content_type { + request = request.header("content-type", content_type); + } + let response = app + .oneshot(request.body(Body::from(body)).unwrap()) + .await + .unwrap(); + let status = response.status(); + let body = response.into_body().collect().await.unwrap().to_bytes(); + ( + status, + serde_json::from_slice(&body).unwrap_or(serde_json::Value::Null), + ) + } + + /// Nimbus submits its attestations this way. + #[tokio::test] + async fn attestations_are_accepted_as_ssz() { + use libssz::SszEncode as _; + let fixture = fixture(); + let attestation = attestation(&fixture, 0, 0); + let (status, json) = post_raw( + &fixture, + "/eth/v2/beacon/pool/attestations", + Some("application/octet-stream"), + fixture.version, + vec![attestation.clone()].to_ssz(), + ) + .await; + assert_eq!(status, StatusCode::OK, "{json}"); + let published = fixture.network.published.lock().unwrap(); + assert_eq!(published.len(), 1); + assert_eq!(published[0].1, attestation); + } + + #[tokio::test] + async fn an_unsupported_content_type_is_a_415_and_malformed_ssz_a_400() { + let fixture = fixture(); + let uri = "/eth/v2/beacon/pool/attestations"; + let (status, json) = + post_raw(&fixture, uri, Some("text/plain"), fixture.version, vec![]).await; + assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE); + assert_eq!(json["code"], 415); + + let (status, _) = post_raw( + &fixture, + uri, + Some("application/octet-stream"), + fixture.version, + vec![1, 2, 3], + ) + .await; + assert_eq!(status, StatusCode::BAD_REQUEST); + + // No content type at all is read as JSON, as it always was. + let (status, _) = post_raw(&fixture, uri, None, fixture.version, b"[]".to_vec()).await; + assert_eq!(status, StatusCode::OK); + } + + /// Prysm publishes its aggregates as SSZ and falls back to JSON only on a 415. + #[tokio::test] + async fn aggregates_are_accepted_as_ssz() { + use libssz::SszEncode as _; + let fixture = fixture(); + let slot = fixture.state.slot(); + let committee = get_beacon_committee(&fixture.state, slot, 0).unwrap(); + let votes: Vec = (0..committee.len()) + .map(|position| attestation(&fixture, 0, position)) + .collect(); + submit(&fixture, &votes).await; + let aggregate = fixture + .pool + .lock() + .unwrap() + .aggregate(votes[0].data.hash_tree_root(), slot, 0) + .unwrap(); + // Two entries, so the list's offset table is exercised. + let first = signed_aggregate(&fixture, committee[0], aggregate.clone()); + let second = signed_aggregate(&fixture, committee[1], aggregate); + + let (status, json) = post_raw( + &fixture, + "/eth/v2/validator/aggregate_and_proofs", + Some("application/octet-stream"), + "fulu", + vec![first.clone(), second.clone()].to_ssz(), + ) + .await; + assert_eq!(status, StatusCode::OK, "{json}"); + let published = fixture.network.aggregates.lock().unwrap(); + assert_eq!( + *published, + [ + SignedAggregateAndProof::Electra(first), + SignedAggregateAndProof::Electra(second) + ] + ); + } + + #[tokio::test] + async fn gloas_aggregates_are_accepted_as_ssz() { + use libssz::SszEncode as _; + let fixture = fixture_at(ForkName::Gloas, 0); + let slot = fixture.state.slot(); + let committee = get_beacon_committee(&fixture.state, slot, 0).unwrap(); + let votes: Vec = (0..committee.len()) + .map(|position| attestation(&fixture, 0, position)) + .collect(); + submit(&fixture, &votes).await; + let pooled = fixture + .pool + .lock() + .unwrap() + .aggregate(votes[0].data.hash_tree_root(), slot, 0) + .unwrap(); + let signed = signed_gloas_aggregate(&fixture, committee[0], &pooled); + + let (status, json) = post_raw( + &fixture, + "/eth/v2/validator/aggregate_and_proofs", + Some("application/octet-stream"), + "gloas", + vec![signed.clone()].to_ssz(), + ) + .await; + assert_eq!(status, StatusCode::OK, "{json}"); + let published = fixture.network.aggregates.lock().unwrap(); + assert_eq!(*published, [SignedAggregateAndProof::Gloas(signed)]); + } } diff --git a/crates/net/rpc/src/beacon/proposal.rs b/crates/net/rpc/src/beacon/proposal.rs index 52be05c6..475c0005 100644 --- a/crates/net/rpc/src/beacon/proposal.rs +++ b/crates/net/rpc/src/beacon/proposal.rs @@ -50,13 +50,13 @@ use ethlambda_types::{ }, primitives::H256, }; -use libssz::{SszDecode as _, SszEncode as _}; +use libssz::SszEncode as _; use libssz_derive::{SszDecode, SszEncode}; use libssz_types::SszList; use serde::Deserialize; use tracing::{info, warn}; -use crate::beacon::{ApiError, validator::FeeRecipients, validator::head}; +use crate::beacon::{ApiError, BodyEncoding, validator::FeeRecipients, validator::head}; use crate::shared::content::{Encoding, ssz_response, with_consensus_version}; /// One KZG proof per cell of every blob, fulu's `kzg_proofs` bound. @@ -69,20 +69,22 @@ pub(crate) type Blobs = SszList Router { ) } -/// `POST /eth/v2/beacon/blocks`, SSZ-encoded `SignedBlockContents`. +/// `POST /eth/v2/beacon/blocks`, `SignedBlockContents` (fulu) or a bare +/// `SignedBeaconBlock` (gloas), as SSZ or JSON by `Content-Type`. /// /// Checked before it goes anywhere: the fork is fulu, it carries no blobs /// (whose data columns this node cannot publish yet, see the module docs), it @@ -117,21 +120,14 @@ async fn post_block( if !matches!(fork, Some(ForkName::Fulu | ForkName::Gloas)) { return ApiError::BadRequest("Eth-Consensus-Version must be fulu or gloas").into_response(); } - let is_ssz = headers - .get(header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .is_some_and(|value| value.starts_with(crate::SSZ_CONTENT_TYPE)); - if !is_ssz { - return ( - StatusCode::UNSUPPORTED_MEDIA_TYPE, - "blocks are accepted as application/octet-stream only", - ) - .into_response(); - } + let encoding = match BodyEncoding::from_headers(&headers) { + Ok(encoding) => encoding, + Err(err) => return err.into_response(), + }; if fork == Some(ForkName::Gloas) { - return post_gloas_block(&store, &p2p, &body).await; + return post_gloas_block(&store, &p2p, encoding, &body).await; } - let Ok(contents) = FuluSignedBlockContents::from_ssz_bytes(&body) else { + let Some(contents) = encoding.decode::(&body) else { return ApiError::BadRequest("the body is not fulu SignedBlockContents").into_response(); }; if !contents.blobs.is_empty() @@ -176,7 +172,7 @@ async fn post_block( StatusCode::OK.into_response() } -/// The gloas half of `publishBlockV2`: a bare SSZ `SignedBeaconBlock`, since a +/// The gloas half of `publishBlockV2`: a bare `SignedBeaconBlock`, since a /// gloas block carries no payload and no blobs of its own (the envelope and /// the data columns follow through `publishExecutionPayloadEnvelope`). /// @@ -185,8 +181,13 @@ async fn post_block( /// slot's, and the proposer's signature verifies against the parent's state /// advanced to the slot. The advance and the signature are CPU-bound and run /// off the runtime. -async fn post_gloas_block(store: &Store, p2p: &RpcToP2PRef, body: &[u8]) -> Response { - let Ok(signed) = containers::gloas::SignedBeaconBlock::from_ssz_bytes(body) else { +async fn post_gloas_block( + store: &Store, + p2p: &RpcToP2PRef, + encoding: BodyEncoding, + body: &[u8], +) -> Response { + let Some(signed) = encoding.decode::(body) else { return ApiError::BadRequest("the body is not a gloas SignedBeaconBlock").into_response(); }; let block = containers::SignedBeaconBlock::Gloas(signed); @@ -579,6 +580,61 @@ mod tests { assert_eq!(json["message"], "blocks are accepted for fulu slots only"); } + /// A fulu body in either encoding reaches the same check and gets the same + /// answer, here the refusal of a slot the schedule does not place at fulu. + #[tokio::test] + async fn fulu_block_contents_are_accepted_as_json_exactly_as_ssz() { + let (store, _) = gloas_scheduled_store(); + let contents = FuluSignedBlockContents { + signed_block: containers::electra::SignedBeaconBlock { + message: containers::electra::BeaconBlock { + slot: 64, + proposer_index: 0, + parent_root: H256::ZERO, + state_root: H256::ZERO, + body: containers::electra::BeaconBlockBody::empty(), + }, + signature: Default::default(), + }, + kzg_proofs: Default::default(), + blobs: Default::default(), + }; + let back: FuluSignedBlockContents = + serde_json::from_slice(&serde_json::to_vec(&contents).unwrap()).unwrap(); + assert_eq!(back, contents); + + let mut answers = Vec::new(); + for (content_type, body) in [ + (crate::SSZ_CONTENT_TYPE, contents.to_ssz()), + ("application/json", serde_json::to_vec(&contents).unwrap()), + ] { + let network: RpcToP2PRef = Arc::new(RecordingNetwork::default()); + let app = routes().with_state(store.clone()).layer(Extension(network)); + let request = Request::post("/eth/v2/beacon/blocks") + .header("eth-consensus-version", "fulu") + .header(header::CONTENT_TYPE, content_type) + .body(Body::from(body)) + .unwrap(); + answers.push(respond(app, request).await); + } + assert_eq!(answers[0].0, StatusCode::BAD_REQUEST); + assert_eq!(answers[0], answers[1]); + } + + #[tokio::test] + async fn a_block_content_type_other_than_json_or_ssz_is_a_415() { + let (store, _) = gloas_scheduled_store(); + let network: RpcToP2PRef = Arc::new(RecordingNetwork::default()); + let app = routes().with_state(store).layer(Extension(network)); + let request = Request::post("/eth/v2/beacon/blocks") + .header("eth-consensus-version", "fulu") + .header(header::CONTENT_TYPE, "text/plain") + .body(Body::from("{}")) + .unwrap(); + let (status, _) = respond(app, request).await; + assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE); + } + #[tokio::test] async fn a_block_over_the_default_body_limit_is_not_a_413() { let (store, _) = gloas_scheduled_store(); @@ -628,6 +684,27 @@ mod tests { assert_eq!(status, StatusCode::OK); assert_eq!(network.blocks.lock().unwrap().len(), 1); + // The same block as JSON is accepted and published identically, and a + // forged one refused. + let post_json = |network: Arc, + signed: &containers::gloas::SignedBeaconBlock| { + let network: RpcToP2PRef = network; + let app = routes().with_state(store.clone()).layer(Extension(network)); + let request = Request::post("/eth/v2/beacon/blocks") + .header("eth-consensus-version", "gloas") + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from(serde_json::to_vec(signed).unwrap())) + .unwrap(); + respond(app, request) + }; + let via_json = Arc::new(RecordingNetwork::default()); + let (status, _) = post_json(via_json.clone(), &signed).await; + assert_eq!(status, StatusCode::OK); + assert_eq!( + *via_json.blocks.lock().unwrap(), + *network.blocks.lock().unwrap() + ); + // A signature from another validator, and a block whose parent is not held. let rejected = Arc::new(RecordingNetwork::default()); let mut forged = signed.clone(); diff --git a/crates/net/rpc/src/beacon/ptc.rs b/crates/net/rpc/src/beacon/ptc.rs index e827d3e5..3e57da6b 100644 --- a/crates/net/rpc/src/beacon/ptc.rs +++ b/crates/net/rpc/src/beacon/ptc.rs @@ -54,7 +54,7 @@ use tracing::{debug, warn}; use crate::{ CustodyColumns, - beacon::{ApiError, validator::head}, + beacon::{ApiError, decode_list, validator::head}, shared::content::{Encoding, ssz_response, with_consensus_version}, }; @@ -377,8 +377,9 @@ async fn post_pool_payload_attestations( if let Err(err) = require_gloas_or_absent(&headers) { return err.into_response(); } - let Ok(messages) = serde_json::from_slice::>(&body) else { - return ApiError::BadRequest("invalid request body").into_response(); + let messages = match decode_list::(&headers, &body) { + Ok(messages) => messages, + Err(err) => return err.into_response(), }; let now_ms = std::time::SystemTime::now() @@ -1084,6 +1085,46 @@ mod tests { assert_eq!(reply.status, StatusCode::BAD_REQUEST); } + /// Lighthouse and prysm submit their payload votes as SSZ. + #[tokio::test] + async fn votes_are_accepted_as_ssz_and_other_content_types_are_a_415() { + use libssz::SszEncode as _; + let state = gloas_state(); + let state_epoch = compute_epoch_at_slot(state.slot()); + let member = window_index(state_epoch, state_epoch, 0); + let (store, root) = store_with_head(state.clone(), gloas_config(), 0); + let message = vote(&state, root, member, true); + + let pool = SharedPayloadAttestationPool::default(); + let network = Arc::new(RecordingNetwork::default()); + let ssz_request = Request::post("/eth/v1/beacon/pool/payload_attestations") + .header("content-type", "application/octet-stream") + .header("eth-consensus-version", "gloas") + .body(Body::from(vec![message.clone()].to_ssz())) + .unwrap(); + let reply = send( + store.clone(), + pool.clone(), + network.clone(), + Vec::new(), + Default::default(), + ssz_request, + ) + .await; + assert_eq!(reply.status, StatusCode::OK); + assert_eq!(*network.payload_attestations.lock().unwrap(), vec![message]); + + let reply = request( + store, + Request::post("/eth/v1/beacon/pool/payload_attestations") + .header("content-type", "text/plain") + .body(Body::from("[]")) + .unwrap(), + ) + .await; + assert_eq!(reply.status, StatusCode::UNSUPPORTED_MEDIA_TYPE); + } + #[tokio::test] async fn the_pool_is_listed_as_aggregates_by_slot() { let state = gloas_state(); diff --git a/crates/net/rpc/src/beacon/states.rs b/crates/net/rpc/src/beacon/states.rs index 2f83b1d1..5d307019 100644 --- a/crates/net/rpc/src/beacon/states.rs +++ b/crates/net/rpc/src/beacon/states.rs @@ -45,6 +45,10 @@ pub(crate) fn routes() -> Router { "/eth/v1/beacon/states/{state_id}/validators", get(get_validators).post(post_validators), ) + .route( + "/eth/v1/beacon/states/{state_id}/validators/{validator_id}", + get(get_validator), + ) } /// Resolve a `state_id` to the block root its state is stored under. @@ -258,6 +262,48 @@ async fn get_validators( validators_response(&store, &state_id, request) } +/// `GET .../validators/{validator_id}`: one registry entry, by index or public +/// key, in the shape of a list entry. Lighthouse's validator client resolves +/// each of its keys to an index this way. An id naming no validator is a 404 +/// here, unlike the list form, which omits it. +async fn get_validator( + Path((state_id, validator_id)): Path<(String, String)>, + State(store): State, +) -> Response { + let id = match ValidatorId::parse(&validator_id) { + Ok(id) => id, + Err(err) => return err.into_response(), + }; + let (root, state) = match load(&store, &state_id) { + Ok(found) => found, + Err(err) => return err.into_response(), + }; + + let found = match id { + ValidatorId::Index(index) => state + .validator(index) + .ok() + .zip(state.balance(index).ok()) + .map(|found| (index, found)), + ValidatorId::Pubkey(pubkey) => state + .iter_validators() + .zip(state.iter_balances()) + .enumerate() + .find(|(_, (validator, _))| validator.pubkey == pubkey) + .map(|(index, found)| (index as ValidatorIndex, found)), + }; + let Some((index, (validator, balance))) = found else { + return ApiError::NotFound("validator not found").into_response(); + }; + + let status = ValidatorStatus::of(validator, balance, compute_epoch_at_slot(state.slot())); + crate::json_response(serde_json::json!({ + "execution_optimistic": crate::shared::optimistic::block_is_optimistic(&store, root), + "finalized": is_finalized(&store, state.slot()), + "data": ValidatorEntry { index, balance, status: status.name(), validator }, + })) +} + /// `POST .../validators`, the form a validator client uses: a long list of /// public keys does not fit in a query string. async fn post_validators( @@ -528,6 +574,41 @@ mod tests { assert_eq!(indices, ["1", "3", "4"]); } + async fn get_one(id: &str) -> axum::response::Response { + let (app, _) = app(); + let request = Request::get(format!("/eth/v1/beacon/states/head/validators/{id}")) + .body(Body::empty()) + .unwrap(); + app.oneshot(request).await.unwrap() + } + + /// What lighthouse's validator client sends for each of its keys. + #[tokio::test] + async fn one_validator_resolves_by_pubkey_or_index() { + let (_, state) = app(); + for id in [pubkey_hex(&state, 3), "3".to_owned()] { + let response = get_one(&id).await; + assert_eq!(response.status(), StatusCode::OK); + let json = body_json(response).await; + assert_eq!(json["execution_optimistic"], false); + assert!(json["finalized"].is_boolean()); + // One object, not a list, in the shape of a list entry. + assert_eq!(json["data"]["index"], "3"); + assert_eq!(json["data"]["status"], "active_ongoing"); + assert!(json["data"]["balance"].is_string()); + assert_eq!(json["data"]["validator"]["pubkey"], pubkey_hex(&state, 3)); + } + } + + #[tokio::test] + async fn an_unknown_single_validator_is_a_404_and_a_malformed_id_a_400() { + assert_eq!(get_one("999").await.status(), StatusCode::NOT_FOUND); + let unknown = format!("0x{}", "ab".repeat(48)); + assert_eq!(get_one(&unknown).await.status(), StatusCode::NOT_FOUND); + assert_eq!(get_one("0x1234").await.status(), StatusCode::BAD_REQUEST); + assert_eq!(get_one("nope").await.status(), StatusCode::BAD_REQUEST); + } + #[test] fn status_follows_the_lifecycle() { let epoch = 10; diff --git a/docs/rpc.md b/docs/rpc.md index 3c335723..17fd4b80 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -233,27 +233,29 @@ surface rather than sitting beside it; a `/lean/v0` path on a beacon node is a | `GET` | `/eth/v1/beacon/states/{state_id}/finality_checkpoints` | JSON | That state's three checkpoints | | `GET` | `/eth/v1/beacon/genesis` | JSON | Genesis time, validators root, fork version | | `GET` | `/eth/v1/config/spec` | JSON | The store's `Config`, plus `PRESET_BASE`, `CONFIG_NAME`, the preset and the constants (see below) | +| `GET` | `/eth/v1/config/fork_schedule` | JSON | Every scheduled fork as `{previous_version, current_version, epoch}` | | `GET` | `/eth/v1/node/syncing` | JSON | Head slot, sync distance, optimistic flag | | `GET` | `/eth/v1/node/health` | *(status only)* | `200` caught up, `206` syncing | | `GET` | `/eth/v1/node/version` | JSON | Client version string | | `GET` | `/eth/v1/node/identity` | JSON | Peer ID and metadata only (see below) | | `GET`, `POST` | `/eth/v1/beacon/states/{state_id}/validators` | JSON | Registry entries by index or pubkey, with status | +| `GET` | `/eth/v1/beacon/states/{state_id}/validators/{validator_id}` | JSON | One registry entry by index or `0x` pubkey; `404` when unknown | | `GET` | `/eth/v1/validator/duties/proposer/{epoch}` | JSON | Proposers for the head's epoch or the next | | `POST` | `/eth/v1/validator/duties/attester/{epoch}` | JSON | Committee assignments for the given indices | | `POST` | `/eth/v1/validator/duties/ptc/{epoch}` | JSON | Payload timeliness committee seats for the given indices (gloas) | | `GET` | `/eth/v1/validator/attestation_data` | JSON | What to attest to at `slot` | | `GET` | `/eth/v1/validator/payload_attestation_data` | JSON or SSZ | What a committee member signs for `slot` (gloas) | -| `POST` | `/eth/v2/beacon/pool/attestations` | *(status only)* | Validate and gossip `SingleAttestation`s | -| `POST` | `/eth/v1/beacon/pool/payload_attestations` | *(status only)* | Validate, pool and gossip `PayloadAttestationMessage`s (gloas) | +| `POST` | `/eth/v2/beacon/pool/attestations` | *(status only)* | Validate and gossip `SingleAttestation`s (JSON or SSZ body) | +| `POST` | `/eth/v1/beacon/pool/payload_attestations` | *(status only)* | Validate, pool and gossip `PayloadAttestationMessage`s (gloas; JSON or SSZ body) | | `GET` | `/eth/v1/beacon/pool/payload_attestations` | JSON | The pool's votes as aggregated `PayloadAttestation`s (gloas) | | `POST` | `/eth/v1/validator/beacon_committee_subscriptions` | *(status only)* | Aggregators' entries join their committee's subnet | | `GET` | `/eth/v2/validator/aggregate_attestation` | JSON | The pooled votes for a data root and committee, aggregated | -| `POST` | `/eth/v2/validator/aggregate_and_proofs` | *(status only)* | Validate and gossip `SignedAggregateAndProof`s | +| `POST` | `/eth/v2/validator/aggregate_and_proofs` | *(status only)* | Validate and gossip `SignedAggregateAndProof`s (JSON or SSZ body) | | `GET` | `/eth/v3/validator/blocks/{slot}` | SSZ or JSON | An unsigned fulu block built on the head (`produceBlockV3`) | | `POST` | `/eth/v4/validator/blocks/{slot}` | SSZ or JSON | An unsigned self-built gloas block, with its envelope and blobs when asked (`produceBlockV4`) | | `GET` | `/eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` | SSZ or JSON | The unsigned envelope `produceBlockV4` built (gloas) | -| `POST` | `/eth/v2/beacon/blocks` | *(status only)* | Gossip and import a signed fulu or gloas block (`publishBlockV2`, SSZ) | -| `POST` | `/eth/v1/beacon/execution_payload_envelopes` | *(status only)* | Gossip a signed envelope and its data columns (gloas, SSZ) | +| `POST` | `/eth/v2/beacon/blocks` | *(status only)* | Gossip and import a signed fulu or gloas block (`publishBlockV2`; JSON or SSZ body) | +| `POST` | `/eth/v1/beacon/execution_payload_envelopes` | *(status only)* | Gossip a signed envelope and its data columns (gloas; JSON or SSZ body) | | `POST` | `/eth/v1/validator/prepare_beacon_proposer` | *(status only)* | Acknowledged, not acted on (see below) | ### Validator endpoints @@ -284,6 +286,12 @@ the chain actor writes, so no request waits on the actor. (`Store::head_payload_status`, or one fresh `get_head_node` walk when none is recorded for this head). A pre-gloas head counts as FULL (see [Spec Deviations](./spec_deviations.md#a-pre-gloas-head-counts-as-full-in-attestation_dataindex)). +- **`states/{state_id}/validators/{validator_id}`** is one entry of the list + endpoint, in the same shape and with the same `state_id` handling, + `execution_optimistic` and `finalized`. The id is an index or a `0x` pubkey; + one naming no validator is a `404` (the list form omits it), a malformed one + a `400`. Lighthouse's validator client resolves each key to its index this + way, and stays inactive without it. - **`pool/attestations`** checks each attestation against the electra `beacon_attestation_{subnet_id}` gossip conditions it can evaluate (clock window, `data.index == 0`, target epoch, the voted block known and the target @@ -319,6 +327,16 @@ the chain actor writes, so no request waits on the actor. nothing about them). What passes is gossiped on the topic and goes into the pool. The `Eth-Consensus-Version` header picks the decoder: `gloas` takes gloas's `SignedAggregateAndProof`, the others electra's. +- **Request body encodings.** `pool/attestations`, `pool/payload_attestations` + and `aggregate_and_proofs` take either JSON (`application/json`, also the + reading of a request with no `Content-Type`) or the SSZ `List[...]` of the + same items (`application/octet-stream`): the offset-table encoding every + variable-size list uses, which is what prysm sends for aggregates and payload + votes, with a JSON retry only on a `415`, and what nimbus can send for + attestations. Any other content type is a `415`, and a body that does not + decode in the type's encoding a `400`. `POST beacon/blocks` and + `POST beacon/execution_payload_envelopes` choose the same way (below); the + other validator-client submissions are JSON. - **The attestation pool** holds, the best-covered per data root and committee: votes from `pool/attestations` and the aggregator subnets, aggregates from `aggregate_and_proofs`, and every electra gossip aggregate @@ -344,8 +362,11 @@ the chain actor writes, so no request waits on the actor. blobs. A slot the schedule does not place at fulu (gloas included) is a `400`, checked before the execution client; `produceBlockV3` is the fulu endpoint, and a gloas slot goes to `produceBlockV4`. -- **`POST beacon/blocks`** takes SSZ and the `Eth-Consensus-Version` header - (`fulu` or `gloas`, else `400`; a non-SSZ content type is a `415`). Fulu: a +- **`POST beacon/blocks`** takes JSON or SSZ, by `Content-Type` as above, and + the `Eth-Consensus-Version` header (`fulu` or `gloas`, else `400`; any other + content type is a `415`). The JSON is the specification's: lodestar's and + nimbus's validator clients post it, teku's falls back to it, and lighthouse + and prysm post SSZ. Both reach the same checks after decoding. Fulu: a `SignedBlockContents`; the node checks the block is after the head and its proposer signature, then gossips it on `beacon_block` and hands it to the chain actor to import. A slot the schedule does not place at fulu is a `400`, @@ -405,8 +426,11 @@ serves the cached unsigned envelope, for a client that asked for the block with root. **`POST /eth/v1/beacon/execution_payload_envelopes`** takes the signed envelope, -SSZ only (`415` otherwise), with `Eth-Consensus-Version: gloas` and -`Eth-Blob-Data-Included` (both required, else `400`): +as JSON or SSZ by `Content-Type` (any other is a `415`; teku's validator client +posts JSON, nimbus posts the bare envelope as JSON and the contents as SSZ, +lighthouse posts SSZ, prysm posts SSZ and retries as JSON on a `415`), with +`Eth-Consensus-Version: gloas` and `Eth-Blob-Data-Included` (both required, else +`400`): - `true`: the body is `SignedExecutionPayloadEnvelopeContents` (the signed envelope, `kzg_proofs`, `blobs`). @@ -536,6 +560,20 @@ Pre-gloas responses are unchanged. One helper implements both rules (`shared/optimistic.rs`) and every response that carries the flag goes through it. +### `GET /eth/v1/config/fork_schedule` + +The `Fork` objects of the `Config`'s schedule, oldest first, as +`{previous_version, current_version, epoch}` with the epoch quoted. Phase0 is +first, at epoch `0` and its own predecessor; every later fork whose epoch is not +`FAR_FUTURE_EPOCH` follows, and an unscheduled fork is skipped without breaking +the chain, so each entry's `previous_version` is the one before it in the list. +Nimbus's validator client reads this every epoch, requires that linked-list +shape, and marks a node it cannot decode as incompatible. Nimbus also compares +`/eth/v1/config/spec` against its own constants before use: the 13 preset +values, the 7 domain types, `SECONDS_PER_SLOT` (or `SLOT_DURATION_MS`, which +this node does not report) and every fork's `*_FORK_VERSION` and `*_FORK_EPOCH`, +with altair scheduled. All are served (a unit test pins the list). + ### `GET /eth/v1/config/spec` One flat object holding the network's configuration, the compiled preset, and