From 15664d61da9ad50f00dfd5cb2a1aabab1bbcdb90 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Thu, 19 Mar 2026 20:57:11 +0800 Subject: [PATCH 1/3] feat: add trasury address protocol param --- docs/ssz-merklization.md | 7 ++- finalizer/src/actor.rs | 4 ++ finalizer/src/ingress.rs | 18 +++++++ finalizer/src/tests/fork_handling.rs | 1 + finalizer/src/tests/state_queries.rs | 1 + finalizer/src/tests/syncing.rs | 1 + finalizer/src/tests/validator_lifecycle.rs | 1 + node/src/args.rs | 56 +++++++------------- node/src/bin/genesis.rs | 2 + node/src/test_harness/common.rs | 1 + node/src/tests/checkpointing/verification.rs | 2 + rpc/src/api.rs | 3 ++ rpc/src/server.rs | 5 ++ rpc/tests/utils.rs | 5 ++ types/src/checkpoint.rs | 9 ++++ types/src/consensus_state.rs | 30 +++++++++++ types/src/consensus_state_query.rs | 17 ++++++ types/src/genesis.rs | 12 +++++ types/src/protocol_params.rs | 42 +++++++++++++-- types/src/ssz_hash.rs | 13 ++--- types/src/ssz_state_tree.rs | 26 ++++++--- types/src/ssz_tree_key.rs | 1 + 22 files changed, 201 insertions(+), 56 deletions(-) diff --git a/docs/ssz-merklization.md b/docs/ssz-merklization.md index 008b7bc0..c71032d6 100644 --- a/docs/ssz-merklization.md +++ b/docs/ssz-merklization.md @@ -36,7 +36,7 @@ The state tree is a two-level design: a fixed top-level tree containing scalar f ### Top-Level Tree -32 leaf slots (depth 5), 18 used. Each leaf is a 32-byte `hash_tree_root` value. Leaves 18–31 are unused (zero-filled). +32 leaf slots (depth 5), 19 used. Each leaf is a 32-byte `hash_tree_root` value. Leaves 19–31 are unused (zero-filled). | Leaf Index | Field | Type | |------------|-------|------| @@ -58,6 +58,7 @@ The state tree is a two-level design: a fixed top-level tree containing scalar f | 15 | `protocol_param_changes` | Collection root | | 16 | `added_validators` | Collection root | | 17 | `removed_validators` | Collection root | +| 18 | `treasury_address` | Scalar | ### Collection Subtrees @@ -146,7 +147,7 @@ All leaf values are 32 bytes, produced by SSZ `hash_tree_root`: - **`bool`**: `0x01` or `0x00`, zero-padded to 32 bytes. Used by: has_pending_deposit, has_pending_withdrawal. - **`ValidatorStatus` (enum)**: Single byte (Active=0, Inactive=1, SubmittedExitRequest=2, Joining=3), zero-padded to 32 bytes. - **`[u8; 32]`**: Used directly as the leaf value. Used by: head_digest, epoch_genesis_hash, forkchoice hashes, withdrawal_credentials (deposit), pubkey (withdrawal). -- **`Address` (20 bytes)**: Zero-padded to 32 bytes. Used by: withdrawal_credentials (validator), address (withdrawal). +- **`Address` (20 bytes)**: Zero-padded to 32 bytes. Used by: withdrawal_credentials (validator), address (withdrawal), treasury_address. - **Ed25519 public key (32 bytes)**: Used directly as the leaf value. Used by: node_pubkey (deposit), node_key (added validator), removed validator pubkeys. - **BLS public key (48 bytes)**: `SHA256(bytes[0..32] || pad(bytes[32..48]))` — 2 chunks hashed. Used by: consensus_pubkey (validator, deposit), consensus_key (added validator). - **Ed25519 signature (64 bytes)**: `SHA256(bytes[0..32] || bytes[32..64])` — 2 chunks hashed. Used by: node_signature (deposit). @@ -170,6 +171,7 @@ Single top-level leaf write + rehash of the 5-level path to root. | `set_minimum_stake()` | `ssz_tree.set_validator_minimum_stake()` | | `set_maximum_stake()` | `ssz_tree.set_validator_maximum_stake()` | | `set_allowed_timestamp_future_ms()` | `ssz_tree.set_allowed_timestamp_future_ms()` | +| `set_treasury_address()` | `ssz_tree.set_treasury_address()` | | `set_next_withdrawal_index()` | `ssz_tree.set_next_withdrawal_index()` | | `set_forkchoice_head()` | `ssz_tree.set_forkchoice_head_block_hash()` | | `set_forkchoice_safe_and_finalized()` | Two setter calls (safe + finalized) | @@ -395,6 +397,7 @@ Keys are human-readable strings parsed by `types/src/ssz_tree_key.rs`: | `validator_minimum_stake` | Minimum validator stake | | `validator_maximum_stake` | Maximum validator stake | | `allowed_timestamp_future_ms` | Allowed timestamp future (ms) | +| `treasury_address` | Treasury address | | `next_withdrawal_index` | Next withdrawal index | | `forkchoice_head_block_hash` | Forkchoice head hash | | `forkchoice_safe_block_hash` | Forkchoice safe hash | diff --git a/finalizer/src/actor.rs b/finalizer/src/actor.rs index d8d73ef9..6bd2b04f 100644 --- a/finalizer/src/actor.rs +++ b/finalizer/src/actor.rs @@ -976,6 +976,10 @@ impl< let ms = self.canonical_state.get_allowed_timestamp_future_ms(); let _ = sender.send(ConsensusStateResponse::AllowedTimestampFuture(ms)); } + ConsensusStateRequest::GetTreasuryAddress => { + let address = self.canonical_state.get_treasury_address(); + let _ = sender.send(ConsensusStateResponse::TreasuryAddress(address)); + } ConsensusStateRequest::GetEpochBounds(epoch) => { let bounds = self .canonical_state diff --git a/finalizer/src/ingress.rs b/finalizer/src/ingress.rs index 18583353..54c83804 100644 --- a/finalizer/src/ingress.rs +++ b/finalizer/src/ingress.rs @@ -297,6 +297,24 @@ impl, B: ConsensusBlock> FinalizerMailbox { ms } + pub async fn get_treasury_address(&self) -> alloy_primitives::Address { + let (response, rx) = oneshot::channel(); + let request = ConsensusStateRequest::GetTreasuryAddress; + let _ = self + .sender + .clone() + .send(FinalizerMessage::QueryState { request, response }) + .await; + + let res = rx + .await + .expect("consensus state query response sender dropped"); + let ConsensusStateResponse::TreasuryAddress(address) = res else { + unreachable!("request and response variants must match"); + }; + address + } + pub async fn get_epoch_bounds(&self, epoch: u64) -> Option<(u64, u64)> { let (response, rx) = oneshot::channel(); let request = ConsensusStateRequest::GetEpochBounds(epoch); diff --git a/finalizer/src/tests/fork_handling.rs b/finalizer/src/tests/fork_handling.rs index 39473fab..8990412a 100644 --- a/finalizer/src/tests/fork_handling.rs +++ b/finalizer/src/tests/fork_handling.rs @@ -119,6 +119,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 64_000_000_000, epoch_length, 10_000, + Address::ZERO, ); state.set_validator_accounts(validator_accounts); state diff --git a/finalizer/src/tests/state_queries.rs b/finalizer/src/tests/state_queries.rs index e716b88c..6d7ac3e4 100644 --- a/finalizer/src/tests/state_queries.rs +++ b/finalizer/src/tests/state_queries.rs @@ -125,6 +125,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 64_000_000_000, epoch_length, 10_000, + Address::ZERO, ); state.set_validator_accounts(validator_accounts); state diff --git a/finalizer/src/tests/syncing.rs b/finalizer/src/tests/syncing.rs index 19d5b427..1ad42c79 100644 --- a/finalizer/src/tests/syncing.rs +++ b/finalizer/src/tests/syncing.rs @@ -119,6 +119,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 64_000_000_000, epoch_length, 10_000, + Address::ZERO, ); state.set_validator_accounts(validator_accounts); state diff --git a/finalizer/src/tests/validator_lifecycle.rs b/finalizer/src/tests/validator_lifecycle.rs index 7f2a27e0..900e5cc6 100644 --- a/finalizer/src/tests/validator_lifecycle.rs +++ b/finalizer/src/tests/validator_lifecycle.rs @@ -125,6 +125,7 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) - 64_000_000_000, epoch_length, 10_000, + Address::ZERO, ); state.set_validator_accounts(validator_accounts); state diff --git a/node/src/args.rs b/node/src/args.rs index 4ce73db1..31207e5d 100644 --- a/node/src/args.rs +++ b/node/src/args.rs @@ -14,7 +14,7 @@ use commonware_runtime::{Handle, Metrics as _, Runner, Spawner as _, tokio}; use summit_rpc::{PathSender, start_rpc_server, start_rpc_server_for_genesis}; use tokio_util::sync::CancellationToken; -use alloy_primitives::B256; +use alloy_primitives::{Address, B256}; use alloy_rpc_types_engine::ForkchoiceState; use commonware_utils::from_hex_formatted; use futures::{channel::oneshot, future::try_join_all}; @@ -273,20 +273,7 @@ impl Command { warn!("checkpoint loaded without finalized headers chain - skipping verification"); } - let genesis_hash: [u8; 32] = from_hex_formatted(&genesis.eth_genesis_hash) - .map(|hash_bytes| hash_bytes.try_into()) - .expect("bad eth_genesis_hash") - .expect("bad eth_genesis_hash"); - let initial_state = get_initial_state( - genesis_hash, - &committee, - maybe_checkpoint, - genesis.validator_minimum_stake, - genesis.validator_maximum_stake, - NonZeroU64::new(genesis.blocks_per_epoch) - .expect("blocks_per_epoch must be nonzero"), - genesis.allowed_timestamp_future_ms, - ); + let initial_state = get_initial_state(&genesis, &committee, maybe_checkpoint); let peers = initial_state.get_validator_keys(); let engine_ipc_path = get_expanded_path(&flags.engine_ipc_path) @@ -492,19 +479,7 @@ pub fn run_node_local( genesis.get_validators().expect("Failed to get validators"); committee.sort_by(|lhs, rhs| lhs.node_public_key.cmp(&rhs.node_public_key)); - let genesis_hash: [u8; 32] = from_hex_formatted(&genesis.eth_genesis_hash) - .map(|hash_bytes| hash_bytes.try_into()) - .expect("bad eth_genesis_hash") - .expect("bad eth_genesis_hash"); - let initial_state = get_initial_state( - genesis_hash, - &committee, - checkpoint, - genesis.validator_minimum_stake, - genesis.validator_maximum_stake, - NonZeroU64::new(genesis.blocks_per_epoch).expect("blocks_per_epoch must be nonzero"), - genesis.allowed_timestamp_future_ms, - ); + let initial_state = get_initial_state(&genesis, &committee, checkpoint); let peers = initial_state.get_validator_keys(); let engine_ipc_path = @@ -656,14 +631,20 @@ pub fn run_node_local( } fn get_initial_state( - genesis_hash: [u8; 32], + genesis: &Genesis, genesis_committee: &Vec, checkpoint: Option, - validator_minimum_stake: u64, - validator_maximum_stake: u64, - epoch_length: NonZeroU64, - allowed_timestamp_future_ms: u64, ) -> ConsensusState { + let epoch_length = + NonZeroU64::new(genesis.blocks_per_epoch).expect("blocks_per_epoch must be nonzero"); + let genesis_hash: [u8; 32] = from_hex_formatted(&genesis.eth_genesis_hash) + .map(|hash_bytes| hash_bytes.try_into()) + .expect("bad eth_genesis_hash") + .expect("bad eth_genesis_hash"); + let treasury_address = genesis + .treasury_address + .parse::
() + .expect("invalid treasury_address"); let genesis_hash: B256 = genesis_hash.into(); checkpoint.unwrap_or_else(|| { let forkchoice = ForkchoiceState { @@ -673,10 +654,11 @@ fn get_initial_state( }; let mut state = ConsensusState::new( forkchoice, - validator_minimum_stake, - validator_maximum_stake, + genesis.validator_minimum_stake, + genesis.validator_maximum_stake, epoch_length, - allowed_timestamp_future_ms, + genesis.allowed_timestamp_future_ms, + treasury_address, ); // Add the genesis nodes to the consensus state with the minimum stake balance. for validator in genesis_committee { @@ -688,7 +670,7 @@ fn get_initial_state( let account = ValidatorAccount { consensus_public_key: validator.consensus_public_key.clone(), withdrawal_credentials: validator.withdrawal_credentials, - balance: validator_minimum_stake, + balance: genesis.validator_minimum_stake, status: ValidatorStatus::Active, has_pending_deposit: false, has_pending_withdrawal: false, diff --git a/node/src/bin/genesis.rs b/node/src/bin/genesis.rs index 8243fb52..8d9061af 100644 --- a/node/src/bin/genesis.rs +++ b/node/src/bin/genesis.rs @@ -18,6 +18,8 @@ pub struct GenesisConfig { namespace: String, blocks_per_epoch: u64, allowed_timestamp_future_ms: u64, + #[serde(default)] + treasury_address: Option, pub validators: Vec, } diff --git a/node/src/test_harness/common.rs b/node/src/test_harness/common.rs index 23419f04..7b7d5809 100644 --- a/node/src/test_harness/common.rs +++ b/node/src/test_harness/common.rs @@ -347,6 +347,7 @@ pub fn get_initial_state( balance, NonZeroU64::new(DEFAULT_BLOCKS_PER_EPOCH).unwrap(), 10_000, // 10 seconds + Address::ZERO, ); // Add the genesis nodes to the consensus state with the minimum stake balance. for ((node_pubkey, consensus_pubkey), address) in committee.iter().zip(addresses.iter()) { diff --git a/node/src/tests/checkpointing/verification.rs b/node/src/tests/checkpointing/verification.rs index 97cc597d..58c182d6 100644 --- a/node/src/tests/checkpointing/verification.rs +++ b/node/src/tests/checkpointing/verification.rs @@ -97,6 +97,7 @@ fn test_checkpoint_verification_fixed_committee() { validator_maximum_stake: 32_000_000_000, blocks_per_epoch: common::DEFAULT_BLOCKS_PER_EPOCH, allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO.to_string(), }; let node_public_keys: Vec<_> = validators.iter().map(|(pk, _)| pk.clone()).collect(); @@ -314,6 +315,7 @@ fn test_checkpoint_verification_dynamic_committee() { validator_maximum_stake: min_stake, blocks_per_epoch: common::DEFAULT_BLOCKS_PER_EPOCH, allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO.to_string(), }; let node_public_keys: Vec<_> = validators.iter().map(|(pk, _)| pk.clone()).collect(); diff --git a/rpc/src/api.rs b/rpc/src/api.rs index 432efb27..87126632 100644 --- a/rpc/src/api.rs +++ b/rpc/src/api.rs @@ -60,6 +60,9 @@ pub trait SummitApi { #[method(name = "getAllowedTimestampFuture")] async fn get_allowed_timestamp_future(&self) -> RpcResult; + #[method(name = "getTreasuryAddress")] + async fn get_treasury_address(&self) -> RpcResult; + #[method(name = "getEpochBounds")] async fn get_epoch_bounds(&self, epoch: u64) -> RpcResult; diff --git a/rpc/src/server.rs b/rpc/src/server.rs index 26760bd0..0467ff6f 100644 --- a/rpc/src/server.rs +++ b/rpc/src/server.rs @@ -294,6 +294,11 @@ impl SummitApiServer for SummitRpcServer { Ok(ms) } + async fn get_treasury_address(&self) -> RpcResult { + let address = self.finalizer_mailbox.get_treasury_address().await; + Ok(address.to_string()) + } + async fn get_epoch_bounds(&self, epoch: u64) -> RpcResult { let bounds = self.finalizer_mailbox.get_epoch_bounds(epoch).await; match bounds { diff --git a/rpc/tests/utils.rs b/rpc/tests/utils.rs index 33768455..ddf5a767 100644 --- a/rpc/tests/utils.rs +++ b/rpc/tests/utils.rs @@ -1,3 +1,4 @@ +use alloy_primitives::Address; use commonware_codec::Encode as _; use commonware_cryptography::{bls12381, ed25519}; use commonware_math::algebra::Random; @@ -131,6 +132,10 @@ pub fn create_test_finalizer_mailbox( let _ = response.send(ConsensusStateResponse::AllowedTimestampFuture(10_000)); } + ConsensusStateRequest::GetTreasuryAddress => { + let _ = + response.send(ConsensusStateResponse::TreasuryAddress(Address::ZERO)); + } ConsensusStateRequest::GetEpochBounds(epoch) => { let first = epoch * 10; let last = first + 9; diff --git a/types/src/checkpoint.rs b/types/src/checkpoint.rs index 7616a945..da4fd8f4 100644 --- a/types/src/checkpoint.rs +++ b/types/src/checkpoint.rs @@ -329,6 +329,7 @@ mod tests { use crate::dynamic_epocher::DynamicEpocher; use crate::ssz_state_tree::SszStateTree; use crate::withdrawal::WithdrawalQueue; + use alloy_primitives::Address; use commonware_codec::DecodeExt; use commonware_cryptography::{Signer, bls12381, ed25519, sha256}; use ssz::{Decode, Encode}; @@ -367,6 +368,7 @@ mod tests { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(10).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -492,6 +494,7 @@ mod tests { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(10).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -541,6 +544,7 @@ mod tests { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(10).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -673,6 +677,7 @@ mod tests { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(10).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -727,6 +732,7 @@ mod tests { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(10).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -786,6 +792,7 @@ mod tests { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(10).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -841,6 +848,7 @@ mod tests { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(10).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -941,6 +949,7 @@ mod tests { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 10_000, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(10).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), diff --git a/types/src/consensus_state.rs b/types/src/consensus_state.rs index d6788039..d14c87f1 100644 --- a/types/src/consensus_state.rs +++ b/types/src/consensus_state.rs @@ -7,6 +7,7 @@ use crate::protocol_params::ProtocolParam; use crate::ssz_state_tree::SszStateTree; use crate::withdrawal::{PendingWithdrawal, WithdrawalQueue}; use crate::{Digest, PublicKey}; +use alloy_primitives::Address; use alloy_rpc_types_engine::ForkchoiceState; use bytes::{Buf, BufMut}; use commonware_codec::{DecodeExt, EncodeSize, Error, Read, ReadExt, Write}; @@ -37,6 +38,7 @@ pub struct ConsensusState { pub(crate) validator_minimum_stake: u64, // in gwei pub(crate) validator_maximum_stake: u64, // in gwei pub(crate) allowed_timestamp_future_ms: u64, + pub(crate) treasury_address: Address, pub(crate) epocher: DynamicEpocher, /// In-memory SSZ binary Merkle tree over the entire consensus state. @@ -84,6 +86,7 @@ impl Default for ConsensusState { validator_minimum_stake: 32_000_000_000, // 32 ETH in gwei validator_maximum_stake: 32_000_000_000, // 32 ETH in gwei allowed_timestamp_future_ms: 50, + treasury_address: Address::ZERO, epocher: DynamicEpocher::new(NonZeroU64::new(1).unwrap()), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -104,6 +107,7 @@ impl ConsensusState { validator_maximum_stake: u64, epoch_length: NonZeroU64, allowed_timestamp_future_ms: u64, + treasury_address: Address, ) -> Self { let mut s = Self { epoch: 0, @@ -123,6 +127,7 @@ impl ConsensusState { validator_minimum_stake, validator_maximum_stake, allowed_timestamp_future_ms, + treasury_address, epocher: DynamicEpocher::new(epoch_length), ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -202,6 +207,15 @@ impl ConsensusState { self.ssz_tree.set_allowed_timestamp_future_ms(ms); } + pub fn get_treasury_address(&self) -> Address { + self.treasury_address + } + + pub fn set_treasury_address(&mut self, address: Address) { + self.treasury_address = address; + self.ssz_tree.set_treasury_address(&address); + } + pub fn get_pending_checkpoint(&self) -> Option<&Checkpoint> { self.pending_checkpoint.as_ref() } @@ -660,6 +674,10 @@ impl ConsensusState { self.allowed_timestamp_future_ms = ms; self.ssz_tree.set_allowed_timestamp_future_ms(ms); } + ProtocolParam::TreasuryAddress(address) => { + self.treasury_address = address; + self.ssz_tree.set_treasury_address(&address); + } } } // Protocol param changes have been consumed — update the (now empty) collection root @@ -694,6 +712,7 @@ impl ConsensusState { &self.protocol_param_changes, &self.added_validators, &self.removed_validators, + &self.treasury_address, ); // Capture root and freeze proof tree so get_state_root() / proof_tree() are valid @@ -742,6 +761,7 @@ impl EncodeSize for ConsensusState { + 8 // validator_minimum_stake + 8 // validator_maximum_stake + 8 // allowed_timestamp_future_ms + + 20 // treasury_address + self.epocher.encode_size() } } @@ -845,6 +865,10 @@ impl Read for ConsensusState { let validator_maximum_stake = buf.get_u64(); let allowed_timestamp_future_ms = buf.get_u64(); + let mut treasury_address_bytes = [0u8; 20]; + buf.copy_to_slice(&mut treasury_address_bytes); + let treasury_address = Address::from(treasury_address_bytes); + let epocher = DynamicEpocher::read_cfg(buf, &())?; let mut state = Self { @@ -865,6 +889,7 @@ impl Read for ConsensusState { validator_minimum_stake, validator_maximum_stake, allowed_timestamp_future_ms, + treasury_address, epocher, ssz_tree: SszStateTree::default(), proof_tree: SszStateTree::default(), @@ -950,6 +975,9 @@ impl Write for ConsensusState { buf.put_u64(self.validator_maximum_stake); buf.put_u64(self.allowed_timestamp_future_ms); + // Write treasury_address + buf.put_slice(self.treasury_address.as_slice()); + // Write epocher self.epocher.write(buf); } @@ -1065,6 +1093,7 @@ mod tests { 0, NonZeroU64::new(100).unwrap(), 10_000, + Address::ZERO, ); original_state.set_epoch(7); @@ -1822,6 +1851,7 @@ mod tests { 32_000_000_000, NonZeroU64::new(10).unwrap(), 10_000, + Address::ZERO, ); // Add 4 genesis validators (like the testnet) diff --git a/types/src/consensus_state_query.rs b/types/src/consensus_state_query.rs index 93c342b9..981b3311 100644 --- a/types/src/consensus_state_query.rs +++ b/types/src/consensus_state_query.rs @@ -5,6 +5,7 @@ use crate::ssz_state_tree::SszProof; use crate::ssz_tree_key::SszStateKey; use crate::withdrawal::PendingWithdrawal; use crate::{Block, FinalizedHeader, PublicKey}; +use alloy_primitives::Address; use commonware_cryptography::certificate::Scheme; use futures::SinkExt; use futures::channel::{mpsc, oneshot}; @@ -22,6 +23,7 @@ pub enum ConsensusStateRequest { GetMaximumStake, GetEpochLength, GetAllowedTimestampFuture, + GetTreasuryAddress, GetEpochBounds(u64), GetDeposit(usize), GetDepositCount, @@ -42,6 +44,7 @@ pub enum ConsensusStateResponse { MaximumStake(u64), EpochLength(u64), AllowedTimestampFuture(u64), + TreasuryAddress(Address), EpochBounds(Option<(u64, u64)>), Deposit(Option), DepositCount(usize), @@ -237,6 +240,20 @@ impl ConsensusStateQuery { ms } + pub async fn get_treasury_address(&self) -> Address { + let (tx, rx) = oneshot::channel(); + let req = ConsensusStateRequest::GetTreasuryAddress; + let _ = self.sender.clone().send((req, tx)).await; + + let res = rx + .await + .expect("consensus state query response sender dropped"); + let ConsensusStateResponse::TreasuryAddress(address) = res else { + unreachable!("request and response variants must match"); + }; + address + } + pub async fn get_epoch_bounds(&self, epoch: u64) -> Option<(u64, u64)> { let (tx, rx) = oneshot::channel(); let req = ConsensusStateRequest::GetEpochBounds(epoch); diff --git a/types/src/genesis.rs b/types/src/genesis.rs index d162d528..4b0733ec 100644 --- a/types/src/genesis.rs +++ b/types/src/genesis.rs @@ -49,6 +49,13 @@ pub struct Genesis { /// and the local wall clock. Blocks with timestamps that exceed local /// time by more than this are rejected during verification. pub allowed_timestamp_future_ms: u64, + /// Address that receives treasury funds. Defaults to the zero address. + #[serde(default = "default_treasury_address")] + pub treasury_address: String, +} + +fn default_treasury_address() -> String { + Address::ZERO.to_string() } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -115,6 +122,11 @@ impl Genesis { ) .into()); } + // Validate treasury_address is a valid address + genesis + .treasury_address + .parse::
() + .map_err(|e| format!("invalid treasury_address: {e}"))?; Ok(genesis) } diff --git a/types/src/protocol_params.rs b/types/src/protocol_params.rs index 6f591ef3..b72f54f4 100644 --- a/types/src/protocol_params.rs +++ b/types/src/protocol_params.rs @@ -1,4 +1,5 @@ use crate::execution_request::ProtocolParamRequest; +use alloy_primitives::Address; use anyhow::anyhow; use bytes::{Buf, BufMut}; use commonware_codec::{EncodeSize, Error, Read, Write}; @@ -14,6 +15,7 @@ pub enum ProtocolParam { MaximumStake(u64), EpochLength(u64), AllowedTimestampFuture(u64), + TreasuryAddress(Address), } impl TryFrom for ProtocolParam { @@ -88,6 +90,16 @@ impl TryFrom for ProtocolParam { allowed_timestamp_future, )) } + 0x04 => { + if request.param.len() != 20 { + return Err(anyhow!( + "Failed to parse treasury address protocol param, invalid length {}", + request.param.len() + )); + } + let bytes: [u8; 20] = request.param.as_slice().try_into()?; + Ok(ProtocolParam::TreasuryAddress(Address::from(bytes))) + } _ => Err(anyhow!( "Failed to parse protocol param request - unknown param_id: {request:?}" )), @@ -97,7 +109,13 @@ impl TryFrom for ProtocolParam { impl EncodeSize for ProtocolParam { fn encode_size(&self) -> usize { - 1 + 8 // 1 byte tag + 8 byte value for all current variants + match self { + ProtocolParam::MinimumStake(_) + | ProtocolParam::MaximumStake(_) + | ProtocolParam::EpochLength(_) + | ProtocolParam::AllowedTimestampFuture(_) => 1 + 8, // 1 byte tag + 8 byte value + ProtocolParam::TreasuryAddress(_) => 1 + 20, // 1 byte tag + 20 byte address + } } } @@ -120,6 +138,10 @@ impl Write for ProtocolParam { buf.put_u8(0x03); buf.put_u64(*value); } + ProtocolParam::TreasuryAddress(address) => { + buf.put_u8(0x04); + buf.put_slice(address.as_slice()); + } } } } @@ -129,11 +151,17 @@ impl Read for ProtocolParam { fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result { let tag = buf.get_u8(); - let value = buf.get_u64(); match tag { - 0x00 => Ok(ProtocolParam::MinimumStake(value)), - 0x01 => Ok(ProtocolParam::MaximumStake(value)), + 0x00 => { + let value = buf.get_u64(); + Ok(ProtocolParam::MinimumStake(value)) + } + 0x01 => { + let value = buf.get_u64(); + Ok(ProtocolParam::MaximumStake(value)) + } 0x02 => { + let value = buf.get_u64(); if !(MIN_EPOCH_LENGTH..=MAX_EPOCH_LENGTH).contains(&value) { return Err(Error::Invalid( "ProtocolParam", @@ -143,6 +171,7 @@ impl Read for ProtocolParam { Ok(ProtocolParam::EpochLength(value)) } 0x03 => { + let value = buf.get_u64(); if !(MIN_ALLOWED_TIMESTAMP_FUTURE_MS..=MAX_ALLOWED_TIMESTAMP_FUTURE_MS) .contains(&value) { @@ -153,6 +182,11 @@ impl Read for ProtocolParam { } Ok(ProtocolParam::AllowedTimestampFuture(value)) } + 0x04 => { + let mut bytes = [0u8; 20]; + buf.copy_to_slice(&mut bytes); + Ok(ProtocolParam::TreasuryAddress(Address::from(bytes))) + } _ => Err(Error::Invalid("ProtocolParam", "unknown tag")), } } diff --git a/types/src/ssz_hash.rs b/types/src/ssz_hash.rs index 19f02487..ffb679b4 100644 --- a/types/src/ssz_hash.rs +++ b/types/src/ssz_hash.rs @@ -94,13 +94,14 @@ impl SszHashTreeRoot for ValidatorStatus { impl SszHashTreeRoot for ProtocolParam { /// ProtocolParam as a 2-field container: (tag, value). fn hash_tree_root(&self) -> [u8; 32] { - let (tag, value) = match self { - ProtocolParam::MinimumStake(v) => (0u64, *v), - ProtocolParam::MaximumStake(v) => (1u64, *v), - ProtocolParam::EpochLength(v) => (2u64, *v), - ProtocolParam::AllowedTimestampFuture(v) => (3u64, *v), + let (tag, value_hash) = match self { + ProtocolParam::MinimumStake(v) => (0u64, v.hash_tree_root()), + ProtocolParam::MaximumStake(v) => (1u64, v.hash_tree_root()), + ProtocolParam::EpochLength(v) => (2u64, v.hash_tree_root()), + ProtocolParam::AllowedTimestampFuture(v) => (3u64, v.hash_tree_root()), + ProtocolParam::TreasuryAddress(addr) => (4u64, addr.hash_tree_root()), }; - merkleize(&[tag.hash_tree_root(), value.hash_tree_root()]) + merkleize(&[tag.hash_tree_root(), value_hash]) } } diff --git a/types/src/ssz_state_tree.rs b/types/src/ssz_state_tree.rs index b858f205..b069b065 100644 --- a/types/src/ssz_state_tree.rs +++ b/types/src/ssz_state_tree.rs @@ -23,6 +23,7 @@ use crate::ssz_hash::{SszHashTreeRoot, hash_fixed_bytes_64, hash_fixed_bytes_96} use crate::ssz_tree::{SszTree, mix_in_length}; use crate::withdrawal::PendingWithdrawal; use crate::withdrawal::WithdrawalQueue; +use alloy_primitives::Address; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap, VecDeque}; @@ -46,9 +47,10 @@ pub const WITHDRAWAL_QUEUE_ROOT: usize = 14; pub const PROTOCOL_PARAM_CHANGES_ROOT: usize = 15; pub const ADDED_VALIDATORS_ROOT: usize = 16; pub const REMOVED_VALIDATORS_ROOT: usize = 17; +pub const TREASURY_ADDRESS: usize = 18; /// Number of used leaf slots in the top-level tree. -pub const NUM_TOP_LEAVES: usize = 18; +pub const NUM_TOP_LEAVES: usize = 19; // --- Validator field indices (within each validator's 8-leaf subtree) --- @@ -212,6 +214,11 @@ impl SszStateTree { .set_leaf(ALLOWED_TIMESTAMP_FUTURE_MS, ms.hash_tree_root()); } + pub fn set_treasury_address(&mut self, address: &Address) { + self.top + .set_leaf(TREASURY_ADDRESS, address.hash_tree_root()); + } + pub fn set_next_withdrawal_index(&mut self, index: u64) { self.top .set_leaf(NEXT_WITHDRAWAL_INDEX, index.hash_tree_root()); @@ -751,14 +758,15 @@ impl SszStateTree { /// Set the 2 field leaves for protocol param at positional slot `i`. fn set_protocol_param_fields(tree: &mut SszTree, slot: usize, param: &ProtocolParam) { let base = slot * PROTOCOL_PARAM_FIELDS_PER_ITEM; - let (tag, value) = match param { - ProtocolParam::MinimumStake(v) => (0u64, *v), - ProtocolParam::MaximumStake(v) => (1u64, *v), - ProtocolParam::EpochLength(v) => (2u64, *v), - ProtocolParam::AllowedTimestampFuture(v) => (3u64, *v), + let (tag, value_hash) = match param { + ProtocolParam::MinimumStake(v) => (0u64, v.hash_tree_root()), + ProtocolParam::MaximumStake(v) => (1u64, v.hash_tree_root()), + ProtocolParam::EpochLength(v) => (2u64, v.hash_tree_root()), + ProtocolParam::AllowedTimestampFuture(v) => (3u64, v.hash_tree_root()), + ProtocolParam::TreasuryAddress(addr) => (4u64, addr.hash_tree_root()), }; tree.set_leaf(base + PROTOCOL_PARAM_FIELD_TAG, tag.hash_tree_root()); - tree.set_leaf(base + PROTOCOL_PARAM_FIELD_VALUE, value.hash_tree_root()); + tree.set_leaf(base + PROTOCOL_PARAM_FIELD_VALUE, value_hash); } fn update_protocol_param_collection_root(&mut self) { @@ -862,6 +870,7 @@ impl SszStateTree { protocol_param_changes: &[ProtocolParam], added_validators: &BTreeMap>, removed_validators: &[PublicKey], + treasury_address: &Address, ) { *self = Self::new(); @@ -878,6 +887,7 @@ impl SszStateTree { self.set_forkchoice_head_block_hash(forkchoice_head); self.set_forkchoice_safe_block_hash(forkchoice_safe); self.set_forkchoice_finalized_block_hash(forkchoice_finalized); + self.set_treasury_address(treasury_address); // Validators self.rebuild_validators(validator_accounts); @@ -1780,6 +1790,7 @@ mod tests { &[], &BTreeMap::new(), &[], + &Address::ZERO, ); assert_eq!(inc.root(), rb.root()); @@ -1811,6 +1822,7 @@ mod tests { &[], &BTreeMap::new(), &[], + &Address::ZERO, ); let root = tree.root(); diff --git a/types/src/ssz_tree_key.rs b/types/src/ssz_tree_key.rs index d688da87..889492c5 100644 --- a/types/src/ssz_tree_key.rs +++ b/types/src/ssz_tree_key.rs @@ -68,6 +68,7 @@ pub fn parse_key(descriptor: &str) -> Result { "forkchoice_finalized_block_hash" => Ok(SszStateKey::Scalar( ssz_state_tree::FORKCHOICE_FINALIZED_BLOCK_HASH, )), + "treasury_address" => Ok(SszStateKey::Scalar(ssz_state_tree::TREASURY_ADDRESS)), _ => { if let Some(rest) = descriptor.strip_prefix("validator_field:") { // Format: "validator_field:0xPUBKEY:field_name" From 414b8731ae24b509f4a258bc153356411cf40045 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Thu, 19 Mar 2026 23:34:34 +0800 Subject: [PATCH 2/3] test: fee recipient and treasury address --- application/src/actor.rs | 4 +- finalizer/src/actor.rs | 30 ++- node/src/test_harness/mock_engine_client.rs | 29 ++- .../execution_requests/protocol_params.rs | 194 ++++++++++++++++++ types/src/lib.rs | 2 +- 5 files changed, 243 insertions(+), 16 deletions(-) diff --git a/application/src/actor.rs b/application/src/actor.rs index ab63948a..5b87f8b4 100644 --- a/application/src/actor.rs +++ b/application/src/actor.rs @@ -481,7 +481,7 @@ impl< aux_data.forkchoice, current, withdrawals, - Default::default(), + aux_data.suggested_fee_recipient, None, parent_block.height(), ) @@ -494,7 +494,7 @@ impl< aux_data.forkchoice, current, withdrawals, - aux_data.withdrawal_credentials, + aux_data.suggested_fee_recipient, Some(aux_data.state_root.into()), ) .await diff --git a/finalizer/src/actor.rs b/finalizer/src/actor.rs index 6bd2b04f..e290c8e8 100644 --- a/finalizer/src/actor.rs +++ b/finalizer/src/actor.rs @@ -829,15 +829,23 @@ impl< return; }; - let withdrawal_credentials = state - .get_account( - self.node_public_key - .as_ref() - .try_into() - .expect("Safe: Ed pub key always 32 bytes"), - ) - .map(|account| account.withdrawal_credentials) - .unwrap_or_default(); + let treasury_address = state.get_treasury_address(); + // The zero address is a sentinel value. + // If the treasury address is the zero address, the suggested_fee_recipient will be + // set to the validator's withdrawal credentials. + let suggested_fee_recipient = if treasury_address.is_zero() { + state + .get_account( + self.node_public_key + .as_ref() + .try_into() + .expect("Safe: Ed pub key always 32 bytes"), + ) + .map(|account| account.withdrawal_credentials) + .unwrap_or_default() + } else { + treasury_address + }; // Create checkpoint if we're at an epoch boundary. // The consensus state is saved every `epoch_num_blocks` blocks. @@ -887,7 +895,7 @@ impl< .unwrap_or_default(), removed_validators: state.get_removed_validators().clone(), forkchoice: *state.get_forkchoice(), - withdrawal_credentials, + suggested_fee_recipient, state_root: state.get_state_root(), allowed_timestamp_future_ms: state.get_allowed_timestamp_future_ms(), } @@ -900,7 +908,7 @@ impl< added_validators: vec![], removed_validators: vec![], forkchoice: *state.get_forkchoice(), - withdrawal_credentials, + suggested_fee_recipient, state_root: state.get_state_root(), allowed_timestamp_future_ms: state.get_allowed_timestamp_future_ms(), } diff --git a/node/src/test_harness/mock_engine_client.rs b/node/src/test_harness/mock_engine_client.rs index ceede055..1f3b41fb 100644 --- a/node/src/test_harness/mock_engine_client.rs +++ b/node/src/test_harness/mock_engine_client.rs @@ -88,6 +88,19 @@ impl MockEngineClient { chain } + /// Get fee recipients from the canonical chain, keyed by block number. + pub fn get_fee_recipients(&self) -> HashMap { + let canonical_chain = self.get_canonical_chain(); + let state = self.state.lock().unwrap(); + let mut fee_recipients = HashMap::new(); + for (height, block_hash) in canonical_chain { + if let Some(block) = state.canonical_blocks.get(&block_hash) { + fee_recipients.insert(height, block.payload_inner.payload_inner.fee_recipient); + } + } + fee_recipients + } + /// Get all withdrawals from the canonical chain pub fn get_withdrawals(&self) -> HashMap> { let canonical_chain = self.get_canonical_chain(); @@ -300,6 +313,7 @@ impl MockEngineState { timestamp: u64, client_id: &str, withdrawals: Vec, + suggested_fee_recipient: Address, ) -> ExecutionPayloadV3 { // Create deterministic but unique block hash use sha3::{Digest, Keccak256}; @@ -312,7 +326,7 @@ impl MockEngineState { let payload_v1 = ExecutionPayloadV1 { parent_hash, - fee_recipient: Address::from([1u8; 20]), + fee_recipient: suggested_fee_recipient, state_root: FixedBytes::from([1u8; 32]), receipts_root: FixedBytes::from([1u8; 32]), logs_bloom: Bloom::ZERO, @@ -349,7 +363,7 @@ impl EngineClient for MockEngineClient { fork_choice_state: ForkchoiceState, timestamp: u64, withdrawals: Vec, - _suggested_fee_recipient: Address, + suggested_fee_recipient: Address, _parent_beacon_block_root: Option>, #[cfg(feature = "bench")] height: u64, ) -> Option { @@ -387,6 +401,7 @@ impl EngineClient for MockEngineClient { timestamp, &self.client_id, withdrawals, + suggested_fee_recipient, ); // Wrap in envelope @@ -715,6 +730,16 @@ impl MockEngineNetwork { .expect("no clients"); best_client.get_withdrawals() } + + /// Get fee recipients from the canonical chain of the best client. + pub fn get_fee_recipients(&self) -> HashMap { + let clients = self.get_clients(); + let best_client = clients + .iter() + .max_by_key(|c| c.get_chain_height()) + .expect("no clients"); + best_client.get_fee_recipients() + } } #[cfg(test)] diff --git a/node/src/tests/execution_requests/protocol_params.rs b/node/src/tests/execution_requests/protocol_params.rs index 6c77bd41..5a13d315 100644 --- a/node/src/tests/execution_requests/protocol_params.rs +++ b/node/src/tests/execution_requests/protocol_params.rs @@ -1,4 +1,5 @@ use super::*; +use summit_types::execution_request::ProtocolParamRequest; #[test_traced("INFO")] fn test_protocol_param_allowed_timestamp_future() { @@ -629,3 +630,196 @@ fn test_protocol_param_stake_update_committee() { context.auditor().state() }) } + +#[test_traced("INFO")] +fn test_protocol_param_treasury_address() { + // Tests that the treasury address protocol parameter controls suggested_fee_recipient: + // - Epoch 0: treasury_address is zero → fee_recipient = proposer's withdrawal credentials + // - Protocol param request at block 5 sets treasury_address to non-zero + // - After epoch 0 boundary is finalized: fee_recipient = treasury_address + let n = 5; + let min_stake = 32_000_000_000; + let treasury_address = Address::from([0xAB; 20]); + let withdrawal_cred = Address::from([0xCC; 20]); + let link = Link { + latency: Duration::from_millis(80), + jitter: Duration::from_millis(10), + success_rate: 0.98, + }; + let cfg = deterministic::Config::default().with_seed(0); + let executor = Runner::from(cfg); + executor.start(|context| async move { + let (network, mut oracle) = Network::new( + context.with_label("network"), + simulated::Config { + max_size: 1024 * 1024, + disconnect_on_block: false, + tracked_peer_sets: Some(n as usize * 10), + }, + ); + + network.start(); + + let mut key_stores = Vec::new(); + let mut validators = Vec::new(); + for i in 0..n { + let mut rng = StdRng::seed_from_u64(i as u64); + let node_key = PrivateKey::random(&mut rng); + let node_public_key = node_key.public_key(); + let consensus_key = bls12381::PrivateKey::random(&mut rng); + let consensus_public_key = consensus_key.public_key(); + let key_store = KeyStore { + node_key, + consensus_key, + }; + key_stores.push(key_store); + validators.push((node_public_key, consensus_public_key)); + } + validators.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0)); + key_stores.sort_by_key(|ks| ks.node_key.public_key()); + + let node_public_keys: Vec<_> = validators.iter().map(|(pk, _)| pk.clone()).collect(); + let mut registrations = common::register_validators(&oracle, &node_public_keys).await; + + common::link_validators(&mut oracle, &node_public_keys, link, None).await; + + let genesis_hash = + from_hex_formatted(common::GENESIS_HASH).expect("failed to decode genesis hash"); + let genesis_hash: [u8; 32] = genesis_hash + .try_into() + .expect("failed to convert genesis hash"); + + // Create a treasury address protocol param request (param_id 0x04, 20-byte address) + let test_protocol_param = ProtocolParamRequest { + param_id: 0x04, + param: treasury_address.as_slice().to_vec(), + }; + + let execution_requests = + vec![ExecutionRequest::ProtocolParam(test_protocol_param)]; + let requests = common::execution_requests_to_requests(execution_requests); + + let last_epoch0 = last_block_in_epoch(DEFAULT_BLOCKS_PER_EPOCH, 0); + let first_epoch1 = last_epoch0 + 1; + let protocol_param_block_height = last_epoch0 / 2; // mid-epoch + let stop_height = first_epoch1 + 1; // one block into epoch 1 + let mut execution_requests_map = HashMap::new(); + execution_requests_map.insert(protocol_param_block_height, requests); + + let engine_client_network = MockEngineNetworkBuilder::new(genesis_hash) + .with_execution_requests(execution_requests_map) + .with_stop_at(stop_height) + .build(); + + let withdrawal_creds = vec![withdrawal_cred; n as usize]; + let initial_state = get_initial_state( + genesis_hash, + &validators, + Some(&withdrawal_creds), + None, + min_stake, + ); + + let mut public_keys = HashSet::new(); + let mut consensus_state_queries = HashMap::new(); + for (idx, key_store) in key_stores.into_iter().enumerate() { + let public_key = key_store.node_key.public_key(); + public_keys.insert(public_key.clone()); + + let uid = format!("validator_{public_key}"); + let namespace = String::from("_SUMMIT"); + + let engine_client = engine_client_network.create_client(uid.clone()); + + let config = get_default_engine_config( + engine_client, + SimulatedOracle::new(oracle.clone()), + uid.clone(), + genesis_hash, + namespace, + key_store, + validators.clone(), + initial_state.clone(), + ); + let engine = Engine::new(context.with_label(&uid), config).await; + consensus_state_queries.insert(idx, engine.finalizer_mailbox.clone()); + + let (pending, recovered, resolver, orchestrator, broadcast) = + registrations.remove(&public_key).unwrap(); + + engine.start(pending, recovered, resolver, orchestrator, broadcast); + } + + // Poll metrics until all validators reach stop_height + let mut height_reached = HashSet::new(); + loop { + let metrics = context.encode(); + let mut success = false; + for line in metrics.lines() { + if !line.starts_with("validator_") { + continue; + } + + let mut parts = line.split_whitespace(); + let metric = parts.next().unwrap(); + let value = parts.next().unwrap(); + + if metric.ends_with("_peers_blocked") { + let value = value.parse::().unwrap(); + assert_eq!(value, 0); + } + + if metric.ends_with("finalizer_height") { + let height = value.parse::().unwrap(); + if height >= stop_height { + height_reached.insert(metric.to_string()); + } + } + + if height_reached.len() as u32 == n { + success = true; + break; + } + } + if success { + break; + } + + context.sleep(Duration::from_secs(1)).await; + } + + // Before the epoch boundary, the treasury address is zero so fee_recipient + // should be the proposer's withdrawal credentials. + let fee_recipients = engine_client_network.get_fee_recipients(); + let epoch0_check_height = last_epoch0 / 2; + let epoch0_recipient = fee_recipients.get(&epoch0_check_height).expect("epoch 0 block should exist"); + assert_eq!( + *epoch0_recipient, withdrawal_cred, + "block {epoch0_check_height}: treasury is zero, fee_recipient should be withdrawal credentials" + ); + + // After the epoch boundary, treasury address is set so fee_recipient + // should be the treasury address. + let epoch1_recipient = fee_recipients.get(&first_epoch1).expect("epoch 1 block should exist"); + assert_eq!( + *epoch1_recipient, treasury_address, + "block {first_epoch1}: treasury was set, fee_recipient should be treasury address" + ); + + // Verify that all nodes agree on the treasury address + for (_, query) in &consensus_state_queries { + assert_eq!(query.get_treasury_address().await, treasury_address); + } + + // Check that all nodes have the same canonical chain + assert!( + engine_client_network + .verify_consensus(None, Some(stop_height)) + .is_ok() + ); + + common::assert_state_root_consensus(&consensus_state_queries).await; + + context.auditor().state() + }) +} diff --git a/types/src/lib.rs b/types/src/lib.rs index a2714c0a..95e56d64 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -50,7 +50,7 @@ pub struct BlockAuxData { pub added_validators: Vec, pub removed_validators: Vec, pub forkchoice: ForkchoiceState, - pub withdrawal_credentials: Address, + pub suggested_fee_recipient: Address, pub state_root: [u8; 32], pub allowed_timestamp_future_ms: u64, } From 351fd6b3562d60891b8bf70fe9da7f284a8f6049 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Fri, 20 Mar 2026 00:26:10 +0800 Subject: [PATCH 3/3] chore: bump version --- Cargo.lock | 14 +++++++------- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c85129ab..5763a14b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6009,7 +6009,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "summit" -version = "0.0.0" +version = "0.0.1-alpha" dependencies = [ "alloy", "alloy-consensus", @@ -6079,7 +6079,7 @@ dependencies = [ [[package]] name = "summit-application" -version = "0.0.0" +version = "0.0.1-alpha" dependencies = [ "anyhow", "commonware-consensus", @@ -6101,7 +6101,7 @@ dependencies = [ [[package]] name = "summit-finalizer" -version = "0.0.0" +version = "0.0.1-alpha" dependencies = [ "alloy-eips", "alloy-primitives", @@ -6132,7 +6132,7 @@ dependencies = [ [[package]] name = "summit-orchestrator" -version = "0.0.0" +version = "0.0.1-alpha" dependencies = [ "bytes", "commonware-broadcast", @@ -6158,7 +6158,7 @@ dependencies = [ [[package]] name = "summit-rpc" -version = "0.0.0" +version = "0.0.1-alpha" dependencies = [ "alloy-primitives", "anyhow", @@ -6194,7 +6194,7 @@ dependencies = [ [[package]] name = "summit-syncer" -version = "0.0.0" +version = "0.0.1-alpha" dependencies = [ "bytes", "commonware-broadcast", @@ -6223,7 +6223,7 @@ dependencies = [ [[package]] name = "summit-types" -version = "0.0.0" +version = "0.0.1-alpha" dependencies = [ "alloy-consensus", "alloy-eips", diff --git a/Cargo.toml b/Cargo.toml index 03066264..e2743b8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "application", "node", "rpc", "types", "finalizer", "orchestrator", resolver = "3" [workspace.package] -version = "0.0.0" +version = "0.0.1-alpha" edition = "2024" [workspace.dependencies]