Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions application/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ impl<
aux_data.forkchoice,
current,
withdrawals,
Default::default(),
aux_data.suggested_fee_recipient,
None,
parent_block.height(),
)
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions docs/ssz-merklization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|------------|-------|------|
Expand All @@ -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

Expand Down Expand Up @@ -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).
Expand All @@ -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) |
Expand Down Expand Up @@ -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 |
Expand Down
34 changes: 23 additions & 11 deletions finalizer/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
}
Expand All @@ -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(),
}
Expand Down Expand Up @@ -976,6 +984,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
Expand Down
18 changes: 18 additions & 0 deletions finalizer/src/ingress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,24 @@ impl<S: Scheme<B::Digest>, B: ConsensusBlock> FinalizerMailbox<S, B> {
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);
Expand Down
1 change: 1 addition & 0 deletions finalizer/src/tests/fork_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions finalizer/src/tests/state_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions finalizer/src/tests/syncing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions finalizer/src/tests/validator_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 19 additions & 37 deletions node/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -656,14 +631,20 @@ pub fn run_node_local(
}

fn get_initial_state(
genesis_hash: [u8; 32],
genesis: &Genesis,
genesis_committee: &Vec<Validator>,
checkpoint: Option<ConsensusState>,
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::<Address>()
.expect("invalid treasury_address");
let genesis_hash: B256 = genesis_hash.into();
checkpoint.unwrap_or_else(|| {
let forkchoice = ForkchoiceState {
Expand All @@ -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 {
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions node/src/bin/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub struct GenesisConfig {
namespace: String,
blocks_per_epoch: u64,
allowed_timestamp_future_ms: u64,
#[serde(default)]
treasury_address: Option<String>,
pub validators: Vec<GenesisValidator>,
}

Expand Down
1 change: 1 addition & 0 deletions node/src/test_harness/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Loading
Loading