Skip to content

Commit 4b5ad02

Browse files
feat: rework execution requests and admit joining validators early to the p2p network (#85)
This updates the execution request handling in the finalizer to uphold the following 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. Additionally, this adds joining validators to the p2p network early. When a validator stakes in epoch n, they will join the committee in epoch n + VALIDATOR_NUM_WARM_UP_EPOCHS. However, they will now be added to the p2p network in epoch n + 1. This allows them to sync blocks and catch up their state, without actively participating in consensus.
1 parent 2435d1b commit 4b5ad02

File tree

15 files changed

+577
-271
lines changed

15 files changed

+577
-271
lines changed

docs/staking-and-participating.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,12 @@ As defined in EIP-7002, the calldata for this transaction is 56 bytes
3232
Note that the validator pubkey is the ED25519 key (left-padded with zeros), and not the BLS key.
3333
When depositing funds into the staking contract (see above), an Ethereum address was specified (withdrawal_credentials).
3434
A valid withdrawal transaction has to be signed by the private key associated with this Ethereum address.
35+
36+
## Invariants
37+
- 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`.
38+
- 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`.
39+
- 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.
40+
- 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.
41+
- 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`.
42+
- 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.
43+
- 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`.

finalizer/src/actor.rs

Lines changed: 216 additions & 138 deletions
Large diffs are not rendered by default.

finalizer/src/config.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ pub struct FinalizerConfig<C: EngineClient, O: NetworkOracle<PublicKey>, V: Vari
1616
pub epoch_num_of_blocks: u64,
1717
pub validator_max_withdrawals_per_block: usize,
1818
pub validator_minimum_stake: u64, // in gwei
19-
pub validator_withdrawal_period: u64,
19+
pub validator_withdrawal_num_epochs: u64,
2020
/// The maximum number of validators that will be onboarded at the same time
2121
pub validator_onboarding_limit_per_block: usize,
22+
/// Number of epochs to wait before activating validators after deposit
23+
pub validator_num_warm_up_epochs: u64,
2224
pub buffer_pool: PoolRef,
2325
pub genesis_hash: [u8; 32],
2426
/// Optional initial state to initialize the finalizer with

finalizer/src/ingress.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use futures::{
66
channel::{mpsc, oneshot},
77
};
88
use summit_syncer::Update;
9+
use summit_types::account::ValidatorAccount;
910
use summit_types::{
1011
Block, BlockAuxData, Digest, PublicKey,
1112
checkpoint::Checkpoint,
@@ -166,6 +167,25 @@ impl<S: Scheme, B: ConsensusBlock + Committable> FinalizerMailbox<S, B> {
166167
};
167168
balance
168169
}
170+
171+
// Added for testing
172+
pub async fn get_validator_account(&self, public_key: PublicKey) -> Option<ValidatorAccount> {
173+
let (response, rx) = oneshot::channel();
174+
let request = ConsensusStateRequest::GetValidatorAccount(public_key);
175+
let _ = self
176+
.sender
177+
.clone()
178+
.send(FinalizerMessage::QueryState { request, response })
179+
.await;
180+
181+
let res = rx
182+
.await
183+
.expect("consensus state query response sender dropped");
184+
let ConsensusStateResponse::ValidatorAccount(account) = res else {
185+
unreachable!("request and response variants must match");
186+
};
187+
account
188+
}
169189
}
170190

171191
impl<S: Scheme, B: ConsensusBlock + Committable> Reporter for FinalizerMailbox<S, B> {

node/src/args.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,8 @@ fn get_initial_state(
665665
balance: VALIDATOR_MINIMUM_STAKE,
666666
pending_withdrawal_amount: 0,
667667
status: ValidatorStatus::Active,
668+
has_pending_withdrawal: false,
669+
joining_epoch: 0,
668670
// TODO(matthias): this index is comes from the deposit contract.
669671
// Since there is no deposit transaction for the genesis nodes, the index will still be
670672
// 0 for the deposit contract. Right now we only use this index to avoid counting the same deposit request twice.

node/src/bin/withdraw_and_exit.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use std::{
3030
thread::JoinHandle,
3131
};
3232
use summit::args::{RunFlags, run_node_local};
33-
use summit::engine::{BLOCKS_PER_EPOCH, VALIDATOR_MINIMUM_STAKE};
33+
use summit::engine::{BLOCKS_PER_EPOCH, VALIDATOR_MINIMUM_STAKE, VALIDATOR_WITHDRAWAL_NUM_EPOCHS};
3434
use summit_types::PublicKey;
3535
use summit_types::reth::Reth;
3636
use tokio::sync::mpsc;
@@ -238,18 +238,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
238238
.expect("failed to send deposit transaction");
239239

240240
// Wait for all nodes to continue making progress
241-
let epoch_end = BLOCKS_PER_EPOCH;
241+
let end_height = BLOCKS_PER_EPOCH * (VALIDATOR_WITHDRAWAL_NUM_EPOCHS + 1);
242242
println!(
243243
"Waiting for all {} nodes to reach height {}",
244-
NUM_NODES, epoch_end
244+
NUM_NODES, end_height
245245
);
246246
loop {
247247
let mut all_ready = true;
248248
for idx in 0..(NUM_NODES - 1) {
249249
let rpc_port = get_node_flags(idx as usize).rpc_port;
250250
match get_latest_height(rpc_port).await {
251251
Ok(height) => {
252-
if height < epoch_end {
252+
if height < end_height {
253253
all_ready = false;
254254
println!("Node {} at height {}", idx, height);
255255
}
@@ -261,7 +261,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
261261
}
262262
}
263263
if all_ready {
264-
println!("All nodes have reached height {}", epoch_end);
264+
println!("All nodes have reached height {}", end_height);
265265
break;
266266
}
267267
context.sleep(Duration::from_secs(2)).await;
@@ -275,7 +275,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
275275
let node0_provider = ProviderBuilder::new().connect_http(node0_url.parse().expect("Invalid URL"));
276276

277277
// Check
278-
279278
let balance_after = node0_provider.get_balance(withdrawal_credentials).await.expect("Failed to get balance after withdrawal");
280279
println!("Withdrawal credentials balance after: {} wei", balance_after);
281280

node/src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub struct EngineConfig<C: EngineClient, S: Signer + ZeroizeOnDrop, O: NetworkOr
6161
impl<C: EngineClient, S: Signer + ZeroizeOnDrop, O: NetworkOracle<S::PublicKey>>
6262
EngineConfig<C, S, O>
6363
{
64+
#[allow(clippy::too_many_arguments)]
6465
pub fn get_engine_config(
6566
engine_client: C,
6667
oracle: O,

node/src/engine.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,10 @@ const MAX_REPAIR: NonZero<usize> = NZUsize!(10);
5252

5353
const VALIDATOR_ONBOARDING_LIMIT_PER_BLOCK: usize = 3;
5454
pub const VALIDATOR_MINIMUM_STAKE: u64 = 32_000_000_000; // in gwei
55-
56-
#[cfg(feature = "e2e")]
57-
pub const VALIDATOR_WITHDRAWAL_PERIOD: u64 = 10;
58-
#[cfg(all(debug_assertions, not(feature = "e2e")))]
59-
pub const VALIDATOR_WITHDRAWAL_PERIOD: u64 = 5;
60-
#[cfg(all(not(debug_assertions), not(feature = "e2e")))]
61-
const VALIDATOR_WITHDRAWAL_PERIOD: u64 = 100;
55+
// Number of epochs after a deposit until a validator joins the committee
56+
pub const VALIDATOR_NUM_WARM_UP_EPOCHS: u64 = 2;
57+
// Number of epochs after a withdrawal request until the payout
58+
pub const VALIDATOR_WITHDRAWAL_NUM_EPOCHS: u64 = 2;
6259
#[cfg(all(feature = "e2e", not(debug_assertions)))]
6360
pub const BLOCKS_PER_EPOCH: u64 = 50;
6461
#[cfg(debug_assertions)]
@@ -280,8 +277,9 @@ where
280277
epoch_num_of_blocks: BLOCKS_PER_EPOCH,
281278
validator_max_withdrawals_per_block: VALIDATOR_MAX_WITHDRAWALS_PER_BLOCK,
282279
validator_minimum_stake: VALIDATOR_MINIMUM_STAKE,
283-
validator_withdrawal_period: VALIDATOR_WITHDRAWAL_PERIOD,
280+
validator_withdrawal_num_epochs: VALIDATOR_WITHDRAWAL_NUM_EPOCHS,
284281
validator_onboarding_limit_per_block: VALIDATOR_ONBOARDING_LIMIT_PER_BLOCK,
282+
validator_num_warm_up_epochs: VALIDATOR_NUM_WARM_UP_EPOCHS,
285283
buffer_pool: buffer_pool.clone(),
286284
genesis_hash: cfg.genesis_hash,
287285
initial_state: cfg.initial_state,

node/src/test_harness/common.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,9 @@ pub fn get_initial_state(
317317
balance,
318318
pending_withdrawal_amount: 0,
319319
status: ValidatorStatus::Active,
320-
// TODO(matthias): this index is comes from the deposit contract.
320+
has_pending_withdrawal: false,
321+
joining_epoch: 0,
322+
// TODO(matthias): this index comes from the deposit contract.
321323
// Since there is no deposit transaction for the genesis nodes, the index will still be
322324
// 0 for the deposit contract. Right now we only use this index to avoid counting the same deposit request twice.
323325
// Since we set the index to 0 here, we cannot rely on the uniqueness. The first actual deposit request will have

0 commit comments

Comments
 (0)