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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions accountsdb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

Accountsdb stores current account state; the [ledger](../ledger/README.md) retains
execution history. The store separates account state that requires persistence
from rebuildable base mirrors and runtime state, which live in memory.
from rebuildable base mirrors, which live in memory.

Routing follows [lifecycle mode](../solana/account/README.md#account-lifecycle),
not memory representation. Delegated, Magic, and undelegating accounts are persisted,
not memory representation. Delegated, Magic, Runtime, and undelegating accounts are persisted,
including undelegating state that no longer permits user writes.
Closed accounts are removed. Reset discards mirrored state while preserving
persisted and runtime accounts.
persisted accounts, including locally supplied `Runtime` program images.

## Storage access and writeback

Expand Down
17 changes: 10 additions & 7 deletions accountsdb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const ACTIVE_DIR: &str = "CURRENT";
pub struct AccountsDB {
/// On-disk store for account modes that require persistence.
persisted: PersistedStore,
/// Rebuildable in-memory store for mirrors and runtime accounts.
/// Rebuildable in-memory store for base-chain mirrors.
volatile: VolatileStore,
/// Database root directory.
root: PathBuf,
Expand All @@ -65,6 +65,12 @@ impl AccountsDB {
Ok(db)
}

/// Whether persisted storage has no allocated account images.
/// Genesis seeding makes this false; reopening alone does not.
pub fn is_new(&self) -> bool {
self.persisted.storage.cursor() == 0
}

/// Returns the active database directory under `root`.
pub fn directory(root: &Path) -> PathBuf {
root.join(ACTIVE_DIR)
Expand Down Expand Up @@ -230,12 +236,8 @@ impl AccountsDB {
self.persisted.checksum().map_err(Into::into)
}

/// Drops chain-mirrored volatile state while retaining system accounts;
/// persisted state is left untouched.
///
/// Chain-owned accounts can be fetched again when synchronization resumes.
/// System accounts hold internal runtime state and survive the reset; their
/// volatile owner indexes are rebuilt. Persisted state is never reset.
/// Drops rebuildable chain mirrors and their owner indexes.
/// Persisted state, including local programs and runtime accounts, is retained.
pub fn reset(&self) {
self.volatile.reset();
}
Expand Down Expand Up @@ -421,6 +423,7 @@ fn persisted((_, account): &&AccountEntry) -> bool {
// Mode changes still need persisted cleanup after borrowed data grows into owned data.
matches!(account.cow(), CoWAccount::Borrowed(_))
|| account.mode().requires_persistence()
|| account.is(AccountMode::Closed)
|| account.markers().contains(DirtyMarkers::MODE)
}

Expand Down
54 changes: 50 additions & 4 deletions accountsdb/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,19 @@ fn mutable_at_least(lamports: u64, units: u64, owner: &Pubkey) -> AccountSharedD
}

/// Proves lifecycle changes route accounts and owner indexes between backends,
/// while closing removes state and reset retains persisted and runtime accounts.
/// while closing removes state and reset retains persisted runtime accounts and programs.
#[test]
fn test_routing_and_persistence_flips() {
let (_dir, db) = db();
let (p, q) = (Pubkey::new_unique(), Pubkey::new_unique());
let (a, b) = (Pubkey::new_unique(), Pubkey::new_unique());

let aacc = delegated_account(10, vec![], p);
let bacc = AccountBuilder::default().lamports(20).owner(p);
let bacc = AccountBuilder::default()
.lamports(20)
.owner(p)
.mode(AccountMode::Mirrored)
.executable(true);
// `a` is authoritative, `b` is non-authoritative; both are owned by `p`.
db.store(&[(a, aacc.build()), (b, bacc.build())]).unwrap();
assert!(in_persisted(&db, &a) && !in_volatile(&db, &a));
Expand Down Expand Up @@ -203,12 +207,40 @@ fn test_routing_and_persistence_flips() {
.mode(AccountMode::Runtime)
.build(),
);
assert!(in_volatile(&db, &runtime) && !in_persisted(&db, &runtime));
assert!(!in_volatile(&db, &runtime) && in_persisted(&db, &runtime));
let allocations = || {
let stats = db.persisted.storage.stats();
(stats.allocs.load(Relaxed), stats.reallocs.load(Relaxed))
};
let before = allocations();
for lamports in [41, 42] {
let mut account = reload(&db, &runtime);
account.set_lamports(lamports);
store(&db, runtime, account);
}
assert_eq!(
allocations(),
before,
"fixed-size runtime updates commit without reallocating"
);
let executable = Pubkey::new_unique();
store(
&db,
executable,
AccountBuilder::default()
.owner(p)
.mode(AccountMode::Runtime)
.executable(true)
.build(),
);
assert!(in_persisted(&db, &executable) && !in_volatile(&db, &executable));
db.reset();
assert!(!in_volatile(&db, &b));
assert!(in_persisted(&db, &c));
assert!(in_volatile(&db, &runtime));
assert!(in_persisted(&db, &runtime));
assert!(program(&db, &p).contains(&runtime));
assert!(in_persisted(&db, &executable));
assert!(program(&db, &p).contains(&executable));
}

// Both migration directions remove the source image and owner mapping, retain
Expand Down Expand Up @@ -346,16 +378,30 @@ fn test_dump_restores_volatile_on_reopen() {

{
let db = AccountsDB::new(dir.path()).unwrap();
assert!(db.is_new(), "persisted storage starts unseeded");
let account = AccountBuilder::default().lamports(42).mode(AccountMode::Mirrored).build();
store(&db, key, account);
assert!(
db.is_new(),
"volatile mirrors do not initialize persisted storage"
);
db.dump(None).unwrap();
assert!(dump.exists(), "dump is written into the active tree");
}

let db = AccountsDB::new(dir.path()).unwrap();
assert!(db.is_new(), "reopening does not seed persisted storage");
assert_eq!(lamports(&db, &key), 42);
assert!(in_volatile(&db, &key));
assert!(!dump.exists(), "restored dump is consumed on open");
store(&db, Pubkey::new_unique(), delegated(1));
assert!(
!db.is_new(),
"persisted allocation marks the store initialized"
);
db.flush(true).unwrap();
drop(db);
assert!(!AccountsDB::new(dir.path()).unwrap().is_new());
}

// A freed span is reused for a same-sized insert instead of growing the file;
Expand Down
10 changes: 2 additions & 8 deletions accountsdb/src/volatile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,10 @@ impl VolatileStore {
self.programs.read_sync(owner, |_, s| s.clone()).unwrap_or_default()
}

/// Drops chain-mirrored accounts while retaining internal system accounts.
/// Drops rebuildable base-chain state and its ownership indexes.
pub(crate) fn reset(&self) {
self.accounts.clear_sync();
self.programs.clear_sync();
self.accounts.retain_sync(|k, a| {
if !a.is(AccountMode::Runtime) {
return false;
}
let mut set = self.programs.entry_sync(a.owner()).or_default();
BTreeSet::insert(&mut set, *k)
});
}

/// Removes the cached account and drops its owner mapping.
Expand Down
8 changes: 6 additions & 2 deletions engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@ impl Engine {
cache.assign_program(id, entry.into());
}
let blockstore = builder.blockstore;
let rent = builder.rent.clone();
let state = Arc::new(builder.build(shutdown).await?);
Self::try_replay(&state, &cache, cpus).await?;
if pacer.is_none() {
state.reset(rent)?;
}
let (service, sequencer) = Sequencer::new(cpus / 2, state.clone(), cache, shutdown, false)?;
service.spawn()?;
let terminating = Arc::new(AtomicBool::new(false));
Expand Down Expand Up @@ -190,7 +194,7 @@ impl Engine {
TransactionVerifier::new(self.authority())
}

/// Drains in-flight execution and keeps the sequencer paused until the handle is dropped.
/// Drains execution and simulation, keeping both paused until the handle is dropped.
pub async fn barrier(&self) -> Result<BarrierHandle> {
self.pause(SequencerMessage::Barrier).await
}
Expand Down Expand Up @@ -237,7 +241,7 @@ impl Engine {
}
OwnedBlockstoreEntry::Reset(reset) => {
let _guard = self.barrier().await?;
self.apply_reset(*reset)?;
self.apply_reset(&reset.payload)?;
}
OwnedBlockstoreEntry::Checkpoint(expected) => {
let _guard = self.barrier().await?;
Expand Down
11 changes: 3 additions & 8 deletions engine/src/pacemaker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,8 @@ impl ExternalBlock {
impl PaceMaker {
/// Registers and starts the pacemaker task.
///
/// Uses an external block source when supplied. Otherwise it records one
/// reset at the keeper's current slot, clears chain-mirrored volatile state,
/// and starts emitting slots on the configured block interval.
/// Uses an external block source when supplied; otherwise emits slots on
/// the configured block interval.
pub(crate) fn spawn(
engine: Engine,
pacer: Option<ExternalPacer>,
Expand All @@ -104,11 +103,7 @@ impl PaceMaker {
) -> Result<()> {
let pacer = match pacer {
Some(rx) => Pacer::External(rx),
None => {
let ticker = BlockTicker::new(&engine, blockstore.blocktime);
engine.reset(ticker.slot)?;
Pacer::Internal(ticker)
}
None => Pacer::Internal(BlockTicker::new(&engine, blockstore.blocktime)),
};
let shutdown = shutdown.handle(Service::PaceMaker);
let superblock = blockstore.superblock;
Expand Down
6 changes: 0 additions & 6 deletions engine/tests/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,6 @@ async fn clean_restart_reopens_persisted_and_volatile_state() {
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)
Expand Down
16 changes: 16 additions & 0 deletions engine/tests/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! receipts, signature observers, duplicate admission, and client wire formats.
#![cfg(test)]

use std::time::Duration;

use agave_transaction_view::MAX_STANDARD_TRANSACTION_SIZE;
use engine::testkit::TestEngine;
use keeper::testkit::{
Expand All @@ -14,6 +16,7 @@ use solana_account::{AccountMode, ReadableAccount};
use solana_instruction_error::InstructionError;
use solana_packet::PACKET_DATA_SIZE;
use solana_transaction::TransactionError;
use tokio::time;
use v42_calculator_interface::builder::{Expr as E, transfer};

// The Engine accepts the same standard wire formats produced by Solana clients
Expand Down Expand Up @@ -231,6 +234,7 @@ async fn failed_execution_surfaces_error_result() {

/// Proves scheduling acknowledges queueing before admission and repeated bytes
/// execute once without consuming signature observers registered around resends.
/// The same barrier also holds simulation until execution is allowed to resume.
#[tokio::test(flavor = "multi_thread")]
async fn schedule_is_fire_and_forget() {
let te = TestEngine::new().await;
Expand All @@ -240,6 +244,15 @@ async fn schedule_is_fire_and_forget() {
signed_view(&te, None, (E::acc(0) + E::lit(7)).compose(output, &[]));
let first = te.transactions().subscribe_signature(signature).await.unwrap();
let barrier = te.barrier().await.unwrap();
{
let simulation = te
.transaction(transaction.inner_data().as_ref().clone())
.unwrap()
.simulate(true);
tokio::pin!(simulation);
// Poll the request into the simulator queue; it must not run across this boundary.
assert!(time::timeout(Duration::from_millis(50), &mut simulation).await.is_err());
}
for _ in 0..3 {
te.transaction(transaction.inner_data().as_ref().clone())
.unwrap()
Expand All @@ -259,6 +272,9 @@ async fn schedule_is_fire_and_forget() {
Err(oneshot::TryRecvError::Empty)
));
drop(barrier);
let txn = transaction.inner_data().as_ref().clone();
let simulation = te.transaction(txn).unwrap().simulate(true);
assert!(simulation.await.unwrap().is_ok());
assert_eq!(first.await.unwrap().result, Ok(()));
assert_eq!(second.await.unwrap().result, Ok(()));
assert_eq!(
Expand Down
9 changes: 6 additions & 3 deletions keeper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ to interpret their state.
## Startup and recovery

Startup validates account storage against ledger progress, restores a retained
snapshot when necessary, and seeds programs, features, and sysvars.
snapshot when necessary, and seeds features and sysvars only on genesis.
Programs and caller-supplied account seeds are inserted whenever absent; existing
images are preserved. Runtime configuration is restored from persisted account state.
[Engine](../engine/README.md#startup-and-recovery) replays the remaining history.
Recovery can fail rather than open inconsistent state and depends on valid retained
snapshots and history.
Expand Down Expand Up @@ -67,8 +69,9 @@ irreversibly closes storage services.

Keeper supplies Solana sysvars without validator epoch policy. Its informational
epoch schedule follows local `blockstore.superblock` configuration, without warmup;
zero disables periodic sealing and uses 432,000-slot epochs. Schedules can differ
between peers or change on restart. Sealing doesn't itself advance the schedule.
zero disables periodic sealing and uses 432,000-slot epochs. The schedule is seeded
at genesis and restored from account state; changing the local sealing interval
doesn't replace it. Sealing doesn't itself advance the schedule.

Execution and simulation see Clock at the slot after the latest completed block,
with epochs derived from that slot and Unix timestamp from the block. The unsupported
Expand Down
17 changes: 8 additions & 9 deletions keeper/src/accessor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use tokio::sync::mpsc::{Receiver, UnboundedReceiver};

use crate::{
AccountLease, ExecutionRecord, FullTransaction, Keeper, ResolvedTransaction,
error::Result,
error::{KeeperError, Result},
util::{execution_commit, request, transaction_status},
};

Expand Down Expand Up @@ -93,22 +93,21 @@ impl<'a> AccountsAccessor<'a> {

/// Updates stored `SlotHashes` and `Clock` sysvar accounts from `block`.
///
/// If either sysvar account is absent, no account updates are stored.
/// Both sysvars must exist; fixed-capacity borrowed commits preserve their index offsets.
pub fn update_sysvars(&self, block: Block) -> Result<()> {
let loader = self.loader();

let Some(mut hacc) = loader.read(&SlotHashes::id(), Clone::clone)? else {
return Ok(());
};
// SAFETY: the sequencer is the exclusive sysvar writer and excludes compaction
// through this block transition. The guarded loader outlives both borrowed images.
let mut hacc = unsafe { loader.load(&SlotHashes::id()) }?
.ok_or(KeeperError::MissingSysvar(SlotHashes::id()))?;
let mut cacc =
unsafe { loader.load(&Clock::id()) }?.ok_or(KeeperError::MissingSysvar(Clock::id()))?;
let mut hashes: SlotHashes = hacc.deserialize_data().map_err(AccountsDBError::from)?;
hashes.add(block.slot, block.hash);
hacc.serialize_data(&hashes).map_err(AccountsDBError::from)?;
let Some(mut cacc) = loader.read(&Clock::id(), Clone::clone)? else {
return Ok(());
};
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
Loading
Loading