iuna

iuna

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

commit 2996a16fa32189fadffa800d7f086fd25e228550
parent 55e3cad8573abf1fd863bb1f430abc51d8bcfcc7
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Thu, 27 Aug 2026 11:26:17 +0200

fix(p2p): recover from forked block pages

Diffstat:
Msrc/adapters/p2p/peer_addr.rs | 14+++++++++++++-
Msrc/adapters/p2p/tests.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 86 insertions(+), 1 deletion(-)

diff --git a/src/adapters/p2p/peer_addr.rs b/src/adapters/p2p/peer_addr.rs @@ -148,16 +148,20 @@ pub(super) fn is_possible_fork_error(error: &anyhow::Error) -> bool { message.contains("does not extend local tip") || message.contains("conflicts with local chain") || message.contains("expected block height") + || message.contains("block page has no common ancestor with local chain") } pub(super) fn inbound_error_counts_as_misbehavior(message: &str) -> bool { !message.contains("block timestamp is too far in the future") && !message.contains("block timestamp is before finalizer rank") + && !message.contains("block page has no common ancestor with local chain") } #[cfg(test)] mod tests { - use super::inbound_error_counts_as_misbehavior; + use anyhow::anyhow; + + use super::{inbound_error_counts_as_misbehavior, is_possible_fork_error}; #[test] fn future_and_unopened_rank_slot_errors_are_temporal_not_misbehavior() { @@ -169,4 +173,12 @@ mod tests { )); assert!(inbound_error_counts_as_misbehavior("block hash is invalid")); } + + #[test] + fn missing_block_page_ancestor_triggers_fork_recovery_without_peer_penalty() { + let message = "block batch: block page has no common ancestor with local chain"; + + assert!(is_possible_fork_error(&anyhow!(message))); + assert!(!inbound_error_counts_as_misbehavior(message)); + } } diff --git a/src/adapters/p2p/tests.rs b/src/adapters/p2p/tests.rs @@ -175,6 +175,79 @@ async fn single_block_fork_error_requests_blocks_by_locator() { } #[tokio::test] +async fn block_page_without_local_ancestor_requests_blocks_by_locator() { + let alice = Wallet::from_seed("block-page-fork-alice"); + let allocations = allocations(std::slice::from_ref(&alice), 1_000); + let mut local_node = node("local-block-page-fork", alice.clone(), allocations.clone()); + let mut remote_node = node("remote-block-page-fork", alice.clone(), allocations); + + queue_plaintext_burn(&mut local_node, &alice, 1); + local_node.drain_outbox(); + local_node.mine_one_at(1).unwrap(); + local_node.drain_outbox(); + + for timestamp_ms in [2, 3] { + queue_plaintext_burn(&mut remote_node, &alice, 1); + remote_node.drain_outbox(); + remote_node.mine_one_at(timestamp_ms).unwrap(); + remote_node.drain_outbox(); + } + let remote_page = remote_node.blocks_from(2, 10); + assert_eq!(remote_page.len(), 1); + assert_ne!( + remote_page[0].prev_hash, + local_node.ledger().tip_hash().to_string() + ); + + let network = gossip_network( + Arc::new(tokio::sync::Mutex::new(local_node)), + Arc::new(tokio::sync::Mutex::new(PeerBook::default())), + "127.0.0.1:9545".parse().unwrap(), + None, + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let client = tokio::net::TcpStream::connect(listener.local_addr().unwrap()) + .await + .unwrap(); + let (server, remote_addr) = listener.accept().await.unwrap(); + let (_server_reader, mut server_writer) = server.into_split(); + let (client_reader, _client_writer) = client.into_split(); + let mut client_reader = super::LimitedLineReader::new(client_reader); + let mut known_peer = None; + + super::process_envelope( + &network, + &mut server_writer, + remote_addr, + &mut known_peer, + GossipEnvelope::Blocks { + blocks: remote_page, + }, + ) + .await + .unwrap(); + + let line = tokio::time::timeout(std::time::Duration::from_secs(1), client_reader.read_line()) + .await + .unwrap() + .unwrap() + .unwrap(); + let GossipEnvelope::BlockLocatorRequest { locator, limit } = + super::parse_envelope(&line).unwrap() + else { + panic!("expected block locator request"); + }; + let fork_blocks = remote_node.blocks_after_locator(&locator, limit); + assert_eq!(fork_blocks.len(), 2); + let local_ledger = network.inner.node.lock().await.clone_ledger(); + let adopted = + super::validate_blocks_extension(local_ledger, fork_blocks, crate::app::now_ms(), |_| {}) + .await + .unwrap(); + assert_eq!(adopted.tip_hash(), remote_node.ledger().tip_hash()); +} + +#[tokio::test] async fn future_block_rejection_does_not_poison_peer_or_later_acceptance() { let alice = Wallet::from_seed("future-block-p2p-alice"); let allocations = allocations(std::slice::from_ref(&alice), 1_000);