Skip to content
Merged
9 changes: 9 additions & 0 deletions docs/staking-and-participating.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ As defined in EIP-7002, the calldata for this transaction is 56 bytes
Note that the validator pubkey is the ED25519 key (left-padded with zeros), and not the BLS key.
When depositing funds into the staking contract (see above), an Ethereum address was specified (withdrawal_credentials).
A valid withdrawal transaction has to be signed by the private key associated with this Ethereum address.

## Invariants
- A validator will join the committee `VALIDATOR_NUM_WARM_UP_EPOCHS` epochs after submitting a valid deposit request. The phase after submitting the deposit request, and before joining the committee is called the `onboarding phase`.
- If a withdrawal request is submitted in epoch `n`, then the validator will be removed from the committee at the end of epoch `n`. The withdrawal will be processed in epoch `n + VALIDATOR_WITHDRAWAL_NUM_EPOCHS`.
- A validator can only submit one withdrawal request at a time. If another withdrawal request is submitted, while a withdrawal request is pending, then the second withdrawal request will be ignored.
- If a withdrawal request is submitted while a validator is in the onboarding phase, then the onboarding phase is aborted, and the withdrawal request will be processed `VALIDATOR_WITHDRAWAL_NUM_EPOCHS` epochs later.
- No partial withdrawals. If the validator balance is `balance`, and a withdrawal request with amount `amount < balance` is submitted, then the withdrawal request will be processed for the amount of `balance`.
- A validator can only have a balance of `VALIDATOR_MINIMUM_STAKE`. If a deposit request with `amount` is submitted, where `amount != VALIDATOR_MINIMUM_STAKE`, then the deposit request will be skipped, and a withdrawal request will be initiated immediately.
- No top up deposits. If a validator already has a balance of `VALIDATOR_MINIMUM_STAKE`, then it cannot submit another deposit request with amount `VALIDATOR_MINIMUM_STAKE`.
354 changes: 216 additions & 138 deletions finalizer/src/actor.rs

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion finalizer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ pub struct FinalizerConfig<C: EngineClient, O: NetworkOracle<PublicKey>, V: Vari
pub epoch_num_of_blocks: u64,
pub validator_max_withdrawals_per_block: usize,
pub validator_minimum_stake: u64, // in gwei
pub validator_withdrawal_period: u64,
pub validator_withdrawal_num_epochs: u64,
/// The maximum number of validators that will be onboarded at the same time
pub validator_onboarding_limit_per_block: usize,
/// Number of epochs to wait before activating validators after deposit
pub validator_num_warm_up_epochs: u64,
pub buffer_pool: PoolRef,
pub genesis_hash: [u8; 32],
/// Optional initial state to initialize the finalizer with
Expand Down
20 changes: 20 additions & 0 deletions finalizer/src/ingress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use futures::{
channel::{mpsc, oneshot},
};
use summit_syncer::Update;
use summit_types::account::ValidatorAccount;
use summit_types::{
Block, BlockAuxData, Digest, PublicKey,
checkpoint::Checkpoint,
Expand Down Expand Up @@ -166,6 +167,25 @@ impl<S: Scheme, B: ConsensusBlock + Committable> FinalizerMailbox<S, B> {
};
balance
}

// Added for testing
pub async fn get_validator_account(&self, public_key: PublicKey) -> Option<ValidatorAccount> {
let (response, rx) = oneshot::channel();
let request = ConsensusStateRequest::GetValidatorAccount(public_key);
let _ = self
.sender
.clone()
.send(FinalizerMessage::QueryState { request, response })
.await;

let res = rx
.await
.expect("consensus state query response sender dropped");
let ConsensusStateResponse::ValidatorAccount(account) = res else {
unreachable!("request and response variants must match");
};
account
}
}

impl<S: Scheme, B: ConsensusBlock + Committable> Reporter for FinalizerMailbox<S, B> {
Expand Down
2 changes: 2 additions & 0 deletions node/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ fn get_initial_state(
balance: VALIDATOR_MINIMUM_STAKE,
pending_withdrawal_amount: 0,
status: ValidatorStatus::Active,
has_pending_withdrawal: false,
joining_epoch: 0,
// TODO(matthias): this index is comes from the deposit contract.
// Since there is no deposit transaction for the genesis nodes, the index will still be
// 0 for the deposit contract. Right now we only use this index to avoid counting the same deposit request twice.
Expand Down
11 changes: 5 additions & 6 deletions node/src/bin/withdraw_and_exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use std::{
thread::JoinHandle,
};
use summit::args::{RunFlags, run_node_local};
use summit::engine::{BLOCKS_PER_EPOCH, VALIDATOR_MINIMUM_STAKE};
use summit::engine::{BLOCKS_PER_EPOCH, VALIDATOR_MINIMUM_STAKE, VALIDATOR_WITHDRAWAL_NUM_EPOCHS};
use summit_types::PublicKey;
use summit_types::reth::Reth;
use tokio::sync::mpsc;
Expand Down Expand Up @@ -238,18 +238,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.expect("failed to send deposit transaction");

// Wait for all nodes to continue making progress
let epoch_end = BLOCKS_PER_EPOCH;
let end_height = BLOCKS_PER_EPOCH * (VALIDATOR_WITHDRAWAL_NUM_EPOCHS + 1);
println!(
"Waiting for all {} nodes to reach height {}",
NUM_NODES, epoch_end
NUM_NODES, end_height
);
loop {
let mut all_ready = true;
for idx in 0..(NUM_NODES - 1) {
let rpc_port = get_node_flags(idx as usize).rpc_port;
match get_latest_height(rpc_port).await {
Ok(height) => {
if height < epoch_end {
if height < end_height {
all_ready = false;
println!("Node {} at height {}", idx, height);
}
Expand All @@ -261,7 +261,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
if all_ready {
println!("All nodes have reached height {}", epoch_end);
println!("All nodes have reached height {}", end_height);
break;
}
context.sleep(Duration::from_secs(2)).await;
Expand All @@ -275,7 +275,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let node0_provider = ProviderBuilder::new().connect_http(node0_url.parse().expect("Invalid URL"));

// Check

let balance_after = node0_provider.get_balance(withdrawal_credentials).await.expect("Failed to get balance after withdrawal");
println!("Withdrawal credentials balance after: {} wei", balance_after);

Expand Down
1 change: 1 addition & 0 deletions node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub struct EngineConfig<C: EngineClient, S: Signer + ZeroizeOnDrop, O: NetworkOr
impl<C: EngineClient, S: Signer + ZeroizeOnDrop, O: NetworkOracle<S::PublicKey>>
EngineConfig<C, S, O>
{
#[allow(clippy::too_many_arguments)]
pub fn get_engine_config(
engine_client: C,
oracle: O,
Expand Down
14 changes: 6 additions & 8 deletions node/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,10 @@ const MAX_REPAIR: NonZero<usize> = NZUsize!(10);

const VALIDATOR_ONBOARDING_LIMIT_PER_BLOCK: usize = 3;
pub const VALIDATOR_MINIMUM_STAKE: u64 = 32_000_000_000; // in gwei

#[cfg(feature = "e2e")]
pub const VALIDATOR_WITHDRAWAL_PERIOD: u64 = 10;
#[cfg(all(debug_assertions, not(feature = "e2e")))]
pub const VALIDATOR_WITHDRAWAL_PERIOD: u64 = 5;
#[cfg(all(not(debug_assertions), not(feature = "e2e")))]
const VALIDATOR_WITHDRAWAL_PERIOD: u64 = 100;
// Number of epochs after a deposit until a validator joins the committee
pub const VALIDATOR_NUM_WARM_UP_EPOCHS: u64 = 2;
// Number of epochs after a withdrawal request until the payout
pub const VALIDATOR_WITHDRAWAL_NUM_EPOCHS: u64 = 2;
#[cfg(all(feature = "e2e", not(debug_assertions)))]
pub const BLOCKS_PER_EPOCH: u64 = 50;
#[cfg(debug_assertions)]
Expand Down Expand Up @@ -280,8 +277,9 @@ where
epoch_num_of_blocks: BLOCKS_PER_EPOCH,
validator_max_withdrawals_per_block: VALIDATOR_MAX_WITHDRAWALS_PER_BLOCK,
validator_minimum_stake: VALIDATOR_MINIMUM_STAKE,
validator_withdrawal_period: VALIDATOR_WITHDRAWAL_PERIOD,
validator_withdrawal_num_epochs: VALIDATOR_WITHDRAWAL_NUM_EPOCHS,
validator_onboarding_limit_per_block: VALIDATOR_ONBOARDING_LIMIT_PER_BLOCK,
validator_num_warm_up_epochs: VALIDATOR_NUM_WARM_UP_EPOCHS,
buffer_pool: buffer_pool.clone(),
genesis_hash: cfg.genesis_hash,
initial_state: cfg.initial_state,
Expand Down
4 changes: 3 additions & 1 deletion node/src/test_harness/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,9 @@ pub fn get_initial_state(
balance,
pending_withdrawal_amount: 0,
status: ValidatorStatus::Active,
// TODO(matthias): this index is comes from the deposit contract.
has_pending_withdrawal: false,
joining_epoch: 0,
// TODO(matthias): this index comes from the deposit contract.
// Since there is no deposit transaction for the genesis nodes, the index will still be
// 0 for the deposit contract. Right now we only use this index to avoid counting the same deposit request twice.
// Since we set the index to 0 here, we cannot rely on the uniqueness. The first actual deposit request will have
Expand Down
Loading