iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 06eb82196846bb74490d3be326e3eb90957f752d
parent a4b370f46ae49cf09c33e39fb796e30b9c7aa180
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed,  2 Sep 2026 13:17:31 +0200

Skip repeated local chain VDF verification

Diffstat:
Msrc/adapters/chain_store.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/domain/ledger_chain.rs | 6++++++
Msrc/main.rs | 22++++++++++++++++++----
3 files changed, 194 insertions(+), 7 deletions(-)

diff --git a/src/adapters/chain_store.rs b/src/adapters/chain_store.rs @@ -25,13 +25,27 @@ CREATE TABLE IF NOT EXISTS chain_snapshots ( snapshot_blob BLOB NOT NULL, updated_at_ms INTEGER NOT NULL ); +CREATE TABLE IF NOT EXISTS chain_verification ( + id INTEGER PRIMARY KEY CHECK (id = 1), + tip_hash TEXT NOT NULL, + verifier_version TEXT NOT NULL, + verified_at_ms INTEGER NOT NULL +); "#; +const CURRENT_VERIFIER_VERSION: &str = env!("CARGO_PKG_VERSION"); + #[derive(Clone, Debug)] pub struct SqliteChainStore { path: PathBuf, } +#[derive(Debug)] +pub struct LoadedChainSnapshot { + pub snapshot: ChainSnapshot, + pub verified_by_current_version: bool, +} + impl SqliteChainStore { pub fn open(path: impl AsRef<Path>) -> Result<Self> { let path = path.as_ref().to_path_buf(); @@ -68,6 +82,24 @@ impl SqliteChainStore { } pub fn load(&self) -> Result<Option<ChainSnapshot>> { + Ok(self + .load_with_verification_status()? + .map(|loaded| loaded.snapshot)) + } + + pub fn contains_chain(&self) -> Result<bool> { + self.with_connection(|connection| { + connection + .query_row( + "SELECT EXISTS(SELECT 1 FROM chain_snapshots WHERE id = 1)", + [], + |row| row.get(0), + ) + .context("failed to inspect chain database") + }) + } + + pub fn load_with_verification_status(&self) -> Result<Option<LoadedChainSnapshot>> { self.with_connection(|connection| { let stored = connection .query_row( @@ -84,7 +116,7 @@ impl SqliteChainStore { .optional() .context("failed to load chain snapshot from database")?; - stored + let snapshot = stored .map(|(stored_height, stored_tip_hash, blob)| { let snapshot = decode_compact_snapshot(&blob) .context("failed to parse compact chain snapshot from database")?; @@ -95,13 +127,45 @@ impl SqliteChainStore { "compact chain snapshot tip does not match database metadata" ); } - Ok(snapshot) + Ok((snapshot, tip_hash)) }) - .transpose() + .transpose()?; + let Some((snapshot, tip_hash)) = snapshot else { + return Ok(None); + }; + let verified_by_current_version = connection + .query_row( + r#" +SELECT 1 FROM chain_verification +WHERE id = 1 AND tip_hash = ?1 AND verifier_version = ?2 +"#, + params![tip_hash, CURRENT_VERIFIER_VERSION], + |_| Ok(()), + ) + .optional() + .context("failed to inspect chain verification status")? + .is_some(); + Ok(Some(LoadedChainSnapshot { + snapshot, + verified_by_current_version, + })) }) } pub fn save(&self, snapshot: &ChainSnapshot) -> Result<()> { + self.save_with_verification_status(snapshot, false) + } + + /// Persist a snapshot that has already passed consensus validation in this binary. + pub fn save_verified(&self, snapshot: &ChainSnapshot) -> Result<()> { + self.save_with_verification_status(snapshot, true) + } + + fn save_with_verification_status( + &self, + snapshot: &ChainSnapshot, + verified: bool, + ) -> Result<()> { let (height, tip_hash) = snapshot_tip(snapshot).context("cannot persist empty chain")?; let snapshot_blob = encode_compact_snapshot(snapshot).context("failed to encode compact chain snapshot")?; @@ -125,6 +189,25 @@ ON CONFLICT(id) DO UPDATE SET params![height, tip_hash, snapshot_blob, updated_at_ms], ) .context("failed to persist chain snapshot")?; + if verified { + transaction + .execute( + r#" +INSERT INTO chain_verification (id, tip_hash, verifier_version, verified_at_ms) +VALUES (1, ?1, ?2, ?3) +ON CONFLICT(id) DO UPDATE SET + tip_hash = excluded.tip_hash, + verifier_version = excluded.verifier_version, + verified_at_ms = excluded.verified_at_ms +"#, + params![tip_hash, CURRENT_VERIFIER_VERSION, updated_at_ms], + ) + .context("failed to persist chain verification status")?; + } else { + transaction + .execute("DELETE FROM chain_verification", []) + .context("failed to clear chain verification status")?; + } transaction .commit() .context("failed to commit chain persistence transaction")?; @@ -141,6 +224,9 @@ ON CONFLICT(id) DO UPDATE SET .execute("DELETE FROM chain_snapshots", []) .context("failed to delete chain snapshot")?; transaction + .execute("DELETE FROM chain_verification", []) + .context("failed to delete chain verification status")?; + transaction .commit() .context("failed to commit chain reset transaction")?; Ok(()) @@ -469,4 +555,85 @@ VALUES (1, 0, 'bad-tip', ?1, 0) .contains("snapshot tip does not match database metadata") ); } + + #[test] + fn verified_snapshot_is_trusted_only_for_current_version_and_tip() { + let dir = tempdir().unwrap(); + let store = SqliteChainStore::open(dir.path().join("chain.sqlite3")).unwrap(); + let snapshot = test_snapshot("chain-store-verification-status"); + store.save_verified(&snapshot).unwrap(); + + let loaded = store.load_with_verification_status().unwrap().unwrap(); + assert!(loaded.verified_by_current_version); + + store + .with_connection_mut(|connection| { + connection.execute( + "UPDATE chain_verification SET verifier_version = 'previous-version'", + [], + )?; + Ok(()) + }) + .unwrap(); + let loaded = store.load_with_verification_status().unwrap().unwrap(); + assert!(!loaded.verified_by_current_version); + + store + .with_connection_mut(|connection| { + connection.execute( + "UPDATE chain_verification SET verifier_version = ?1, tip_hash = 'other-tip'", + [super::CURRENT_VERIFIER_VERSION], + )?; + Ok(()) + }) + .unwrap(); + let loaded = store.load_with_verification_status().unwrap().unwrap(); + assert!(!loaded.verified_by_current_version); + } + + #[test] + fn ordinary_save_invalidates_previous_verification_status() { + let dir = tempdir().unwrap(); + let store = SqliteChainStore::open(dir.path().join("chain.sqlite3")).unwrap(); + let snapshot = test_snapshot("chain-store-unverified-save"); + store.save_verified(&snapshot).unwrap(); + assert!( + store + .load_with_verification_status() + .unwrap() + .unwrap() + .verified_by_current_version + ); + + store.save(&snapshot).unwrap(); + + assert!(store.contains_chain().unwrap()); + assert!( + !store + .load_with_verification_status() + .unwrap() + .unwrap() + .verified_by_current_version + ); + } + + #[test] + fn opening_database_without_verification_table_migrates_as_untrusted() { + let dir = tempdir().unwrap(); + let path = dir.path().join("chain.sqlite3"); + let store = SqliteChainStore::open(&path).unwrap(); + let snapshot = test_snapshot("chain-store-verification-migration"); + store.save_verified(&snapshot).unwrap(); + drop(store); + Connection::open(&path) + .unwrap() + .execute("DROP TABLE chain_verification", []) + .unwrap(); + + let reopened = SqliteChainStore::open(&path).unwrap(); + let loaded = reopened.load_with_verification_status().unwrap().unwrap(); + + assert_eq!(loaded.snapshot, snapshot); + assert!(!loaded.verified_by_current_version); + } } diff --git a/src/domain/ledger_chain.rs b/src/domain/ledger_chain.rs @@ -113,6 +113,12 @@ impl Ledger { Self::from_snapshot_with_vdf_policy(snapshot, verify_vdf, u64::MAX) } + /// Restore state from a local snapshot whose VDF proofs were already verified by this + /// software version. All other block validation still runs while rebuilding the ledger. + pub fn from_locally_verified_snapshot(snapshot: ChainSnapshot) -> Result<Self> { + Self::from_snapshot_with_vdf_policy(snapshot, false, u64::MAX) + } + pub(crate) fn from_preverified_snapshot(snapshot: ChainSnapshot) -> Result<Self> { Self::from_snapshot_with_vdf_policy(snapshot, false, u64::MAX) } diff --git a/src/main.rs b/src/main.rs @@ -72,7 +72,7 @@ async fn main() -> Result<()> { let ui_data_db_path = ui_data_db_path(&chain_db_path); let chain_store = SqliteChainStore::open(&chain_db_path)?; let ui_data_store = SqliteUiDataStore::open(&ui_data_db_path)?; - let persisted_chain_exists = chain_store.load()?.is_some(); + let persisted_chain_exists = chain_store.contains_chain()?; if opts.chain_mode == ChainMode::Genesis && persisted_chain_exists { bail!( "--genesis refuses to run because chain database already contains a blockchain at {}; start without --genesis to resume it", @@ -493,7 +493,8 @@ async fn initialize_ledger( advertised_p2p_addr: SocketAddr, local_testnet: bool, ) -> Result<InitializedLedger> { - if let Some(snapshot) = chain_store.load()? { + if let Some(loaded) = chain_store.load_with_verification_status()? { + let snapshot = loaded.snapshot; if opts.chain_mode == ChainMode::Genesis { bail!( "--genesis refuses to run because chain database already contains a blockchain at {}; start without --genesis to resume it", @@ -512,7 +513,20 @@ async fn initialize_ledger( }); } let height = snapshot_height(&snapshot); - let ledger = Ledger::from_persisted_snapshot(snapshot).with_context(|| { + let ledger = if loaded.verified_by_current_version { + println!( + "local chain was verified by version {}; skipping VDF reverification", + env!("CARGO_PKG_VERSION") + ); + Ledger::from_locally_verified_snapshot(snapshot) + } else { + println!( + "verifying local chain for version {}...", + env!("CARGO_PKG_VERSION") + ); + Ledger::from_persisted_snapshot(snapshot) + } + .with_context(|| { format!( "failed to load chain database {}", chain_store.path().display() @@ -1139,7 +1153,7 @@ async fn persist_chain_and_project_ui_data( async fn persist_chain_snapshot(store: &SqliteChainStore, snapshot: ChainSnapshot) -> Result<()> { let store = store.clone(); - tokio::task::spawn_blocking(move || store.save(&snapshot)) + tokio::task::spawn_blocking(move || store.save_verified(&snapshot)) .await .context("chain persistence worker failed")??; Ok(())