Skip to content

Commit cedc89a

Browse files
feat: make allowed timestamp future a protocol param (#146)
When validators verify the timestamp of a proposed block they check that the timestamp is later than the parent timestamp, and that the timestamp is not too far in the future, relative to the local time. This change turns this parameter into a protocol param, that can be updated via execution request.
1 parent e24cb8c commit cedc89a

29 files changed

Lines changed: 389 additions & 41 deletions

application/src/actor.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ pub struct Actor<
4646
built_block: Arc<Mutex<Option<(Block, Round)>>>,
4747
genesis_hash: [u8; 32],
4848
epocher: ES,
49-
allowed_timestamp_future_ms: u64,
5049
cancellation_token: CancellationToken,
5150
_scheme_marker: PhantomData<S>,
5251
_key_marker: PhantomData<P>,
@@ -77,7 +76,6 @@ impl<
7776
built_block: Arc::new(Mutex::new(None)),
7877
genesis_hash,
7978
epocher: cfg.epocher,
80-
allowed_timestamp_future_ms: cfg.allowed_timestamp_future.as_millis() as u64,
8179
cancellation_token: cfg.cancellation_token,
8280
_scheme_marker: PhantomData,
8381
_key_marker: PhantomData,
@@ -236,7 +234,6 @@ impl<
236234
let mut syncer = syncer.clone();
237235
let mut finalizer_clone = finalizer.clone();
238236
let epocher = self.epocher.clone();
239-
let allowed_timestamp_future_ms = self.allowed_timestamp_future_ms;
240237
move |context| async move {
241238
let requester = try_join(parent_request, block_request);
242239
select! {
@@ -282,7 +279,7 @@ impl<
282279
}
283280

284281
let now_millis = context.current().epoch_millis();
285-
if handle_verify(&block, parent, &epocher, &aux_data, now_millis, allowed_timestamp_future_ms) {
282+
if handle_verify(&block, parent, &epocher, &aux_data, now_millis) {
286283
// persist valid block
287284
syncer.verified(round, block).await;
288285

@@ -581,7 +578,6 @@ fn handle_verify<ES: Epocher>(
581578
epocher: &ES,
582579
aux_data: &BlockAuxData,
583580
now_millis: u64,
584-
allowed_timestamp_future_ms: u64,
585581
) -> bool {
586582
// You can only re-propose the same block if it's the last height in the epoch.
587583
if parent.digest() == block.digest() {
@@ -604,10 +600,12 @@ fn handle_verify<ES: Epocher>(
604600
warn!("block timestamp not increasing");
605601
return false;
606602
}
607-
if block.timestamp() > now_millis + allowed_timestamp_future_ms {
603+
if block.timestamp() > now_millis + aux_data.allowed_timestamp_future_ms {
608604
warn!(
609605
block_timestamp = block.timestamp(),
610-
now_millis, allowed_timestamp_future_ms, "block timestamp too far in the future"
606+
now_millis,
607+
allowed_timestamp_future_ms = aux_data.allowed_timestamp_future_ms,
608+
"block timestamp too far in the future"
611609
);
612610
return false;
613611
}

application/src/config.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use commonware_consensus::types::Epocher;
2-
use std::time::Duration;
32
use summit_types::EngineClient;
43
use tokio_util::sync::CancellationToken;
54

@@ -18,11 +17,5 @@ pub struct ApplicationConfig<C: EngineClient, ES: Epocher> {
1817
/// Epocher for determining epoch boundaries.
1918
pub epocher: ES,
2019

21-
/// Maximum allowed delta between a block's timestamp and the
22-
/// local wall clock. Blocks with timestamps that differ from
23-
/// the local time by more than this are rejected during
24-
/// verification.
25-
pub allowed_timestamp_future: Duration,
26-
2720
pub cancellation_token: CancellationToken,
2821
}

docs/ssz-merklization.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ The state tree is a two-level design: a fixed top-level tree containing scalar f
3636

3737
### Top-Level Tree
3838

39-
32 leaf slots (depth 5), 17 used. Each leaf is a 32-byte `hash_tree_root` value. Leaves 17–31 are unused (zero-filled).
39+
32 leaf slots (depth 5), 18 used. Each leaf is a 32-byte `hash_tree_root` value. Leaves 18–31 are unused (zero-filled).
4040

4141
| Leaf Index | Field | Type |
4242
|------------|-------|------|
@@ -51,12 +51,13 @@ The state tree is a two-level design: a fixed top-level tree containing scalar f
5151
| 8 | `forkchoice_head_block_hash` | Scalar |
5252
| 9 | `forkchoice_safe_block_hash` | Scalar |
5353
| 10 | `forkchoice_finalized_block_hash` | Scalar |
54-
| 11 | `validator_accounts` | Collection root |
55-
| 12 | `deposit_queue` | Collection root |
56-
| 13 | `withdrawal_queue` | Collection root |
57-
| 14 | `protocol_param_changes` | Collection root |
58-
| 15 | `added_validators` | Collection root |
59-
| 16 | `removed_validators` | Collection root |
54+
| 11 | `allowed_timestamp_future_ms` | Scalar |
55+
| 12 | `validator_accounts` | Collection root |
56+
| 13 | `deposit_queue` | Collection root |
57+
| 14 | `withdrawal_queue` | Collection root |
58+
| 15 | `protocol_param_changes` | Collection root |
59+
| 16 | `added_validators` | Collection root |
60+
| 17 | `removed_validators` | Collection root |
6061

6162
### Collection Subtrees
6263

@@ -141,7 +142,7 @@ A `HashMap<pubkey, (epoch_slot, item_slot)>` index enables O(1) proof lookup by
141142

142143
All leaf values are 32 bytes, produced by SSZ `hash_tree_root`:
143144

144-
- **`u64`**: Little-endian encoded, zero-padded to 32 bytes. Used by: epoch, view, latest_height, balance, amount, index, joining_epoch, last_deposit_index, next_withdrawal_index, minimum/maximum_stake, validator_index, balance_deduction.
145+
- **`u64`**: Little-endian encoded, zero-padded to 32 bytes. Used by: epoch, view, latest_height, balance, amount, index, joining_epoch, last_deposit_index, next_withdrawal_index, minimum/maximum_stake, allowed_timestamp_future_ms, validator_index, balance_deduction.
145146
- **`bool`**: `0x01` or `0x00`, zero-padded to 32 bytes. Used by: has_pending_deposit, has_pending_withdrawal.
146147
- **`ValidatorStatus` (enum)**: Single byte (Active=0, Inactive=1, SubmittedExitRequest=2, Joining=3), zero-padded to 32 bytes.
147148
- **`[u8; 32]`**: Used directly as the leaf value. Used by: head_digest, epoch_genesis_hash, forkchoice hashes, withdrawal_credentials (deposit), pubkey (withdrawal).
@@ -168,6 +169,7 @@ Single top-level leaf write + rehash of the 5-level path to root.
168169
| `set_epoch_genesis_hash()` | `ssz_tree.set_epoch_genesis_hash()` |
169170
| `set_minimum_stake()` | `ssz_tree.set_validator_minimum_stake()` |
170171
| `set_maximum_stake()` | `ssz_tree.set_validator_maximum_stake()` |
172+
| `set_allowed_timestamp_future_ms()` | `ssz_tree.set_allowed_timestamp_future_ms()` |
171173
| `set_next_withdrawal_index()` | `ssz_tree.set_next_withdrawal_index()` |
172174
| `set_forkchoice_head()` | `ssz_tree.set_forkchoice_head_block_hash()` |
173175
| `set_forkchoice_safe_and_finalized()` | Two setter calls (safe + finalized) |
@@ -392,6 +394,7 @@ Keys are human-readable strings parsed by `types/src/ssz_tree_key.rs`:
392394
| `epoch_genesis_hash` | Genesis hash for current epoch |
393395
| `validator_minimum_stake` | Minimum validator stake |
394396
| `validator_maximum_stake` | Maximum validator stake |
397+
| `allowed_timestamp_future_ms` | Allowed timestamp future (ms) |
395398
| `next_withdrawal_index` | Next withdrawal index |
396399
| `forkchoice_head_block_hash` | Forkchoice head hash |
397400
| `forkchoice_safe_block_hash` | Forkchoice safe hash |

example_genesis.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace = "_SUMMIT"
99
validator_minimum_stake = 32000000000
1010
validator_maximum_stake = 32000000000
1111
blocks_per_epoch = 10000
12+
allowed_timestamp_future_ms = 10000
1213

1314
[[validators]]
1415
node_public_key = "1be3cb06d7cc347602421fb73838534e4b54934e28959de98906d120d0799ef2"

finalizer/src/actor.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,7 @@ impl<
889889
forkchoice: *state.get_forkchoice(),
890890
withdrawal_credentials,
891891
state_root: state.get_state_root(),
892+
allowed_timestamp_future_ms: state.get_allowed_timestamp_future_ms(),
892893
}
893894
} else {
894895
BlockAuxData {
@@ -901,6 +902,7 @@ impl<
901902
forkchoice: *state.get_forkchoice(),
902903
withdrawal_credentials,
903904
state_root: state.get_state_root(),
905+
allowed_timestamp_future_ms: state.get_allowed_timestamp_future_ms(),
904906
}
905907
};
906908
trace!(
@@ -970,6 +972,10 @@ impl<
970972
let length = self.canonical_state.get_epocher().current_length();
971973
let _ = sender.send(ConsensusStateResponse::EpochLength(length));
972974
}
975+
ConsensusStateRequest::GetAllowedTimestampFuture => {
976+
let ms = self.canonical_state.get_allowed_timestamp_future_ms();
977+
let _ = sender.send(ConsensusStateResponse::AllowedTimestampFuture(ms));
978+
}
973979
ConsensusStateRequest::GetEpochBounds(epoch) => {
974980
let bounds = self
975981
.canonical_state

finalizer/src/ingress.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,24 @@ impl<S: Scheme<B::Digest>, B: ConsensusBlock> FinalizerMailbox<S, B> {
279279
length
280280
}
281281

282+
pub async fn get_allowed_timestamp_future(&self) -> u64 {
283+
let (response, rx) = oneshot::channel();
284+
let request = ConsensusStateRequest::GetAllowedTimestampFuture;
285+
let _ = self
286+
.sender
287+
.clone()
288+
.send(FinalizerMessage::QueryState { request, response })
289+
.await;
290+
291+
let res = rx
292+
.await
293+
.expect("consensus state query response sender dropped");
294+
let ConsensusStateResponse::AllowedTimestampFuture(ms) = res else {
295+
unreachable!("request and response variants must match");
296+
};
297+
ms
298+
}
299+
282300
pub async fn get_epoch_bounds(&self, epoch: u64) -> Option<(u64, u64)> {
283301
let (response, rx) = oneshot::channel();
284302
let request = ConsensusStateRequest::GetEpochBounds(epoch);

finalizer/src/tests/fork_handling.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) -
113113
safe_block_hash: genesis_hash.into(),
114114
finalized_block_hash: genesis_hash.into(),
115115
};
116-
let mut state = ConsensusState::new(forkchoice, 32_000_000_000, 64_000_000_000, epoch_length);
116+
let mut state = ConsensusState::new(
117+
forkchoice,
118+
32_000_000_000,
119+
64_000_000_000,
120+
epoch_length,
121+
10_000,
122+
);
117123
state.set_validator_accounts(validator_accounts);
118124
state
119125
}

finalizer/src/tests/state_queries.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,13 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) -
119119
safe_block_hash: genesis_hash.into(),
120120
finalized_block_hash: genesis_hash.into(),
121121
};
122-
let mut state = ConsensusState::new(forkchoice, 32_000_000_000, 64_000_000_000, epoch_length);
122+
let mut state = ConsensusState::new(
123+
forkchoice,
124+
32_000_000_000,
125+
64_000_000_000,
126+
epoch_length,
127+
10_000,
128+
);
123129
state.set_validator_accounts(validator_accounts);
124130
state
125131
}

finalizer/src/tests/syncing.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) -
113113
safe_block_hash: genesis_hash.into(),
114114
finalized_block_hash: genesis_hash.into(),
115115
};
116-
let mut state = ConsensusState::new(forkchoice, 32_000_000_000, 64_000_000_000, epoch_length);
116+
let mut state = ConsensusState::new(
117+
forkchoice,
118+
32_000_000_000,
119+
64_000_000_000,
120+
epoch_length,
121+
10_000,
122+
);
117123
state.set_validator_accounts(validator_accounts);
118124
state
119125
}

finalizer/src/tests/validator_lifecycle.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,13 @@ fn create_test_initial_state(genesis_hash: [u8; 32], epoch_length: NonZeroU64) -
119119
safe_block_hash: genesis_hash.into(),
120120
finalized_block_hash: genesis_hash.into(),
121121
};
122-
let mut state = ConsensusState::new(forkchoice, 32_000_000_000, 64_000_000_000, epoch_length);
122+
let mut state = ConsensusState::new(
123+
forkchoice,
124+
32_000_000_000,
125+
64_000_000_000,
126+
epoch_length,
127+
10_000,
128+
);
123129
state.set_validator_accounts(validator_accounts);
124130
state
125131
}

0 commit comments

Comments
 (0)