Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e325bde
fix: enforce stake bounds against validators with an insufficient pen…
matthias-wright Jun 8, 2026
34cec91
chore: add consensus-key guard when processing deposits
matthias-wright Jun 8, 2026
a9bbe35
fix: refund queued deposits that outlived their account
matthias-wright Jun 26, 2026
b2eccab
test: cover refund of queued deposits that outlived their account
matthias-wright Jun 26, 2026
1c58223
feat: rework deposit/withdrawal/stake processing and move it into Con…
matthias-wright Jun 30, 2026
0884f0b
feat: consensus state payout methods, FullPayoutPending status, and t…
matthias-wright Jun 30, 2026
71c6ae4
chore: extract committee transition into consensus state + add lifecy…
matthias-wright Jun 30, 2026
4aa6d9b
chore: route deposit/withdrawal/payout/committee through ConsensusState
matthias-wright Jun 30, 2026
5fe5511
test: split deposit signature reasons; convert node tests to query st…
matthias-wright Jul 1, 2026
790c80a
fix: revert cancelled joining validator to Inactive + align node test…
matthias-wright Jul 1, 2026
cb1c293
test: align full-exit tests and e2e binaries with amount-0 full exit
matthias-wright Jul 1, 2026
6644e3f
test: add test_stake_increase_topup_keeps_active_validator
matthias-wright Jul 1, 2026
1317103
docs: update deposit and withdrawal docs
matthias-wright Jul 1, 2026
dd54cbd
test: regression for #211 (refund vs same-pubkey exit must not merge)
matthias-wright Jul 1, 2026
ff57109
fix: bound epoch-boundary withdrawal work to the cap
matthias-wright Jul 1, 2026
f18d2e2
docs: update ssz docs
matthias-wright Jul 1, 2026
0f50959
chore: remove MaximumStake and move exit-budget reset into ConsensusS…
matthias-wright Jul 1, 2026
5f5eae5
chore: drop dead has_pending_deposit/has_pending_withdrawal fields
matthias-wright Jul 1, 2026
10e3f23
docs: update ssz docs
matthias-wright Jul 1, 2026
17c6c1f
fix: enforce non-decreasing epoch order when decoding the withdrawal …
matthias-wright Jul 1, 2026
79b4c99
test: pin deposit/withdrawal edge cases and restart persistence
matthias-wright Jul 2, 2026
97b3ae0
chore: remove redundant PendingWithdrawal.balance_deduction
matthias-wright Jul 2, 2026
ec9bfe3
fix: harden withdrawal decode + finalizer boundary, repair fuzz crate
matthias-wright Jul 2, 2026
4f3a161
test: pin out-of-committee statuses are ignored by stake-bound enforc…
matthias-wright Jul 2, 2026
cb3f0c9
fix: reconcile audit checkpoint tests with the deposit/withdrawal ref…
matthias-wright Jul 2, 2026
4806e4d
test: cover buffered withdrawal edge cases
matthias-wright Jul 2, 2026
2509de4
fix: clamp terminal-block payouts against the prospective minimum stake
matthias-wright Jul 6, 2026
29d299c
chore: remove unused pending_deposit_amount
matthias-wright Jul 6, 2026
fcbf2eb
chore: remove dead code with no callers
matthias-wright Jul 7, 2026
ef6099b
fix: reject unverified block withdrawals before EL forkchoice adoption
matthias-wright Jul 7, 2026
d4f0951
chore: batch the push-side withdrawal subtree rebuild
matthias-wright Jul 7, 2026
6d8e3e5
fix: repopulate pending_checkpoint when restoring from a checkpoint
matthias-wright Jul 7, 2026
d32a5cc
chore: remove stale commented-out tests
matthias-wright Jul 7, 2026
5df6d65
fix: keyed withdrawal proof resolves the earliest entry per pubkey
matthias-wright Jul 7, 2026
2f6ce17
fix: reject epoch-mismatched blocks before EL forkchoice adoption
matthias-wright Jul 7, 2026
751028c
feat: cap pending withdrawals per validator
matthias-wright Jul 17, 2026
b8b0a0f
fix: consume stale withdrawal entries on credential mismatch
matthias-wright Jul 17, 2026
938b255
docs: update deposit and withdrawals doc
matthias-wright Aug 7, 2026
9125900
fix: block validator activation while pending withdrawals exist
matthias-wright Aug 11, 2026
cbb270d
fix: same-block MinimumValidatorCount increase must block full exits
matthias-wright Aug 12, 2026
2ea4744
fix: reject single-byte execution requests at verify
matthias-wright Aug 13, 2026
a002160
fix: keep min-stake floor on partials after exit request
matthias-wright Aug 17, 2026
0f839e2
fix: keep min-stake floor through full-exit window
matthias-wright Aug 18, 2026
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
42 changes: 28 additions & 14 deletions application/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,8 @@ impl<

// aux_data.forkchoice.head_block_hash = parent_block.eth_block_hash().into();

// Add pending withdrawals to the block
let withdrawals = pending_withdrawals.into_iter().map(|w| w.inner).collect();
// Add the EIP-4895 withdrawals (re-clamped payouts) to the block.
let withdrawals = pending_withdrawals;
let payload_id = {
#[cfg(feature = "bench")]
{
Expand Down Expand Up @@ -900,21 +900,28 @@ impl<
}

/// Returns `true` if the EIP-7685 execution-request list is ordered by request
/// type byte in strictly ascending order, with no empty elements.
/// type byte in strictly ascending order, with no empty or request_data-less
/// elements.
///
/// The engine API requires request-list elements to be sorted by type
/// (`OutOfOrderExecutionRequest` / `DuplicatedExecutionRequestType` otherwise).
/// Seismic's protocol-param request (type `0xFF`) is the maximum type, so it must
/// come last. We sort the list we propose; this predicate lets us reject a peer's
/// block that violates the ordering rather than relaying it into a payload the EL
/// will refuse.
/// (`OutOfOrderExecutionRequest` / `DuplicatedExecutionRequestType` otherwise) and
/// to carry non-empty request data (`EmptyExecutionRequest` otherwise, for
/// elements of one byte or shorter). Seismic's protocol-param request (type
/// `0xFF`) is the maximum type, so it must come last. We sort the list we
/// propose; this predicate lets us reject a peer's block that violates these
/// rules rather than relaying it into a payload the EL will refuse. A single
/// type byte with no data must be caught here, since the EL would treat it as
/// a fatal engine error rather than a block-level invalid payload.
fn execution_requests_ascending(requests: &[impl AsRef<[u8]>]) -> bool {
let mut prev: Option<u8> = None;
for req in requests {
let Some(&request_type) = req.as_ref().first() else {
// An element with no type byte is malformed.
let req = req.as_ref();
// A bare type byte has no request_data. The EL rejects it with
// `EmptyExecutionRequest`, so surface it as a block rejection here.
if req.len() <= 1 {
return false;
};
}
let request_type = req[0];
if prev.is_some_and(|p| request_type <= p) {
// Out of order or a duplicate request type.
return false;
Expand Down Expand Up @@ -1153,10 +1160,11 @@ fn handle_verify<ES: Epocher>(
return false;
}

// Validate withdrawals
let expected_withdrawals: Vec<_> = aux_data.withdrawals.iter().map(|w| w.inner).collect();
// Validate withdrawals: the block's EIP-4895 withdrawals must equal the
// re-clamped payouts the finalizer emitted into the aux data.
let expected_withdrawals: &[_] = &aux_data.withdrawals;
let actual_withdrawals: &[_] = &block.payload.payload_inner.withdrawals;
if actual_withdrawals != expected_withdrawals.as_slice() {
if actual_withdrawals != expected_withdrawals {
warn!(
expected_count = expected_withdrawals.len(),
actual_count = actual_withdrawals.len(),
Expand Down Expand Up @@ -1298,6 +1306,12 @@ mod tests {

// An element with no type byte is malformed.
assert!(!execution_requests_ascending(&[Vec::<u8>::new()]));

// A bare type byte with no request_data is also malformed; the EL
// rejects it as "EmptyExecutionRequest".
assert!(!execution_requests_ascending(&[vec![0x00]]));
assert!(!execution_requests_ascending(&[vec![0x01]]));
assert!(!execution_requests_ascending(&[vec![0xFF]]));
}

fn empty_payload(height: u64, parent_hash: [u8; 32], timestamp: u64) -> ExecutionPayloadV3 {
Expand Down
9 changes: 6 additions & 3 deletions docs/checkpointing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ Checkpoints are created at the **penultimate block of each epoch** (block `epoch
### Creation Flow

1. **Penultimate Block Processing** (`process_execution_requests`):
- Pending deposit requests are processed from `deposit_queue`
- Buffered execution requests are processed: withdrawal requests are validated
and enqueued, and deposits are drained from `deposit_queue` (up to
`max_deposits_per_epoch`)
- New validators are added to `added_validators` for the appropriate activation epoch
- The `removed_validators` list is populated with validators exiting this epoch
- Validators exiting this epoch — voluntary full exits and minimum-stake removals
(`enforce_minimum_stake`) — are added to `removed_validators`

2. **Checkpoint Creation** (`process_block`):
```
Expand All @@ -42,7 +45,7 @@ A checkpoint contains the serialized `ConsensusState`, which includes:
| `latest_height` | Height of the last finalized block |
| `head_digest` | Digest of the last finalized block |
| `deposit_queue` | Pending deposit requests |
| `withdrawal_queue` | Scheduled withdrawals by epoch |
| `withdrawal_queue` | Pending payout queue (validator withdrawals and deposit refunds) |
| `validator_accounts` | All validator account states |
| `added_validators` | Validators scheduled to join, by activation epoch |
| `removed_validators` | Validators exiting at the current epoch boundary |
Expand Down
Loading
Loading