iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 1cd76f829c7fb9d1ef8a8f434c2e565ecc0dfbb7
parent beb30a8860b54178cf665532f43a0d51da20274c
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed, 29 Jul 2026 14:59:00 +0200

Fix network time replay edge cases

Diffstat:
Msrc/adapters/p2p.rs | 56+++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/domain.rs | 4++++
Msrc/main.rs | 40+++++++++++++++++++++++++++++++++++++++-
3 files changed, 98 insertions(+), 2 deletions(-)

diff --git a/src/adapters/p2p.rs b/src/adapters/p2p.rs @@ -2047,7 +2047,7 @@ async fn process_hello( network, known_peer, remote_addr, - &PeerStatus::new(hello.height, hello.tip_hash.clone()), + &PeerStatus::with_time(hello.height, hello.tip_hash.clone(), hello.time_ms), ) .await; if request_snapshot { @@ -3010,6 +3010,60 @@ mod tests { } #[tokio::test] + async fn hello_records_remote_clock_observation() { + let alice = Wallet::from_seed("hello-clock-alice"); + let allocations = allocations(std::slice::from_ref(&alice), 1_000); + let node = Arc::new(tokio::sync::Mutex::new(node("alice", alice, allocations))); + let network = super::GossipNetwork { + inner: Arc::new(super::GossipNetworkInner { + node, + peers: Arc::new(tokio::sync::Mutex::new(PeerBook::default())), + listen_addr: "127.0.0.1:9544".parse().unwrap(), + node_id: super::new_node_id(), + sessions: tokio::sync::Mutex::new(BTreeMap::new()), + tx_delivery: tokio::sync::Mutex::new(BTreeMap::new()), + metrics: super::P2pMetricsCounters::default(), + }), + }; + let remote_time_ms = crate::app::now_ms().saturating_add(60_000); + let hello = ProtocolHello { + protocol_version: PROTOCOL_VERSION, + network_id: NETWORK_ID.to_string(), + genesis_hash: network + .inner + .node + .lock() + .await + .ledger() + .genesis_hash() + .to_string(), + listen_addr: Some("127.0.0.1:9545".to_string()), + node_id: None, + height: 0, + tip_hash: "tip".to_string(), + time_ms: remote_time_ms, + }; + + let mut known_peer = None; + super::process_hello( + &network, + "127.0.0.1:9545".parse().unwrap(), + &mut known_peer, + hello, + ) + .await + .unwrap(); + + let peers = network.inner.peers.lock().await.list(); + let peer = peers + .iter() + .find(|peer| peer.address == "127.0.0.1:9545") + .unwrap(); + assert!(peer.last_clock_offset_ms.unwrap() > 30_000); + assert_eq!(peer.last_clock_offset_accepted, Some(true)); + } + + #[tokio::test] async fn setup_placeholder_accepts_remote_genesis_and_adopts_snapshot() { let local_wallet = Wallet::from_seed("setup-placeholder-local"); let local_ledger = Ledger::new(BTreeMap::new(), 1); diff --git a/src/domain.rs b/src/domain.rs @@ -1232,6 +1232,10 @@ impl Ledger { Self::from_snapshot_at(snapshot, unix_now_ms()) } + pub fn from_persisted_snapshot(snapshot: ChainSnapshot) -> Result<Self> { + Self::from_snapshot_at(snapshot, u64::MAX) + } + pub(crate) fn from_snapshot_at(snapshot: ChainSnapshot, now_ms: u64) -> Result<Self> { Self::from_snapshot_with_vdf_policy(snapshot, true, now_ms) } diff --git a/src/main.rs b/src/main.rs @@ -211,7 +211,7 @@ async fn initialize_ledger( ); } let height = snapshot_height(&snapshot); - let ledger = Ledger::from_snapshot(snapshot).with_context(|| { + let ledger = Ledger::from_persisted_snapshot(snapshot).with_context(|| { format!( "failed to load chain database {}", chain_store.path().display() @@ -1038,6 +1038,44 @@ mod tests { } #[tokio::test] + async fn startup_resumes_persisted_chain_with_network_accepted_future_tip() { + let dir = tempdir().unwrap(); + let chain_path = dir.path().join("chain.sqlite3"); + let store = SqliteChainStore::open(&chain_path).unwrap(); + let persisted_wallet = Wallet::from_seed("persisted-future-chain-owner"); + let mut persisted = ledger_with_one_spendable_iuna(&persisted_wallet); + let burn = persisted.build_burn(&persisted_wallet, 1, 0).unwrap(); + persisted.submit_transaction(burn).unwrap(); + let future_tip_ms = iuna::app::now_ms().saturating_add(VDF_TARGET_BLOCK_MS); + let future_block = persisted + .mine_next_block(&persisted_wallet, future_tip_ms) + .unwrap(); + let mut snapshot = persisted.snapshot(); + snapshot.blocks.push(future_block); + assert!( + Ledger::from_snapshot(snapshot.clone()) + .unwrap_err() + .to_string() + .contains("too far in the future") + ); + store.save(&snapshot).unwrap(); + let fresh_wallet = Wallet::from_seed("fresh-start-wallet"); + let opts = parse(&["--chain-db", chain_path.to_str().unwrap()]) + .unwrap() + .unwrap(); + + let resumed = initialize_ledger(&opts, fresh_wallet.address(), &store) + .await + .unwrap(); + + assert_eq!(resumed.status().height, 1); + assert_eq!( + resumed.status().tip_hash, + snapshot.blocks.last().unwrap().hash + ); + } + + #[tokio::test] async fn persisted_chain_satisfies_join_mode_without_contacting_peer() { let dir = tempdir().unwrap(); let chain_path = dir.path().join("chain.sqlite3");