commit 388dc361137e4e46d39e8ceab2199860852ad24a
parent b6dfdeebb3a318e423a6e2092cef2ae4cb8a250e
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Fri, 14 Aug 2026 14:38:27 +0200
Add adversarial reveal and p2p regression tests
Diffstat:
4 files changed, 125 insertions(+), 2 deletions(-)
diff --git a/src/adapters/p2p/line_codec.rs b/src/adapters/p2p/line_codec.rs
@@ -257,7 +257,10 @@ mod tests {
use tokio::io::AsyncWriteExt;
use crate::{
- adapters::p2p::{MAX_INVENTORY_ITEMS, MAX_OBJECT_REQUESTS, metrics::P2pMetricsCounters},
+ adapters::p2p::{
+ MAX_GOSSIP_LINE_BYTES, MAX_INVENTORY_ITEMS, MAX_OBJECT_REQUESTS,
+ metrics::P2pMetricsCounters,
+ },
app::{BlockInventory, GossipEnvelope},
domain::{BlindedReveal, BlindedTransaction},
};
@@ -398,4 +401,14 @@ mod tests {
Some(line.as_str())
);
}
+
+ #[tokio::test]
+ async fn limited_line_reader_rejects_oversized_partial_frame_without_newline() {
+ let bytes = vec![b'a'; MAX_GOSSIP_LINE_BYTES + 1];
+ let mut reader = LimitedLineReader::new(bytes.as_slice());
+
+ let error = reader.read_line().await.unwrap_err();
+
+ assert!(error.to_string().contains("p2p message exceeds"));
+ }
}
diff --git a/src/adapters/p2p/tests.rs b/src/adapters/p2p/tests.rs
@@ -136,6 +136,58 @@ async fn single_block_fork_error_requests_chain_snapshot() {
}
#[tokio::test]
+async fn chain_snapshot_request_only_writes_snapshot_without_mutating_local_state() {
+ let alice = Wallet::from_seed("snapshot-request-spam-alice");
+ let allocations = allocations(std::slice::from_ref(&alice), 1_000);
+ let mut local_node = node("snapshot-request-spam", 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();
+ let before = local_node.chain_snapshot();
+
+ let peers = Arc::new(tokio::sync::Mutex::new(PeerBook::default()));
+ let network = gossip_network(
+ Arc::new(tokio::sync::Mutex::new(local_node)),
+ Arc::clone(&peers),
+ "127.0.0.1:9544".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::ChainSnapshotRequest,
+ )
+ .await
+ .unwrap();
+
+ let line = tokio::time::timeout(std::time::Duration::from_secs(1), client_reader.read_line())
+ .await
+ .unwrap()
+ .unwrap()
+ .unwrap();
+ assert_eq!(
+ super::parse_envelope(&line).unwrap(),
+ GossipEnvelope::ChainSnapshot(before.clone())
+ );
+ assert_eq!(network.inner.node.lock().await.chain_snapshot(), before);
+ assert!(peers.lock().await.list().is_empty());
+ assert!(known_peer.is_none());
+}
+
+#[tokio::test]
async fn hello_rejects_wrong_network_or_genesis_without_banning() {
let alice = Wallet::from_seed("hello-alice");
let allocations = allocations(std::slice::from_ref(&alice), 1_000);
diff --git a/src/domain/ledger_mempool.rs b/src/domain/ledger_mempool.rs
@@ -53,13 +53,28 @@ impl Ledger {
return Ok(false);
}
self.validate_blinded_reveal_terms(&reveal)?;
- if self.pending_reveals.len() >= MAX_PENDING_TRANSACTIONS {
+ if self.pending_reveals.len() >= MAX_PENDING_TRANSACTIONS
+ && (!self.has_active_blinded_transaction(&reveal.commitment)
+ || !self.drop_one_invalid_pending_blinded_reveal())
+ {
bail!("blinded reveal pool is full");
}
self.pending_reveals.push(reveal);
Ok(true)
}
+ fn drop_one_invalid_pending_blinded_reveal(&mut self) -> bool {
+ let Some(index) = self
+ .pending_reveals
+ .iter()
+ .position(|reveal| self.pending_reveal_transaction(reveal).is_err())
+ else {
+ return false;
+ };
+ self.pending_reveals.remove(index);
+ true
+ }
+
pub fn submit_transaction_with_outcome(
&mut self,
transaction: Transaction,
diff --git a/src/domain/tests.rs b/src/domain/tests.rs
@@ -2264,6 +2264,49 @@ fn expired_blinded_reveal_is_not_selected() {
}
#[test]
+fn active_blinded_reveal_displaces_invalid_reveal_spam_when_pool_is_full() {
+ let alice = Wallet::from_seed("blinded-spam-finalizer-alice");
+ let bob = Wallet::from_seed("blinded-spam-finalizer-bob");
+ let carol = Wallet::from_seed("blinded-spam-carol");
+ let finalizers = [alice.clone(), bob.clone()];
+ let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]);
+ let blinded = ledger
+ .build_blinded_burn(&carol, 3, 7, ledger.height() + 4)
+ .unwrap();
+ ledger
+ .submit_blinded_transaction(blinded.transaction.clone())
+ .unwrap();
+ queue_next_leader_burn(&mut ledger, &finalizers);
+ mine_preverified_as_next_leader(&mut ledger, &finalizers, 1);
+
+ for index in 0..MAX_PENDING_TRANSACTIONS {
+ ledger
+ .submit_blinded_reveal(BlindedReveal {
+ commitment: hex_hash(format!("unknown-blinded-reveal-spam:{index}")),
+ key: "00".repeat(BLINDED_KEY_BYTES),
+ })
+ .unwrap();
+ }
+ assert_eq!(
+ ledger.pending_blinded_reveals().len(),
+ MAX_PENDING_TRANSACTIONS
+ );
+ assert!(ledger.valid_pending_blinded_reveals().is_empty());
+
+ assert!(
+ ledger
+ .submit_blinded_reveal(blinded.reveal.clone())
+ .unwrap()
+ );
+
+ assert_eq!(
+ ledger.pending_blinded_reveals().len(),
+ MAX_PENDING_TRANSACTIONS
+ );
+ assert_eq!(ledger.valid_pending_blinded_reveals(), vec![blinded.reveal]);
+}
+
+#[test]
fn recovery_block_includes_pending_blinded_transactions_when_space_allows() {
let alice = Wallet::from_seed("recovery-blinded-commit-alice");
let bob = Wallet::from_seed("recovery-blinded-commit-bob");