From 33ff8b17e7e9f6f982e19ef94ebb497ef7de1cd3 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Wed, 2 Sep 2026 18:04:47 -0500 Subject: [PATCH 1/8] smite-scenarios: add executor test Fixture Fixture wraps the executor and its mocked peer connection, mocked bitcoind, and mocked target RPC interface, eliminating lots of boilerplate and simplifying tests. By default the Fixture is also funded with sample_utxo so that tests don't need to manually configure it as a wallet input. --- smite-scenarios/src/executor/tests.rs | 357 ++++-------------- smite-scenarios/src/executor/tests/harness.rs | 101 +++++ 2 files changed, 180 insertions(+), 278 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index b2d1a023..4adcec63 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -12,14 +12,6 @@ use smite::bolt::{AcceptChannelTlvs, GossipTimestampFilter, Init, Ping}; use smite_ir::Instruction; use smite_ir::operation::ShutdownScriptVariant; -/// Decodes a sent message expected to be a `channel_announcement`. -fn decode_sent_channel_announcement(bytes: &[u8]) -> ChannelAnnouncement { - match Message::decode(bytes).expect("valid message") { - Message::ChannelAnnouncement(ca) => ca, - other => panic!("expected channel_announcement(256), got {other}"), - } -} - fn decode_open_channel(bytes: &[u8]) -> OpenChannel { match Message::decode(bytes).expect("valid message") { Message::OpenChannel(oc) => oc, @@ -41,21 +33,13 @@ fn execute_load_build_send() { inputs: vec![20], }); - let program = Program { + let mut fx = Fixture::new(); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); - assert_eq!(executor.conn.sent.len(), 1); - let oc = decode_open_channel(&executor.conn.sent[0]); + assert_eq!(fx.sent_len(), 1); + let oc: OpenChannel = fx.sent(0); assert_eq!(oc.chain_hash, [0xcc; 32]); assert_eq!(oc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); assert_eq!(oc.funding_satoshis, 100_000); @@ -126,24 +110,13 @@ fn execute_build_channel_announcement() { }, ]; - let program = Program { + let mut fx = Fixture::new(); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); - assert_eq!(executor.conn.sent.len(), 1); - let ca = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::ChannelAnnouncement(ca) => ca, - other => panic!("expected channel_announcement(256), got {other}"), - }; + assert_eq!(fx.sent_len(), 1); + let ca: ChannelAnnouncement = fx.sent(0); let secp = Secp256k1::new(); let pk = |b: &[u8; 32]| PublicKey::from_secret_key(&secp, &SecretKey::from_slice(b).unwrap()); @@ -194,24 +167,13 @@ fn execute_build_node_announcement() { }, ]; - let program = Program { + let mut fx = Fixture::new(); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); - assert_eq!(executor.conn.sent.len(), 1); - let na = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::NodeAnnouncement(na) => na, - other => panic!("expected node_announcement(257), got {other}"), - }; + assert_eq!(fx.sent_len(), 1); + let na: NodeAnnouncement = fx.sent(0); let secp = Secp256k1::new(); let expected_node_id = @@ -287,24 +249,13 @@ fn execute_build_channel_update() { }, ]; - let program = Program { + let mut fx = Fixture::new(); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); - assert_eq!(executor.conn.sent.len(), 1); - let cu = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::ChannelUpdate(cu) => cu, - other => panic!("expected channel_update(258), got {other}"), - }; + assert_eq!(fx.sent_len(), 1); + let cu: ChannelUpdate = fx.sent(0); assert_eq!(cu.chain_hash, sample_context().chain_hash); assert_eq!(cu.short_channel_id, scid); @@ -399,24 +350,13 @@ fn execute_build_announcement_signatures() { }, ]; - let program = Program { + let mut fx = Fixture::new(); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); - assert_eq!(executor.conn.sent.len(), 1); - let ann_sigs = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::AnnouncementSignatures(s) => s, - other => panic!("expected announcement_signatures(259), got {other}"), - }; + assert_eq!(fx.sent_len(), 1); + let ann_sigs: AnnouncementSignatures = fx.sent(0); assert_eq!(ann_sigs.channel_id, ChannelId::new(channel_id_bytes)); assert_eq!(ann_sigs.short_channel_id, scid); @@ -493,20 +433,12 @@ fn execute_build_open_channel_with_tlvs() { inputs: vec![20], }); - let program = Program { + let mut fx = Fixture::new(); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); - let oc = decode_open_channel(&executor.conn.sent[0]); + let oc: OpenChannel = fx.sent(0); assert_eq!( oc.tlvs.upfront_shutdown_script, Some(vec![0x00, 0x14, 0xab]) @@ -543,20 +475,12 @@ fn execute_derive_point() { inputs: vec![base + 20], }); - let program = Program { + let mut fx = Fixture::new(); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); - let oc = decode_open_channel(&executor.conn.sent[0]); + let oc: OpenChannel = fx.sent(0); let secp = Secp256k1::new(); let expected = PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&[0x11; 32]).unwrap()); assert_eq!(oc.funding_pubkey, expected); @@ -1055,13 +979,7 @@ fn execute_wrong_input_count_panics() { inputs: vec![], // expects 1 input }], }; - let _ = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ) - .execute(&program, std::time::Instant::now()); + Fixture::new().run(&program); } #[test] @@ -1079,13 +997,7 @@ fn execute_type_mismatch_panics() { }, ], }; - let _ = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ) - .execute(&program, std::time::Instant::now()); + Fixture::new().run(&program); } #[test] @@ -1097,13 +1009,7 @@ fn execute_variable_out_of_bounds_panics() { inputs: vec![99], }], }; - let _ = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ) - .execute(&program, std::time::Instant::now()); + Fixture::new().run(&program); } #[test] @@ -1121,13 +1027,7 @@ fn execute_forward_variable_reference_panics() { }, ], }; - let _ = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ) - .execute(&program, std::time::Instant::now()); + Fixture::new().run(&program); } #[test] @@ -1146,13 +1046,7 @@ fn execute_void_variable_reference_panics() { }, ], }; - let _ = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ) - .execute(&program, std::time::Instant::now()); + Fixture::new().run(&program); } #[test] @@ -1170,13 +1064,7 @@ fn execute_invalid_private_key_panics() { }, ], }; - let _ = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ) - .execute(&program, std::time::Instant::now()); + Fixture::new().run(&program); } #[test] @@ -1197,13 +1085,7 @@ fn execute_send_open_channel_wrong_type_panics() { instructions: instrs, }; - let _ = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ) - .execute(&program, std::time::Instant::now()); + Fixture::new().run(&program); } #[test] @@ -1242,23 +1124,15 @@ fn execute_mine_blocks_invokes_cli() { operation: Operation::MineBlocks(6), inputs: vec![], }]; - let program = Program { + let mut fx = Fixture::new(); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); // Verify that mine_blocks was called with the correct number - assert_eq!(executor.bitcoin_cli.mine_blocks_calls, vec![6]); - assert!(executor.bitcoin_cli.mined_private_mempool.is_empty()); - assert_eq!(executor.rpc.chain_syncs, 1); + assert_eq!(fx.bitcoin().mine_blocks_calls, vec![6]); + assert!(fx.bitcoin().mined_private_mempool.is_empty()); + assert_eq!(fx.rpc().chain_syncs, 1); } #[test] @@ -1277,44 +1151,23 @@ fn execute_mine_blocks_wrong_input() { let program = Program { instructions: instrs, }; - let _ = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ) - .execute(&program, std::time::Instant::now()); + Fixture::new().run(&program); } #[test] fn execute_create_and_broadcast_tx() { - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute( - &Program { - instructions: create_and_broadcast_tx_instructions(), - }, - std::time::Instant::now(), - ) - .expect("tx construction and broadcast should succeed"); + let mut fx = Fixture::new(); + fx.run(&Program { + instructions: create_and_broadcast_tx_instructions(), + }); - assert_eq!(executor.bitcoin_cli.broadcast_calls.len(), 1); - let broadcast_tx = &executor.bitcoin_cli.broadcast_calls[0]; + assert_eq!(fx.bitcoin().broadcast_calls.len(), 1); + let broadcast_tx = &fx.bitcoin().broadcast_calls[0]; assert_eq!( broadcast_tx.compute_txid().to_string(), "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" ); - assert_eq!(executor.rpc.chain_syncs, 0); + assert_eq!(fx.rpc().chain_syncs, 0); } // LookupShortChannelId should combine the confirmed block position with @@ -1322,11 +1175,6 @@ fn execute_create_and_broadcast_tx() { // by feeding it into a channel_announcement and decoding the sent message. #[test] fn execute_lookup_short_channel_id_confirmed() { - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; let mut instrs = create_and_broadcast_tx_instructions(); instrs.push(Instruction { operation: Operation::MineBlocks(6), @@ -1342,34 +1190,21 @@ fn execute_lookup_short_channel_id_confirmed() { // Build and send a channel_announcement carrying the looked-up SCID. instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 9)); - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .expect("lookup after confirmation should succeed"); + let mut fx = Fixture::new(); + fx.run(&Program { + instructions: instrs, + }); - assert_eq!(executor.bitcoin_cli.mine_blocks_calls, vec![6]); + assert_eq!(fx.bitcoin().mine_blocks_calls, vec![6]); // The executor must have queried the mock with the broadcast // transaction's txid. - assert_eq!(executor.bitcoin_cli.block_position_lookups.len(), 1); - let broadcast_txid = executor.bitcoin_cli.broadcast_calls[0].compute_txid(); - assert_eq!( - executor.bitcoin_cli.block_position_lookups[0], - broadcast_txid, - ); + assert_eq!(fx.bitcoin().block_position_lookups.len(), 1); + let broadcast_txid = fx.bitcoin().broadcast_calls[0].compute_txid(); + assert_eq!(fx.bitcoin().block_position_lookups[0], broadcast_txid); // The mock returns block_height=800_042, tx_index=7 for a confirmed // tx, and the funding output is always at vout 0. - let ca = decode_sent_channel_announcement(&executor.conn.sent[0]); + let ca: ChannelAnnouncement = fx.sent(0); assert_eq!(ca.short_channel_id, ShortChannelId::new(800_042, 7, 0)); } @@ -1379,11 +1214,6 @@ fn execute_lookup_short_channel_id_confirmed() { // via the SCID carried in a channel_announcement. #[test] fn execute_lookup_short_channel_id_unconfirmed_returns_sentinel() { - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; // No BroadcastTransaction and no MineBlocks: the mock reports zero // confirmations and get_transaction_block_position returns None. let mut instrs = vec![ @@ -1423,26 +1253,17 @@ fn execute_lookup_short_channel_id_unconfirmed_returns_sentinel() { ]; instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 7)); - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .expect("lookup on unconfirmed tx should not fail"); + let mut fx = Fixture::new(); + fx.run(&Program { + instructions: instrs, + }); + // The mock was queried but returned None (zero confirmations), so the // executor took the sentinel path without panicking. - assert!(executor.bitcoin_cli.mine_blocks_calls.is_empty()); - assert_eq!(executor.bitcoin_cli.block_position_lookups.len(), 1); + assert!(fx.bitcoin().mine_blocks_calls.is_empty()); + assert_eq!(fx.bitcoin().block_position_lookups.len(), 1); - let ca = decode_sent_channel_announcement(&executor.conn.sent[0]); + let ca: ChannelAnnouncement = fx.sent(0); assert_eq!(ca.short_channel_id, ShortChannelId::new(0, 0, 0)); } @@ -2119,21 +1940,11 @@ fn execute_send_shutdown() { ], }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + let mut fx = Fixture::new(); + fx.run(&program); - assert_eq!(executor.conn.sent.len(), 1); - let sd = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::Shutdown(sd) => sd, - other => panic!("expected shutdown(38), got {other}"), - }; + assert_eq!(fx.sent_len(), 1); + let sd: Shutdown = fx.sent(0); assert_eq!(sd.channel_id, channel_id); assert_eq!(sd.scriptpubkey, script.encode()); } @@ -2160,21 +1971,11 @@ fn execute_send_shutdown_empty_scriptpubkey() { ], }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + let mut fx = Fixture::new(); + fx.run(&program); - assert_eq!(executor.conn.sent.len(), 1); - let sd = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::Shutdown(sd) => sd, - other => panic!("expected shutdown(38), got {other}"), - }; + assert_eq!(fx.sent_len(), 1); + let sd: Shutdown = fx.sent(0); assert_eq!(sd.channel_id, channel_id); assert!(sd.scriptpubkey.is_empty()); } diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index 5c6bdccb..52a8fbfd 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -124,6 +124,107 @@ impl TargetRpc for MockTargetRpc { } } +// -- Fixture -- + +/// An [`Executor`] wired to a mock peer and a mock bitcoind. +pub struct Fixture { + executor: Executor, +} + +impl Fixture { + /// A fixture with a silent peer and a wallet holding [`sample_utxo`]. + pub fn new() -> Self { + let bitcoin_cli = MockBitcoinCli { + utxos: vec![sample_utxo()], + change_spk: sample_change_spk(), + ..Default::default() + }; + Self { + executor: Executor::new( + MockConnection::new(), + bitcoin_cli, + MockTargetRpc::default(), + sample_context(), + ), + } + } + + /// Runs `program` against the target, panicking if execution fails. + pub fn run(&mut self, program: &Program) { + self.executor + .execute(program, std::time::Instant::now()) + .expect("program execution successful"); + } + + /// Returns the mock bitcoind the executor drives. + pub fn bitcoin(&self) -> &MockBitcoinCli { + &self.executor.bitcoin_cli + } + + /// Returns the mock RPC interface to the target. + pub fn rpc(&self) -> &MockTargetRpc { + &self.executor.rpc + } + + /// Returns the number of messages the executor sent. + pub fn sent_len(&self) -> usize { + self.executor.conn.sent.len() + } + + /// Decodes the `n`th message the executor sent, panicking if it is not an + /// `M`. + pub fn sent(&self, n: usize) -> M { + let bytes = self.executor.conn.sent.get(n).unwrap_or_else(|| { + panic!( + "expected at least {} sent messages, got {}", + n + 1, + self.sent_len() + ) + }); + let msg = Message::decode(bytes).expect("valid message"); + let got = msg.to_string(); + M::from_message(msg).unwrap_or_else(|| panic!("expected {}, got {got}", M::TYPE)) + } +} + +/// Extracts a specific BOLT message from a decoded [`Message`]. +pub trait FromMessage: Sized { + /// Wire type of the expected BOLT message. + const TYPE: MessageType; + + /// Returns the extracted BOLT message if `msg`'s type matches, `None` + /// otherwise. + fn from_message(msg: Message) -> Option; +} + +/// Implements [`FromMessage`] for BOLT messages whose [`Message`] variant has +/// the same name. +macro_rules! impl_from_message { + ($($bolt_msg:ident => $msg_type:ident,)*) => { + $( + impl FromMessage for $bolt_msg { + const TYPE: MessageType = MessageType::$msg_type; + + fn from_message(msg: Message) -> Option { + match msg { + Message::$bolt_msg(bolt_msg) => Some(bolt_msg), + _ => None, + } + } + } + )* + }; +} + +impl_from_message! { + OpenChannel => OPEN_CHANNEL, + Shutdown => SHUTDOWN, + ChannelAnnouncement => CHANNEL_ANNOUNCEMENT, + NodeAnnouncement => NODE_ANNOUNCEMENT, + ChannelUpdate => CHANNEL_UPDATE, + AnnouncementSignatures => ANNOUNCEMENT_SIGNATURES, +} + // -- Helpers -- pub fn sample_pubkey(byte: u8) -> PublicKey { From d3f09250c3f55e0aff0d7a66ef137de8ffa36869 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Thu, 3 Sep 2026 08:44:38 -0500 Subject: [PATCH 2/8] smite-scenarios: run recv and negotiation tests through Fixture Adds additional Fixture methods so that tests that receive messages or set/get negotiations can use Fixture: - queue: queues messages on the mock connection for the executor to receive - run_err: runs the program expecting an error - with_negotiation: adds an entry to the executor's negotiation map - negotiation: gets an entry from the executor's negotiation map --- smite-scenarios/src/executor/tests.rs | 441 +++++------------- smite-scenarios/src/executor/tests/harness.rs | 30 ++ 2 files changed, 134 insertions(+), 337 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 4adcec63..8bc072ca 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -12,12 +12,6 @@ use smite::bolt::{AcceptChannelTlvs, GossipTimestampFilter, Init, Ping}; use smite_ir::Instruction; use smite_ir::operation::ShutdownScriptVariant; -fn decode_open_channel(bytes: &[u8]) -> OpenChannel { - match Message::decode(bytes).expect("valid message") { - Message::OpenChannel(oc) => oc, - other => panic!("expected open_channel(32), got {other}"), - } -} // -- execute() tests -- #[test] @@ -488,9 +482,6 @@ fn execute_derive_point() { #[test] fn execute_recv_and_extract_all_fields() { - let ac = sample_accept_channel(); - let ac_bytes = Message::AcceptChannel(ac).encode(); - // Receive accept_channel (v0), then extract all 16 fields (v1..v16). let fields = [ AcceptChannelField::TemporaryChannelId, @@ -529,25 +520,15 @@ fn execute_recv_and_extract_all_fields() { // rebuild a message from the extracted fields and verify it matches the // original. - let program = Program { - instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(ac_bytes); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + Fixture::new() + .queue(&Message::AcceptChannel(sample_accept_channel())) + .run(&Program { + instructions: instrs, + }); } #[test] fn execute_recv_unexpected_message() { - let init_bytes = Message::Init(Init::empty()).encode(); - let mut instrs = send_open_channel_instructions(); let sent_open_channel = instrs.len() - 1; instrs.push(Instruction { @@ -555,19 +536,11 @@ fn execute_recv_unexpected_message() { inputs: vec![sent_open_channel], }); - let program = Program { - instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(init_bytes); - let err = executor - .execute(&program, std::time::Instant::now()) - .unwrap_err(); + let err = Fixture::new() + .queue(&Message::Init(Init::empty())) + .run_err(&Program { + instructions: instrs, + }); assert!(matches!( err, ExecuteError::UnexpectedMessage { @@ -580,7 +553,6 @@ fn execute_recv_unexpected_message() { #[test] fn execute_recv_peer_error() { let peer_error = smite::bolt::Error::all_channels("Wrong channel id in channel_ready"); - let error_bytes = Message::Error(peer_error.clone()).encode(); let mut instrs = send_open_channel_instructions(); let sent_open_channel = instrs.len() - 1; @@ -589,19 +561,11 @@ fn execute_recv_peer_error() { inputs: vec![sent_open_channel], }); - let program = Program { - instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(error_bytes); - let err = executor - .execute(&program, std::time::Instant::now()) - .unwrap_err(); + let err = Fixture::new() + .queue(&Message::Error(peer_error.clone())) + .run_err(&Program { + instructions: instrs, + }); assert!(matches!(err, ExecuteError::PeerError(e) if e == peer_error)); } @@ -612,8 +576,6 @@ fn execute_recv_auto_pong() { num_pong_bytes: 4, ignored: vec![0xaa], }; - let ping_bytes = Message::Ping(ping).encode(); - let ac_bytes = Message::AcceptChannel(sample_accept_channel()).encode(); let mut instrs = send_open_channel_instructions(); let sent_open_channel = instrs.len() - 1; @@ -622,43 +584,23 @@ fn execute_recv_auto_pong() { inputs: vec![sent_open_channel], }); - let program = Program { + let mut fx = Fixture::new() + .queue(&Message::Ping(ping)) + .queue(&Message::AcceptChannel(sample_accept_channel())); + fx.run(&Program { instructions: instrs, - }; - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(ping_bytes); - executor.conn.queue_recv(ac_bytes); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + }); // Verify exactly two messages were sent: `open_channel` and `pong`. - assert_eq!(executor.conn.sent.len(), 2); - - // Verify the first message was `open_channel`. - let oc = Message::decode(&executor.conn.sent[0]).unwrap(); - let Message::OpenChannel(_) = oc else { - panic!("expected open_channel(32), got {oc}"); - }; - - // Verify the second message was the pong. - let pong = Message::decode(&executor.conn.sent[1]).unwrap(); - let Message::Pong(pong) = pong else { - panic!("expected pong(19), got {pong}"); - }; + assert_eq!(fx.sent_len(), 2); + fx.sent::(0); + let pong: Pong = fx.sent(1); assert_eq!(pong.ignored.len(), 4); } #[test] fn execute_recv_skips_gossip() { let gossip = GossipTimestampFilter::new([0u8; 32], 0, 86400); - let gossip_bytes = Message::GossipTimestampFilter(gossip).encode(); - let ac_bytes = Message::AcceptChannel(sample_accept_channel()).encode(); let mut instrs = send_open_channel_instructions(); let sent_open_channel = instrs.len() - 1; @@ -666,28 +608,17 @@ fn execute_recv_skips_gossip() { operation: Operation::RecvAcceptChannel, inputs: vec![sent_open_channel], }); - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(gossip_bytes); - executor.conn.queue_recv(ac_bytes); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new() + .queue(&Message::GossipTimestampFilter(gossip)) + .queue(&Message::AcceptChannel(sample_accept_channel())); + fx.run(&Program { + instructions: instrs, + }); - let accept_channel = executor - .negotiations - .values() - .next() - .and_then(|pending| pending.accept_channel.as_ref()) + let accept_channel = fx + .negotiation(&TemporaryChannelId::new([0xbb; 32])) + .accept_channel + .as_ref() .expect("accept_channel recorded"); assert_eq!(accept_channel.clone(), sample_accept_channel()); } @@ -695,7 +626,6 @@ fn execute_recv_skips_gossip() { #[test] fn execute_records_negotiation_for_open_and_accept() { let temporary_channel_id = TemporaryChannelId::new([0xbb; 32]); - let ac_bytes = Message::AcceptChannel(sample_accept_channel()).encode(); let mut instrs = send_open_channel_instructions(); let sent_open_channel = instrs.len() - 1; @@ -703,23 +633,12 @@ fn execute_records_negotiation_for_open_and_accept() { operation: Operation::RecvAcceptChannel, inputs: vec![sent_open_channel], }); - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(ac_bytes); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new().queue(&Message::AcceptChannel(sample_accept_channel())); + fx.run(&Program { + instructions: instrs, + }); - let pending = executor.negotiations.get(&temporary_channel_id).unwrap(); + let pending = fx.negotiation(&temporary_channel_id); assert_eq!( pending.open_channel.temporary_channel_id, temporary_channel_id @@ -732,11 +651,6 @@ fn execute_records_negotiation_for_open_and_accept() { #[test] fn execute_recv_accept_channel_unknown_channel() { let unknown_id = TemporaryChannelId::new([0xcc; 32]); - let ac_bytes = Message::AcceptChannel(AcceptChannel { - temporary_channel_id: unknown_id, - ..sample_accept_channel() - }) - .encode(); let mut instrs = send_open_channel_instructions(); let sent_open_channel = instrs.len() - 1; @@ -744,21 +658,14 @@ fn execute_recv_accept_channel_unknown_channel() { operation: Operation::RecvAcceptChannel, inputs: vec![sent_open_channel], }); - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(ac_bytes); - let err = executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap_err(); + let err = Fixture::new() + .queue(&Message::AcceptChannel(AcceptChannel { + temporary_channel_id: unknown_id, + ..sample_accept_channel() + })) + .run_err(&Program { + instructions: instrs, + }); let ExecuteError::Violation(Violation::InvalidAcceptChannel(id, reason)) = &err else { panic!("unexpected error: {err:?}"); @@ -774,7 +681,6 @@ fn execute_recv_accept_channel_unknown_channel() { #[test] fn execute_recv_accept_channel_opener_cannot_afford_fee() { let temporary_channel_id = TemporaryChannelId::new([0xbb; 32]); - let ac_bytes = Message::AcceptChannel(sample_accept_channel()).encode(); // Set `push_msat` so the opener cannot afford the commitment fee // requiring the peer to reject the `open_channel` per BOLT 2. @@ -789,21 +695,11 @@ fn execute_recv_accept_channel_opener_cannot_afford_fee() { inputs: vec![sent_open_channel], }); - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(ac_bytes); - let err = executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap_err(); + let err = Fixture::new() + .queue(&Message::AcceptChannel(sample_accept_channel())) + .run_err(&Program { + instructions: instrs, + }); let ExecuteError::Violation(Violation::InvalidAcceptChannel(id, reason)) = &err else { panic!("unexpected error: {err:?}"); @@ -819,7 +715,6 @@ fn execute_recv_accept_channel_opener_cannot_afford_fee() { #[test] fn execute_recv_accept_channel_rejects_reuse_before_funding() { let temporary_channel_id = TemporaryChannelId::new([0xbb; 32]); - let ac_bytes = Message::AcceptChannel(sample_accept_channel()).encode(); let mut instrs = send_open_channel_instructions(); let built_open_channel = instrs.len() - 2; @@ -838,22 +733,12 @@ fn execute_recv_accept_channel_rejects_reuse_before_funding() { inputs: vec![resent_open_channel], }); - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(ac_bytes.clone()); - executor.conn.queue_recv(ac_bytes.clone()); - let err = executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap_err(); + let err = Fixture::new() + .queue(&Message::AcceptChannel(sample_accept_channel())) + .queue(&Message::AcceptChannel(sample_accept_channel())) + .run_err(&Program { + instructions: instrs, + }); let ExecuteError::Violation(Violation::InvalidAcceptChannel(id, reason)) = &err else { panic!("unexpected error: {err:?}"); @@ -891,44 +776,23 @@ fn execute_records_only_first_open_channel_for_duplicate_id_before_funding() { inputs: vec![built], }); - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new(); + fx.run(&Program { + instructions: instrs, + }); // Both open_channel messages went out on the wire, but only the first // negotiation is recorded for the shared id. - assert_eq!(executor.conn.sent.len(), 2); - assert_eq!( - decode_open_channel(&executor.conn.sent[0]).funding_satoshis, - 100_000 - ); - assert_eq!( - decode_open_channel(&executor.conn.sent[1]).funding_satoshis, - 200_000 - ); - let pending = executor.negotiations.get(&temporary_channel_id).unwrap(); + assert_eq!(fx.sent_len(), 2); + assert_eq!(fx.sent::(0).funding_satoshis, 100_000); + assert_eq!(fx.sent::(1).funding_satoshis, 200_000); + let pending = fx.negotiation(&temporary_channel_id); assert_eq!(pending.open_channel.funding_satoshis, 100_000); } #[test] fn execute_records_open_channel_for_duplicate_id_after_funding() { let temporary_channel_id = TemporaryChannelId::new([0xbb; 32]); - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; // Negotiated open_channel: funding_satoshis = 10_000_000. // Second open_channel: same temporary_channel_id, funding_satoshis = 100_000. @@ -944,25 +808,12 @@ fn execute_records_open_channel_for_duplicate_id_after_funding() { instrs.push(instr); } - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .negotiations - .insert(temporary_channel_id, sample_funding_negotiation()); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new().with_negotiation(sample_funding_negotiation()); + fx.run(&Program { + instructions: instrs, + }); - let pending = executor.negotiations.get(&temporary_channel_id).unwrap(); + let pending = fx.negotiation(&temporary_channel_id); assert_eq!(pending.open_channel.funding_satoshis, 100_000); assert!(pending.accept_channel.is_none()); assert!(!pending.funding_built); @@ -1103,18 +954,11 @@ fn execute_affine_overuse_panics() { inputs: vec![sent_open_channel], }, ]); - let program = Program { - instructions: instrs, - }; - let ac_bytes = Message::AcceptChannel(sample_accept_channel()).encode(); - let mut executor = Executor::new( - MockConnection::new(), - MockBitcoinCli::default(), - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(ac_bytes); - let _ = executor.execute(&program, std::time::Instant::now()); + Fixture::new() + .queue(&Message::AcceptChannel(sample_accept_channel())) + .run(&Program { + instructions: instrs, + }); } // MineBlocks should track calls to mine_blocks @@ -1579,28 +1423,11 @@ fn execute_send_funding_created_push_exceeds_funding() { // commitment construction error. let mut negotiation = sample_funding_negotiation(); negotiation.open_channel.push_msat = 20_000_000_000; - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .negotiations - .insert(TemporaryChannelId::new([0xbb; 32]), negotiation); - let err = executor - .execute( - &Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }, - std::time::Instant::now(), - ) - .unwrap_err(); + let err = Fixture::new() + .with_negotiation(negotiation) + .run_err(&Program { + instructions: send_funding_created_and_recv_funding_signed_instructions(), + }); assert!(matches!( err, ExecuteError::Commitment(smite::channel_tx::CommitmentError::PushExceedsFunding) @@ -1613,28 +1440,11 @@ fn execute_send_funding_created_funding_msat_overflow() { // millisatoshis. let mut negotiation = sample_funding_negotiation(); negotiation.open_channel.funding_satoshis = u64::MAX; - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .negotiations - .insert(TemporaryChannelId::new([0xbb; 32]), negotiation); - let err = executor - .execute( - &Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }, - std::time::Instant::now(), - ) - .unwrap_err(); + let err = Fixture::new() + .with_negotiation(negotiation) + .run_err(&Program { + instructions: send_funding_created_and_recv_funding_signed_instructions(), + }); assert!(matches!( err, ExecuteError::Commitment(smite::channel_tx::CommitmentError::FundingMsatOverflow) @@ -1732,41 +1542,19 @@ fn execute_send_funding_created_no_accept_channel() { #[test] fn execute_recv_funding_signed_unknown_channel() { - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - let channel_id = ChannelId::new([0xbb; 32]); // The expected signature here was computed using LDK as the source of // truth. - let fs_bytes = Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - }) - .encode(); - - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(fs_bytes); - executor.negotiations.insert( - TemporaryChannelId::new([0xbb; 32]), - sample_funding_negotiation(), - ); - let err = executor - .execute( - &Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }, - std::time::Instant::now(), - ) - .unwrap_err(); + let err = Fixture::new() + .with_negotiation(sample_funding_negotiation()) + .queue(&Message::FundingSigned(FundingSigned { + channel_id, + signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + })) + .run_err(&Program { + instructions: send_funding_created_and_recv_funding_signed_instructions(), + }); assert!(matches!( err, ExecuteError::Violation(Violation::UnknownChannel(id)) if id == channel_id @@ -1775,43 +1563,22 @@ fn execute_recv_funding_signed_unknown_channel() { #[test] fn execute_recv_funding_signed_invalid_signature() { - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" .parse() .unwrap(), vout: 0, }); - let fs_bytes = Message::FundingSigned(FundingSigned { - channel_id, - signature: Signature::from_compact(&[0u8; 64]).expect("zero bytes parse as a signature"), - }) - .encode(); - - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(fs_bytes); - executor.negotiations.insert( - TemporaryChannelId::new([0xbb; 32]), - sample_funding_negotiation(), - ); - let err = executor - .execute( - &Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }, - std::time::Instant::now(), - ) - .unwrap_err(); + let err = Fixture::new() + .with_negotiation(sample_funding_negotiation()) + .queue(&Message::FundingSigned(FundingSigned { + channel_id, + signature: Signature::from_compact(&[0u8; 64]) + .expect("zero bytes parse as a signature"), + })) + .run_err(&Program { + instructions: send_funding_created_and_recv_funding_signed_instructions(), + }); assert!(matches!( err, ExecuteError::Violation(Violation::InvalidCounterpartySignature(id)) if id == channel_id diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index 52a8fbfd..21e472ae 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -149,6 +149,20 @@ impl Fixture { } } + /// Records `pending` as the negotiation for its `temporary_channel_id`. + pub fn with_negotiation(mut self, pending: PendingChannel) -> Self { + self.executor + .negotiations + .insert(pending.open_channel.temporary_channel_id, pending); + self + } + + /// Queues `msg` as the peer's next reply. + pub fn queue(mut self, msg: &Message) -> Self { + self.executor.conn.recv_queue.push_back(msg.encode()); + self + } + /// Runs `program` against the target, panicking if execution fails. pub fn run(&mut self, program: &Program) { self.executor @@ -156,6 +170,21 @@ impl Fixture { .expect("program execution successful"); } + /// Runs `program` against the target, returning the error it fails with. + pub fn run_err(&mut self, program: &Program) -> ExecuteError { + self.executor + .execute(program, std::time::Instant::now()) + .expect_err("program execution failure") + } + + /// Returns the negotiation recorded for `id`. + pub fn negotiation(&self, id: &TemporaryChannelId) -> &PendingChannel { + self.executor + .negotiations + .get(id) + .expect("negotiation recorded") + } + /// Returns the mock bitcoind the executor drives. pub fn bitcoin(&self) -> &MockBitcoinCli { &self.executor.bitcoin_cli @@ -217,6 +246,7 @@ macro_rules! impl_from_message { } impl_from_message! { + Pong => PONG, OpenChannel => OPEN_CHANNEL, Shutdown => SHUTDOWN, ChannelAnnouncement => CHANNEL_ANNOUNCEMENT, From 25f9850328390a5428c64d37e462ab8e69ee003c Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Thu, 3 Sep 2026 12:16:36 -0500 Subject: [PATCH 3/8] smite-scenarios: run funding and channel_ready tests through Fixture Adds additional Fixture methods so that tests for the funding/channel_ready flows can use Fixture: - with_utxos: sets custom UTXOs for the bitcoind wallet - queued_len: gets the number of queued and unreceived peer messages - channel_state: gets a channel state from the executor's map - channel_states: gets the executor's entire channel state map - private_mempool: gets the executor's privat mempool --- smite-scenarios/src/executor/tests.rs | 476 +++++------------- smite-scenarios/src/executor/tests/harness.rs | 35 +- 2 files changed, 166 insertions(+), 345 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 8bc072ca..e95eeffb 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -1113,12 +1113,6 @@ fn execute_lookup_short_channel_id_unconfirmed_returns_sentinel() { #[test] fn execute_broadcast_dedupes_rejected_tx_in_private_mempool() { - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - // Fund with a dust amount so the built funding tx carries a below-dust // output. let mut instrs = create_and_broadcast_tx_instructions(); @@ -1136,34 +1130,20 @@ fn execute_broadcast_dedupes_rejected_tx_in_private_mempool() { inputs: vec![], }); - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new(); + fx.run(&Program { + instructions: instrs, + }); - assert_eq!(executor.bitcoin_cli.broadcast_calls.len(), 2); + assert_eq!(fx.bitcoin().broadcast_calls.len(), 2); assert_eq!( - executor.bitcoin_cli.broadcast_calls[0].compute_txid(), - executor.bitcoin_cli.broadcast_calls[1].compute_txid(), + fx.bitcoin().broadcast_calls[0].compute_txid(), + fx.bitcoin().broadcast_calls[1].compute_txid(), ); - let rejected_hex = - bitcoin::consensus::encode::serialize_hex(&executor.bitcoin_cli.broadcast_calls[0]); - assert!(executor.private_mempool.is_empty()); - assert_eq!( - executor.bitcoin_cli.mined_private_mempool, - vec![rejected_hex] - ); + let rejected_hex = bitcoin::consensus::encode::serialize_hex(&fx.bitcoin().broadcast_calls[0]); + assert!(fx.private_mempool().is_empty()); + assert_eq!(fx.bitcoin().mined_private_mempool, vec![rejected_hex]); } #[test] @@ -1173,24 +1153,11 @@ fn execute_create_funding_transaction_insufficient_funds() { amount: Amount::from_sat(1_000), ..sample_utxo() }; - let mock_cli = MockBitcoinCli { - utxos: vec![small_utxo], - change_spk: sample_change_spk(), - ..Default::default() - }; - let err = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ) - .execute( - &Program { + let err = Fixture::new() + .with_utxos(vec![small_utxo]) + .run_err(&Program { instructions: create_and_broadcast_tx_instructions(), - }, - std::time::Instant::now(), - ) - .unwrap_err(); + }); let ExecuteError::InsufficientFunds(funds_err) = err else { panic!("expected InsufficientFunds, got {err:?}"); }; @@ -1200,12 +1167,6 @@ fn execute_create_funding_transaction_insufficient_funds() { #[test] fn execute_send_funding_created_and_recv_funding_signed() { - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - // The acceptor replies with funding_signed carrying its signature over // the opener's commitment. let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { @@ -1217,37 +1178,18 @@ fn execute_send_funding_created_and_recv_funding_signed() { // The expected signature here was computed using LDK as the source of // truth. - let fs_bytes = Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - }) - .encode(); - - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(fs_bytes); - executor.negotiations.insert( - TemporaryChannelId::new([0xbb; 32]), - sample_funding_negotiation(), - ); - executor - .execute( - &Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new() + .with_negotiation(sample_funding_negotiation()) + .queue(&Message::FundingSigned(FundingSigned { + channel_id, + signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + })); + fx.run(&Program { + instructions: send_funding_created_and_recv_funding_signed_instructions(), + }); - assert_eq!(executor.conn.sent.len(), 1); - let fc = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::FundingCreated(fc) => fc, - other => panic!("expected funding_created(34), got {other}"), - }; + assert_eq!(fx.sent_len(), 1); + let fc: FundingCreated = fx.sent(0); assert_eq!(fc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); assert_eq!( @@ -1257,7 +1199,7 @@ fn execute_send_funding_created_and_recv_funding_signed() { assert_eq!(fc.funding_output_index, 0); // Verify the signature sent by the opener on the acceptor side. - let state = executor.channel_states.get(&channel_id).unwrap(); + let state = fx.channel_state(&channel_id); let holder = HolderIdentity { side: Side::Acceptor, funding_privkey: SecretKey::from_str( @@ -1272,22 +1214,13 @@ fn execute_send_funding_created_and_recv_funding_signed() { .verify_counterparty_signature(&state.commitment, &holder, &fc.signature) ); - let pending = executor - .negotiations - .get(&TemporaryChannelId::new([0xbb; 32])) - .unwrap(); + let pending = fx.negotiation(&TemporaryChannelId::new([0xbb; 32])); assert!(pending.funding_built); - assert_eq!(executor.rpc.chain_syncs, 0); + assert_eq!(fx.rpc().chain_syncs, 0); } #[test] fn execute_send_funding_created_uses_wire_funding_pubkey() { - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" .parse() @@ -1295,42 +1228,25 @@ fn execute_send_funding_created_uses_wire_funding_pubkey() { vout: 0, }); - // The same acceptor signature as the happy path (computed using LDK as - // the source of truth): computed over the the commitment implied by the - // negotiated funding pubkeys. - let fs_bytes = Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - }) - .encode(); - // Swap out the SendFundingCreated privkey. This should not affect the // constructed channel config, which uses the negotiated pubkeys. It // should only change the signature sent to the target. let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs[9].inputs[1] = 2; - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(fs_bytes); - executor.negotiations.insert( - TemporaryChannelId::new([0xbb; 32]), - sample_funding_negotiation(), - ); - // The acceptor's funding_signed still verifies, because the config is + // The same acceptor signature as the happy path (computed using LDK as + // the source of truth): computed over the the commitment implied by the + // negotiated funding pubkeys. It still verifies, because the config is // built from the wire pubkeys rather than from the swapped privkey. - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new() + .with_negotiation(sample_funding_negotiation()) + .queue(&Message::FundingSigned(FundingSigned { + channel_id, + signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + })); + fx.run(&Program { + instructions: instrs, + }); let secp = Secp256k1::new(); let opener_pk = PublicKey::from_secret_key( @@ -1339,7 +1255,7 @@ fn execute_send_funding_created_uses_wire_funding_pubkey() { .unwrap(), ); // The funding pubkey matches what was negotiated. - let state = executor.channel_states.get(&channel_id).unwrap(); + let state = fx.channel_state(&channel_id); assert_eq!(state.config.opener.funding_pubkey, opener_pk); // But the swapped privkey used for signing is the acceptor's, which // does not match what was negotiated. @@ -1364,11 +1280,6 @@ fn execute_send_funding_created_after_funding_built_does_not_track_channel() { }, ..sample_utxo() }; - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo(), second_utxo], - change_spk: sample_change_spk(), - ..Default::default() - }; // Channel id derived from the first funding transaction's outpoint. let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { @@ -1392,29 +1303,17 @@ fn execute_send_funding_created_after_funding_built_does_not_track_channel() { }, ]); - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor.negotiations.insert( - TemporaryChannelId::new([0xbb; 32]), - sample_funding_negotiation(), - ); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new() + .with_utxos(vec![sample_utxo(), second_utxo]) + .with_negotiation(sample_funding_negotiation()); + fx.run(&Program { + instructions: instrs, + }); // The message still goes out, only the state tracking is suppressed. - assert_eq!(executor.conn.sent.len(), 2); - assert_eq!(executor.channel_states.len(), 1); - assert!(executor.channel_states.contains_key(&channel_id)); + assert_eq!(fx.sent_len(), 2); + assert_eq!(fx.channel_states().len(), 1); + assert!(fx.channel_states().contains_key(&channel_id)); } #[test] @@ -1456,33 +1355,15 @@ fn execute_send_funding_created_no_open_channel() { // No negotiation exists for this temporary_channel_id, so we get a // `funding_created` with an all-zero signature and no recorded channel // state. - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new(); + fx.run(&Program { + instructions: instrs, + }); - let fc = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::FundingCreated(fc) => fc, - other => panic!("expected funding_created(34), got {other}"), - }; + let fc: FundingCreated = fx.sent(0); assert_eq!(fc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); assert_eq!( fc.funding_txid.to_string(), @@ -1490,7 +1371,7 @@ fn execute_send_funding_created_no_open_channel() { ); assert_eq!(fc.funding_output_index, 0); assert_eq!(fc.signature, Signature::from_compact(&[0u8; 64]).unwrap()); - assert!(executor.channel_states.is_empty()); + assert!(fx.channel_states().is_empty()); } #[test] @@ -1500,36 +1381,15 @@ fn execute_send_funding_created_no_accept_channel() { // state. let mut negotiation = sample_funding_negotiation(); negotiation.accept_channel = None; - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor - .negotiations - .insert(TemporaryChannelId::new([0xbb; 32]), negotiation); - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + let mut fx = Fixture::new().with_negotiation(negotiation); + fx.run(&Program { + instructions: instrs, + }); - let fc = match Message::decode(&executor.conn.sent[0]).expect("valid message") { - Message::FundingCreated(fc) => fc, - other => panic!("expected funding_created(34), got {other}"), - }; + let fc: FundingCreated = fx.sent(0); assert_eq!(fc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); assert_eq!( fc.funding_txid.to_string(), @@ -1537,7 +1397,7 @@ fn execute_send_funding_created_no_accept_channel() { ); assert_eq!(fc.funding_output_index, 0); assert_eq!(fc.signature, Signature::from_compact(&[0u8; 64]).unwrap()); - assert!(executor.channel_states.is_empty()); + assert!(fx.channel_states().is_empty()); } #[test] @@ -1594,12 +1454,6 @@ fn execute_send_channel_ready() { vout: 0, }); let alias = ShortChannelId::new(538_532, 845, 1); - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.extend([ Instruction { @@ -1620,43 +1474,26 @@ fn execute_send_channel_ready() { }, ]); - let program = Program { - instructions: instrs, - }; - // We also need to send this `funding_signed`, since the instructions reused // by this test expect one to be present in the executor's receive queue. // The expected signature here was computed using LDK as the source of // truth. - let fs_bytes = Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - }) - .encode(); - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(fs_bytes); - executor.negotiations.insert( - TemporaryChannelId::new([0xbb; 32]), - sample_funding_negotiation(), - ); - executor - .execute(&program, std::time::Instant::now()) - .unwrap(); + let mut fx = Fixture::new() + .with_negotiation(sample_funding_negotiation()) + .queue(&Message::FundingSigned(FundingSigned { + channel_id, + signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + })); + fx.run(&Program { + instructions: instrs, + }); // The instructions send 1 `funding_created` and 2 `channel_ready` messages. - assert_eq!(executor.conn.sent.len(), 3); + assert_eq!(fx.sent_len(), 3); // The first channel_ready was sent with include_alias = false, so it must // not carry the short_channel_id TLV. - let cr1 = match Message::decode(&executor.conn.sent[1]).expect("valid message") { - Message::ChannelReady(cr) => cr, - other => panic!("expected channel_ready(36), got {other}"), - }; + let cr1: ChannelReady = fx.sent(1); let expected_pcp1 = PublicKey::from_str("023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb") .unwrap(); @@ -1666,10 +1503,7 @@ fn execute_send_channel_ready() { // The second channel_ready was sent with include_alias = true, so it must // carry the alias SCID we loaded in its short_channel_id TLV. - let cr2 = match Message::decode(&executor.conn.sent[2]).expect("valid message") { - Message::ChannelReady(cr) => cr, - other => panic!("expected channel_ready(36), got {other}"), - }; + let cr2: ChannelReady = fx.sent(2); let expected_pcp2 = PublicKey::from_str("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1") .unwrap(); @@ -1679,7 +1513,7 @@ fn execute_send_channel_ready() { // The holder's next per-commitment point must hold the first // `channel_ready`'s point, not any subsequent one. - let state = executor.channel_states.get_mut(&channel_id).unwrap(); + let state = fx.channel_state(&channel_id); assert_eq!( *state.next_holder_per_commitment_point(), Some(expected_pcp1) @@ -1747,78 +1581,62 @@ fn execute_send_shutdown_empty_scriptpubkey() { assert!(sd.scriptpubkey.is_empty()); } -fn recv_channel_ready_executor() -> ( - Executor, - ChannelId, - PublicKey, -) { - let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { +/// The channel id the funding flow's transaction produces. +fn funding_channel_id() -> ChannelId { + ChannelId::v1_from_funding_outpoint(OutPoint { txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" .parse() - .unwrap(), + .expect("valid txid"), vout: 0, - }); - let mock_cli = MockBitcoinCli { - utxos: vec![sample_utxo()], - change_spk: sample_change_spk(), - ..Default::default() - }; - - // We also need to send this `funding_signed`, since the instructions reused - // by this test expect one to be present in the executor's receive queue. - // The expected signature here was computed using LDK as the source of - // truth. - let fs_bytes = Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), }) - .encode(); +} - let target_pcp = sample_pubkey(1); - let cr_bytes = Message::ChannelReady(ChannelReady { - channel_id, - second_per_commitment_point: target_pcp, +/// The target's `channel_ready` for the funding flow's channel. +fn channel_ready_reply(second_per_commitment_point: PublicKey) -> Message { + Message::ChannelReady(ChannelReady { + channel_id: funding_channel_id(), + second_per_commitment_point, tlvs: ChannelReadyTlvs::default(), }) - .encode(); +} - let mut executor = Executor::new( - MockConnection::new(), - mock_cli, - MockTargetRpc::default(), - sample_context(), - ); - executor.conn.queue_recv(fs_bytes); - executor.conn.queue_recv(cr_bytes); - executor.negotiations.insert( - TemporaryChannelId::new([0xbb; 32]), - sample_funding_negotiation(), - ); +/// A fixture with the funding negotiation seeded and both target replies +/// queued, plus the target's per-commitment point for the assertions. +fn recv_channel_ready_fixture() -> (Fixture, PublicKey) { + let target_pcp = sample_pubkey(1); - (executor, channel_id, target_pcp) + // We also need to queue this `funding_signed`, since the instructions + // reused by these tests expect one to be present in the receive queue. + // The expected signature here was computed using LDK as the source of + // truth. + let fx = Fixture::new() + .with_negotiation(sample_funding_negotiation()) + .queue(&Message::FundingSigned(FundingSigned { + channel_id: funding_channel_id(), + signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + })) + .queue(&channel_ready_reply(target_pcp)); + + (fx, target_pcp) } #[test] fn execute_recv_channel_ready_invalid_funding_outpoint_is_noop() { - let (mut executor, channel_id, _) = recv_channel_ready_executor(); - // Corrupt the negotiated opener funding pubkey so the broadcast funding // transaction's output no longer pays the negotiated 2-of-2 script, // marking the funding outpoint invalid. - executor - .negotiations - .get_mut(&TemporaryChannelId::new([0xbb; 32])) - .unwrap() - .open_channel - .funding_pubkey = sample_pubkey(1); + let mut negotiation = sample_funding_negotiation(); + negotiation.open_channel.funding_pubkey = sample_pubkey(1); // The corrupted pubkey changes the funding script, so our precomputed // funding_signed signature will no longer verify correctly. That - // exchange is not what this test is about, we just skip receiving the - // funding_signed. + // exchange is not what this test is about, so we neither queue the + // funding_signed nor receive it. + let mut fx = Fixture::new() + .with_negotiation(negotiation) + .queue(&channel_ready_reply(sample_pubkey(1))); let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.pop(); - executor.conn.recv_queue.pop_front(); instrs.extend([ Instruction { @@ -1833,83 +1651,64 @@ fn execute_recv_channel_ready_invalid_funding_outpoint_is_noop() { // With invalid funding outpoint the target does not owe us a // `channel_ready`, so `RecvChannelReady` must be a no-op. - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + fx.run(&Program { + instructions: instrs, + }); // The target's next per-commitment point is still unknown and the queued // `channel_ready` remains untouched. - let state = executor.channel_states.get_mut(&channel_id).unwrap(); + let state = fx.channel_state(&funding_channel_id()); assert!(state.next_counterparty_per_commitment_point().is_none()); - assert_eq!(executor.conn.recv_queue.len(), 1); + assert_eq!(fx.queued_len(), 1); } #[test] fn execute_recv_channel_ready_below_minimum_depth_is_noop() { - let (mut executor, channel_id, _) = recv_channel_ready_executor(); + let (mut fx, _) = recv_channel_ready_fixture(); // Mine one block fewer than the `minimum_depth` negotiated in `accept_channel` by // `sample_funding_negotiation()`. - let instrs = recv_channel_ready_instructions(5); - // With fewer than the negotiated `minimum_depth` confirmations the target // does not yet owe us a `channel_ready`, so `RecvChannelReady` must be a // no-op. - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); - assert!(executor.bitcoin_cli.mined_private_mempool.is_empty()); + fx.run(&Program { + instructions: recv_channel_ready_instructions(5), + }); + assert!(fx.bitcoin().mined_private_mempool.is_empty()); // The target's next per-commitment point is still unknown and the queued // `channel_ready` remains untouched. - let state = executor.channel_states.get_mut(&channel_id).unwrap(); + let state = fx.channel_state(&funding_channel_id()); assert!(state.next_counterparty_per_commitment_point().is_none()); - assert_eq!(executor.conn.recv_queue.len(), 1); + assert_eq!(fx.queued_len(), 1); } #[test] fn execute_recv_channel_ready_at_minimum_depth_records_point() { - let (mut executor, channel_id, target_pcp) = recv_channel_ready_executor(); + let (mut fx, target_pcp) = recv_channel_ready_fixture(); // Mine exactly the `minimum_depth` negotiated in `accept_channel` by // `sample_funding_negotiation()`. - let instrs = recv_channel_ready_instructions(6); - // At the negotiated `minimum_depth` confirmations the target owes us a // `channel_ready`, which `RecvChannelReady` receives and records. - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); - assert!(executor.bitcoin_cli.mined_private_mempool.is_empty()); + fx.run(&Program { + instructions: recv_channel_ready_instructions(6), + }); + assert!(fx.bitcoin().mined_private_mempool.is_empty()); // The `channel_ready` was consumed and the target's next per-commitment // point is now recorded. - let state = executor.channel_states.get_mut(&channel_id).unwrap(); + let state = fx.channel_state(&funding_channel_id()); assert_eq!( *state.next_counterparty_per_commitment_point(), Some(target_pcp) ); - assert!(executor.conn.recv_queue.is_empty()); + assert_eq!(fx.queued_len(), 0); } #[test] fn execute_recv_channel_ready_funding_mined_prematurely_is_noop() { - let (mut executor, channel_id, _) = recv_channel_ready_executor(); + let (mut fx, _) = recv_channel_ready_fixture(); let mut instrs = create_and_broadcast_tx_instructions(); instrs.extend([ @@ -1940,21 +1739,16 @@ fn execute_recv_channel_ready_funding_mined_prematurely_is_noop() { // The funding transaction confirmed before `funding_created`, so the // target may never observe the confirmation and `RecvChannelReady` must // be a no-op even though the confirmation count is sufficient. - executor - .execute( - &Program { - instructions: instrs, - }, - std::time::Instant::now(), - ) - .unwrap(); + fx.run(&Program { + instructions: instrs, + }); // The target's next per-commitment point is still unknown and the queued // `channel_ready` remains untouched. - let state = executor.channel_states.get_mut(&channel_id).unwrap(); + let state = fx.channel_state(&funding_channel_id()); assert!(state.was_funding_mined_prematurely); assert!(state.next_counterparty_per_commitment_point().is_none()); - assert_eq!(executor.conn.recv_queue.len(), 1); + assert_eq!(fx.queued_len(), 1); } // -- extract_field tests -- diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index 21e472ae..b5c6810f 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -20,10 +20,6 @@ impl MockConnection { sent: Vec::new(), } } - - pub fn queue_recv(&mut self, msg_bytes: Vec) { - self.recv_queue.push_back(msg_bytes); - } } impl Connection for MockConnection { @@ -149,6 +145,12 @@ impl Fixture { } } + /// Funds the wallet with `utxos` instead of the default [`sample_utxo`]. + pub fn with_utxos(mut self, utxos: Vec) -> Self { + self.executor.bitcoin_cli.utxos = utxos; + self + } + /// Records `pending` as the negotiation for its `temporary_channel_id`. pub fn with_negotiation(mut self, pending: PendingChannel) -> Self { self.executor @@ -163,6 +165,11 @@ impl Fixture { self } + /// Returns the number of queued peer replies the executor has not read. + pub fn queued_len(&self) -> usize { + self.executor.conn.recv_queue.len() + } + /// Runs `program` against the target, panicking if execution fails. pub fn run(&mut self, program: &Program) { self.executor @@ -185,6 +192,19 @@ impl Fixture { .expect("negotiation recorded") } + /// Returns the channel state recorded for `id`. + pub fn channel_state(&self, id: &ChannelId) -> &ChannelState { + self.executor + .channel_states + .get(id) + .expect("channel state recorded") + } + + /// Returns every channel state the executor recorded. + pub fn channel_states(&self) -> &HashMap { + &self.executor.channel_states + } + /// Returns the mock bitcoind the executor drives. pub fn bitcoin(&self) -> &MockBitcoinCli { &self.executor.bitcoin_cli @@ -195,6 +215,11 @@ impl Fixture { &self.executor.rpc } + /// Returns the transactions held outside Bitcoin Core's mempool. + pub fn private_mempool(&self) -> &[(Txid, String)] { + &self.executor.private_mempool + } + /// Returns the number of messages the executor sent. pub fn sent_len(&self) -> usize { self.executor.conn.sent.len() @@ -248,6 +273,8 @@ macro_rules! impl_from_message { impl_from_message! { Pong => PONG, OpenChannel => OPEN_CHANNEL, + FundingCreated => FUNDING_CREATED, + ChannelReady => CHANNEL_READY, Shutdown => SHUTDOWN, ChannelAnnouncement => CHANNEL_ANNOUNCEMENT, NodeAnnouncement => NODE_ANNOUNCEMENT, From ca2b76510ba2914c0a2f099187f7dd47189d6410 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Thu, 3 Sep 2026 12:56:37 -0500 Subject: [PATCH 4/8] smite-scenarios: move funding fixture helpers to harness.rs --- smite-scenarios/src/executor/tests.rs | 39 ------------------ smite-scenarios/src/executor/tests/harness.rs | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index e95eeffb..c12cf621 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -1581,45 +1581,6 @@ fn execute_send_shutdown_empty_scriptpubkey() { assert!(sd.scriptpubkey.is_empty()); } -/// The channel id the funding flow's transaction produces. -fn funding_channel_id() -> ChannelId { - ChannelId::v1_from_funding_outpoint(OutPoint { - txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - .parse() - .expect("valid txid"), - vout: 0, - }) -} - -/// The target's `channel_ready` for the funding flow's channel. -fn channel_ready_reply(second_per_commitment_point: PublicKey) -> Message { - Message::ChannelReady(ChannelReady { - channel_id: funding_channel_id(), - second_per_commitment_point, - tlvs: ChannelReadyTlvs::default(), - }) -} - -/// A fixture with the funding negotiation seeded and both target replies -/// queued, plus the target's per-commitment point for the assertions. -fn recv_channel_ready_fixture() -> (Fixture, PublicKey) { - let target_pcp = sample_pubkey(1); - - // We also need to queue this `funding_signed`, since the instructions - // reused by these tests expect one to be present in the receive queue. - // The expected signature here was computed using LDK as the source of - // truth. - let fx = Fixture::new() - .with_negotiation(sample_funding_negotiation()) - .queue(&Message::FundingSigned(FundingSigned { - channel_id: funding_channel_id(), - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - })) - .queue(&channel_ready_reply(target_pcp)); - - (fx, target_pcp) -} - #[test] fn execute_recv_channel_ready_invalid_funding_outpoint_is_noop() { // Corrupt the negotiated opener funding pubkey so the broadcast funding diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index b5c6810f..da3d2f5c 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -347,6 +347,47 @@ pub fn sample_accept_channel() -> AcceptChannel { } } +// -- Funding fixture -- + +/// The channel id the funding flow's transaction produces. +pub fn funding_channel_id() -> ChannelId { + ChannelId::v1_from_funding_outpoint(OutPoint { + txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" + .parse() + .expect("valid txid"), + vout: 0, + }) +} + +/// The target's `channel_ready` for the funding flow's channel. +pub fn channel_ready_reply(second_per_commitment_point: PublicKey) -> Message { + Message::ChannelReady(ChannelReady { + channel_id: funding_channel_id(), + second_per_commitment_point, + tlvs: ChannelReadyTlvs::default(), + }) +} + +/// A fixture with the funding negotiation seeded and both target replies +/// queued, plus the target's per-commitment point for the assertions. +pub fn recv_channel_ready_fixture() -> (Fixture, PublicKey) { + let target_pcp = sample_pubkey(1); + + // We also need to queue this `funding_signed`, since the instructions + // reused by these tests expect one to be present in the receive queue. + // The expected signature here was computed using LDK as the source of + // truth. + let fx = Fixture::new() + .with_negotiation(sample_funding_negotiation()) + .queue(&Message::FundingSigned(FundingSigned { + channel_id: funding_channel_id(), + signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + })) + .queue(&channel_ready_reply(target_pcp)); + + (fx, target_pcp) +} + #[allow(clippy::similar_names)] pub fn sample_funding_negotiation() -> PendingChannel { let secp = Secp256k1::new(); From 24b8aa58cc7c775126a5287e60fd9e3b3551fe79 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Thu, 3 Sep 2026 13:10:18 -0500 Subject: [PATCH 5/8] smite-scenarios: name funding fixture constants Name and move constant definitions to harness.rs. --- smite-scenarios/src/executor/tests.rs | 108 ++++-------------- smite-scenarios/src/executor/tests/harness.rs | 61 +++++++--- .../src/executor/tests/programs.rs | 12 +- 3 files changed, 65 insertions(+), 116 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index c12cf621..0b992a29 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -1007,10 +1007,7 @@ fn execute_create_and_broadcast_tx() { assert_eq!(fx.bitcoin().broadcast_calls.len(), 1); let broadcast_tx = &fx.bitcoin().broadcast_calls[0]; - assert_eq!( - broadcast_tx.compute_txid().to_string(), - "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - ); + assert_eq!(broadcast_tx.compute_txid(), funding_outpoint().txid); assert_eq!(fx.rpc().chain_syncs, 0); } @@ -1169,21 +1166,11 @@ fn execute_create_funding_transaction_insufficient_funds() { fn execute_send_funding_created_and_recv_funding_signed() { // The acceptor replies with funding_signed carrying its signature over // the opener's commitment. - let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { - txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - .parse() - .unwrap(), - vout: 0, - }); + let channel_id = funding_channel_id(); - // The expected signature here was computed using LDK as the source of - // truth. let mut fx = Fixture::new() .with_negotiation(sample_funding_negotiation()) - .queue(&Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - })); + .queue(&funding_signed_reply(channel_id)); fx.run(&Program { instructions: send_funding_created_and_recv_funding_signed_instructions(), }); @@ -1192,20 +1179,14 @@ fn execute_send_funding_created_and_recv_funding_signed() { let fc: FundingCreated = fx.sent(0); assert_eq!(fc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); - assert_eq!( - fc.funding_txid.to_string(), - "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - ); + assert_eq!(fc.funding_txid, funding_outpoint().txid); assert_eq!(fc.funding_output_index, 0); // Verify the signature sent by the opener on the acceptor side. let state = fx.channel_state(&channel_id); let holder = HolderIdentity { side: Side::Acceptor, - funding_privkey: SecretKey::from_str( - "1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e13", - ) - .unwrap(), + funding_privkey: acceptor_funding_sk(), }; assert!( @@ -1221,12 +1202,7 @@ fn execute_send_funding_created_and_recv_funding_signed() { #[test] fn execute_send_funding_created_uses_wire_funding_pubkey() { - let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { - txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - .parse() - .unwrap(), - vout: 0, - }); + let channel_id = funding_channel_id(); // Swap out the SendFundingCreated privkey. This should not affect the // constructed channel config, which uses the negotiated pubkeys. It @@ -1234,36 +1210,23 @@ fn execute_send_funding_created_uses_wire_funding_pubkey() { let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs[9].inputs[1] = 2; - // The same acceptor signature as the happy path (computed using LDK as - // the source of truth): computed over the the commitment implied by the - // negotiated funding pubkeys. It still verifies, because the config is - // built from the wire pubkeys rather than from the swapped privkey. + // The acceptor's signature still verifies, because the config is built + // from the wire pubkeys rather than from the swapped privkey. let mut fx = Fixture::new() .with_negotiation(sample_funding_negotiation()) - .queue(&Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - })); + .queue(&funding_signed_reply(channel_id)); fx.run(&Program { instructions: instrs, }); let secp = Secp256k1::new(); - let opener_pk = PublicKey::from_secret_key( - &secp, - &SecretKey::from_str("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f3749") - .unwrap(), - ); + let opener_pk = PublicKey::from_secret_key(&secp, &opener_funding_sk()); // The funding pubkey matches what was negotiated. let state = fx.channel_state(&channel_id); assert_eq!(state.config.opener.funding_pubkey, opener_pk); // But the swapped privkey used for signing is the acceptor's, which // does not match what was negotiated. - assert_eq!( - state.holder.funding_privkey, - SecretKey::from_str("1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e13") - .unwrap() - ); + assert_eq!(state.holder.funding_privkey, acceptor_funding_sk()); assert_ne!( state.config.opener.funding_pubkey, PublicKey::from_secret_key(&secp, &state.holder.funding_privkey) @@ -1282,12 +1245,7 @@ fn execute_send_funding_created_after_funding_built_does_not_track_channel() { }; // Channel id derived from the first funding transaction's outpoint. - let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { - txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - .parse() - .unwrap(), - vout: 0, - }); + let channel_id = funding_channel_id(); let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. @@ -1365,10 +1323,7 @@ fn execute_send_funding_created_no_open_channel() { let fc: FundingCreated = fx.sent(0); assert_eq!(fc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); - assert_eq!( - fc.funding_txid.to_string(), - "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - ); + assert_eq!(fc.funding_txid, funding_outpoint().txid); assert_eq!(fc.funding_output_index, 0); assert_eq!(fc.signature, Signature::from_compact(&[0u8; 64]).unwrap()); assert!(fx.channel_states().is_empty()); @@ -1391,10 +1346,7 @@ fn execute_send_funding_created_no_accept_channel() { let fc: FundingCreated = fx.sent(0); assert_eq!(fc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); - assert_eq!( - fc.funding_txid.to_string(), - "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - ); + assert_eq!(fc.funding_txid, funding_outpoint().txid); assert_eq!(fc.funding_output_index, 0); assert_eq!(fc.signature, Signature::from_compact(&[0u8; 64]).unwrap()); assert!(fx.channel_states().is_empty()); @@ -1404,14 +1356,9 @@ fn execute_send_funding_created_no_accept_channel() { fn execute_recv_funding_signed_unknown_channel() { let channel_id = ChannelId::new([0xbb; 32]); - // The expected signature here was computed using LDK as the source of - // truth. let err = Fixture::new() .with_negotiation(sample_funding_negotiation()) - .queue(&Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - })) + .queue(&funding_signed_reply(channel_id)) .run_err(&Program { instructions: send_funding_created_and_recv_funding_signed_instructions(), }); @@ -1423,12 +1370,7 @@ fn execute_recv_funding_signed_unknown_channel() { #[test] fn execute_recv_funding_signed_invalid_signature() { - let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { - txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - .parse() - .unwrap(), - vout: 0, - }); + let channel_id = funding_channel_id(); let err = Fixture::new() .with_negotiation(sample_funding_negotiation()) .queue(&Message::FundingSigned(FundingSigned { @@ -1447,12 +1389,7 @@ fn execute_recv_funding_signed_invalid_signature() { #[test] fn execute_send_channel_ready() { - let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint { - txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" - .parse() - .unwrap(), - vout: 0, - }); + let channel_id = funding_channel_id(); let alias = ShortChannelId::new(538_532, 845, 1); let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.extend([ @@ -1474,16 +1411,11 @@ fn execute_send_channel_ready() { }, ]); - // We also need to send this `funding_signed`, since the instructions reused - // by this test expect one to be present in the executor's receive queue. - // The expected signature here was computed using LDK as the source of - // truth. + // We also need to queue a `funding_signed`, since the instructions reused + // by this test expect one to be present in the receive queue. let mut fx = Fixture::new() .with_negotiation(sample_funding_negotiation()) - .queue(&Message::FundingSigned(FundingSigned { - channel_id, - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - })); + .queue(&funding_signed_reply(channel_id)); fx.run(&Program { instructions: instrs, }); diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index da3d2f5c..f353ebad 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -348,14 +348,48 @@ pub fn sample_accept_channel() -> AcceptChannel { } // -- Funding fixture -- +// +// The funding keys are chosen from BOLT 3 test vectors. All other constants are +// derived from these keys. + +/// The opener's funding key for the funding flow. +pub fn opener_funding_sk() -> SecretKey { + SecretKey::from_str("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f3749") + .expect("valid secret key") +} -/// The channel id the funding flow's transaction produces. -pub fn funding_channel_id() -> ChannelId { - ChannelId::v1_from_funding_outpoint(OutPoint { +/// The acceptor's funding key for the funding flow. +pub fn acceptor_funding_sk() -> SecretKey { + SecretKey::from_str("1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e13") + .expect("valid secret key") +} + +/// The outpoint of the funding transaction the funding-flow programs build. +pub fn funding_outpoint() -> OutPoint { + OutPoint { txid: "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" .parse() .expect("valid txid"), vout: 0, + } +} + +/// The channel id the funding flow's transaction produces. +pub fn funding_channel_id() -> ChannelId { + ChannelId::v1_from_funding_outpoint(funding_outpoint()) +} + +/// The acceptor's `funding_signed` for `channel_id`. +/// +/// The signature was computed by LDK over this fixture's commitment, so the +/// executor accepting it shows both implementations built the same commitment +/// transaction. +pub fn funding_signed_reply(channel_id: ChannelId) -> Message { + Message::FundingSigned(FundingSigned { + channel_id, + signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7" + .parse() + .expect("valid DER signature"), }) } @@ -373,16 +407,11 @@ pub fn channel_ready_reply(second_per_commitment_point: PublicKey) -> Message { pub fn recv_channel_ready_fixture() -> (Fixture, PublicKey) { let target_pcp = sample_pubkey(1); - // We also need to queue this `funding_signed`, since the instructions - // reused by these tests expect one to be present in the receive queue. - // The expected signature here was computed using LDK as the source of - // truth. + // We also need to queue a `funding_signed`, since the instructions reused + // by these tests expect one to be present in the receive queue. let fx = Fixture::new() .with_negotiation(sample_funding_negotiation()) - .queue(&Message::FundingSigned(FundingSigned { - channel_id: funding_channel_id(), - signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), - })) + .queue(&funding_signed_reply(funding_channel_id())) .queue(&channel_ready_reply(target_pcp)); (fx, target_pcp) @@ -391,14 +420,8 @@ pub fn recv_channel_ready_fixture() -> (Fixture, PublicKey) { #[allow(clippy::similar_names)] pub fn sample_funding_negotiation() -> PendingChannel { let secp = Secp256k1::new(); - let opener_sk = - SecretKey::from_str("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f3749") - .unwrap(); - let acceptor_sk = - SecretKey::from_str("1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e13") - .unwrap(); - let opener_pk = PublicKey::from_secret_key(&secp, &opener_sk); - let acceptor_pk = PublicKey::from_secret_key(&secp, &acceptor_sk); + let opener_pk = PublicKey::from_secret_key(&secp, &opener_funding_sk()); + let acceptor_pk = PublicKey::from_secret_key(&secp, &acceptor_funding_sk()); PendingChannel { open_channel: OpenChannel { diff --git a/smite-scenarios/src/executor/tests/programs.rs b/smite-scenarios/src/executor/tests/programs.rs index b5207217..209cd463 100644 --- a/smite-scenarios/src/executor/tests/programs.rs +++ b/smite-scenarios/src/executor/tests/programs.rs @@ -2,9 +2,9 @@ //! //! Each helper returns the instructions for one flow. +use super::harness::{acceptor_funding_sk, opener_funding_sk}; use crate::executor::*; use smite_ir::Instruction; -use std::str::FromStr; /// Builds the 20 `open_channel` input instructions in wire order. pub fn open_channel_instructions() -> Vec { @@ -93,14 +93,8 @@ pub fn open_channel_instructions() -> Vec { } pub fn create_and_broadcast_tx_instructions() -> Vec { - let opener_privkey = - SecretKey::from_str("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f3749") - .unwrap() - .secret_bytes(); - let acceptor_privkey = - SecretKey::from_str("1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e13") - .unwrap() - .secret_bytes(); + let opener_privkey = opener_funding_sk().secret_bytes(); + let acceptor_privkey = acceptor_funding_sk().secret_bytes(); vec![ Instruction { From dff7a57b84f760520f77ff89371594077ac53d8e Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Thu, 3 Sep 2026 14:24:57 -0500 Subject: [PATCH 6/8] smite-scenarios: narrow harness visibility MockConnection is no longer used outside harness.rs and can become private, as can several MockBitcoinCli fields and sample_change_spk. --- smite-scenarios/src/executor/tests/harness.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index f353ebad..b298ff64 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -8,13 +8,13 @@ use std::str::FromStr; // -- MockConnection -- -pub struct MockConnection { - pub recv_queue: VecDeque>, - pub sent: Vec>, +struct MockConnection { + recv_queue: VecDeque>, + sent: Vec>, } impl MockConnection { - pub fn new() -> Self { + fn new() -> Self { Self { recv_queue: VecDeque::new(), sent: Vec::new(), @@ -51,9 +51,9 @@ pub struct MockBitcoinCli { pub mined_private_mempool: Vec, pub broadcast_calls: Vec, pub block_position_lookups: Vec, - pub utxos: Vec, - pub change_spk: ScriptBuf, - pub confirmations: u32, + utxos: Vec, + change_spk: ScriptBuf, + confirmations: u32, } impl BitcoinRpc for MockBitcoinCli { @@ -317,7 +317,7 @@ pub fn sample_utxo() -> Utxo { } } -pub fn sample_change_spk() -> ScriptBuf { +fn sample_change_spk() -> ScriptBuf { ScriptBuf::from( hex::decode("00142e532c12351a5c81e23c8a76d19345ca7b6de57a") .expect("valid P2WPKH scriptpubkey hex"), From f049f101eb8d4cceb30ef72da10ea277ab05768e Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Tue, 8 Sep 2026 16:06:39 -0500 Subject: [PATCH 7/8] smite: move FromMessage into bolt The trait is useful outside of the executor tests, so it belongs next to the Message impl. We also now implement it for all currently-supported message types rather than just the types we need for executor tests. We also refactor the executor to use this trait when receiving specific message types. --- smite-scenarios/src/executor.rs | 54 ++++++--------- smite-scenarios/src/executor/tests/harness.rs | 43 +----------- smite/src/bolt.rs | 65 +++++++++++++++++++ 3 files changed, 87 insertions(+), 75 deletions(-) diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index f06bc928..a63eb3b3 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -9,8 +9,8 @@ use bitcoin::{OutPoint, ScriptBuf, Txid}; use smite::bitcoin::{BitcoinCli, TxBlockPosition, Utxo}; use smite::bolt::{ AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelId, ChannelReady, - ChannelReadyTlvs, ChannelUpdate, Features, FundingCreated, FundingSigned, Message, MessageType, - NodeAnnouncement, OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, + ChannelReadyTlvs, ChannelUpdate, Features, FromMessage, FundingCreated, FundingSigned, Message, + MessageType, NodeAnnouncement, OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, TemporaryChannelId, }; use smite::channel_tx::{ @@ -497,7 +497,7 @@ impl Executor { instr.operation.input_types()[0], ); log::debug!("[{:?}] RecvAcceptChannel: waiting", start.elapsed()); - let ac = recv_accept_channel(&mut self.conn)?; + let ac: AcceptChannel = recv_bolt(&mut self.conn, RECV_IDLE_TIMEOUT)?; log::debug!("[{:?}] RecvAcceptChannel: received", start.elapsed()); AcceptChannelOracle.evaluate(&AcceptChannelContext { accept_channel: &ac, @@ -514,7 +514,7 @@ impl Executor { instr.operation.input_types()[0], ); log::debug!("[{:?}] RecvFundingSigned: waiting", start.elapsed()); - let fs = recv_funding_signed(&mut self.conn)?; + let fs: FundingSigned = recv_bolt(&mut self.conn, RECV_IDLE_TIMEOUT)?; log::debug!("[{:?}] RecvFundingSigned: received", start.elapsed()); verify_funding_signed(&fs, &self.channel_states)?; Some(Variable::ChannelId(fs.channel_id)) @@ -1165,26 +1165,22 @@ fn recv_non_ping(conn: &mut impl Connection, timeout: Duration) -> Result Result { - match recv_non_ping(conn, RECV_IDLE_TIMEOUT)? { - Message::AcceptChannel(ac) => Ok(ac), - other => Err(ExecuteError::UnexpectedMessage { - expected: MessageType::ACCEPT_CHANNEL, - got: other.msg_type(), - }), - } -} - -/// Receives and decodes a `funding_signed` message. -fn recv_funding_signed(conn: &mut impl Connection) -> Result { - match recv_non_ping(conn, RECV_IDLE_TIMEOUT)? { - Message::FundingSigned(fs) => Ok(fs), - other => Err(ExecuteError::UnexpectedMessage { - expected: MessageType::FUNDING_SIGNED, - got: other.msg_type(), - }), - } +/// Receives and decodes the next message, requiring it to be an `M`. +/// +/// # Errors +/// +/// Returns [`ExecuteError::UnexpectedMessage`] if the received message is not +/// an `M`. +fn recv_bolt( + conn: &mut impl Connection, + timeout: Duration, +) -> Result { + let msg = recv_non_ping(conn, timeout)?; + let got = msg.msg_type(); + M::from_message(msg).ok_or(ExecuteError::UnexpectedMessage { + expected: M::TYPE, + got, + }) } /// Receives and decodes a `channel_ready` message. @@ -1201,15 +1197,7 @@ fn recv_channel_ready( conn: &mut impl Connection, channel_states: &mut HashMap, ) -> Result<(), ExecuteError> { - let cr = match recv_non_ping(conn, RECV_CHANNEL_READY_TIMEOUT)? { - Message::ChannelReady(cr) => cr, - other => { - return Err(ExecuteError::UnexpectedMessage { - expected: MessageType::CHANNEL_READY, - got: other.msg_type(), - }); - } - }; + let cr: ChannelReady = recv_bolt(conn, RECV_CHANNEL_READY_TIMEOUT)?; let state = channel_states .get_mut(&cr.channel_id) diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index b298ff64..dcf52890 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -2,7 +2,7 @@ use crate::executor::*; use bitcoin::{Amount, Transaction}; -use smite::bolt::AcceptChannelTlvs; +use smite::bolt::{AcceptChannelTlvs, FromMessage}; use std::collections::VecDeque; use std::str::FromStr; @@ -241,47 +241,6 @@ impl Fixture { } } -/// Extracts a specific BOLT message from a decoded [`Message`]. -pub trait FromMessage: Sized { - /// Wire type of the expected BOLT message. - const TYPE: MessageType; - - /// Returns the extracted BOLT message if `msg`'s type matches, `None` - /// otherwise. - fn from_message(msg: Message) -> Option; -} - -/// Implements [`FromMessage`] for BOLT messages whose [`Message`] variant has -/// the same name. -macro_rules! impl_from_message { - ($($bolt_msg:ident => $msg_type:ident,)*) => { - $( - impl FromMessage for $bolt_msg { - const TYPE: MessageType = MessageType::$msg_type; - - fn from_message(msg: Message) -> Option { - match msg { - Message::$bolt_msg(bolt_msg) => Some(bolt_msg), - _ => None, - } - } - } - )* - }; -} - -impl_from_message! { - Pong => PONG, - OpenChannel => OPEN_CHANNEL, - FundingCreated => FUNDING_CREATED, - ChannelReady => CHANNEL_READY, - Shutdown => SHUTDOWN, - ChannelAnnouncement => CHANNEL_ANNOUNCEMENT, - NodeAnnouncement => NODE_ANNOUNCEMENT, - ChannelUpdate => CHANNEL_UPDATE, - AnnouncementSignatures => ANNOUNCEMENT_SIGNATURES, -} - // -- Helpers -- pub fn sample_pubkey(byte: u8) -> PublicKey { diff --git a/smite/src/bolt.rs b/smite/src/bolt.rs index 0461786f..978fa434 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -530,6 +530,71 @@ impl Message { } } +/// A BOLT message that can be extracted from a decoded [`Message`]. +pub trait FromMessage: Sized { + /// Wire type of this message. + const TYPE: MessageType; + + /// Returns the extracted BOLT message if `msg`'s type matches + /// [`Self::TYPE`], `None` otherwise. + fn from_message(msg: Message) -> Option; +} + +/// Implements [`FromMessage`] for BOLT messages whose [`Message`] variant has +/// the same name. +macro_rules! impl_from_message { + ($($bolt_msg:ident => $msg_type:ident,)*) => { + $( + impl FromMessage for $bolt_msg { + const TYPE: MessageType = MessageType::$msg_type; + + fn from_message(msg: Message) -> Option { + match msg { + Message::$bolt_msg(bolt_msg) => Some(bolt_msg), + _ => None, + } + } + } + )* + }; +} + +impl_from_message! { + Warning => WARNING, + Init => INIT, + Error => ERROR, + Ping => PING, + Pong => PONG, + OpenChannel => OPEN_CHANNEL, + AcceptChannel => ACCEPT_CHANNEL, + FundingCreated => FUNDING_CREATED, + FundingSigned => FUNDING_SIGNED, + ChannelReady => CHANNEL_READY, + Shutdown => SHUTDOWN, + ClosingComplete => CLOSING_COMPLETE, + ClosingSig => CLOSING_SIG, + OpenChannel2 => OPEN_CHANNEL2, + AcceptChannel2 => ACCEPT_CHANNEL2, + TxAddInput => TX_ADD_INPUT, + TxRemoveInput => TX_REMOVE_INPUT, + TxRemoveOutput => TX_REMOVE_OUTPUT, + TxComplete => TX_COMPLETE, + TxInitRbf => TX_INIT_RBF, + TxAckRbf => TX_ACK_RBF, + TxAbort => TX_ABORT, + UpdateAddHtlc => UPDATE_ADD_HTLC, + UpdateFulfillHtlc => UPDATE_FULFILL_HTLC, + UpdateFailHtlc => UPDATE_FAIL_HTLC, + CommitmentSigned => COMMITMENT_SIGNED, + RevokeAndAck => REVOKE_AND_ACK, + UpdateFailMalformedHtlc => UPDATE_FAIL_MALFORMED_HTLC, + ChannelAnnouncement => CHANNEL_ANNOUNCEMENT, + NodeAnnouncement => NODE_ANNOUNCEMENT, + ChannelUpdate => CHANNEL_UPDATE, + AnnouncementSignatures => ANNOUNCEMENT_SIGNATURES, + GossipTimestampFilter => GOSSIP_TIMESTAMP_FILTER, +} + /// Creates a raw message with the given type and payload. /// /// This is useful for fuzzing - it allows sending arbitrary payloads From 6993485b5fe0e49986a965f86a8f020a99ec417f Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Tue, 8 Sep 2026 16:19:57 -0500 Subject: [PATCH 8/8] smite-scenarios: add recv_funding_signed_fixture helper --- smite-scenarios/src/executor/tests.rs | 29 +++++-------------- smite-scenarios/src/executor/tests/harness.rs | 20 +++++++------ 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 0b992a29..93eb76e8 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -1166,11 +1166,7 @@ fn execute_create_funding_transaction_insufficient_funds() { fn execute_send_funding_created_and_recv_funding_signed() { // The acceptor replies with funding_signed carrying its signature over // the opener's commitment. - let channel_id = funding_channel_id(); - - let mut fx = Fixture::new() - .with_negotiation(sample_funding_negotiation()) - .queue(&funding_signed_reply(channel_id)); + let mut fx = recv_funding_signed_fixture(); fx.run(&Program { instructions: send_funding_created_and_recv_funding_signed_instructions(), }); @@ -1183,7 +1179,7 @@ fn execute_send_funding_created_and_recv_funding_signed() { assert_eq!(fc.funding_output_index, 0); // Verify the signature sent by the opener on the acceptor side. - let state = fx.channel_state(&channel_id); + let state = fx.channel_state(&funding_channel_id()); let holder = HolderIdentity { side: Side::Acceptor, funding_privkey: acceptor_funding_sk(), @@ -1202,8 +1198,6 @@ fn execute_send_funding_created_and_recv_funding_signed() { #[test] fn execute_send_funding_created_uses_wire_funding_pubkey() { - let channel_id = funding_channel_id(); - // Swap out the SendFundingCreated privkey. This should not affect the // constructed channel config, which uses the negotiated pubkeys. It // should only change the signature sent to the target. @@ -1212,9 +1206,7 @@ fn execute_send_funding_created_uses_wire_funding_pubkey() { // The acceptor's signature still verifies, because the config is built // from the wire pubkeys rather than from the swapped privkey. - let mut fx = Fixture::new() - .with_negotiation(sample_funding_negotiation()) - .queue(&funding_signed_reply(channel_id)); + let mut fx = recv_funding_signed_fixture(); fx.run(&Program { instructions: instrs, }); @@ -1222,7 +1214,7 @@ fn execute_send_funding_created_uses_wire_funding_pubkey() { let secp = Secp256k1::new(); let opener_pk = PublicKey::from_secret_key(&secp, &opener_funding_sk()); // The funding pubkey matches what was negotiated. - let state = fx.channel_state(&channel_id); + let state = fx.channel_state(&funding_channel_id()); assert_eq!(state.config.opener.funding_pubkey, opener_pk); // But the swapped privkey used for signing is the acceptor's, which // does not match what was negotiated. @@ -1244,9 +1236,6 @@ fn execute_send_funding_created_after_funding_built_does_not_track_channel() { ..sample_utxo() }; - // Channel id derived from the first funding transaction's outpoint. - let channel_id = funding_channel_id(); - let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. instrs.extend(vec![ @@ -1271,7 +1260,9 @@ fn execute_send_funding_created_after_funding_built_does_not_track_channel() { // The message still goes out, only the state tracking is suppressed. assert_eq!(fx.sent_len(), 2); assert_eq!(fx.channel_states().len(), 1); - assert!(fx.channel_states().contains_key(&channel_id)); + // The tracked channel id derives from the first funding transaction's + // outpoint. + assert!(fx.channel_states().contains_key(&funding_channel_id())); } #[test] @@ -1411,11 +1402,7 @@ fn execute_send_channel_ready() { }, ]); - // We also need to queue a `funding_signed`, since the instructions reused - // by this test expect one to be present in the receive queue. - let mut fx = Fixture::new() - .with_negotiation(sample_funding_negotiation()) - .queue(&funding_signed_reply(channel_id)); + let mut fx = recv_funding_signed_fixture(); fx.run(&Program { instructions: instrs, }); diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index dcf52890..a76226fa 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -361,17 +361,19 @@ pub fn channel_ready_reply(second_per_commitment_point: PublicKey) -> Message { }) } -/// A fixture with the funding negotiation seeded and both target replies -/// queued, plus the target's per-commitment point for the assertions. -pub fn recv_channel_ready_fixture() -> (Fixture, PublicKey) { - let target_pcp = sample_pubkey(1); - - // We also need to queue a `funding_signed`, since the instructions reused - // by these tests expect one to be present in the receive queue. - let fx = Fixture::new() +/// A fixture with the funding negotiation seeded and the target's +/// `funding_signed` queued, as the funding-flow instructions expect. +pub fn recv_funding_signed_fixture() -> Fixture { + Fixture::new() .with_negotiation(sample_funding_negotiation()) .queue(&funding_signed_reply(funding_channel_id())) - .queue(&channel_ready_reply(target_pcp)); +} + +/// A [`recv_funding_signed_fixture`] with the target's `channel_ready` queued +/// too, plus the per-commitment point it carries for the assertions. +pub fn recv_channel_ready_fixture() -> (Fixture, PublicKey) { + let target_pcp = sample_pubkey(1); + let fx = recv_funding_signed_fixture().queue(&channel_ready_reply(target_pcp)); (fx, target_pcp) }