From 7526d5034f39a4ba5e4c6140a83438765891fb3b Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Thu, 10 Sep 2026 16:24:58 -0500 Subject: [PATCH 1/7] smite-scenarios: build open_channel programs with ProgramBuilder ProgramBuilder validates programs as they're constructed and automatically returns indices for each instruction, so we no longer need to manually check that programs are valid or count instructions to find specific open_channel fields. The 20 BuildOpenChannel inputs are named in OpenChannelVars, which allows tests to easily and readably override inputs without patching instructions at fixed offsets in the program. Additionally SampleOpenChannel and build_open_channel helpers are introduced to simplify construction of custom open_channel programs and verification of the actual open_channel messages sent by the executor. --- smite-scenarios/src/executor/tests.rs | 108 +++------------ smite-scenarios/src/executor/tests/harness.rs | 80 ++++++++++- .../src/executor/tests/programs.rs | 126 +++++++++++++++++- 3 files changed, 220 insertions(+), 94 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 93eb76e8..13ef4523 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -14,46 +14,16 @@ use smite_ir::operation::ShutdownScriptVariant; // -- execute() tests -- +// All fields of the sent `open_channel` must match what we expected. #[test] fn execute_load_build_send() { - let pk = sample_pubkey(1); - let mut instrs = open_channel_instructions(); - instrs.push(Instruction { - operation: Operation::BuildOpenChannel, - inputs: (0..20).collect(), - }); - instrs.push(Instruction { - operation: Operation::SendOpenChannel, - inputs: vec![20], - }); + let oc = announced_open_channel(); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&send_open_channel_program(&oc)); 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); - assert_eq!(oc.push_msat, 0); - assert_eq!(oc.dust_limit_satoshis, 546); - assert_eq!(oc.max_htlc_value_in_flight_msat, 100_000_000); - assert_eq!(oc.channel_reserve_satoshis, 10_000); - assert_eq!(oc.htlc_minimum_msat, 1_000); - assert_eq!(oc.feerate_per_kw, 253); - assert_eq!(oc.to_self_delay, 144); - assert_eq!(oc.max_accepted_htlcs, 483); - assert_eq!(oc.funding_pubkey, pk); - assert_eq!(oc.revocation_basepoint, pk); - assert_eq!(oc.payment_basepoint, pk); - assert_eq!(oc.delayed_payment_basepoint, pk); - assert_eq!(oc.htlc_basepoint, pk); - assert_eq!(oc.first_per_commitment_point, pk); - assert_eq!(oc.channel_flags, 1); - assert_eq!(oc.tlvs.upfront_shutdown_script, Some(vec![])); - assert_eq!(oc.tlvs.channel_type, Some(vec![0x40, 0x10, 0x00])); + assert_eq!(fx.sent::(0), oc.message); } #[test] @@ -409,75 +379,29 @@ fn execute_build_announcement_signatures() { #[test] fn execute_build_open_channel_with_tlvs() { - let mut instrs = open_channel_instructions(); - instrs[18] = Instruction { - operation: Operation::LoadBytes(vec![0x00, 0x14, 0xab]), - inputs: vec![], - }; - instrs[19] = Instruction { - operation: Operation::LoadFeatures(vec![0x01, 0x02]), - inputs: vec![], + let mut oc = announced_open_channel(); + oc.message.tlvs = OpenChannelTlvs { + upfront_shutdown_script: Some(vec![0x00, 0x14, 0xab]), + channel_type: Some(vec![0x01, 0x02]), }; - instrs.push(Instruction { - operation: Operation::BuildOpenChannel, - inputs: (0..20).collect(), - }); - instrs.push(Instruction { - operation: Operation::SendOpenChannel, - inputs: vec![20], - }); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&send_open_channel_program(&oc)); - let oc: OpenChannel = fx.sent(0); - assert_eq!( - oc.tlvs.upfront_shutdown_script, - Some(vec![0x00, 0x14, 0xab]) - ); - assert_eq!(oc.tlvs.channel_type, Some(vec![0x01, 0x02])); + assert_eq!(fx.sent::(0), oc.message); } +// Every pubkey of the `open_channel` is derived from one private key, so the +// message arriving with the expected pubkeys means `DerivePoint` produced the +// correct Point variable. #[test] fn execute_derive_point() { - let mut instrs = vec![ - Instruction { - operation: Operation::LoadPrivateKey([0x11; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::DerivePoint, - inputs: vec![0], - }, - ]; - - // Use the derived point in a BuildOpenChannel to verify it produced a - // valid Point variable. - let base = instrs.len(); - instrs.extend(open_channel_instructions()); - // Replace funding_pubkey (input 11) with the derived point (v1). - let mut build_inputs: Vec = (base..base + 20).collect(); - build_inputs[11] = 1; - instrs.push(Instruction { - operation: Operation::BuildOpenChannel, - inputs: build_inputs, - }); - instrs.push(Instruction { - operation: Operation::SendOpenChannel, - inputs: vec![base + 20], - }); + let oc = SampleOpenChannel::new(PointSource::Secret([0x11; 32])); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&send_open_channel_program(&oc)); - 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); + assert_eq!(fx.sent::(0), oc.message); } #[test] diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index a76226fa..f6e3ea44 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, FromMessage}; +use smite::bolt::{AcceptChannelTlvs, ChannelTypeVariant, FromMessage}; use std::collections::VecDeque; use std::str::FromStr; @@ -306,6 +306,84 @@ pub fn sample_accept_channel() -> AcceptChannel { } } +// -- Sample open_channel -- + +/// How a program produces the six pubkeys of an `open_channel`. +/// +/// The IR cannot load a literal point, so program builders need a `PointSource` +/// to derive them. +#[derive(Clone, Copy)] +pub enum PointSource { + /// The target's pubkey, read from the program context. + TargetContext, + /// A pubkey derived from this private key. + Secret([u8; 32]), +} + +impl PointSource { + /// The pubkey a program built from this source should derive. + pub fn pubkey(self) -> PublicKey { + match self { + Self::TargetContext => sample_context().target_pubkey, + Self::Secret(bytes) => { + let sk = SecretKey::from_slice(&bytes).expect("valid private key"); + PublicKey::from_secret_key(&Secp256k1::new(), &sk) + } + } + } +} + +/// An `open_channel` a test sends, paired with the source of its pubkeys. +/// +/// `message` is exactly what the executor must put on the wire, so tests can +/// compare it to what the executor actually sent. +pub struct SampleOpenChannel { + /// What the executor must send. + pub message: OpenChannel, + /// How the program produces the six pubkeys of `message`. + pub points: PointSource, +} + +impl SampleOpenChannel { + /// A sample `open_channel` whose pubkeys come from `points`. + pub fn new(points: PointSource) -> Self { + let pubkey = points.pubkey(); + Self { + message: OpenChannel { + chain_hash: sample_context().chain_hash, + temporary_channel_id: TemporaryChannelId::new([0xbb; 32]), + funding_satoshis: 100_000, + push_msat: 0, + dust_limit_satoshis: 546, + max_htlc_value_in_flight_msat: 100_000_000, + channel_reserve_satoshis: 10_000, + htlc_minimum_msat: 1_000, + feerate_per_kw: 253, + to_self_delay: 144, + max_accepted_htlcs: 483, + funding_pubkey: pubkey, + revocation_basepoint: pubkey, + payment_basepoint: pubkey, + delayed_payment_basepoint: pubkey, + htlc_basepoint: pubkey, + first_per_commitment_point: pubkey, + channel_flags: 1, + tlvs: OpenChannelTlvs { + upfront_shutdown_script: Some(vec![]), + channel_type: Some(ChannelTypeVariant::Anchors.encode()), + }, + }, + points, + } + } +} + +/// The `open_channel` the message-building tests send: an announced channel +/// carrying the target's pubkey in every pubkey field. +pub fn announced_open_channel() -> SampleOpenChannel { + SampleOpenChannel::new(PointSource::TargetContext) +} + // -- Funding fixture -- // // The funding keys are chosen from BOLT 3 test vectors. All other constants are diff --git a/smite-scenarios/src/executor/tests/programs.rs b/smite-scenarios/src/executor/tests/programs.rs index 209cd463..aca31b51 100644 --- a/smite-scenarios/src/executor/tests/programs.rs +++ b/smite-scenarios/src/executor/tests/programs.rs @@ -2,9 +2,133 @@ //! //! Each helper returns the instructions for one flow. -use super::harness::{acceptor_funding_sk, opener_funding_sk}; +use super::harness::{PointSource, SampleOpenChannel, acceptor_funding_sk, opener_funding_sk}; use crate::executor::*; use smite_ir::Instruction; +use smite_ir::builder::ProgramBuilder; + +// -- open_channel -- + +/// The variables a `BuildOpenChannel` fragment produces, named so that callers +/// don't need to manually track instruction indices. +#[derive(Clone, Copy)] +pub struct OpenChannelVars { + pub chain_hash: usize, + pub temporary_channel_id: usize, + pub funding_satoshis: usize, + pub push_msat: usize, + pub dust_limit_satoshis: usize, + pub max_htlc_value_in_flight_msat: usize, + pub channel_reserve_satoshis: usize, + pub htlc_minimum_msat: usize, + pub feerate_per_kw: usize, + pub to_self_delay: usize, + pub max_accepted_htlcs: usize, + pub funding_pubkey: usize, + pub revocation_basepoint: usize, + pub payment_basepoint: usize, + pub delayed_payment_basepoint: usize, + pub htlc_basepoint: usize, + pub first_per_commitment_point: usize, + pub channel_flags: usize, + pub upfront_shutdown_script: usize, + pub channel_type: usize, + /// The `BuildOpenChannel` result. + pub built: usize, +} + +impl OpenChannelVars { + /// The `BuildOpenChannel` inputs in order. + pub fn build_inputs(&self) -> [usize; 20] { + [ + self.chain_hash, + self.temporary_channel_id, + self.funding_satoshis, + self.push_msat, + self.dust_limit_satoshis, + self.max_htlc_value_in_flight_msat, + self.channel_reserve_satoshis, + self.htlc_minimum_msat, + self.feerate_per_kw, + self.to_self_delay, + self.max_accepted_htlcs, + self.funding_pubkey, + self.revocation_basepoint, + self.payment_basepoint, + self.delayed_payment_basepoint, + self.htlc_basepoint, + self.first_per_commitment_point, + self.channel_flags, + self.upfront_shutdown_script, + self.channel_type, + ] + } +} + +/// Emits inputs needed to construct `oc.message`, then the `BuildOpenChannel` +/// consuming them. +/// +/// Uses `oc.points` to derive all six pubkeys and loads the chain hash from the +/// program context, ignoring those corresponding fields in `oc.message`. +pub fn build_open_channel(b: &mut ProgramBuilder, oc: &SampleOpenChannel) -> OpenChannelVars { + let msg = &oc.message; + let pubkey = match oc.points { + PointSource::TargetContext => b.append(Operation::LoadTargetPubkeyFromContext, &[]), + PointSource::Secret(bytes) => { + let sk = b.append(Operation::LoadPrivateKey(bytes), &[]); + b.append(Operation::DerivePoint, &[sk]) + } + }; + + let mut vars = OpenChannelVars { + chain_hash: b.append(Operation::LoadChainHashFromContext, &[]), + temporary_channel_id: b.append(Operation::LoadChannelId(msg.temporary_channel_id.0), &[]), + funding_satoshis: b.append(Operation::LoadAmount(msg.funding_satoshis), &[]), + push_msat: b.append(Operation::LoadAmount(msg.push_msat), &[]), + dust_limit_satoshis: b.append(Operation::LoadAmount(msg.dust_limit_satoshis), &[]), + max_htlc_value_in_flight_msat: b.append( + Operation::LoadAmount(msg.max_htlc_value_in_flight_msat), + &[], + ), + channel_reserve_satoshis: b + .append(Operation::LoadAmount(msg.channel_reserve_satoshis), &[]), + htlc_minimum_msat: b.append(Operation::LoadAmount(msg.htlc_minimum_msat), &[]), + feerate_per_kw: b.append(Operation::LoadFeeratePerKw(msg.feerate_per_kw), &[]), + to_self_delay: b.append(Operation::LoadU16(msg.to_self_delay), &[]), + max_accepted_htlcs: b.append(Operation::LoadU16(msg.max_accepted_htlcs), &[]), + funding_pubkey: pubkey, + revocation_basepoint: pubkey, + payment_basepoint: pubkey, + delayed_payment_basepoint: pubkey, + htlc_basepoint: pubkey, + first_per_commitment_point: pubkey, + channel_flags: b.append(Operation::LoadU8(msg.channel_flags), &[]), + upfront_shutdown_script: b.append( + Operation::LoadBytes(msg.tlvs.upfront_shutdown_script.clone().unwrap_or_default()), + &[], + ), + channel_type: b.append( + Operation::LoadFeatures(msg.tlvs.channel_type.clone().unwrap_or_default()), + &[], + ), + // Filled in as soon as the inputs it consumes exist. + built: 0, + }; + vars.built = b.append(Operation::BuildOpenChannel, &vars.build_inputs()); + + vars +} + +/// A program that builds and sends `oc`. +pub fn send_open_channel_program(oc: &SampleOpenChannel) -> Program { + let mut b = ProgramBuilder::new(); + let vars = build_open_channel(&mut b, oc); + b.append(Operation::SendOpenChannel, &[vars.built]); + + b.build() +} + +// -- Instruction fragments -- /// Builds the 20 `open_channel` input instructions in wire order. pub fn open_channel_instructions() -> Vec { From 9aa72b70d5f9e665229a5c19bd237b9db14a9eb0 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Fri, 11 Sep 2026 10:18:34 -0500 Subject: [PATCH 2/7] smite-scenarios: build gossip programs with ProgramBuilder ProgramBuilder automatically returns indices for each instruction, allowing us to name each variable and significantly improve readability. Each gossip program is used once, so there's no benefit to adding helpers for them at this point. --- smite-scenarios/src/executor/tests.rs | 308 +++++++++----------------- 1 file changed, 105 insertions(+), 203 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 13ef4523..4f7c8773 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -10,6 +10,7 @@ use harness::*; use programs::*; use smite::bolt::{AcceptChannelTlvs, GossipTimestampFilter, Init, Ping}; use smite_ir::Instruction; +use smite_ir::builder::ProgramBuilder; use smite_ir::operation::ShutdownScriptVariant; // -- execute() tests -- @@ -33,58 +34,39 @@ fn execute_build_channel_announcement() { let bitcoin_sk_1_bytes = [0x33; 32]; let bitcoin_sk_2_bytes = [0x44; 32]; let scid = ShortChannelId::new(539_268, 845, 1); - let features = vec![0x01, 0x02]; - - let instrs = vec![ - Instruction { - operation: Operation::LoadFeatures(features.clone()), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadChainHashFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadShortChannelId(scid.as_u64()), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey(node_sk_1_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey(node_sk_2_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey(bitcoin_sk_1_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey(bitcoin_sk_2_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::BuildChannelAnnouncement, - inputs: vec![0, 1, 2, 3, 4, 5, 6], - }, - Instruction { - operation: Operation::SendMessage, - inputs: vec![7], - }, - ]; + let features_bytes = vec![0x01, 0x02]; + + let mut b = ProgramBuilder::new(); + let features = b.append(Operation::LoadFeatures(features_bytes.clone()), &[]); + let chain_hash = b.append(Operation::LoadChainHashFromContext, &[]); + let short_channel_id = b.append(Operation::LoadShortChannelId(scid.as_u64()), &[]); + let node_sk_1 = b.append(Operation::LoadPrivateKey(node_sk_1_bytes), &[]); + let node_sk_2 = b.append(Operation::LoadPrivateKey(node_sk_2_bytes), &[]); + let bitcoin_sk_1 = b.append(Operation::LoadPrivateKey(bitcoin_sk_1_bytes), &[]); + let bitcoin_sk_2 = b.append(Operation::LoadPrivateKey(bitcoin_sk_2_bytes), &[]); + let announcement = b.append( + Operation::BuildChannelAnnouncement, + &[ + features, + chain_hash, + short_channel_id, + node_sk_1, + node_sk_2, + bitcoin_sk_1, + bitcoin_sk_2, + ], + ); + b.append(Operation::SendMessage, &[announcement]); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); 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()); - assert_eq!(ca.features, features); + assert_eq!(ca.features, features_bytes); assert_eq!(ca.chain_hash, sample_context().chain_hash); assert_eq!(ca.short_channel_id, scid); assert_eq!(ca.node_id_1, pk(&node_sk_1_bytes)); @@ -102,39 +84,21 @@ fn execute_build_node_announcement() { let rgb_color = [0x11, 0x22, 0x33]; let mut alias = [0u8; 32]; alias[..5].copy_from_slice(b"smite"); - let addresses = vec![0xaa, 0xbb, 0xcc]; - - let instrs = vec![ - Instruction { - operation: Operation::LoadPrivateKey(sk_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadFeatures(vec![0x01, 0x02]), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadTimestamp(1_700_000_000), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadBytes(addresses.clone()), - inputs: vec![], - }, - Instruction { - operation: Operation::BuildNodeAnnouncement { rgb_color, alias }, - inputs: vec![0, 1, 2, 3], - }, - Instruction { - operation: Operation::SendMessage, - inputs: vec![4], - }, - ]; + let addresses_bytes = vec![0xaa, 0xbb, 0xcc]; + + let mut b = ProgramBuilder::new(); + let node_sk = b.append(Operation::LoadPrivateKey(sk_bytes), &[]); + let features = b.append(Operation::LoadFeatures(vec![0x01, 0x02]), &[]); + let timestamp = b.append(Operation::LoadTimestamp(1_700_000_000), &[]); + let addresses = b.append(Operation::LoadBytes(addresses_bytes.clone()), &[]); + let announcement = b.append( + Operation::BuildNodeAnnouncement { rgb_color, alias }, + &[node_sk, features, timestamp, addresses], + ); + b.append(Operation::SendMessage, &[announcement]); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); assert_eq!(fx.sent_len(), 1); let na: NodeAnnouncement = fx.sent(0); @@ -147,7 +111,7 @@ fn execute_build_node_announcement() { assert_eq!(na.timestamp, 1_700_000_000); assert_eq!(na.rgb_color, rgb_color); assert_eq!(na.alias, alias); - assert_eq!(na.addresses, addresses); + assert_eq!(na.addresses, addresses_bytes); assert!(na.extra.is_empty()); assert!(na.verify()); } @@ -158,65 +122,38 @@ fn execute_build_channel_update() { sk_bytes[31] = 0x42; let scid = ShortChannelId::new(538_532, 845, 1); - let instrs = vec![ - Instruction { - operation: Operation::LoadPrivateKey(sk_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadChainHashFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadShortChannelId(scid.as_u64()), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadTimestamp(1_715_000_000), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadU8(0x01), // message_flags: must_be_one - inputs: vec![], - }, - Instruction { - operation: Operation::LoadU8(0x00), // channel_flags - inputs: vec![], - }, - Instruction { - operation: Operation::LoadU16(144), // cltv_expiry_delta - inputs: vec![], - }, - Instruction { - operation: Operation::LoadAmount(1_000), // htlc_minimum_msat - inputs: vec![], - }, - Instruction { - operation: Operation::LoadForwardingFee(1_000), // fee_base_msat - inputs: vec![], - }, - Instruction { - operation: Operation::LoadForwardingFee(100), // fee_proportional_millionths - inputs: vec![], - }, - Instruction { - operation: Operation::LoadAmount(99_000_000), // htlc_maximum_msat - inputs: vec![], - }, - Instruction { - operation: Operation::BuildChannelUpdate, - inputs: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - }, - Instruction { - operation: Operation::SendMessage, - inputs: vec![11], - }, - ]; + let mut b = ProgramBuilder::new(); + let node_sk = b.append(Operation::LoadPrivateKey(sk_bytes), &[]); + let chain_hash = b.append(Operation::LoadChainHashFromContext, &[]); + let short_channel_id = b.append(Operation::LoadShortChannelId(scid.as_u64()), &[]); + let timestamp = b.append(Operation::LoadTimestamp(1_715_000_000), &[]); + let message_flags = b.append(Operation::LoadU8(0x01), &[]); // must_be_one + let channel_flags = b.append(Operation::LoadU8(0x00), &[]); + let cltv_expiry_delta = b.append(Operation::LoadU16(144), &[]); + let htlc_minimum_msat = b.append(Operation::LoadAmount(1_000), &[]); + let fee_base_msat = b.append(Operation::LoadForwardingFee(1_000), &[]); + let fee_proportional_millionths = b.append(Operation::LoadForwardingFee(100), &[]); + let htlc_maximum_msat = b.append(Operation::LoadAmount(99_000_000), &[]); + let update = b.append( + Operation::BuildChannelUpdate, + &[ + node_sk, + chain_hash, + short_channel_id, + timestamp, + message_flags, + channel_flags, + cltv_expiry_delta, + htlc_minimum_msat, + fee_base_msat, + fee_proportional_millionths, + htlc_maximum_msat, + ], + ); + b.append(Operation::SendMessage, &[update]); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); assert_eq!(fx.sent_len(), 1); let cu: ChannelUpdate = fx.sent(0); @@ -240,7 +177,6 @@ fn execute_build_channel_update() { } #[test] -#[allow(clippy::too_many_lines)] fn execute_build_announcement_signatures() { let node_sk_1_bytes = [0x11; 32]; let node_sk_2_bytes = [0x22; 32]; @@ -248,76 +184,42 @@ fn execute_build_announcement_signatures() { let bitcoin_sk_2_bytes = [0x44; 32]; let channel_id_bytes = [0xbb; 32]; let scid = ShortChannelId::new(539_268, 845, 1); - let features = vec![0x01, 0x02]; - - // Instruction layout: - // v0 = LoadChannelId - // v1 = LoadFeatures - // v2 = LoadChainHashFromContext - // v3 = LoadShortChannelId - // v4 = LoadPrivateKey(node_sk_1) -- our node signing key - // v5 = LoadPrivateKey(node_sk_2) -- target's node key (derive pubkey from) - // v6 = DerivePoint(v5) -- node_id_2 (target's node pubkey) - // v7 = LoadPrivateKey(bitcoin_sk_1) -- our bitcoin signing key - // v8 = LoadPrivateKey(bitcoin_sk_2) -- target's bitcoin key (derive pubkey from) - // v9 = DerivePoint(v8) -- bitcoin_key_2 (target's bitcoin pubkey) - // v10 = BuildAnnouncementSignatures(v0, v1, v2, v3, v4, v6, v7, v9) - // v11 = SendMessage(v10) - let instrs = vec![ - Instruction { - operation: Operation::LoadChannelId(channel_id_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadFeatures(features.clone()), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadChainHashFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadShortChannelId(scid.as_u64()), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey(node_sk_1_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey(node_sk_2_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::DerivePoint, - inputs: vec![5], - }, - Instruction { - operation: Operation::LoadPrivateKey(bitcoin_sk_1_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey(bitcoin_sk_2_bytes), - inputs: vec![], - }, - Instruction { - operation: Operation::DerivePoint, - inputs: vec![8], - }, - Instruction { - operation: Operation::BuildAnnouncementSignatures, - inputs: vec![0, 1, 2, 3, 4, 6, 7, 9], - }, - Instruction { - operation: Operation::SendMessage, - inputs: vec![10], - }, - ]; + let features_bytes = vec![0x01, 0x02]; + + // We sign with our own keys and carry the target's as points, so the + // target's keys are loaded only to derive them. + let program = { + let mut b = ProgramBuilder::new(); + let channel_id = b.append(Operation::LoadChannelId(channel_id_bytes), &[]); + let features = b.append(Operation::LoadFeatures(features_bytes.clone()), &[]); + let chain_hash = b.append(Operation::LoadChainHashFromContext, &[]); + let short_channel_id = b.append(Operation::LoadShortChannelId(scid.as_u64()), &[]); + let node_sk_1 = b.append(Operation::LoadPrivateKey(node_sk_1_bytes), &[]); + let node_sk_2 = b.append(Operation::LoadPrivateKey(node_sk_2_bytes), &[]); + let node_id_2 = b.append(Operation::DerivePoint, &[node_sk_2]); + let bitcoin_sk_1 = b.append(Operation::LoadPrivateKey(bitcoin_sk_1_bytes), &[]); + let bitcoin_sk_2 = b.append(Operation::LoadPrivateKey(bitcoin_sk_2_bytes), &[]); + let bitcoin_key_2 = b.append(Operation::DerivePoint, &[bitcoin_sk_2]); + let ann_sigs = b.append( + Operation::BuildAnnouncementSignatures, + &[ + channel_id, + features, + chain_hash, + short_channel_id, + node_sk_1, + node_id_2, + bitcoin_sk_1, + bitcoin_key_2, + ], + ); + b.append(Operation::SendMessage, &[ann_sigs]); + + b.build() + }; let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&program); assert_eq!(fx.sent_len(), 1); let ann_sigs: AnnouncementSignatures = fx.sent(0); @@ -357,7 +259,7 @@ fn execute_build_announcement_signatures() { node_signature_2: placeholder, bitcoin_signature_1: placeholder, bitcoin_signature_2: placeholder, - features, + features: features_bytes, chain_hash: sample_context().chain_hash, short_channel_id: scid, node_id_1: n1, From 1b093dc0d5d35d5e951a0aed59715d5b829d446b Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Fri, 11 Sep 2026 11:20:11 -0500 Subject: [PATCH 3/7] smite-scenarios: build funding transactions with ProgramBuilder Adds create_funding_tx and create_funding_tx_with helpers so that tests don't need to manually patch instructions anymore and can add broadcasts or other operations after the funding transaction is created. --- smite-scenarios/src/executor/tests.rs | 40 +++++++---------- .../src/executor/tests/programs.rs | 43 +++++++++++++++++++ 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 4f7c8773..de5e422a 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -826,10 +826,12 @@ fn execute_mine_blocks_wrong_input() { #[test] fn execute_create_and_broadcast_tx() { + let mut b = ProgramBuilder::new(); + let funding_tx = create_funding_tx(&mut b); + b.append(Operation::BroadcastTransaction, &[funding_tx]); + let mut fx = Fixture::new(); - fx.run(&Program { - instructions: create_and_broadcast_tx_instructions(), - }); + fx.run(&b.build()); assert_eq!(fx.bitcoin().broadcast_calls.len(), 1); let broadcast_tx = &fx.bitcoin().broadcast_calls[0]; @@ -937,26 +939,15 @@ fn execute_lookup_short_channel_id_unconfirmed_returns_sentinel() { #[test] fn execute_broadcast_dedupes_rejected_tx_in_private_mempool() { // Fund with a dust amount so the built funding tx carries a below-dust - // output. - let mut instrs = create_and_broadcast_tx_instructions(); - instrs[4] = Instruction { - operation: Operation::LoadAmount(200), - inputs: vec![], - }; - let funding_tx = instrs.len() - 2; - instrs.push(Instruction { - operation: Operation::BroadcastTransaction, - inputs: vec![funding_tx], - }); - instrs.push(Instruction { - operation: Operation::MineBlocks(1), - inputs: vec![], - }); + // output, and broadcast it twice. + let mut b = ProgramBuilder::new(); + let funding_tx = create_funding_tx_with(&mut b, 200, 15_000); + b.append(Operation::BroadcastTransaction, &[funding_tx]); + b.append(Operation::BroadcastTransaction, &[funding_tx]); + b.append(Operation::MineBlocks(1), &[]); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); assert_eq!(fx.bitcoin().broadcast_calls.len(), 2); assert_eq!( @@ -976,11 +967,12 @@ fn execute_create_funding_transaction_insufficient_funds() { amount: Amount::from_sat(1_000), ..sample_utxo() }; + let mut b = ProgramBuilder::new(); + create_funding_tx(&mut b); + let err = Fixture::new() .with_utxos(vec![small_utxo]) - .run_err(&Program { - instructions: create_and_broadcast_tx_instructions(), - }); + .run_err(&b.build()); let ExecuteError::InsufficientFunds(funds_err) = err else { panic!("expected InsufficientFunds, got {err:?}"); }; diff --git a/smite-scenarios/src/executor/tests/programs.rs b/smite-scenarios/src/executor/tests/programs.rs index aca31b51..aadade0e 100644 --- a/smite-scenarios/src/executor/tests/programs.rs +++ b/smite-scenarios/src/executor/tests/programs.rs @@ -128,6 +128,49 @@ pub fn send_open_channel_program(oc: &SampleOpenChannel) -> Program { b.build() } +// -- Funding transaction -- + +/// The funding amount and feerate the funding-flow programs negotiate. +const FUNDING_SATOSHIS: u64 = 10_000_000; +const FUNDING_FEERATE_PER_KW: u32 = 15_000; + +/// Emits a `CreateFundingTransaction` for [`FUNDING_SATOSHIS`] at +/// [`FUNDING_FEERATE_PER_KW`], returning its `FundingTransaction` variable. +pub fn create_funding_tx(b: &mut ProgramBuilder) -> usize { + create_funding_tx_with(b, FUNDING_SATOSHIS, FUNDING_FEERATE_PER_KW) +} + +/// Emits a `CreateFundingTransaction` between the opener and acceptor funding +/// keys, returning its `FundingTransaction` variable. +pub fn create_funding_tx_with( + b: &mut ProgramBuilder, + funding_satoshis: u64, + feerate_per_kw: u32, +) -> usize { + let opener_privkey = b.append( + Operation::LoadPrivateKey(opener_funding_sk().secret_bytes()), + &[], + ); + let opener_pubkey = b.append(Operation::DerivePoint, &[opener_privkey]); + let acceptor_privkey = b.append( + Operation::LoadPrivateKey(acceptor_funding_sk().secret_bytes()), + &[], + ); + let acceptor_pubkey = b.append(Operation::DerivePoint, &[acceptor_privkey]); + let funding_satoshis = b.append(Operation::LoadAmount(funding_satoshis), &[]); + let feerate_per_kw = b.append(Operation::LoadFeeratePerKw(feerate_per_kw), &[]); + + b.append( + Operation::CreateFundingTransaction, + &[ + opener_pubkey, + acceptor_pubkey, + funding_satoshis, + feerate_per_kw, + ], + ) +} + // -- Instruction fragments -- /// Builds the 20 `open_channel` input instructions in wire order. From 13aaf16065d8dac6866d050d85b886401150054f Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Fri, 11 Sep 2026 11:39:11 -0500 Subject: [PATCH 4/7] smite-scenarios: build channel_announcements with ProgramBuilder Replaces channel_announcement_from_scid_instructions() with send_channel_announcement(), which eliminates all the hard-to-understand instruction offset calculations by relying on ProgramBuilder. --- smite-scenarios/src/executor/tests.rs | 68 +++------------ .../src/executor/tests/programs.rs | 82 ++++++------------- 2 files changed, 38 insertions(+), 112 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index de5e422a..6ad18e1c 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -844,25 +844,15 @@ 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 mut instrs = create_and_broadcast_tx_instructions(); - instrs.push(Instruction { - operation: Operation::MineBlocks(6), - inputs: vec![], - }); - instrs.push(Instruction { - // Feed the FundingTransaction produced by - // CreateFundingTransaction (instruction 6) into the lookup. The - // resulting ShortChannelId is variable 9. - operation: Operation::LookupShortChannelId, - inputs: vec![6], - }); - // Build and send a channel_announcement carrying the looked-up SCID. - instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 9)); + let mut b = ProgramBuilder::new(); + let funding_tx = create_funding_tx(&mut b); + b.append(Operation::BroadcastTransaction, &[funding_tx]); + b.append(Operation::MineBlocks(6), &[]); + let scid = b.append(Operation::LookupShortChannelId, &[funding_tx]); + send_channel_announcement(&mut b, scid); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); assert_eq!(fx.bitcoin().mine_blocks_calls, vec![6]); // The executor must have queried the mock with the broadcast @@ -885,47 +875,13 @@ fn execute_lookup_short_channel_id_confirmed() { fn execute_lookup_short_channel_id_unconfirmed_returns_sentinel() { // No BroadcastTransaction and no MineBlocks: the mock reports zero // confirmations and get_transaction_block_position returns None. - let mut instrs = vec![ - Instruction { - operation: Operation::LoadPrivateKey([1u8; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::DerivePoint, - inputs: vec![0], - }, - Instruction { - operation: Operation::LoadPrivateKey([2u8; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::DerivePoint, - inputs: vec![2], - }, - Instruction { - operation: Operation::LoadAmount(10_000_000), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadFeeratePerKw(15_000), - inputs: vec![], - }, - Instruction { - operation: Operation::CreateFundingTransaction, - inputs: vec![1, 3, 4, 5], - }, - // The looked-up SCID is variable 7. - Instruction { - operation: Operation::LookupShortChannelId, - inputs: vec![6], - }, - ]; - instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 7)); + let mut b = ProgramBuilder::new(); + let funding_tx = create_funding_tx(&mut b); + let scid = b.append(Operation::LookupShortChannelId, &[funding_tx]); + send_channel_announcement(&mut b, scid); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); // The mock was queried but returned None (zero confirmations), so the // executor took the sentinel path without panicking. diff --git a/smite-scenarios/src/executor/tests/programs.rs b/smite-scenarios/src/executor/tests/programs.rs index aadade0e..b303c7bc 100644 --- a/smite-scenarios/src/executor/tests/programs.rs +++ b/smite-scenarios/src/executor/tests/programs.rs @@ -171,6 +171,32 @@ pub fn create_funding_tx_with( ) } +// -- Gossip -- + +/// Emits a `channel_announcement` carrying the `ShortChannelId` variable +/// `scid`, and sends it. +pub fn send_channel_announcement(b: &mut ProgramBuilder, scid: usize) { + let features = b.append(Operation::LoadFeatures(vec![0x01, 0x02]), &[]); + let chain_hash = b.append(Operation::LoadChainHashFromContext, &[]); + let node_sk_1 = b.append(Operation::LoadPrivateKey([0x11; 32]), &[]); + let node_sk_2 = b.append(Operation::LoadPrivateKey([0x22; 32]), &[]); + let bitcoin_sk_1 = b.append(Operation::LoadPrivateKey([0x33; 32]), &[]); + let bitcoin_sk_2 = b.append(Operation::LoadPrivateKey([0x44; 32]), &[]); + let announcement = b.append( + Operation::BuildChannelAnnouncement, + &[ + features, + chain_hash, + scid, + node_sk_1, + node_sk_2, + bitcoin_sk_1, + bitcoin_sk_2, + ], + ); + b.append(Operation::SendMessage, &[announcement]); +} + // -- Instruction fragments -- /// Builds the 20 `open_channel` input instructions in wire order. @@ -299,62 +325,6 @@ pub fn create_and_broadcast_tx_instructions() -> Vec { ] } -/// Builds instructions that construct and send a `channel_announcement` -/// referencing the `ShortChannelId` produced at variable index `scid_var`. -/// -/// `base` is the variable index the first appended instruction will occupy -/// (i.e. the current program length), used to wire up the inputs to -/// `BuildChannelAnnouncement`. -pub fn channel_announcement_from_scid_instructions( - base: usize, - scid_var: usize, -) -> Vec { - vec![ - Instruction { - operation: Operation::LoadFeatures(vec![0x01, 0x02]), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadChainHashFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey([0x11; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey([0x22; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey([0x33; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadPrivateKey([0x44; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::BuildChannelAnnouncement, - // features, chain_hash, short_channel_id, node_sk_1, node_sk_2, - // bitcoin_sk_1, bitcoin_sk_2. - inputs: vec![ - base, - base + 1, - scid_var, - base + 2, - base + 3, - base + 4, - base + 5, - ], - }, - Instruction { - operation: Operation::SendMessage, - inputs: vec![base + 6], - }, - ] -} - pub fn send_open_channel_instructions() -> Vec { let mut instructions = open_channel_instructions(); instructions.extend([ From 3a5d835e44286eafdd1c7b8c954b5c699c5f89a0 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Fri, 11 Sep 2026 12:10:54 -0500 Subject: [PATCH 5/7] smite-scenarios: negotiate channels with ProgramBuilder Adds helpers for building programs that send open_channel and optionally receive the accept_channel response, while allowing customization of inputs and parameters. --- smite-scenarios/src/executor/tests.rs | 184 +++++------------- .../src/executor/tests/programs.rs | 48 ++++- 2 files changed, 91 insertions(+), 141 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 6ad18e1c..9afe5370 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -328,18 +328,13 @@ fn execute_recv_and_extract_all_fields() { AcceptChannelField::ChannelType, ]; - let mut instrs = send_open_channel_instructions(); - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); - let accept_channel_idx = instrs.len() - 1; + let mut b = ProgramBuilder::new(); + let negotiated = negotiate_channel(&mut b, &announced_open_channel()); for field in fields { - instrs.push(Instruction { - operation: Operation::ExtractAcceptChannel(field), - inputs: vec![accept_channel_idx], - }); + b.append( + Operation::ExtractAcceptChannel(field), + &[negotiated.accept_channel], + ); } // TODO: Once we add IR support for building accept_channel messages, @@ -348,25 +343,14 @@ fn execute_recv_and_extract_all_fields() { Fixture::new() .queue(&Message::AcceptChannel(sample_accept_channel())) - .run(&Program { - instructions: instrs, - }); + .run(&b.build()); } #[test] fn execute_recv_unexpected_message() { - let mut instrs = send_open_channel_instructions(); - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); - let err = Fixture::new() .queue(&Message::Init(Init::empty())) - .run_err(&Program { - instructions: instrs, - }); + .run_err(&negotiate_channel_program(&announced_open_channel())); assert!(matches!( err, ExecuteError::UnexpectedMessage { @@ -380,18 +364,9 @@ fn execute_recv_unexpected_message() { fn execute_recv_peer_error() { let peer_error = smite::bolt::Error::all_channels("Wrong channel id in channel_ready"); - let mut instrs = send_open_channel_instructions(); - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); - let err = Fixture::new() .queue(&Message::Error(peer_error.clone())) - .run_err(&Program { - instructions: instrs, - }); + .run_err(&negotiate_channel_program(&announced_open_channel())); assert!(matches!(err, ExecuteError::PeerError(e) if e == peer_error)); } @@ -403,19 +378,10 @@ fn execute_recv_auto_pong() { ignored: vec![0xaa], }; - let mut instrs = send_open_channel_instructions(); - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); - let mut fx = Fixture::new() .queue(&Message::Ping(ping)) .queue(&Message::AcceptChannel(sample_accept_channel())); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&negotiate_channel_program(&announced_open_channel())); // Verify exactly two messages were sent: `open_channel` and `pong`. assert_eq!(fx.sent_len(), 2); @@ -428,18 +394,10 @@ fn execute_recv_auto_pong() { fn execute_recv_skips_gossip() { let gossip = GossipTimestampFilter::new([0u8; 32], 0, 86400); - let mut instrs = send_open_channel_instructions(); - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); let mut fx = Fixture::new() .queue(&Message::GossipTimestampFilter(gossip)) .queue(&Message::AcceptChannel(sample_accept_channel())); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&negotiate_channel_program(&announced_open_channel())); let accept_channel = fx .negotiation(&TemporaryChannelId::new([0xbb; 32])) @@ -453,16 +411,8 @@ fn execute_recv_skips_gossip() { fn execute_records_negotiation_for_open_and_accept() { let temporary_channel_id = TemporaryChannelId::new([0xbb; 32]); - let mut instrs = send_open_channel_instructions(); - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); let mut fx = Fixture::new().queue(&Message::AcceptChannel(sample_accept_channel())); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&negotiate_channel_program(&announced_open_channel())); let pending = fx.negotiation(&temporary_channel_id); assert_eq!( @@ -478,20 +428,12 @@ fn execute_records_negotiation_for_open_and_accept() { fn execute_recv_accept_channel_unknown_channel() { let unknown_id = TemporaryChannelId::new([0xcc; 32]); - let mut instrs = send_open_channel_instructions(); - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); let err = Fixture::new() .queue(&Message::AcceptChannel(AcceptChannel { temporary_channel_id: unknown_id, ..sample_accept_channel() })) - .run_err(&Program { - instructions: instrs, - }); + .run_err(&negotiate_channel_program(&announced_open_channel())); let ExecuteError::Violation(Violation::InvalidAcceptChannel(id, reason)) = &err else { panic!("unexpected error: {err:?}"); @@ -510,22 +452,12 @@ fn execute_recv_accept_channel_opener_cannot_afford_fee() { // Set `push_msat` so the opener cannot afford the commitment fee // requiring the peer to reject the `open_channel` per BOLT 2. - let mut instrs = send_open_channel_instructions(); - instrs[3] = Instruction { - operation: Operation::LoadAmount(99_900_000), - inputs: vec![], - }; - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); + let mut oc = announced_open_channel(); + oc.message.push_msat = 99_900_000; let err = Fixture::new() .queue(&Message::AcceptChannel(sample_accept_channel())) - .run_err(&Program { - instructions: instrs, - }); + .run_err(&negotiate_channel_program(&oc)); let ExecuteError::Violation(Violation::InvalidAcceptChannel(id, reason)) = &err else { panic!("unexpected error: {err:?}"); @@ -542,29 +474,20 @@ fn execute_recv_accept_channel_opener_cannot_afford_fee() { fn execute_recv_accept_channel_rejects_reuse_before_funding() { let temporary_channel_id = TemporaryChannelId::new([0xbb; 32]); - let mut instrs = send_open_channel_instructions(); - let built_open_channel = instrs.len() - 2; - let sent_open_channel = instrs.len() - 1; - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }); - let resent_open_channel = instrs.len(); - instrs.push(Instruction { - operation: Operation::SendOpenChannel, - inputs: vec![built_open_channel], - }); - instrs.push(Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![resent_open_channel], - }); + // Send the same `open_channel` a second time and receive another + // `accept_channel` for it. + let mut b = ProgramBuilder::new(); + let negotiated = negotiate_channel(&mut b, &announced_open_channel()); + let resent = b.append( + Operation::SendOpenChannel, + &[negotiated.open_channel.vars.built], + ); + b.append(Operation::RecvAcceptChannel, &[resent]); let err = Fixture::new() .queue(&Message::AcceptChannel(sample_accept_channel())) .queue(&Message::AcceptChannel(sample_accept_channel())) - .run_err(&Program { - instructions: instrs, - }); + .run_err(&b.build()); let ExecuteError::Violation(Violation::InvalidAcceptChannel(id, reason)) = &err else { panic!("unexpected error: {err:?}"); @@ -581,31 +504,17 @@ fn execute_records_only_first_open_channel_for_duplicate_id_before_funding() { // First open_channel: funding_satoshis = 100_000. // Second open_channel: same temporary_channel_id, funding_satoshis = 200_000. - let mut instrs = send_open_channel_instructions(); + let mut b = ProgramBuilder::new(); + let first = send_open_channel(&mut b, &announced_open_channel()); // Override only funding_satoshis; reuse the first open_channel's other 19 inputs. - let funding_satoshis = instrs.len(); - instrs.push(Instruction { - operation: Operation::LoadAmount(200_000), - inputs: vec![], - }); - let mut build_inputs: Vec = (0..20).collect(); - build_inputs[2] = funding_satoshis; - - let built = instrs.len(); - instrs.push(Instruction { - operation: Operation::BuildOpenChannel, - inputs: build_inputs, - }); - instrs.push(Instruction { - operation: Operation::SendOpenChannel, - inputs: vec![built], - }); + let mut second = first.vars; + second.funding_satoshis = b.append(Operation::LoadAmount(200_000), &[]); + second.built = b.append(Operation::BuildOpenChannel, &second.build_inputs()); + b.append(Operation::SendOpenChannel, &[second.built]); let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); // Both open_channel messages went out on the wire, but only the first // negotiation is recorded for the shared id. @@ -768,23 +677,20 @@ fn execute_send_open_channel_wrong_type_panics() { #[test] #[should_panic(expected = "is void")] fn execute_affine_overuse_panics() { - let mut instrs = send_open_channel_instructions(); - let sent_open_channel = instrs.len() - 1; - instrs.extend([ - Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }, - Instruction { - operation: Operation::RecvAcceptChannel, - inputs: vec![sent_open_channel], - }, - ]); + let mut b = ProgramBuilder::new(); + let negotiated = negotiate_channel(&mut b, &announced_open_channel()); + let mut program = b.build(); + + // `ProgramBuilder` rejects the reuse itself, so we manually append the + // second receive instruction. + program.instructions.push(Instruction { + operation: Operation::RecvAcceptChannel, + inputs: vec![negotiated.open_channel.sent], + }); + Fixture::new() .queue(&Message::AcceptChannel(sample_accept_channel())) - .run(&Program { - instructions: instrs, - }); + .run(&program); } // MineBlocks should track calls to mine_blocks diff --git a/smite-scenarios/src/executor/tests/programs.rs b/smite-scenarios/src/executor/tests/programs.rs index b303c7bc..da93ef55 100644 --- a/smite-scenarios/src/executor/tests/programs.rs +++ b/smite-scenarios/src/executor/tests/programs.rs @@ -119,11 +119,55 @@ pub fn build_open_channel(b: &mut ProgramBuilder, oc: &SampleOpenChannel) -> Ope vars } +/// The variables a sent `open_channel` produces. +#[derive(Clone, Copy)] +pub struct SentOpenChannel { + /// The inputs of the message and the message itself. + pub vars: OpenChannelVars, + /// The `SendOpenChannel` result, an affine variable a single + /// `RecvAcceptChannel` may consume. + pub sent: usize, +} + +/// Emits `oc` and sends it. +pub fn send_open_channel(b: &mut ProgramBuilder, oc: &SampleOpenChannel) -> SentOpenChannel { + let vars = build_open_channel(b, oc); + let sent = b.append(Operation::SendOpenChannel, &[vars.built]); + + SentOpenChannel { vars, sent } +} + /// A program that builds and sends `oc`. pub fn send_open_channel_program(oc: &SampleOpenChannel) -> Program { let mut b = ProgramBuilder::new(); - let vars = build_open_channel(&mut b, oc); - b.append(Operation::SendOpenChannel, &[vars.built]); + send_open_channel(&mut b, oc); + + b.build() +} + +/// The variables a channel negotiation produces. +#[derive(Clone, Copy)] +pub struct NegotiatedChannel { + pub open_channel: SentOpenChannel, + /// The `RecvAcceptChannel` result. + pub accept_channel: usize, +} + +/// Emits `oc`, sends it, and receives the peer's `accept_channel`. +pub fn negotiate_channel(b: &mut ProgramBuilder, oc: &SampleOpenChannel) -> NegotiatedChannel { + let open_channel = send_open_channel(b, oc); + let accept_channel = b.append(Operation::RecvAcceptChannel, &[open_channel.sent]); + + NegotiatedChannel { + open_channel, + accept_channel, + } +} + +/// A program that negotiates `oc`. +pub fn negotiate_channel_program(oc: &SampleOpenChannel) -> Program { + let mut b = ProgramBuilder::new(); + negotiate_channel(&mut b, oc); b.build() } From 417f76e9279d28f0fcd610083712ee9dae2ec408 Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Fri, 11 Sep 2026 15:55:27 -0500 Subject: [PATCH 6/7] smite-scenarios: build funding programs with ProgramBuilder The funding tests all started from send_funding_created_and_recv_funding_signed_instructions() and then edited it by manually swapping indices or dropping the last instruction. This editing is now gone entirely, and tests build their desired programs by composing helper functions instead. --- smite-scenarios/src/executor/tests.rs | 232 ++++++-------- .../src/executor/tests/programs.rs | 293 +++++++----------- 2 files changed, 198 insertions(+), 327 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 9afe5370..a0dc7cf7 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -531,22 +531,12 @@ fn execute_records_open_channel_for_duplicate_id_after_funding() { // Negotiated open_channel: funding_satoshis = 10_000_000. // Second open_channel: same temporary_channel_id, funding_satoshis = 100_000. - let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); - instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. - // The second program's input indices are shifted past the funding - // flow's variables. - let offset = instrs.len(); - for mut instr in send_open_channel_instructions() { - for input in &mut instr.inputs { - *input += offset; - } - instrs.push(instr); - } + let mut b = ProgramBuilder::new(); + send_funding_created(&mut b); + send_open_channel(&mut b, &announced_open_channel()); let mut fx = Fixture::new().with_negotiation(sample_funding_negotiation()); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); let pending = fx.negotiation(&temporary_channel_id); assert_eq!(pending.open_channel.funding_satoshis, 100_000); @@ -733,8 +723,8 @@ fn execute_mine_blocks_wrong_input() { #[test] fn execute_create_and_broadcast_tx() { let mut b = ProgramBuilder::new(); - let funding_tx = create_funding_tx(&mut b); - b.append(Operation::BroadcastTransaction, &[funding_tx]); + let funding = create_funding_tx(&mut b); + b.append(Operation::BroadcastTransaction, &[funding.tx]); let mut fx = Fixture::new(); fx.run(&b.build()); @@ -751,10 +741,10 @@ fn execute_create_and_broadcast_tx() { #[test] fn execute_lookup_short_channel_id_confirmed() { let mut b = ProgramBuilder::new(); - let funding_tx = create_funding_tx(&mut b); - b.append(Operation::BroadcastTransaction, &[funding_tx]); + let funding = create_funding_tx(&mut b); + b.append(Operation::BroadcastTransaction, &[funding.tx]); b.append(Operation::MineBlocks(6), &[]); - let scid = b.append(Operation::LookupShortChannelId, &[funding_tx]); + let scid = b.append(Operation::LookupShortChannelId, &[funding.tx]); send_channel_announcement(&mut b, scid); let mut fx = Fixture::new(); @@ -782,8 +772,8 @@ fn execute_lookup_short_channel_id_unconfirmed_returns_sentinel() { // No BroadcastTransaction and no MineBlocks: the mock reports zero // confirmations and get_transaction_block_position returns None. let mut b = ProgramBuilder::new(); - let funding_tx = create_funding_tx(&mut b); - let scid = b.append(Operation::LookupShortChannelId, &[funding_tx]); + let funding = create_funding_tx(&mut b); + let scid = b.append(Operation::LookupShortChannelId, &[funding.tx]); send_channel_announcement(&mut b, scid); let mut fx = Fixture::new(); @@ -803,9 +793,9 @@ fn execute_broadcast_dedupes_rejected_tx_in_private_mempool() { // Fund with a dust amount so the built funding tx carries a below-dust // output, and broadcast it twice. let mut b = ProgramBuilder::new(); - let funding_tx = create_funding_tx_with(&mut b, 200, 15_000); - b.append(Operation::BroadcastTransaction, &[funding_tx]); - b.append(Operation::BroadcastTransaction, &[funding_tx]); + let funding = create_funding_tx_with(&mut b, 200, 15_000); + b.append(Operation::BroadcastTransaction, &[funding.tx]); + b.append(Operation::BroadcastTransaction, &[funding.tx]); b.append(Operation::MineBlocks(1), &[]); let mut fx = Fixture::new(); @@ -847,9 +837,7 @@ fn execute_send_funding_created_and_recv_funding_signed() { // The acceptor replies with funding_signed carrying its signature over // the opener's commitment. let mut fx = recv_funding_signed_fixture(); - fx.run(&Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }); + fx.run(&send_funding_created_and_recv_funding_signed_program()); assert_eq!(fx.sent_len(), 1); let fc: FundingCreated = fx.sent(0); @@ -881,15 +869,16 @@ fn execute_send_funding_created_uses_wire_funding_pubkey() { // 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 b = ProgramBuilder::new(); + let funding = create_funding_tx(&mut b); + b.append(Operation::BroadcastTransaction, &[funding.tx]); + let funding_created = send_funding_created_with(&mut b, funding, funding.acceptor_privkey); + b.append(Operation::RecvFundingSigned, &[funding_created.sent]); // The acceptor's signature still verifies, because the config is built // from the wire pubkeys rather than from the swapped privkey. let mut fx = recv_funding_signed_fixture(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); let secp = Secp256k1::new(); let opener_pk = PublicKey::from_secret_key(&secp, &opener_funding_sk()); @@ -916,26 +905,34 @@ fn execute_send_funding_created_after_funding_built_does_not_track_channel() { ..sample_utxo() }; - let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); - instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. - instrs.extend(vec![ - // Different funding spk, hence a different outpoint. - Instruction { - operation: Operation::CreateFundingTransaction, - inputs: vec![1, 1, 4, 5], - }, - Instruction { - operation: Operation::SendFundingCreated, - inputs: vec![10, 0, 8], - }, - ]); + let mut b = ProgramBuilder::new(); + let first = send_funding_created(&mut b); + + // The opener's pubkey is used on both sides of the funding script, creating + // a different outpoint than the first funding transaction's. + let funding = first.tx; + let second_tx = b.append( + Operation::CreateFundingTransaction, + &[ + funding.opener_pubkey, + funding.opener_pubkey, + funding.funding_satoshis, + funding.feerate_per_kw, + ], + ); + b.append( + Operation::SendFundingCreated, + &[ + second_tx, + funding.opener_privkey, + first.temporary_channel_id, + ], + ); let mut fx = Fixture::new() .with_utxos(vec![sample_utxo(), second_utxo]) .with_negotiation(sample_funding_negotiation()); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); // The message still goes out, only the state tracking is suppressed. assert_eq!(fx.sent_len(), 2); @@ -953,9 +950,7 @@ fn execute_send_funding_created_push_exceeds_funding() { negotiation.open_channel.push_msat = 20_000_000_000; let err = Fixture::new() .with_negotiation(negotiation) - .run_err(&Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }); + .run_err(&send_funding_created_and_recv_funding_signed_program()); assert!(matches!( err, ExecuteError::Commitment(smite::channel_tx::CommitmentError::PushExceedsFunding) @@ -970,9 +965,7 @@ fn execute_send_funding_created_funding_msat_overflow() { negotiation.open_channel.funding_satoshis = u64::MAX; let err = Fixture::new() .with_negotiation(negotiation) - .run_err(&Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }); + .run_err(&send_funding_created_and_recv_funding_signed_program()); assert!(matches!( err, ExecuteError::Commitment(smite::channel_tx::CommitmentError::FundingMsatOverflow) @@ -984,13 +977,8 @@ 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 mut instrs = send_funding_created_and_recv_funding_signed_instructions(); - instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. - let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&send_funding_created_program()); let fc: FundingCreated = fx.sent(0); assert_eq!(fc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); @@ -1007,13 +995,9 @@ fn execute_send_funding_created_no_accept_channel() { // state. let mut negotiation = sample_funding_negotiation(); negotiation.accept_channel = None; - let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); - instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. let mut fx = Fixture::new().with_negotiation(negotiation); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&send_funding_created_program()); let fc: FundingCreated = fx.sent(0); assert_eq!(fc.temporary_channel_id, TemporaryChannelId::new([0xbb; 32])); @@ -1030,9 +1014,7 @@ fn execute_recv_funding_signed_unknown_channel() { let err = Fixture::new() .with_negotiation(sample_funding_negotiation()) .queue(&funding_signed_reply(channel_id)) - .run_err(&Program { - instructions: send_funding_created_and_recv_funding_signed_instructions(), - }); + .run_err(&send_funding_created_and_recv_funding_signed_program()); assert!(matches!( err, ExecuteError::Violation(Violation::UnknownChannel(id)) if id == channel_id @@ -1049,9 +1031,7 @@ fn execute_recv_funding_signed_invalid_signature() { 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(), - }); + .run_err(&send_funding_created_and_recv_funding_signed_program()); assert!(matches!( err, ExecuteError::Violation(Violation::InvalidCounterpartySignature(id)) if id == channel_id @@ -1062,30 +1042,33 @@ fn execute_recv_funding_signed_invalid_signature() { fn execute_send_channel_ready() { 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([ - Instruction { - operation: Operation::LoadShortChannelId(alias.as_u64()), - inputs: vec![], - }, - Instruction { - operation: Operation::SendChannelReady { - include_alias: false, - }, - inputs: vec![10, 1, 11], + let mut b = ProgramBuilder::new(); + let funding_created = send_funding_created(&mut b); + let funded_channel_id = b.append(Operation::RecvFundingSigned, &[funding_created.sent]); + let alias_scid = b.append(Operation::LoadShortChannelId(alias.as_u64()), &[]); + b.append( + Operation::SendChannelReady { + include_alias: false, }, - Instruction { - operation: Operation::SendChannelReady { - include_alias: true, - }, - inputs: vec![10, 3, 11], + &[ + funded_channel_id, + funding_created.tx.opener_pubkey, + alias_scid, + ], + ); + b.append( + Operation::SendChannelReady { + include_alias: true, }, - ]); + &[ + funded_channel_id, + funding_created.tx.acceptor_pubkey, + alias_scid, + ], + ); let mut fx = recv_funding_signed_fixture(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); // The instructions send 1 `funding_created` and 2 `channel_ready` messages. assert_eq!(fx.sent_len(), 3); @@ -1195,25 +1178,14 @@ fn execute_recv_channel_ready_invalid_funding_outpoint_is_noop() { 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(); - - instrs.extend([ - Instruction { - operation: Operation::MineBlocks(8), - inputs: vec![], - }, - Instruction { - operation: Operation::RecvChannelReady, - inputs: vec![], - }, - ]); + let mut b = ProgramBuilder::new(); + send_funding_created(&mut b); + b.append(Operation::MineBlocks(8), &[]); + b.append(Operation::RecvChannelReady, &[]); // With invalid funding outpoint the target does not owe us a // `channel_ready`, so `RecvChannelReady` must be a no-op. - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); // The target's next per-commitment point is still unknown and the queued // `channel_ready` remains untouched. @@ -1231,9 +1203,7 @@ fn execute_recv_channel_ready_below_minimum_depth_is_noop() { // 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. - fx.run(&Program { - instructions: recv_channel_ready_instructions(5), - }); + fx.run(&recv_channel_ready_program(5)); assert!(fx.bitcoin().mined_private_mempool.is_empty()); // The target's next per-commitment point is still unknown and the queued @@ -1251,9 +1221,7 @@ fn execute_recv_channel_ready_at_minimum_depth_records_point() { // `sample_funding_negotiation()`. // At the negotiated `minimum_depth` confirmations the target owes us a // `channel_ready`, which `RecvChannelReady` receives and records. - fx.run(&Program { - instructions: recv_channel_ready_instructions(6), - }); + fx.run(&recv_channel_ready_program(6)); assert!(fx.bitcoin().mined_private_mempool.is_empty()); // The `channel_ready` was consumed and the target's next per-commitment @@ -1270,38 +1238,20 @@ fn execute_recv_channel_ready_at_minimum_depth_records_point() { fn execute_recv_channel_ready_funding_mined_prematurely_is_noop() { let (mut fx, _) = recv_channel_ready_fixture(); - let mut instrs = create_and_broadcast_tx_instructions(); - instrs.extend([ - Instruction { - // Mine past the negotiated `minimum_depth` *before* sending - // `funding_created`. - operation: Operation::MineBlocks(8), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadChannelId([0xbb; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::SendFundingCreated, - inputs: vec![6, 0, 9], - }, - Instruction { - operation: Operation::RecvFundingSigned, - inputs: vec![10], - }, - Instruction { - operation: Operation::RecvChannelReady, - inputs: vec![], - }, - ]); + let mut b = ProgramBuilder::new(); + let funding = create_funding_tx(&mut b); + b.append(Operation::BroadcastTransaction, &[funding.tx]); + // Mine past the negotiated `minimum_depth` *before* sending + // `funding_created`. + b.append(Operation::MineBlocks(8), &[]); + let funding_created = send_funding_created_with(&mut b, funding, funding.opener_privkey); + b.append(Operation::RecvFundingSigned, &[funding_created.sent]); + b.append(Operation::RecvChannelReady, &[]); // 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. - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); // The target's next per-commitment point is still unknown and the queued // `channel_ready` remains untouched. diff --git a/smite-scenarios/src/executor/tests/programs.rs b/smite-scenarios/src/executor/tests/programs.rs index da93ef55..f149c283 100644 --- a/smite-scenarios/src/executor/tests/programs.rs +++ b/smite-scenarios/src/executor/tests/programs.rs @@ -1,10 +1,11 @@ //! Program fragments used by executor tests. //! -//! Each helper returns the instructions for one flow. +//! Each helper appends one fragment to a [`ProgramBuilder`] and returns the +//! variables it produced, so that callers compose fragments without tracking +//! instruction indices. `*_program` helpers build a whole program. use super::harness::{PointSource, SampleOpenChannel, acceptor_funding_sk, opener_funding_sk}; use crate::executor::*; -use smite_ir::Instruction; use smite_ir::builder::ProgramBuilder; // -- open_channel -- @@ -178,19 +179,32 @@ pub fn negotiate_channel_program(oc: &SampleOpenChannel) -> Program { const FUNDING_SATOSHIS: u64 = 10_000_000; const FUNDING_FEERATE_PER_KW: u32 = 15_000; +/// The variables a funding transaction fragment produces. +#[derive(Clone, Copy)] +pub struct FundingTxVars { + pub opener_privkey: usize, + pub opener_pubkey: usize, + pub acceptor_privkey: usize, + pub acceptor_pubkey: usize, + pub funding_satoshis: usize, + pub feerate_per_kw: usize, + /// The `CreateFundingTransaction` result. + pub tx: usize, +} + /// Emits a `CreateFundingTransaction` for [`FUNDING_SATOSHIS`] at -/// [`FUNDING_FEERATE_PER_KW`], returning its `FundingTransaction` variable. -pub fn create_funding_tx(b: &mut ProgramBuilder) -> usize { +/// [`FUNDING_FEERATE_PER_KW`]. +pub fn create_funding_tx(b: &mut ProgramBuilder) -> FundingTxVars { create_funding_tx_with(b, FUNDING_SATOSHIS, FUNDING_FEERATE_PER_KW) } /// Emits a `CreateFundingTransaction` between the opener and acceptor funding -/// keys, returning its `FundingTransaction` variable. +/// keys. pub fn create_funding_tx_with( b: &mut ProgramBuilder, funding_satoshis: u64, feerate_per_kw: u32, -) -> usize { +) -> FundingTxVars { let opener_privkey = b.append( Operation::LoadPrivateKey(opener_funding_sk().secret_bytes()), &[], @@ -203,8 +217,7 @@ pub fn create_funding_tx_with( let acceptor_pubkey = b.append(Operation::DerivePoint, &[acceptor_privkey]); let funding_satoshis = b.append(Operation::LoadAmount(funding_satoshis), &[]); let feerate_per_kw = b.append(Operation::LoadFeeratePerKw(feerate_per_kw), &[]); - - b.append( + let tx = b.append( Operation::CreateFundingTransaction, &[ opener_pubkey, @@ -212,7 +225,92 @@ pub fn create_funding_tx_with( funding_satoshis, feerate_per_kw, ], - ) + ); + + FundingTxVars { + opener_privkey, + opener_pubkey, + acceptor_privkey, + acceptor_pubkey, + funding_satoshis, + feerate_per_kw, + tx, + } +} + +// -- funding_created -- + +/// The variables a sent `funding_created` produces. +#[derive(Clone, Copy)] +pub struct SentFundingCreated { + pub tx: FundingTxVars, + pub temporary_channel_id: usize, + /// The `SendFundingCreated` result, an affine variable a single + /// `RecvFundingSigned` may consume. + pub sent: usize, +} + +/// Creates a funding transaction, broadcasts it, and sends `funding_created` +/// signed with the opener's funding key. +pub fn send_funding_created(b: &mut ProgramBuilder) -> SentFundingCreated { + let tx = create_funding_tx(b); + b.append(Operation::BroadcastTransaction, &[tx.tx]); + + send_funding_created_with(b, tx, tx.opener_privkey) +} + +/// Sends `funding_created` for `tx`, signed with the `PrivateKey` variable +/// `signing_privkey`. +/// +/// The `temporary_channel_id` is the one `announced_open_channel` and +/// `sample_funding_negotiation` use, so that the executor finds the negotiation +/// whose commitment it must sign. +pub fn send_funding_created_with( + b: &mut ProgramBuilder, + tx: FundingTxVars, + signing_privkey: usize, +) -> SentFundingCreated { + let temporary_channel_id = b.append(Operation::LoadChannelId([0xbb; 32]), &[]); + let sent = b.append( + Operation::SendFundingCreated, + &[tx.tx, signing_privkey, temporary_channel_id], + ); + + SentFundingCreated { + tx, + temporary_channel_id, + sent, + } +} + +/// A program that sends `funding_created` without awaiting `funding_signed`. +pub fn send_funding_created_program() -> Program { + let mut b = ProgramBuilder::new(); + send_funding_created(&mut b); + + b.build() +} + +/// A program that sends `funding_created` and receives the peer's +/// `funding_signed`. +pub fn send_funding_created_and_recv_funding_signed_program() -> Program { + let mut b = ProgramBuilder::new(); + let funding_created = send_funding_created(&mut b); + b.append(Operation::RecvFundingSigned, &[funding_created.sent]); + + b.build() +} + +/// A program that sends `funding_created`, receives `funding_signed`, mines +/// `confirmations` blocks, and receives the target's `channel_ready`. +pub fn recv_channel_ready_program(confirmations: u8) -> Program { + let mut b = ProgramBuilder::new(); + let funding_created = send_funding_created(&mut b); + b.append(Operation::RecvFundingSigned, &[funding_created.sent]); + b.append(Operation::MineBlocks(confirmations), &[]); + b.append(Operation::RecvChannelReady, &[]); + + b.build() } // -- Gossip -- @@ -240,180 +338,3 @@ pub fn send_channel_announcement(b: &mut ProgramBuilder, scid: usize) { ); b.append(Operation::SendMessage, &[announcement]); } - -// -- Instruction fragments -- - -/// Builds the 20 `open_channel` input instructions in wire order. -pub fn open_channel_instructions() -> Vec { - vec![ - Instruction { - operation: Operation::LoadChainHashFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadChannelId([0xbb; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadAmount(100_000), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadAmount(0), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadAmount(546), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadAmount(100_000_000), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadAmount(10_000), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadAmount(1_000), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadFeeratePerKw(253), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadU16(144), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadU16(483), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadTargetPubkeyFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadTargetPubkeyFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadTargetPubkeyFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadTargetPubkeyFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadTargetPubkeyFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadTargetPubkeyFromContext, - inputs: vec![], - }, - Instruction { - operation: Operation::LoadU8(1), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadBytes(vec![]), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadFeatures(vec![0x40, 0x10, 0x00]), - inputs: vec![], - }, - ] -} - -pub fn create_and_broadcast_tx_instructions() -> Vec { - let opener_privkey = opener_funding_sk().secret_bytes(); - let acceptor_privkey = acceptor_funding_sk().secret_bytes(); - - vec![ - Instruction { - operation: Operation::LoadPrivateKey(opener_privkey), - inputs: vec![], - }, - Instruction { - operation: Operation::DerivePoint, - inputs: vec![0], - }, - Instruction { - operation: Operation::LoadPrivateKey(acceptor_privkey), - inputs: vec![], - }, - Instruction { - operation: Operation::DerivePoint, - inputs: vec![2], - }, - Instruction { - operation: Operation::LoadAmount(10_000_000), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadFeeratePerKw(15_000), - inputs: vec![], - }, - Instruction { - operation: Operation::CreateFundingTransaction, - inputs: vec![1, 3, 4, 5], - }, - Instruction { - operation: Operation::BroadcastTransaction, - inputs: vec![6], - }, - ] -} - -pub fn send_open_channel_instructions() -> Vec { - let mut instructions = open_channel_instructions(); - instructions.extend([ - Instruction { - operation: Operation::BuildOpenChannel, - inputs: (0..20).collect(), - }, - Instruction { - operation: Operation::SendOpenChannel, - inputs: vec![20], - }, - ]); - instructions -} - -pub fn send_funding_created_and_recv_funding_signed_instructions() -> Vec { - let mut instrs = create_and_broadcast_tx_instructions(); - instrs.extend(vec![ - Instruction { - operation: Operation::LoadChannelId([0xbb; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::SendFundingCreated, - inputs: vec![6, 0, 8], - }, - Instruction { - operation: Operation::RecvFundingSigned, - inputs: vec![9], - }, - ]); - instrs -} - -pub fn recv_channel_ready_instructions(confirmations: u8) -> Vec { - let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); - instrs.extend([ - Instruction { - operation: Operation::MineBlocks(confirmations), - inputs: vec![], - }, - Instruction { - operation: Operation::RecvChannelReady, - inputs: vec![], - }, - ]); - instrs -} From 476ab00bc5bb775f519bda5aad7afc689707cecf Mon Sep 17 00:00:00 2001 From: Matt Morehouse Date: Fri, 11 Sep 2026 16:03:22 -0500 Subject: [PATCH 7/7] smite-scenarios: build remaining programs with ProgramBuilder Now the only manually-constructed programs remaining are ones that are invalid and thus would cause ProgramBuilder to panic. --- smite-scenarios/src/executor/tests.rs | 77 ++++++++------------------- 1 file changed, 23 insertions(+), 54 deletions(-) diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index a0dc7cf7..92bbc936 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -628,19 +628,11 @@ fn execute_void_variable_reference_panics() { #[test] #[should_panic(expected = "valid private key")] fn execute_invalid_private_key_panics() { - let program = Program { - instructions: vec![ - Instruction { - operation: Operation::LoadPrivateKey([0; 32]), - inputs: vec![], - }, - Instruction { - operation: Operation::DerivePoint, - inputs: vec![0], - }, - ], - }; - Fixture::new().run(&program); + let mut b = ProgramBuilder::new(); + let sk = b.append(Operation::LoadPrivateKey([0; 32]), &[]); + b.append(Operation::DerivePoint, &[sk]); + + Fixture::new().run(&b.build()); } #[test] @@ -686,14 +678,11 @@ fn execute_affine_overuse_panics() { // MineBlocks should track calls to mine_blocks #[test] fn execute_mine_blocks_invokes_cli() { - let instrs = vec![Instruction { - operation: Operation::MineBlocks(6), - inputs: vec![], - }]; + let mut b = ProgramBuilder::new(); + b.append(Operation::MineBlocks(6), &[]); + let mut fx = Fixture::new(); - fx.run(&Program { - instructions: instrs, - }); + fx.run(&b.build()); // Verify that mine_blocks was called with the correct number assert_eq!(fx.bitcoin().mine_blocks_calls, vec![6]); @@ -1106,25 +1095,14 @@ fn execute_send_channel_ready() { fn execute_send_shutdown() { let channel_id = ChannelId::new([0x7a; 32]); let script = ShutdownScriptVariant::P2wpkh([0xab; 20]); - let program = Program { - instructions: vec![ - Instruction { - operation: Operation::LoadChannelId(channel_id.0), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadShutdownScript(script.clone()), - inputs: vec![], - }, - Instruction { - operation: Operation::SendShutdown, - inputs: vec![0, 1], - }, - ], - }; + + let mut b = ProgramBuilder::new(); + let channel_id_var = b.append(Operation::LoadChannelId(channel_id.0), &[]); + let scriptpubkey = b.append(Operation::LoadShutdownScript(script.clone()), &[]); + b.append(Operation::SendShutdown, &[channel_id_var, scriptpubkey]); let mut fx = Fixture::new(); - fx.run(&program); + fx.run(&b.build()); assert_eq!(fx.sent_len(), 1); let sd: Shutdown = fx.sent(0); @@ -1137,25 +1115,16 @@ fn execute_send_shutdown_empty_scriptpubkey() { let channel_id = ChannelId::new([0x7a; 32]); // The fuzzer should allow an empty scriptpubkey in the shutdown message // to exercise the target's behavior even though it's protocol-invalid. - let program = Program { - instructions: vec![ - Instruction { - operation: Operation::LoadChannelId(channel_id.0), - inputs: vec![], - }, - Instruction { - operation: Operation::LoadShutdownScript(ShutdownScriptVariant::Empty), - inputs: vec![], - }, - Instruction { - operation: Operation::SendShutdown, - inputs: vec![0, 1], - }, - ], - }; + let mut b = ProgramBuilder::new(); + let channel_id_var = b.append(Operation::LoadChannelId(channel_id.0), &[]); + let scriptpubkey = b.append( + Operation::LoadShutdownScript(ShutdownScriptVariant::Empty), + &[], + ); + b.append(Operation::SendShutdown, &[channel_id_var, scriptpubkey]); let mut fx = Fixture::new(); - fx.run(&program); + fx.run(&b.build()); assert_eq!(fx.sent_len(), 1); let sd: Shutdown = fx.sent(0);