Skip to content

Commit bf20cbe

Browse files
authored
feat: derive informational epochs from superblock slots (#156)
1 parent 6304610 commit bf20cbe

11 files changed

Lines changed: 299 additions & 119 deletions

File tree

‎engine/tests/recovery.rs‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async fn assert_rejected_transaction(
6262
}
6363

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

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

105107
let te2 = TestEngine::with(dirs, authority).await;
108+
assert_eq!(te2.clock(te2.blocks().latest()), expected_clock);
109+
let stored: solana_sysvar::clock::Clock = te2
110+
.get_account(solana_sdk_ids::sysvar::clock::ID)
111+
.unwrap()
112+
.deserialize_data()
113+
.unwrap();
114+
assert_eq!(stored, expected_clock);
106115
assert_eq!(
107116
load_v42_data(&te2, key),
108117
Some(21),
@@ -167,7 +176,8 @@ async fn replay_aborts_on_checksum_mismatch() {
167176
}
168177
}
169178

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

200212
let te2 = TestEngine::with(dirs, authority).await;
213+
// Leader startup reconstructs the block timestamp as zero; epoch semantics
214+
// depend only on the recovered slot, not on that existing timestamp policy.
215+
let expected_clock = solana_sysvar::clock::Clock {
216+
unix_timestamp: 0,
217+
..expected_clock
218+
};
219+
assert_eq!(te2.clock(te2.blocks().latest()), expected_clock);
220+
let stored: solana_sysvar::clock::Clock = te2
221+
.get_account(solana_sdk_ids::sysvar::clock::ID)
222+
.unwrap()
223+
.deserialize_data()
224+
.unwrap();
225+
assert_eq!(stored, expected_clock);
201226
assert_eq!(
202227
load_v42_data(&te2, key),
203228
Some(21),

‎keeper/README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ bounded duplicate protection from retained history. Recovered signatures do not
5050
necessarily have retained execution results. Snapshot bootstrap may also leave
5151
account-state transaction counts ahead of locally retained history.
5252

53+
## Epochs and Clock
54+
55+
Use `Keeper::epoch_schedule()` to interpret slots as informational epochs. Each
56+
epoch spans `blockstore.superblock` slots without warmup; zero disables periodic
57+
sealing and uses 432,000-slot epochs. The schedule follows local configuration,
58+
so it can differ between peers or change on restart. Sealing does not advance it.
59+
60+
Execution and simulation see Clock at the slot after the latest completed block,
61+
with epochs derived from that slot and the Unix timestamp from the block.
62+
`epoch_start_timestamp` is unsupported and remains zero.
63+
5364
## State boundaries
5465

5566
Full superblocks capture recoverable state and rotate history. Checksum-only

‎keeper/src/accessor.rs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,7 @@ impl<'a> AccountsAccessor<'a> {
9292
let Some(mut cacc) = loader.read(&Clock::id(), Clone::clone)? else {
9393
return Ok(());
9494
};
95-
let mut clock: Clock = cacc.deserialize_data().map_err(AccountsDBError::from)?;
96-
clock.slot = block.slot;
97-
clock.unix_timestamp = block.time;
95+
let clock = self.keeper.clock(block);
9896
cacc.serialize_data(&clock).map_err(AccountsDBError::from)?;
9997
drop(loader);
10098
self.store(&[(SlotHashes::id(), hacc), (Clock::id(), cacc)]).map_err(Into::into)

‎keeper/src/builder.rs‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,12 @@ impl KeeperBuilder {
7979
let accountsdb = self.accountsdb(&ledger)?;
8080
let (caches, featureset) = self.prepopulate(&accountsdb, &ledger).await?;
8181
metrics::init();
82+
let epoch_schedule = self.epoch_schedule();
8283
Ok(Keeper {
8384
authority: self.authority,
8485
featureset,
8586
rent: self.rent,
87+
epoch_schedule,
8688
accountsdb,
8789
ledger,
8890
caches,
@@ -112,6 +114,17 @@ impl KeeperBuilder {
112114
Ok((caches, featureset))
113115
}
114116

117+
/// Epochs follow the local sealing interval; zero uses Solana's no-warmup default.
118+
fn epoch_schedule(&self) -> EpochSchedule {
119+
let mut schedule = EpochSchedule::without_warmup();
120+
// Direct assignment supports intervals below Solana's 32-slot minimum.
121+
if self.blockstore.superblock != 0 {
122+
schedule.slots_per_epoch = self.blockstore.superblock;
123+
schedule.leader_schedule_slot_offset = self.blockstore.superblock;
124+
}
125+
schedule
126+
}
127+
115128
/// Builds read-side caches using blocktime-derived slot TTLs.
116129
fn caches(&self, latest: Block) -> Caches {
117130
Caches::new(
@@ -225,13 +238,8 @@ impl KeeperBuilder {
225238
self.leader_caches(&slothashes)
226239
};
227240

228-
// Set the clock slot one ahead from the last
229-
let latest = caches.latest();
230-
let clock = Clock {
231-
slot: latest.slot + 1,
232-
unix_timestamp: latest.time,
233-
..Default::default()
234-
};
241+
let epoch_schedule = self.epoch_schedule();
242+
let clock = crate::clock(&epoch_schedule, caches.latest());
235243
accounts.push((Clock::id(), self.account(&clock, &sysvar::ID)?.build()));
236244
accounts.push((Rent::id(), self.account(&self.rent, &sysvar::ID)?.build()));
237245
#[allow(deprecated)]
@@ -249,7 +257,7 @@ impl KeeperBuilder {
249257
));
250258
accounts.push((
251259
EpochSchedule::id(),
252-
self.account(&EpochSchedule::default(), &sysvar::ID)?.build(),
260+
self.account(&epoch_schedule, &sysvar::ID)?.build(),
253261
));
254262
accounts.push((
255263
EpochRewards::id(),

‎keeper/src/lib.rs‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use nucleus::{
2626
config::Authority,
2727
ledger::{ACCOUNTSDB_SNAPSHOT_FILE, Checkpoint, Reset, Signed, SuperblockSeal},
2828
};
29-
use solana_sysvar::rent::Rent;
29+
use solana_sysvar::{clock::Clock, epoch_schedule::EpochSchedule, rent::Rent};
3030

3131
use crate::{
3232
accessor::{AccountsAccessor, BlocksAccessor, SuperblockAccessor, TransactionsAccessor},
@@ -69,6 +69,8 @@ pub struct Keeper {
6969
featureset: FeatureSet,
7070
/// Rent parameters applied during execution.
7171
rent: Rent,
72+
/// Local slot-based schedule shared by sysvars and execution.
73+
epoch_schedule: EpochSchedule,
7274
/// Account state store.
7375
accountsdb: AccountsDB,
7476
/// Ledger worker handles and append path.
@@ -214,6 +216,16 @@ impl Keeper {
214216
&self.rent
215217
}
216218

219+
/// Returns the informational epoch schedule derived from local superblock configuration.
220+
pub fn epoch_schedule(&self) -> &EpochSchedule {
221+
&self.epoch_schedule
222+
}
223+
224+
/// Clock for the slot executing after `block`. Epoch-start timestamps are unsupported.
225+
pub fn clock(&self, block: nucleus::ledger::Block) -> Clock {
226+
clock(&self.epoch_schedule, block)
227+
}
228+
217229
/// Waits for queued ledger work to become durable, then synchronously
218230
/// flushes persisted account storage. Volatile accounts are not serialized.
219231
///
@@ -289,3 +301,16 @@ impl Keeper {
289301
Ok(())
290302
}
291303
}
304+
305+
/// Shared by startup seeding and live transitions; seals never enter this calculation.
306+
fn clock(schedule: &EpochSchedule, block: nucleus::ledger::Block) -> Clock {
307+
let slot = block.slot + 1;
308+
Clock {
309+
slot,
310+
epoch: schedule.get_epoch(slot),
311+
leader_schedule_epoch: schedule.get_leader_schedule_epoch(slot),
312+
unix_timestamp: block.time,
313+
// Engine does not track epoch-start timestamps.
314+
epoch_start_timestamp: 0,
315+
}
316+
}

0 commit comments

Comments
 (0)