Skip to content

Commit 5de83ee

Browse files
ksn6AshwinSekar
authored andcommitted
feat: alpenglow clock checks (anza-xyz#597)
We enforce Alpenglow clock checks, outlined in detail here: solana-foundation/solana-improvement-documents#363.
1 parent 21e811b commit 5de83ee

File tree

4 files changed

+115
-10
lines changed

4 files changed

+115
-10
lines changed

core/src/block_creation_loop.rs

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use {
3737
atomic::{AtomicBool, Ordering},
3838
},
3939
thread::{self, Builder, JoinHandle},
40-
time::{Duration, Instant},
40+
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
4141
},
4242
thiserror::Error,
4343
};
@@ -137,7 +137,7 @@ enum StartLeaderError {
137137
),
138138
}
139139

140-
fn produce_block_footer(block_producer_time_nanos: u64) -> BlockFooterV1 {
140+
fn produce_block_footer_with_timestamp(block_producer_time_nanos: u64) -> BlockFooterV1 {
141141
BlockFooterV1 {
142142
bank_hash: Hash::default(),
143143
block_producer_time_nanos,
@@ -359,6 +359,57 @@ fn produce_window(
359359
Ok(())
360360
}
361361

362+
/// Clamps the block producer timestamp to ensure that the leader produces a timestamp that conforms
363+
/// to Alpenglow clock bounds.
364+
fn skew_block_producer_time_nanos(
365+
parent_slot: Slot,
366+
parent_time_nanos: i64,
367+
working_bank_slot: Slot,
368+
working_bank_time_nanos: i64,
369+
) -> i64 {
370+
let (min_working_bank_time, max_working_bank_time) =
371+
BlockComponentProcessor::nanosecond_time_bounds(
372+
parent_slot,
373+
parent_time_nanos,
374+
working_bank_slot,
375+
);
376+
377+
working_bank_time_nanos
378+
.max(min_working_bank_time)
379+
.min(max_working_bank_time)
380+
}
381+
382+
/// Produces a block footer with the current timestamp and version information.
383+
/// The bank_hash field is left as default and will be filled in after the bank freezes.
384+
fn produce_block_footer(bank: Arc<Bank>) -> BlockFooterV1 {
385+
let mut block_producer_time_nanos = i64::try_from(
386+
SystemTime::now()
387+
.duration_since(UNIX_EPOCH)
388+
.expect("Misconfigured system clock; couldn't measure block producer time.")
389+
.as_nanos(),
390+
)
391+
.unwrap_or(i64::MAX);
392+
393+
let slot = bank.slot();
394+
395+
if let Some(parent_bank) = bank.parent() {
396+
// Get parent time from alpenglow clock (nanoseconds) or fall back to clock sysvar.
397+
let parent_time_nanos = parent_bank
398+
.get_nanosecond_clock()
399+
.unwrap_or_else(|| parent_bank.clock().unix_timestamp.saturating_mul(1_000_000_000));
400+
let parent_slot = parent_bank.slot();
401+
402+
block_producer_time_nanos = skew_block_producer_time_nanos(
403+
parent_slot,
404+
parent_time_nanos,
405+
slot,
406+
block_producer_time_nanos,
407+
);
408+
}
409+
410+
let block_producer_time_nanos = u64::try_from(block_producer_time_nanos).unwrap_or_default();
411+
produce_block_footer_with_timestamp(block_producer_time_nanos)
412+
}
362413
/// Records incoming transactions until we reach the block timeout.
363414
/// Afterwards:
364415
/// - Shutdown the record receiver
@@ -399,15 +450,13 @@ fn record_and_complete_block(
399450

400451
// Construct and send the block footer
401452
let mut w_poh_recorder = poh_recorder.write().unwrap();
402-
let block_producer_time_nanos = w_poh_recorder.working_bank_block_producer_time_nanos();
403-
let footer = produce_block_footer(block_producer_time_nanos);
404-
w_poh_recorder.send_marker(VersionedBlockMarker::new_block_footer(footer.clone()))?;
405-
406-
// Alpentick and clear bank
407453
let bank = w_poh_recorder
408454
.bank()
409455
.expect("Bank cannot have been cleared as BlockCreationLoop is the only modifier");
456+
let footer = produce_block_footer(bank.clone());
457+
w_poh_recorder.send_marker(VersionedBlockMarker::new_block_footer(footer.clone()))?;
410458

459+
// Alpentick and clear bank
411460
trace!(
412461
"{}: bank {} has reached block timeout, ticking",
413462
bank.leader_id(),

ledger/src/blockstore_processor.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,9 +1577,13 @@ pub fn confirm_slot(
15771577
)?;
15781578
}
15791579
BlockComponent::BlockMarker(marker) => {
1580+
let parent_bank = bank
1581+
.parent()
1582+
.unwrap_or_else(|| bank.clone_without_scheduler());
15801583
processor
15811584
.on_marker(
15821585
bank.clone_without_scheduler(),
1586+
parent_bank,
15831587
&marker,
15841588
migration_status,
15851589
is_final,

runtime/src/bank.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,6 @@ pub const MAX_LEADER_SCHEDULE_STAKES: Epoch = 5;
236236
/// This will be guaranteed through the VAT rules,
237237
/// only the top 2000 validators by stake will be present in vote account structures.
238238
pub const MAX_ALPENGLOW_VOTE_ACCOUNTS: usize = 2000;
239-
240239
/// The off-curve account where we store the Alpenglow clock. The clock sysvar has seconds
241240
/// resolution while the Alpenglow clock has nanosecond resolution.
242241
static NANOSECOND_CLOCK_ACCOUNT: LazyLock<Pubkey> = LazyLock::new(|| {

runtime/src/block_component_processor.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use {
22
crate::bank::Bank,
33
agave_votor_messages::migration::MigrationStatus,
4+
solana_clock::{Slot, DEFAULT_MS_PER_SLOT},
45
solana_entry::block_component::{
56
BlockFooterV1, BlockMarkerV1, VersionedBlockFooter, VersionedBlockHeader,
67
VersionedBlockMarker,
@@ -21,6 +22,8 @@ pub enum BlockComponentProcessorError {
2122
MultipleBlockHeaders,
2223
#[error("BlockComponent detected pre-migration")]
2324
BlockComponentPreMigration,
25+
#[error("Nanosecond clock out of bounds")]
26+
NanosecondClockOutOfBounds,
2427
}
2528

2629
#[derive(Default)]
@@ -67,6 +70,7 @@ impl BlockComponentProcessor {
6770
pub fn on_marker(
6871
&mut self,
6972
bank: Arc<Bank>,
73+
parent_bank: Arc<Bank>,
7074
marker: &VersionedBlockMarker,
7175
migration_status: &MigrationStatus,
7276
is_final: bool,
@@ -79,7 +83,9 @@ impl BlockComponentProcessor {
7983
let VersionedBlockMarker::V1(marker) = marker;
8084

8185
match marker {
82-
BlockMarkerV1::BlockFooter(footer) => self.on_footer(bank, footer.inner()),
86+
BlockMarkerV1::BlockFooter(footer) => {
87+
self.on_footer(bank, parent_bank, footer.inner())
88+
}
8389
BlockMarkerV1::BlockHeader(header) => self.on_header(header.inner()),
8490
// We process UpdateParent messages on shred ingest, so no callback needed here.
8591
BlockMarkerV1::UpdateParent(_) => Ok(()),
@@ -96,6 +102,7 @@ impl BlockComponentProcessor {
96102
fn on_footer(
97103
&mut self,
98104
bank: Arc<Bank>,
105+
parent_bank: Arc<Bank>,
99106
footer: &VersionedBlockFooter,
100107
) -> Result<(), BlockComponentProcessorError> {
101108
// The block header must be the first component of each block.
@@ -108,6 +115,8 @@ impl BlockComponentProcessor {
108115
}
109116

110117
let VersionedBlockFooter::V1(footer) = footer;
118+
119+
Self::enforce_nanosecond_clock_bounds(bank.clone(), parent_bank, footer)?;
111120
Self::update_bank_with_footer(bank, footer);
112121

113122
self.has_footer = true;
@@ -126,9 +135,53 @@ impl BlockComponentProcessor {
126135
Ok(())
127136
}
128137

138+
fn enforce_nanosecond_clock_bounds(
139+
bank: Arc<Bank>,
140+
parent_bank: Arc<Bank>,
141+
footer: &BlockFooterV1,
142+
) -> Result<(), BlockComponentProcessorError> {
143+
// If nanosecond clock hasn't been populated, don't enforce bounds yet.
144+
let Some(parent_time_nanos) = parent_bank.get_nanosecond_clock() else {
145+
return Ok(());
146+
};
147+
148+
let parent_slot = parent_bank.slot();
149+
let current_time_nanos = i64::try_from(footer.block_producer_time_nanos).unwrap_or(i64::MAX);
150+
let current_slot = bank.slot();
151+
152+
let (lower_bound_nanos, upper_bound_nanos) =
153+
Self::nanosecond_time_bounds(parent_slot, parent_time_nanos, current_slot);
154+
155+
if lower_bound_nanos <= current_time_nanos && current_time_nanos <= upper_bound_nanos {
156+
Ok(())
157+
} else {
158+
Err(BlockComponentProcessorError::NanosecondClockOutOfBounds)
159+
}
160+
}
161+
162+
/// Given the parent slot, parent time, and slot, calculate the inclusive
163+
/// bounds for the block producer timestamp.
164+
pub fn nanosecond_time_bounds(
165+
parent_slot: Slot,
166+
parent_time_nanos: i64,
167+
slot: Slot,
168+
) -> (i64, i64) {
169+
let default_ns_per_slot = i64::try_from(DEFAULT_MS_PER_SLOT)
170+
.unwrap_or(i64::MAX)
171+
.saturating_mul(1_000_000);
172+
let diff_slots = i64::try_from(slot.saturating_sub(parent_slot)).unwrap_or(i64::MAX);
173+
174+
let min_working_bank_time = parent_time_nanos.saturating_add(1);
175+
let max_working_bank_time = parent_time_nanos
176+
.saturating_add(diff_slots.saturating_mul(2).saturating_mul(default_ns_per_slot));
177+
178+
(min_working_bank_time, max_working_bank_time)
179+
}
180+
129181
pub fn update_bank_with_footer(bank: Arc<Bank>, footer: &BlockFooterV1) {
130182
// Update clock sysvar from footer timestamp.
131-
bank.update_clock_from_footer(footer.block_producer_time_nanos as i64);
183+
let unix_timestamp_nanos = i64::try_from(footer.block_producer_time_nanos).unwrap_or(i64::MAX);
184+
bank.update_clock_from_footer(unix_timestamp_nanos);
132185

133186
// TODO: rewards
134187
}

0 commit comments

Comments
 (0)