From 61b31406a5a1f9346a16fbb129990d505baec921 Mon Sep 17 00:00:00 2001 From: mctursh Date: Tue, 8 Sep 2026 15:13:36 +0100 Subject: [PATCH 1/2] feat(replay): gate builtins and precompiles on the per-slot feature set Two places assumed the epoch-808 feature state. register_builtins registered every solana_builtins::BUILTINS entry regardless of feature state. Three of the nine sit behind enable_feature_id, so a replay could make a program invokable that the cluster didn't have. The feature set was already there: build_feature_set reads it off the on-chain feature accounts and backfill already handed it to the Replayer. register_builtins just never asked. Not cosmetic at epoch 808. zk_token_proof and loader_v4 have never activated on mainnet and were both being registered. No state was written (all three program accounts already exist on chain, so add_builtin never stubbed) and nothing in the verified 50k range invokes them, so that run's byte-exact result stands. The precompile callbacks hardcoded |_| true and all_enabled(). secp256r1 sits behind enable_secp256r1_precompile, active from slot 345,600,000, so a range below that would resolve a precompile the cluster didn't have. ReplayBank now carries the feature set, defaulting to all-enabled so fixtures are unchanged, and backfill sets it after seeding. The SVM reaches these callbacks through the bank rather than the Replayer, which is why the Replayer already holding one wasn't enough. --- README.md | 5 +- slate-replay/examples/check_gated_features.rs | 76 ++++++++ slate-replay/src/backfill.rs | 4 +- slate-replay/src/lib.rs | 163 +++++++++++++++--- slate-replay/src/oracle.rs | 7 +- 5 files changed, 221 insertions(+), 34 deletions(-) create mode 100644 slate-replay/examples/check_gated_features.rs diff --git a/README.md b/README.md index c77b745..42c1a2c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ v0.2. Live ingest is v1, proven on devnet, not yet mainnet-scale. Backfill has replayed a full snapshot-to-snapshot mainnet window, 50,079 slots, verified two independent ways: every slot's bank hash checked against the consensus hash carried in that block's own vote transactions, and the end state diffed byte-for-byte against the official snapshot at the end of the range, 8,412,739 accounts with zero mismatches. -That run is epoch 808. Other epochs need the feature gating in [Roadmap](#roadmap) first, since builtin registration and precompiles currently assume the feature set at that floor. Fidelity has a tail still being closed, so the replay records coverage up to the last verified slot and never guesses. +That run is epoch 808, the only range verified so far. Builtin registration and precompile verification key off the per-slot feature set the replay builds from the on-chain feature accounts, so a range elsewhere in history gets the programs that actually existed at those slots, as long as it stays inside one epoch (see [Roadmap](#roadmap)). Fidelity has a tail still being closed, so the replay records coverage up to the last verified slot and never guesses. ## How it works @@ -229,9 +229,8 @@ cargo test --workspace -- --test-threads=1 ## Roadmap -- **Feature gating for other epochs.** Builtin registration and precompile verification currently assume the feature set at the epoch-808 floor, so an earlier range would use programs that weren't active yet. Gate them on the per-slot feature set the replay already builds. - **Backfill fidelity.** Close the remaining tail of historical transactions the replay can't yet reproduce, a class at a time. -- **Multi-epoch backfill.** Span successive snapshot windows to reconstruct a whole epoch and beyond. +- **Multi-epoch backfill.** Span successive snapshot windows to reconstruct a whole epoch and beyond. A range has to stay inside one epoch for now: the feature set is built once from the range's first slot, and features activate on epoch boundaries, so a range that crosses one would replay its tail against the previous epoch's set. - **Gap repair.** Heal recorded coverage holes from incremental snapshots while they're still in retention. - **Durable source.** Ingest from a replayable stream (Triton's Fumarole, Helius's LaserStream, and the like), so a reconnect rewinds and most gaps heal on their own. - **asOfTime.** Query by timestamp, not just slot. diff --git a/slate-replay/examples/check_gated_features.rs b/slate-replay/examples/check_gated_features.rs new file mode 100644 index 0000000..4d903e3 --- /dev/null +++ b/slate-replay/examples/check_gated_features.rs @@ -0,0 +1,76 @@ +// One-shot: is the feature-gating change actually a no-op at the verified epoch-808 window? +// Reads the real seeded store and reports the activation state of every gated builtin/precompile. +// Usage: cargo run --release --example check_gated_features -- +use slate_replay::{ReplayBank, build_feature_set, store::DiskStore}; + +fn main() -> anyhow::Result<()> { + let mut args = std::env::args().skip(1); + let path = args.next().expect("usage: "); + let slot: u64 = args.next().expect("usage: ").parse()?; + + let store = DiskStore::create(&path, 1 << 30)?; + let bank = ReplayBank::with_store(Box::new(store)); + let feature_set = build_feature_set(&bank, slot); + + println!("store {path}\nslot {slot}\n"); + + println!("gated BUILTINS (solana_builtins::BUILTINS):"); + for b in solana_builtins::BUILTINS { + if let Some(fid) = b.enable_feature_id { + let active = feature_set.is_active(&fid); + println!( + " {:<22} {:<7} feature {fid}", + b.name, + if active { "ACTIVE" } else { "INACTIVE" } + ); + } + } + + println!("\ngated PRECOMPILE:"); + let r1 = agave_feature_set::enable_secp256r1_precompile::id(); + println!( + " secp256r1 {:<7} feature {r1}", + if feature_set.is_active(&r1) { + "ACTIVE" + } else { + "INACTIVE" + } + ); + + let ed = agave_feature_set::ed25519_precompile_verify_strict::id(); + println!( + " ed25519 verify_strict {:<7} feature {ed}", + if feature_set.is_active(&ed) { + "ACTIVE" + } else { + "INACTIVE" + } + ); + + // add_builtin only stubs an account when the store lacks one, so store presence decides whether + // registering an inactive builtin wrote phantom state or merely populated the program cache. + println!("\nprogram account present in the seeded store?"); + for b in solana_builtins::BUILTINS { + if b.enable_feature_id.is_some() { + let present = bank.store().get(&b.program_id).is_some(); + println!( + " {:<24} {}", + b.name, + if present { + "PRESENT (no stub written)" + } else { + "ABSENT (old code stubbed it)" + } + ); + } + } + + println!( + "\ntotal active features: {}", + agave_feature_set::FEATURE_NAMES + .keys() + .filter(|id| feature_set.is_active(id)) + .count() + ); + Ok(()) +} diff --git a/slate-replay/src/backfill.rs b/slate-replay/src/backfill.rs index 1d834f5..dbb8798 100644 --- a/slate-replay/src/backfill.rs +++ b/slate-replay/src/backfill.rs @@ -146,8 +146,10 @@ pub async fn backfill( let result = if let Some(&first_slot) = replay_slots.first() { let epoch = first_slot / 432_000; let feature_set = build_feature_set(&bank, first_slot); + // The bank needs its own copy: the SVM reaches the precompile callbacks through the bank, not the replayer. + bank.set_feature_set(feature_set.clone()); let replayer = Replayer::new_with_feature_set(first_slot, epoch, feature_set); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, replayer.feature_set()); // Compat: re-supply native builtins agave deleted post core-BPF migration (e.g. Stake), gated per feature so it's a no-op once active. compat::register_removed_builtins(&mut bank, &replayer.processor, replayer.feature_set()); diff --git a/slate-replay/src/lib.rs b/slate-replay/src/lib.rs index 82ff8c2..606dc17 100644 --- a/slate-replay/src/lib.rs +++ b/slate-replay/src/lib.rs @@ -80,6 +80,9 @@ pub struct ReplayBank { slot_dirty: Option>>, // Running lattice + bank hash; None for tests that don't need forward bank hashes. bankhash_roller: Option, + // Per-slot feature set, consulted by the precompile callbacks. Defaults to all-enabled so + // fixtures keep working; a faithful replay overwrites it via set_feature_set once seeded. + feature_set: FeatureSet, } impl Default for ReplayBank { @@ -90,6 +93,7 @@ impl Default for ReplayBank { write_version: 0, slot_dirty: None, bankhash_roller: None, + feature_set: FeatureSet::all_enabled(), } } } @@ -106,13 +110,17 @@ impl ReplayBank { pub fn with_store(store: Box) -> Self { Self { store, - writes: Vec::new(), - write_version: 0, - slot_dirty: None, - bankhash_roller: None, + ..Self::default() } } + // The precompile callbacks read this. Set it after seeding, since build_feature_set derives the + // set from the feature accounts the seed just supplied; the default is all-enabled, which is + // right for fixtures and wrong for any range below a precompile's activation slot. + pub fn set_feature_set(&mut self, feature_set: FeatureSet) { + self.feature_set = feature_set; + } + // Flush buffered writes to disk (no-op for the in-memory store). pub fn flush(&mut self) { self.store.flush(); @@ -411,8 +419,14 @@ impl ReplayBank { pub fn register_builtins( bank: &mut ReplayBank, processor: &TransactionBatchProcessor, + feature_set: &FeatureSet, ) { for builtin in solana_builtins::BUILTINS { + if let Some(feature_id) = builtin.enable_feature_id + && !feature_set.is_active(&feature_id) + { + continue; // gated behind a feature not yet active at this slot: it did not exist here + } bank.add_builtin( processor, builtin.program_id, @@ -448,10 +462,12 @@ pub fn build_feature_set(bank: &ReplayBank, slot: u64) -> FeatureSet { feature_set } -// Precompile verification wired to agave-precompiles; all precompile features are active at the epoch-808 floor, so all count as enabled. +// Precompile verification wired to agave-precompiles, gated on the bank's per-slot feature set. +// secp256k1 and ed25519 are always enabled; secp256r1 is behind enable_secp256r1_precompile, so a +// range before its activation must not resolve it as a precompile at all. impl InvokeContextCallback for ReplayBank { fn is_precompile(&self, program_id: &Pubkey) -> bool { - agave_precompiles::is_precompile(program_id, |_| true) + agave_precompiles::is_precompile(program_id, |id| self.feature_set.is_active(id)) } fn process_precompile( @@ -460,10 +476,8 @@ impl InvokeContextCallback for ReplayBank { data: &[u8], instruction_datas: Vec<&[u8]>, ) -> Result<(), PrecompileError> { - match agave_precompiles::get_precompile(program_id, |_| true) { - Some(precompile) => { - precompile.verify(data, &instruction_datas, &FeatureSet::all_enabled()) - } + match agave_precompiles::get_precompile(program_id, |id| self.feature_set.is_active(id)) { + Some(precompile) => precompile.verify(data, &instruction_datas, &self.feature_set), None => Err(PrecompileError::InvalidPublicKey), } } @@ -945,7 +959,7 @@ mod tests { use solana_account::ReadableAccount; let mut bank = fixture::seed_bank(); let replayer = Replayer::new(fixture::SLOT, fixture::SLOT / 432_000); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); let (acct, _) = bank .get_account_shared_data(&solana_system_program::id()) @@ -959,7 +973,7 @@ mod tests { use solana_account::ReadableAccount; let mut bank = fixture::seed_bank(); let replayer = Replayer::new(fixture::SLOT, fixture::SLOT / 432_000); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); // Every builtin agave lists is registered, executable, and native-owned. for builtin in solana_builtins::BUILTINS { @@ -980,6 +994,53 @@ mod tests { ); } + // A builtin behind an inactive feature did not exist at that slot, so registering it would let + // transactions invoke a program the real cluster would have rejected. zk_elgamal is the gate that + // matters in practice: it is the only one of the three that ever activated on mainnet (slot + // 315_792_000), so every range before that must not have it. + #[test] + fn a_builtin_gated_by_an_inactive_feature_is_not_registered() { + let gated = solana_sdk_ids::zk_elgamal_proof_program::id(); + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&agave_feature_set::zk_elgamal_proof_program_enabled::id()); + + let mut bank = fixture::seed_bank(); + let replayer = Replayer::new(fixture::SLOT, fixture::SLOT / 432_000); + register_builtins(&mut bank, &replayer.processor, &feature_set); + + let cache = replayer.processor.global_program_cache.read().unwrap(); + assert!( + cache.get_slot_versions_for_tests(&gated).is_empty(), + "zk_elgamal is gated off, so it must not reach the program cache" + ); + } + + // The inverse, and the one that actually earns its keep: a gate that reads the wrong field skips + // every builtin, which the test above would still pass. This one fails on it. + #[test] + fn an_ungated_builtin_survives_a_partial_feature_set() { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&agave_feature_set::zk_elgamal_proof_program_enabled::id()); + + let mut bank = fixture::seed_bank(); + let replayer = Replayer::new(fixture::SLOT, fixture::SLOT / 432_000); + register_builtins(&mut bank, &replayer.processor, &feature_set); + + let cache = replayer.processor.global_program_cache.read().unwrap(); + for builtin in solana_builtins::BUILTINS + .iter() + .filter(|b| b.enable_feature_id.is_none()) + { + assert!( + !cache + .get_slot_versions_for_tests(&builtin.program_id) + .is_empty(), + "{} has no feature gate, deactivating an unrelated feature must not drop it", + builtin.name + ); + } + } + #[test] fn configures_slot_hashes_sysvar() { use solana_account::ReadableAccount; @@ -1015,7 +1076,7 @@ mod tests { ); let replayer = Replayer::new(0, 0); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); let (acct, _) = bank .get_account_shared_data(&system) @@ -1103,7 +1164,7 @@ mod tests { let mut bank = fixture::seed_bank(); let replayer = Replayer::new(fixture::SLOT, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(fixture::SLOT, fixture::BLOCK_TIME); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -1164,7 +1225,7 @@ mod tests { let mut bank = fixture::memo::seed_bank(); let replayer = Replayer::new(m, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(m, fixture::memo::BLOCK_TIME); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -1222,7 +1283,7 @@ mod tests { let mut bank = fixture::cpi::seed_bank(); let replayer = Replayer::new(s, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(s, fixture::cpi::BLOCK_TIME); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -1375,7 +1436,7 @@ mod tests { let epoch = slot / 432_000; let replayer = Replayer::new(slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(slot, fixture::cpi::BLOCK_TIME); replayer.processor.fill_missing_sysvar_cache_entries(&bank); let tx = fixture::cpi::sanitized_transaction(); @@ -1413,7 +1474,7 @@ mod tests { let epoch = s / 432_000; let mut bank = fixture::cpi::seed_bank(); let replayer = Replayer::new(s, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(s, fixture::cpi::BLOCK_TIME); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -1459,7 +1520,7 @@ mod tests { let epoch = s / 432_000; let mut bank = fixture::cpi::seed_bank(); let replayer = Replayer::new(s, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(s, fixture::cpi::BLOCK_TIME); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -1523,7 +1584,7 @@ mod tests { let epoch = s / 432_000; let mut bank = fixture::cpi::seed_bank(); let replayer = Replayer::new(s, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); let block = fixture::cpi::block(); let outcome = replayer.replay_block(&mut bank, &block, epoch); @@ -1571,7 +1632,7 @@ mod tests { let epoch = slot / 432_000; let mut bank = snapshot::seed_bank_from_snapshot(SNAPSHOT, None).unwrap(); let replayer = Replayer::new(slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); let transfer = |from: &Pubkey, to: &Pubkey, lamports: u64| -> VersionedTransaction { let mut data = vec![2u8, 0, 0, 0]; // SystemInstruction::Transfer discriminant @@ -1756,7 +1817,7 @@ mod tests { slot, ); let replayer = Replayer::new(slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); let tx = sanitize(&build(cu_limit), &crate::block::LoadedAddresses::default()).unwrap(); let result = replayer.execute(&bank, &tx, 5_000, epoch, Hash::default()); matches!(&result, Ok(ProcessedTransaction::Executed(e)) if e.was_successful()) @@ -1796,7 +1857,7 @@ mod tests { slot, ); let replayer = Replayer::new(slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); // Transfer 10x the payer's balance: loads and executes, then fails on insufficient funds, fee charged, transfer rolled back. let mut data = vec![2u8, 0, 0, 0]; // System Transfer (4-byte disc) @@ -1893,7 +1954,7 @@ mod tests { slot, ); let replayer = Replayer::new(slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); // AdvanceNonceAccount reads RecentBlockhashes from the sysvar cache, so configure + pull it in first. bank.configure_sysvars(slot, 1_700_000_000); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -2013,7 +2074,7 @@ mod tests { slot, ); let replayer = Replayer::new(slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(slot, 1_700_000_000); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -2110,7 +2171,7 @@ mod tests { bank.insert(vote_pubkey, wallet(0), current_slot); // tx1 creates it let replayer = Replayer::new(current_slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(current_slot, 1_700_000_000); bank.set_slot_hashes(&[(voted_slot, voted_hash)]); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -2230,7 +2291,7 @@ mod tests { base_slot, ); let replayer = Replayer::new(base_slot, base_slot / 432_000); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); // Block N: src -> mid; block N+1: mid -> dst, which only reconciles if block N's write to mid rolled forward. let blocks = [ @@ -2291,4 +2352,52 @@ mod tests { "a corrupted precompile instruction must fail" ); } + + // secp256r1 is the only feature-gated precompile (enable_secp256r1_precompile, activated at slot + // 345_600_000, the first slot of epoch 800). Before that the cluster had no secp256r1 precompile + // at all, so a range below it must not resolve one. + #[test] + fn a_precompile_gated_by_an_inactive_feature_is_not_recognised() { + let secp256r1 = solana_sdk_ids::secp256r1_program::id(); + + // Baseline: with the feature active it IS a precompile, so the assertion below is about the + // gate and not about secp256r1 being unsupported outright. + assert!(ReplayBank::default().is_precompile(&secp256r1)); + + let mut bank = ReplayBank::default(); + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&agave_feature_set::enable_secp256r1_precompile::id()); + bank.set_feature_set(feature_set); + + assert!( + !bank.is_precompile(&secp256r1), + "secp256r1 is gated off, so the runtime must not treat it as a precompile" + ); + assert!( + bank.process_precompile(&secp256r1, &[], vec![]).is_err(), + "verifying through a precompile that did not exist yet must fail" + ); + } + + // Weaker than its builtin counterpart, deliberately: Precompile::check_id short-circuits on + // `feature.is_none_or(..)`, so the predicate is never consulted for an ungated precompile and no + // predicate bug can drop these two. The over-gating guard is the baseline assert in the test + // above. This pins the invariant against a future rewrite of the callback itself. + #[test] + fn ungated_precompiles_survive_a_partial_feature_set() { + let mut bank = ReplayBank::default(); + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&agave_feature_set::enable_secp256r1_precompile::id()); + bank.set_feature_set(feature_set); + + for id in [ + solana_sdk_ids::ed25519_program::id(), + solana_sdk_ids::secp256k1_program::id(), + ] { + assert!( + bank.is_precompile(&id), + "{id} has no feature gate, deactivating an unrelated feature must not disable it" + ); + } + } } diff --git a/slate-replay/src/oracle.rs b/slate-replay/src/oracle.rs index b5cb089..383b703 100644 --- a/slate-replay/src/oracle.rs +++ b/slate-replay/src/oracle.rs @@ -180,13 +180,14 @@ fn check_token_balances( mod tests { use super::*; use crate::{Replayer, block::LoadedAddresses, fixture, register_builtins}; + use agave_feature_set::FeatureSet; fn replay_cpi() -> (Vec, TransactionProcessingResult) { let s = fixture::cpi::SLOT; let epoch = s / 432_000; let mut bank = fixture::cpi::seed_bank(); let replayer = Replayer::new(s, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); bank.configure_sysvars(s, fixture::cpi::BLOCK_TIME); replayer.processor.fill_missing_sysvar_cache_entries(&bank); @@ -268,7 +269,7 @@ mod tests { slot, ); let replayer = Replayer::new(slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); let ix = Instruction { program_id: fake_program, @@ -418,7 +419,7 @@ mod tests { slot, ); let replayer = Replayer::new(slot, epoch); - register_builtins(&mut bank, &replayer.processor); + register_builtins(&mut bank, &replayer.processor, &FeatureSet::all_enabled()); let mut data = vec![2u8, 0, 0, 0]; // System Transfer data.extend_from_slice(&100_000_000u64.to_le_bytes()); // more than the payer holds From 17761c80f2ff0d0a0cecec37375f5ff4948c4583 Mon Sep 17 00:00:00 2001 From: mctursh Date: Tue, 8 Sep 2026 15:28:20 +0100 Subject: [PATCH 2/2] ran fmt --- slate-replay/examples/check_gated_features.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/slate-replay/examples/check_gated_features.rs b/slate-replay/examples/check_gated_features.rs index 4d903e3..857cc7e 100644 --- a/slate-replay/examples/check_gated_features.rs +++ b/slate-replay/examples/check_gated_features.rs @@ -6,7 +6,10 @@ use slate_replay::{ReplayBank, build_feature_set, store::DiskStore}; fn main() -> anyhow::Result<()> { let mut args = std::env::args().skip(1); let path = args.next().expect("usage: "); - let slot: u64 = args.next().expect("usage: ").parse()?; + let slot: u64 = args + .next() + .expect("usage: ") + .parse()?; let store = DiskStore::create(&path, 1 << 30)?; let bank = ReplayBank::with_store(Box::new(store));