Skip to content
Merged
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
29 changes: 27 additions & 2 deletions engine/tests/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async fn assert_rejected_transaction(
}

/// Proves snapshot-tail replay verifies fresh checkpoints, rebuilds state, and
/// refreshes processed signatures.
/// refreshes processed signatures and preserves epochs at the recovered slot.
///
/// Dropping superblock 2's archive forces the restore back onto snapshot 1, so
/// re-executing B crosses superblock 2's sealed checksum before C is rebuilt
Expand Down Expand Up @@ -92,6 +92,8 @@ async fn replay_rebuilds_state_after_counter_lag() {
unsafe { te.checkpoint(None) }.unwrap();
}
te.advance(2).await;
let expected_clock = te.clock(te.blocks().latest());
assert!(expected_clock.epoch > 0);
let (dirs, authority) = te.close().await;

// Lag only accountsdb's durable checkpoint in the closed store, preserving
Expand All @@ -103,6 +105,13 @@ async fn replay_rebuilds_state_after_counter_lag() {
std::fs::remove_file(&s2).unwrap();

let te2 = TestEngine::with(dirs, authority).await;
assert_eq!(te2.clock(te2.blocks().latest()), expected_clock);
let stored: solana_sysvar::clock::Clock = te2
.get_account(solana_sdk_ids::sysvar::clock::ID)
.unwrap()
.deserialize_data()
.unwrap();
assert_eq!(stored, expected_clock);
assert_eq!(
load_v42_data(&te2, key),
Some(21),
Expand Down Expand Up @@ -167,7 +176,8 @@ async fn replay_aborts_on_checksum_mismatch() {
}
}

/// Proves a clean restart restores processed signatures without re-execution.
/// Proves a clean restart restores processed signatures and the same slot-based
/// epoch without re-execution.
///
/// Persisted state reopens as-is with the clean-shutdown volatile dump. A failed
/// execution still counts on both durable sides without writing accounts. The
Expand Down Expand Up @@ -195,9 +205,24 @@ async fn clean_restart_reopens_persisted_and_volatile_state() {
te.advance(1).await;
let direct = store_v42(&te, 7, AccountMode::Delegated);
let volatile = store_v42(&te, 8, AccountMode::ReadOnly);
let expected_clock = te.clock(te.blocks().latest());
assert!(expected_clock.epoch > 0);
let (dirs, authority) = te.close().await;

let te2 = TestEngine::with(dirs, authority).await;
// Leader startup reconstructs the block timestamp as zero; epoch semantics
// depend only on the recovered slot, not on that existing timestamp policy.
let expected_clock = solana_sysvar::clock::Clock {
unix_timestamp: 0,
..expected_clock
};
assert_eq!(te2.clock(te2.blocks().latest()), expected_clock);
let stored: solana_sysvar::clock::Clock = te2
.get_account(solana_sdk_ids::sysvar::clock::ID)
.unwrap()
.deserialize_data()
.unwrap();
assert_eq!(stored, expected_clock);
assert_eq!(
load_v42_data(&te2, key),
Some(21),
Expand Down
11 changes: 11 additions & 0 deletions keeper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ bounded duplicate protection from retained history. Recovered signatures do not
necessarily have retained execution results. Snapshot bootstrap may also leave
account-state transaction counts ahead of locally retained history.

## Epochs and Clock

Use `Keeper::epoch_schedule()` to interpret slots as informational epochs. Each
epoch spans `blockstore.superblock` slots without warmup; zero disables periodic
sealing and uses 432,000-slot epochs. The schedule follows local configuration,
so it can differ between peers or change on restart. Sealing does not advance it.

Execution and simulation see Clock at the slot after the latest completed block,
with epochs derived from that slot and the Unix timestamp from the block.
`epoch_start_timestamp` is unsupported and remains zero.

## State boundaries

Full superblocks capture recoverable state and rotate history. Checksum-only
Expand Down
4 changes: 1 addition & 3 deletions keeper/src/accessor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@ impl<'a> AccountsAccessor<'a> {
let Some(mut cacc) = loader.read(&Clock::id(), Clone::clone)? else {
return Ok(());
};
let mut clock: Clock = cacc.deserialize_data().map_err(AccountsDBError::from)?;
clock.slot = block.slot;
clock.unix_timestamp = block.time;
let clock = self.keeper.clock(block);
cacc.serialize_data(&clock).map_err(AccountsDBError::from)?;
drop(loader);
self.store(&[(SlotHashes::id(), hacc), (Clock::id(), cacc)]).map_err(Into::into)
Expand Down
24 changes: 16 additions & 8 deletions keeper/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,12 @@ impl KeeperBuilder {
let accountsdb = self.accountsdb(&ledger)?;
let (caches, featureset) = self.prepopulate(&accountsdb, &ledger).await?;
metrics::init();
let epoch_schedule = self.epoch_schedule();
Ok(Keeper {
authority: self.authority,
featureset,
rent: self.rent,
epoch_schedule,
accountsdb,
ledger,
caches,
Expand Down Expand Up @@ -112,6 +114,17 @@ impl KeeperBuilder {
Ok((caches, featureset))
}

/// Epochs follow the local sealing interval; zero uses Solana's no-warmup default.
fn epoch_schedule(&self) -> EpochSchedule {
let mut schedule = EpochSchedule::without_warmup();
// Direct assignment supports intervals below Solana's 32-slot minimum.
if self.blockstore.superblock != 0 {
schedule.slots_per_epoch = self.blockstore.superblock;
schedule.leader_schedule_slot_offset = self.blockstore.superblock;
}
schedule
}

/// Builds read-side caches using blocktime-derived slot TTLs.
fn caches(&self, latest: Block) -> Caches {
Caches::new(
Expand Down Expand Up @@ -225,13 +238,8 @@ impl KeeperBuilder {
self.leader_caches(&slothashes)
};

// Set the clock slot one ahead from the last
let latest = caches.latest();
let clock = Clock {
slot: latest.slot + 1,
unix_timestamp: latest.time,
..Default::default()
};
let epoch_schedule = self.epoch_schedule();
let clock = crate::clock(&epoch_schedule, caches.latest());
accounts.push((Clock::id(), self.account(&clock, &sysvar::ID)?.build()));
accounts.push((Rent::id(), self.account(&self.rent, &sysvar::ID)?.build()));
#[allow(deprecated)]
Expand All @@ -249,7 +257,7 @@ impl KeeperBuilder {
));
accounts.push((
EpochSchedule::id(),
self.account(&EpochSchedule::default(), &sysvar::ID)?.build(),
self.account(&epoch_schedule, &sysvar::ID)?.build(),
));
accounts.push((
EpochRewards::id(),
Expand Down
27 changes: 26 additions & 1 deletion keeper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use nucleus::{
config::Authority,
ledger::{ACCOUNTSDB_SNAPSHOT_FILE, Checkpoint, Reset, Signed, SuperblockSeal},
};
use solana_sysvar::rent::Rent;
use solana_sysvar::{clock::Clock, epoch_schedule::EpochSchedule, rent::Rent};

use crate::{
accessor::{AccountsAccessor, BlocksAccessor, SuperblockAccessor, TransactionsAccessor},
Expand Down Expand Up @@ -69,6 +69,8 @@ pub struct Keeper {
featureset: FeatureSet,
/// Rent parameters applied during execution.
rent: Rent,
/// Local slot-based schedule shared by sysvars and execution.
epoch_schedule: EpochSchedule,
/// Account state store.
accountsdb: AccountsDB,
/// Ledger worker handles and append path.
Expand Down Expand Up @@ -214,6 +216,16 @@ impl Keeper {
&self.rent
}

/// Returns the informational epoch schedule derived from local superblock configuration.
pub fn epoch_schedule(&self) -> &EpochSchedule {
&self.epoch_schedule
}

/// Clock for the slot executing after `block`. Epoch-start timestamps are unsupported.
pub fn clock(&self, block: nucleus::ledger::Block) -> Clock {
clock(&self.epoch_schedule, block)
}

/// Waits for queued ledger work to become durable, then synchronously
/// flushes persisted account storage. Volatile accounts are not serialized.
///
Expand Down Expand Up @@ -289,3 +301,16 @@ impl Keeper {
Ok(())
}
}

/// Shared by startup seeding and live transitions; seals never enter this calculation.
fn clock(schedule: &EpochSchedule, block: nucleus::ledger::Block) -> Clock {
let slot = block.slot + 1;
Clock {
slot,
epoch: schedule.get_epoch(slot),
leader_schedule_epoch: schedule.get_leader_schedule_epoch(slot),
unix_timestamp: block.time,
// Engine does not track epoch-start timestamps.
epoch_start_timestamp: 0,
}
}
Loading
Loading