Skip to content
Closed
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
813 changes: 467 additions & 346 deletions Cargo.lock

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ summit-finalizer = {path = "finalizer"}
summit-rpc = {path = "rpc"}
summit-orchestrator = {path = "orchestrator"}

commonware-actor = "2026.7.0"
commonware-formatting = "2026.7.0"
commonware-consensus = "2026.7.0"
commonware-cryptography = "2026.7.0"
commonware-storage = "2026.7.0"
commonware-runtime = "2026.7.0"
commonware-codec = "2026.7.0"
commonware-p2p = "2026.7.0"
commonware-broadcast = "2026.7.0"
commonware-utils = "2026.7.0"
commonware-resolver = "2026.7.0"
commonware-macros = "2026.7.0"
commonware-math = "2026.7.0"
commonware-parallel = "2026.7.0"
commonware-actor = "2026.9.0"
commonware-formatting = "2026.9.0"
commonware-consensus = "2026.9.0"
commonware-cryptography = "2026.9.0"
commonware-storage = "2026.9.0"
commonware-runtime = "2026.9.0"
commonware-codec = "2026.9.0"
commonware-p2p = "2026.9.0"
commonware-broadcast = "2026.9.0"
commonware-utils = "2026.9.0"
commonware-resolver = "2026.9.0"
commonware-macros = "2026.9.0"
commonware-math = "2026.9.0"
commonware-parallel = "2026.9.0"

alloy-consensus = "1.0.12"
alloy-eips = { version = "1.0.19", features = ["ssz"] }
Expand Down
10 changes: 4 additions & 6 deletions application/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,13 +467,11 @@ impl<
let requester = try_join(parent_request, block_request);
select! {
result = requester => {
// The syncer drops (cancels) a block subscription for a
// round older than its last_processed_round, so a stale
// verify path can see the request canceled. Treat that as
// a terminal "cannot verify" (vote false) rather than an
// invariant violation.
// Local subscriptions survive resolver-floor denial.
// Shutdown or a closed subscription can still cancel
// this work; it is not evidence of an invalid peer.
let Ok((parent, block)) = result else {
warn!(?round, "verify aborted: block subscription canceled (likely stale round)");
warn!(?round, "verify aborted: block subscription canceled");
let _ = response.send(false);
return;
};
Expand Down
2 changes: 1 addition & 1 deletion application/src/ingress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ mod tests {
let (tx, mut rx) = mpsc::channel(4);
let mut mailbox = Mailbox::<ed25519::PublicKey>::new(tx);

let digest = Sha256::hash(b"proposal");
let digest = Sha256::hash(&[b"proposal"]);
let round = Round::new(Epoch::new(3), View::new(7));
let peers = vec![test_public_key(1), test_public_key(2)];

Expand Down
30 changes: 29 additions & 1 deletion docs/deposits-and-withdrawals.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
- Potential validators have to deposit at least **MINIMUM_STAKE** to join the network.
- If a potential validator makes an initial deposit with *amount* < **MINIMUM_STAKE**, then the validator account is still created, but it won't be set to active.
- Top-up deposits are allowed.
- Once a processed deposit brings an inactive validator's balance to at least **MINIMUM_STAKE**, activation is scheduled **VALIDATOR_NUM_WARM_UP_EPOCHS** later. Deposit processing occurs near epoch end and is subject to **MAX_DEPOSITS_PER_EPOCH**, so activation may be delayed from submission.
- Once a processed deposit brings an inactive validator's balance to at least **MINIMUM_STAKE**, activation is scheduled **VALIDATOR_NUM_WARM_UP_EPOCHS** later, provided fewer than **MAX_VALIDATOR_COUNT** validators are active or joining. Deposit processing occurs near epoch end and is subject to **MAX_DEPOSITS_PER_EPOCH**, so activation may be delayed from submission.
- If **MAX_VALIDATOR_COUNT** is already occupied by active and joining validators, a valid deposit is still credited but the validator remains inactive and no activation is scheduled.
- Deposit requests with invalid signatures will be refunded as a withdrawal. K% of the deposited amount (**INVALID_DEPOSIT_TAX**, default 5%) is sent to the treasury address (the zero address by default, which effectively burns it). This prevents invalid deposits from becoming a DDOS vector.
- if the deposit's keys are malformed, it is refunded with the same K% tax applied to invalid signatures.
- If the deposit's consensus (BLS) key does not match the key already on the account (or is already used by another validator), the deposit is refunded with the same K% tax applied to invalid signatures.
Expand All @@ -22,6 +23,33 @@
- An invalid deposit may create separate refund and treasury withdrawals. Each withdrawal consumes one slot under the payout cap.
- Requests included in the last block of epoch E remain buffered until the penultimate block of E+1. An accepted exit then occurs at the end of E+1, with payout scheduled for epoch **E + 1 + VALIDATOR_WITHDRAWAL_NUM_EPOCHS**, subject to the withdrawal cap.

## Validator Count Limits
- **MAX_VALIDATOR_COUNT** (`MaxValidatorCount`, configured in genesis as `max_validator_count`) defaults to **128** and accepts values from **1 to 4,096**. It limits admission of **Active + Joining** validators, not the total number of stored accounts. Joining validators reserve a slot throughout warm-up.
- At epoch processing, the final proposed **MINIMUM_VALIDATOR_COUNT** and **MAX_VALIDATOR_COUNT** are evaluated together, using the last valid request for each. If minimum exceeds maximum, all updates to those two parameters in the pending batch are discarded before withdrawal or deposit decisions; the existing pair remains in effect. Unrelated parameter updates are retained. Valid paired changes are independent of request ordering.
- A proposed maximum below the current **Active + Joining** count is rejected before withdrawals or deposits are processed. Equality is allowed. All pending maximum-count updates are discarded on rejection, retaining the existing maximum rather than falling back to an earlier request; other updates remain subject to minimum/maximum consistency validation. Same-batch exits or joining cancellations cannot make the reduction valid; submit a later update after membership has decreased. No validators are evicted and no activation reservations are canceled by a cap change.
- Raising the maximum does not automatically activate funded inactive accounts. Another valid deposit must trigger admission, and any pending withdrawal still prevents reactivation.
- Withdrawals are processed before queued deposits are credited. An accepted active full exit or joining-validator cancellation frees an admission slot for that batch; a rejected withdrawal or active partial withdrawal does not. Deposits compete for available slots in deposit-queue order.
- A withdrawal for an account first created by a deposit in that same batch is dropped because the account does not yet exist. The deposit is still credited; a later authorized withdrawal can reclaim it. Cap-blocked inactive accounts can withdraw without the active-validator minimum-balance floor.
- Stored state must have minimum no greater than maximum. Membership can temporarily exceed the stored maximum while an accepted queued increase is already being used for admission, before boundary application. Checkpoint recovery and network sizing must account for that pending increase.

### Network Capacity and Restarts

Summit allocates its fixed P2P queue capacity at startup using the validator and observer limits from recovered consensus state. Accepted pending increases are included; pending reductions do not reduce the startup allocation. The capacity includes one extra slot for the local identity when it is outside the authorized peer sets:

```text
peer capacity = startup validator limit × (1 + startup observers per validator) + 1
```

With the defaults of **128 validators** and **16 observers per validator**, this is **2,177 identities per peer set**. Authorized observer identities count even when they are offline.

**Increasing `MaxValidatorCount` does not resize a running node's queues or automatically restart Summit.** To use a higher maximum that requires more peer capacity:

1. Ensure the raised value is durably recorded in each affected node's consensus state, either as an effective parameter or an accepted pending increase.
2. Coordinate restarts of the affected Summit instances so they allocate capacity using that value.
3. Restart before larger validator/observer sets exceed the previous allocation. Restarting against unchanged protocol state does not increase capacity.

A decrease, or an increase already covered by the startup allocation, does not require a restart for capacity reasons. The same capacity constraint applies when increasing the observers-per-validator parameter. If a registered peer set exceeds a node's fixed capacity, the node fails an assertion; this is an operational limit, not a node-local consensus rule rejecting the protocol parameter update.

## Validator Balance
- All active validators must have a balance of at least **MINIMUM_STAKE**.
- There is no upper limit on validator balance, however, there is no advantage (such as higher chance of becoming a leader) in having a balance that exceeds **MINIMUM_STAKE**.
Expand Down
5 changes: 4 additions & 1 deletion 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), 28 used. Each leaf is a 32-byte `hash_tree_root` value. Leaves 28–31 are unused (zero-filled).
32 leaf slots (depth 5), 29 used. Each leaf is a 32-byte `hash_tree_root` value. Leaves 29–31 are unused (zero-filled).

| Leaf Index | Field | Type |
|------------|-------|------|
Expand Down Expand Up @@ -67,6 +67,8 @@ The state tree is a two-level design: a fixed top-level tree containing scalar f
| 24 | `minimum_validator_count` | Scalar |
| 25 | `pending_active_validator_exits` | Scalar |
| 26 | `invalid_deposit_tax` | Scalar |
| 27 | `max_pending_withdrawals_per_validator` | Scalar |
| 28 | `max_validator_count` | Scalar |

### Collection Subtrees

Expand Down Expand Up @@ -200,6 +202,7 @@ Single top-level leaf write + rehash of the 5-level path to root.
| `set_max_deposits_per_epoch()` | `ssz_tree.set_max_deposits_per_epoch()` |
| `set_max_withdrawals_per_epoch()` | `ssz_tree.set_max_withdrawals_per_epoch()` |
| `set_observers_per_validator()` | `ssz_tree.set_observers_per_validator()` |
| `set_max_validator_count()` | `ssz_tree.set_max_validator_count()` |
| `set_minimum_validator_count()` | `ssz_tree.set_minimum_validator_count()` |
| `increment_pending_active_validator_exits()` / `reset_pending_active_validator_exits()` | `ssz_tree.set_pending_active_validator_exits()` |
| `set_next_withdrawal_index()` | `ssz_tree.set_next_withdrawal_index()` |
Expand Down
3 changes: 2 additions & 1 deletion example_genesis.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ allowed_timestamp_future_ms = 10000
treasury_address = "0x0000000000000000000000000000000000000000"
max_deposits_per_epoch = 3
max_withdrawals_per_epoch = 16
observers_per_validator = 5
observers_per_validator = 16
max_validator_count = 128
minimum_validator_count = 3
invalid_deposit_tax = 0

Expand Down
2 changes: 2 additions & 0 deletions finalizer/benches/consensus_state_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ fn main() {
log: commonware_storage::journal::contiguous::variable::Config {
partition: "bench-log".to_string(),
write_buffer: NZUsize!(64 * 1024),
replay_buffer: NZUsize!(64 * 1024),
compression: None,
codec_config: ((), ()),
items_per_section: NZU64!(4),
Expand All @@ -96,6 +97,7 @@ fn main() {
},
translator: EightCap,
init_cache_size: Some(NZUsize!(1024)),
init_buffer: NZUsize!(64 * 1024),
};

let mut db =
Expand Down
30 changes: 9 additions & 21 deletions finalizer/src/actor.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::config::ProtocolConsts;
use crate::db::{Config as StateConfig, FinalizerState};
use crate::db::FinalizerState;
use crate::{FinalizerConfig, FinalizerMailbox, FinalizerMessage};
use alloy_rpc_types_engine::ForkchoiceState;
use anyhow::{Result, anyhow};
Expand All @@ -17,17 +17,14 @@ use commonware_runtime::telemetry::metrics::{Gauge, MetricsExt as _};
use commonware_runtime::{
BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner, Storage, spawn_cell,
};
use commonware_storage::translator::EightCap;
use commonware_utils::acknowledgement::{Acknowledgement, Exact};
use commonware_utils::{NZU64, NZUsize};
use futures::channel::{mpsc, oneshot};
use futures::{FutureExt, StreamExt as _, select_biased};
#[cfg(feature = "prom")]
use metrics::{counter, histogram};
use rand::Rng;
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::marker::PhantomData;
use std::num::NonZero;
use std::time::{Duration, Instant};
use summit_orchestrator::Message;
use summit_syncer::{FaultEvidence, Update};
Expand All @@ -51,8 +48,6 @@ use summit_types::{EngineClient, consensus_state::ConsensusState};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, trace, warn};

const WRITE_BUFFER: NonZero<usize> = NZUsize!(1024 * 1024);

type FinalizerScheme<V> = bls12381_multisig::Scheme<PublicKey, V>;
type StateQueryResponse<V> = ConsensusStateResponse<FinalizerScheme<V>>;
type StateQueryMessage<V> = (
Expand Down Expand Up @@ -313,18 +308,7 @@ impl<
let (tx, rx) = mpsc::channel(cfg.mailbox_size);
let (updates_tx, updates_rx) = mpsc::unbounded();
let (state_query, state_query_rx) = ConsensusStateQuery::new(cfg.mailbox_size);
let state_cfg = StateConfig {
log: commonware_storage::journal::contiguous::variable::Config {
partition: format!("{}-finalizer_state-log", cfg.db_prefix),
write_buffer: WRITE_BUFFER,
compression: None,
codec_config: ((), ()),
items_per_section: NZU64!(262_144),
page_cache: cfg.page_cache,
},
translator: EightCap,
init_cache_size: Some(NZUsize!(1024)),
};
let state_cfg = crate::db::config(&cfg.db_prefix, cfg.page_cache);

let db = FinalizerState::<R, V>::new(
context.child("finalizer_state"),
Expand All @@ -333,9 +317,9 @@ impl<
)
.await;

// Check if the state exists in the database. Otherwise, use the initial state.
// The initial state could be from the genesis or a checkpoint.
// If we want to load a checkpoint, we have to make sure that the DB is cleared.
// Checkpoint startup durably promotes selected state before constructing
// this actor (startup::prepare). Never replace existing state here from
// an unqualified initial-state hint; the fallback is for empty storage.
let state = if let Some(state) = db.get_latest_consensus_state().await {
info!(
epoch = state.get_epoch(),
Expand Down Expand Up @@ -2010,6 +1994,10 @@ impl<
let value = self.canonical_state.get_observers_per_validator();
let _ = sender.send(ConsensusStateResponse::ObserversPerValidator(value));
}
ConsensusStateRequest::GetMaxValidatorCount => {
let value = self.canonical_state.get_max_validator_count();
let _ = sender.send(ConsensusStateResponse::MaxValidatorCount(value));
}
ConsensusStateRequest::GetMinimumValidatorCount => {
let value = self.canonical_state.get_minimum_validator_count();
let _ = sender.send(ConsensusStateResponse::MinimumValidatorCount(value));
Expand Down
Loading
Loading