From 67c87b07fab9725129a6b8c012e95a6b28ecffdf Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Thu, 21 May 2026 05:00:46 +0000 Subject: [PATCH 1/8] smite-ir: Introduce affine types Add `SentOpenChannel` to the `Variable` and `VariableType` enums. Unlike standard data variables which can be referenced infinitely, this new type is designed to be "affine" (single-use). Affine variables represent strict topological locks in the LN state machine (e.g., `SentOpenChannel` ensures a channel is actively pending before allowing a `RecvAcceptChannel`). --- smite-ir/src/variable.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/smite-ir/src/variable.rs b/smite-ir/src/variable.rs index 5bbd35a..d72072f 100644 --- a/smite-ir/src/variable.rs +++ b/smite-ir/src/variable.rs @@ -47,6 +47,10 @@ pub enum Variable { AcceptChannel(AcceptChannel), /// Constructed funding transaction with funding output index. FundingTransaction(FundingTransaction), + + // Affine (single-use) variables + /// `open_channel` has been sent, so `accept_channel` may now be received. + SentOpenChannel, } impl Variable { @@ -70,6 +74,7 @@ impl Variable { Self::Message(_) => VariableType::Message, Self::AcceptChannel(_) => VariableType::AcceptChannel, Self::FundingTransaction(_) => VariableType::FundingTransaction, + Self::SentOpenChannel => VariableType::SentOpenChannel, } } } @@ -94,4 +99,31 @@ pub enum VariableType { Message, AcceptChannel, FundingTransaction, + SentOpenChannel, +} + +impl VariableType { + #[must_use] + pub fn is_affine(&self) -> bool { + match self { + Self::SentOpenChannel => true, + + Self::Bytes + | Self::ChainHash + | Self::ChannelId + | Self::Point + | Self::PrivateKey + | Self::Amount + | Self::FeeratePerKw + | Self::BlockHeight + | Self::Timestamp + | Self::U16 + | Self::U8 + | Self::Features + | Self::Message + | Self::AcceptChannel + | Self::ShortChannelId + | Self::FundingTransaction => false, + } + } } From b0ebefc3970175d3285e74e579835e4a0bcb398d Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Thu, 21 May 2026 08:13:08 +0000 Subject: [PATCH 2/8] smite-ir/builder: Implement affine selection in `ProgramBuilder` Update `pick_variable` with a dedicated branch for affine types. Unlike data variables which use a probabilistic 75/15/10 strategy, affine types use deterministic tip-tracking. --- smite-ir/src/builder.rs | 45 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/smite-ir/src/builder.rs b/smite-ir/src/builder.rs index bd77944..aff97c0 100644 --- a/smite-ir/src/builder.rs +++ b/smite-ir/src/builder.rs @@ -71,6 +71,25 @@ impl ProgramBuilder { actual_type, expected_type, "{operation:?} input {i}: expected {expected_type:?}, got {actual_type:?}", ); + if actual_type.is_affine() { + let cands = self.candidates.get_mut(&actual_type).unwrap_or_else(|| { + panic!("{operation:?} input {i}: no candidates for {actual_type:?}") + }); + + // Find the position, or panic if it doesn't exist. + let pos = cands + .iter() + .position(|c| match c { + Candidate::Direct(idx) => *idx == input_idx, + Candidate::Extract { compound_idx, .. } => *compound_idx == input_idx, + }) + .unwrap_or_else(|| { + panic!("{operation:?} input {i}: affine {actual_type:?} already consumed") + }); + + // Consume it. + cands.remove(pos); + } } let idx = self.instructions.len(); @@ -78,6 +97,10 @@ impl ProgramBuilder { if let Some(out_type) = operation.output_type() { // Register extractable fields for compound types. for (extract_op, field_type) in operation.extractable_fields() { + assert!( + !out_type.is_affine(), + "Affine variables cannot be extracted" + ); self.candidates .entry(field_type) .or_default() @@ -103,12 +126,27 @@ impl ProgramBuilder { /// Selects or creates a variable of the given type using probabilistic /// variable selection (75% most recent, 15% any existing, 10% fresh). - #[allow(clippy::missing_panics_doc)] // candidates is always non-empty + /// + /// # Panics + /// + /// Panics in the following scenarios: + /// * An affine type is requested but its candidate pool is empty (all instances have been consumed). + /// * Attempts to generate a fresh variable for a type that cannot be instantiated out-of-thin-air + /// (e.g., `Message`, `AcceptChannel`, or affine types). + /// * Resolving the chosen candidate triggers a panic in the underlying `append` operation. pub fn pick_variable(&mut self, var_type: VariableType, rng: &mut impl Rng) -> usize { - let Some(candidates) = self.candidates.get(&var_type) else { + let Some(candidates) = self.candidates.get_mut(&var_type) else { return self.generate_fresh(var_type, rng); }; + if var_type.is_affine() { + let chosen_candidate = candidates + .last() + .unwrap_or_else(|| panic!("no candidates for {var_type:?}")) + .clone(); + return self.resolve_candidate(chosen_candidate); + } + let roll = rng.random_range(0..20); match roll { // 10%: generate a fresh value even though candidates exist. @@ -174,6 +212,9 @@ impl ProgramBuilder { VariableType::FundingTransaction => { panic!("cannot generate fresh FundingTransaction: requires composed inputs") } + VariableType::SentOpenChannel => { + panic!("cannot generate fresh affine type") + } } } From d6cec1fb46d2b1714e6e024317c53f30eb9fe984 Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Fri, 22 May 2026 06:19:21 +0000 Subject: [PATCH 3/8] smite-ir: Enforce linear data-flow constraints in Validator Update `Program::validate()` to enforce the single-use nature of affine state variables, preventing mutators from generating impossible state-machine sequences. Add a validation rule: An affine variable cannot be consumed as an input more than once. This prevents mutators from reusing old state variables to bypass protocol ordering. --- smite-ir/src/program.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/smite-ir/src/program.rs b/smite-ir/src/program.rs index 63aea36..553725d 100644 --- a/smite-ir/src/program.rs +++ b/smite-ir/src/program.rs @@ -85,6 +85,14 @@ pub enum ValidateError { /// Actual byte length. len: usize, }, + /// An affine variable is used more than once. + #[error("Variable {index}: affine {var_type:?} consumed twice (max 1)")] + AffineOverUse { + /// The overused affine variable index. + index: usize, + /// Type of the affine variable. + var_type: VariableType, + }, } impl Program { @@ -101,6 +109,8 @@ impl Program { /// /// Returns the first violation encountered. pub fn validate(&self) -> Result<(), ValidateError> { + let mut affine_consumed = vec![false; self.instructions.len()]; + for (instr_idx, instr) in self.instructions.iter().enumerate() { let expected = instr.operation.input_types(); if instr.inputs.len() != expected.len() { @@ -135,6 +145,15 @@ impl Program { got: actual_type, }); } + if expected_type.is_affine() { + if affine_consumed[input_idx] { + return Err(ValidateError::AffineOverUse { + index: input_idx, + var_type: expected_type, + }); + } + affine_consumed[input_idx] = true; + } } if let Operation::LoadBytes(b) | Operation::LoadFeatures(b) = &instr.operation && b.len() > MAX_MESSAGE_SIZE From 80b7b131c44ad3647611bee9b7893c5b299ca759 Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Sat, 30 May 2026 06:03:42 +0000 Subject: [PATCH 4/8] smite-ir/mutators: Disable input swapping for affine types Affine variables carry no data and act purely as state tokens, so swapping them yields no practical fuzzing value. --- smite-ir/src/mutators/input_swap.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/smite-ir/src/mutators/input_swap.rs b/smite-ir/src/mutators/input_swap.rs index d3442f8..0803474 100644 --- a/smite-ir/src/mutators/input_swap.rs +++ b/smite-ir/src/mutators/input_swap.rs @@ -19,7 +19,16 @@ impl Mutator for InputSwapMutator { .instructions .iter() .enumerate() - .flat_map(|(i, instr)| (0..instr.inputs.len()).map(move |j| (i, j))) + .flat_map(|(i, instr)| { + instr + .operation + .input_types() + .into_iter() + .enumerate() + // Affine variables carry no data, so swapping one affine variable with + // another has no practical effect. + .filter_map(move |(j, ty)| (!ty.is_affine()).then_some((i, j))) + }) .choose(rng) else { return false; From ae4113e03f4fd2a45b206fe451d7389903878104 Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Tue, 2 Jun 2026 06:07:27 +0000 Subject: [PATCH 5/8] smite-ir: Introduce `OpenChannelMessage` Introduce a dedicated `OpenChannelMessage` variable, separating it from the generic `Message` type. This strongly types the output of `BuildOpenChannel`, and is used as an input to `SendOpenChannel` in a subsequent commit. This prevents `InputSwapMutator` from swapping it with a different message type, possibly causing a timeout. --- smite-ir/src/builder.rs | 3 +++ smite-ir/src/operation.rs | 7 ++++--- smite-ir/src/tests.rs | 21 +++++++++------------ smite-ir/src/variable.rs | 5 +++++ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/smite-ir/src/builder.rs b/smite-ir/src/builder.rs index aff97c0..5d3a09f 100644 --- a/smite-ir/src/builder.rs +++ b/smite-ir/src/builder.rs @@ -206,6 +206,9 @@ impl ProgramBuilder { VariableType::Message => { panic!("cannot generate fresh Message: requires composed inputs") } + VariableType::OpenChannelMessage => { + panic!("cannot generate fresh OpenChannelMessage: requires composed inputs") + } VariableType::AcceptChannel => { panic!("cannot generate fresh AcceptChannel: requires protocol interaction") } diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 471d723..8706420 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -615,9 +615,10 @@ impl Operation { Self::LoadChainHashFromContext => Some(VariableType::ChainHash), Self::ExtractAcceptChannel(field) => Some(field.output_type()), Self::CreateFundingTransaction => Some(VariableType::FundingTransaction), - Self::BuildOpenChannel - | Self::BuildChannelAnnouncement - | Self::BuildNodeAnnouncement { .. } => Some(VariableType::Message), + Self::BuildChannelAnnouncement | Self::BuildNodeAnnouncement { .. } => { + Some(VariableType::Message) + } + Self::BuildOpenChannel => Some(VariableType::OpenChannelMessage), Self::SendMessage | Self::MineBlocks(_) | Self::BroadcastTransaction => None, Self::RecvAcceptChannel => Some(VariableType::AcceptChannel), } diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index 8740a1e..a13f161 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -1138,10 +1138,10 @@ fn append_out_of_bounds_panics() { fn append_void_reference_panics() { let mut rng = SmallRng::seed_from_u64(0); let mut builder = ProgramBuilder::new(); - OpenChannelGenerator.generate(&mut builder, &mut rng); + NodeAnnouncementGenerator.generate(&mut builder, &mut rng); let program = builder.build(); - // SendMessage is second-to-last and has void output. - let send_idx = program.instructions.len() - 2; + // SendMessage is last and has void output. + let send_idx = program.instructions.len() - 1; assert!( program.instructions[send_idx] .operation @@ -1152,7 +1152,7 @@ fn append_void_reference_panics() { // Rebuild the same program and try to reference the void instruction. let mut rng = SmallRng::seed_from_u64(0); let mut builder = ProgramBuilder::new(); - OpenChannelGenerator.generate(&mut builder, &mut rng); + NodeAnnouncementGenerator.generate(&mut builder, &mut rng); builder.append(Operation::SendMessage, &[send_idx]); } @@ -1280,17 +1280,14 @@ fn validate_rejects_self_reference() { #[test] fn validate_rejects_void_input() { - // Build a valid program, then append a DerivePoint that references the void - // SendMessage instruction emitted by the generator. + // Build a valid program, then append a DerivePoint that references + // the void output emitted by SendMessage. let mut rng = SmallRng::seed_from_u64(0); let mut builder = ProgramBuilder::new(); - OpenChannelGenerator.generate(&mut builder, &mut rng); + NodeAnnouncementGenerator.generate(&mut builder, &mut rng); let mut program = builder.build(); - let send_idx = program - .instructions - .iter() - .position(|i| matches!(i.operation, Operation::SendMessage)) - .expect("generator emits SendMessage"); + // SendMessage is the last instruction in the program. + let send_idx = program.instructions.len() - 1; program.instructions.push(Instruction { operation: Operation::DerivePoint, inputs: vec![send_idx], diff --git a/smite-ir/src/variable.rs b/smite-ir/src/variable.rs index d72072f..08a8d3c 100644 --- a/smite-ir/src/variable.rs +++ b/smite-ir/src/variable.rs @@ -43,6 +43,8 @@ pub enum Variable { Features(Vec), /// Encoded BOLT message with type prefix, ready to send. Message(Vec), + /// Encoded BOLT `open_channel` message with type 32 prefix, ready to send. + OpenChannelMessage(Vec), /// Parsed `accept_channel` response. AcceptChannel(AcceptChannel), /// Constructed funding transaction with funding output index. @@ -72,6 +74,7 @@ impl Variable { Self::U8(_) => VariableType::U8, Self::Features(_) => VariableType::Features, Self::Message(_) => VariableType::Message, + Self::OpenChannelMessage(_) => VariableType::OpenChannelMessage, Self::AcceptChannel(_) => VariableType::AcceptChannel, Self::FundingTransaction(_) => VariableType::FundingTransaction, Self::SentOpenChannel => VariableType::SentOpenChannel, @@ -97,6 +100,7 @@ pub enum VariableType { U8, Features, Message, + OpenChannelMessage, AcceptChannel, FundingTransaction, SentOpenChannel, @@ -121,6 +125,7 @@ impl VariableType { | Self::U8 | Self::Features | Self::Message + | Self::OpenChannelMessage | Self::AcceptChannel | Self::ShortChannelId | Self::FundingTransaction => false, From d7f0788c2dbcacc9400af4b4560ca0dd4a312e72 Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Thu, 21 May 2026 05:33:22 +0000 Subject: [PATCH 6/8] smite-ir: Implement `SendOpenChannel` and modify `RecvAcceptChannel` Introduce the strict topological link for channel initialization. - Add `SendOpenChannel` operation: Consumes a composed `Message` and outputs a `SentOpenChannel` affine variable. The executor verifies the outgoing message is type `open_channel`. - Modify `RecvAcceptChannel`: Now requires `SentOpenChannel` as an input, enforcing that the fuzzer cannot wait for an `accept_channel` unless it actually opened a channel first. - Update unit tests to accommodate the new input requirements for `RecvAcceptChannel`. --- smite-ir/src/mutators/operation_param.rs | 3 +- smite-ir/src/operation.rs | 18 ++- smite-scenarios/src/executor.rs | 146 +++++++++++++++++------ 3 files changed, 126 insertions(+), 41 deletions(-) diff --git a/smite-ir/src/mutators/operation_param.rs b/smite-ir/src/mutators/operation_param.rs index b6201f0..7c0910a 100644 --- a/smite-ir/src/mutators/operation_param.rs +++ b/smite-ir/src/mutators/operation_param.rs @@ -103,7 +103,8 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool { | Operation::BuildChannelAnnouncement | Operation::SendMessage | Operation::RecvAcceptChannel - | Operation::BroadcastTransaction => { + | Operation::BroadcastTransaction + | Operation::SendOpenChannel => { unreachable!("is_param_mutable returned true for {op:?}") } } diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 8706420..613166b 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -138,6 +138,10 @@ pub enum Operation { /// Send an encoded message over the connection. /// Input: `Message`. SendMessage, + /// Send an `open_channel` message over the connection. + /// Produces a `SentOpenChannel` variable. + /// Input: `OpenChannelMessage`. + SendOpenChannel, /// Receive and parse an `accept_channel` response. /// Produces an `AcceptChannel` compound variable. RecvAcceptChannel, @@ -590,6 +594,7 @@ impl fmt::Display for Operation { format_hex(alias), ), Self::SendMessage => write!(f, "SendMessage"), + Self::SendOpenChannel => write!(f, "SendOpenChannel"), } } } @@ -620,6 +625,7 @@ impl Operation { } Self::BuildOpenChannel => Some(VariableType::OpenChannelMessage), Self::SendMessage | Self::MineBlocks(_) | Self::BroadcastTransaction => None, + Self::SendOpenChannel => Some(VariableType::SentOpenChannel), Self::RecvAcceptChannel => Some(VariableType::AcceptChannel), } } @@ -643,9 +649,9 @@ impl Operation { | Self::LoadChannelType(_) | Self::LoadTargetPubkeyFromContext | Self::LoadChainHashFromContext - | Self::MineBlocks(_) - | Self::RecvAcceptChannel => vec![], + | Self::MineBlocks(_) => vec![], + Self::RecvAcceptChannel => vec![VariableType::SentOpenChannel], Self::DerivePoint => vec![VariableType::PrivateKey], Self::ExtractAcceptChannel(_) => vec![VariableType::AcceptChannel], Self::CreateFundingTransaction => vec![ @@ -657,6 +663,8 @@ impl Operation { Self::SendMessage => vec![VariableType::Message], Self::BroadcastTransaction => vec![VariableType::FundingTransaction], + Self::SendOpenChannel => vec![VariableType::OpenChannelMessage], + Self::BuildOpenChannel => vec![ VariableType::ChainHash, // chain_hash VariableType::ChannelId, // temporary_channel_id @@ -730,7 +738,8 @@ impl Operation { | Self::BuildNodeAnnouncement { .. } | Self::SendMessage | Self::MineBlocks(_) - | Self::BroadcastTransaction => vec![], + | Self::BroadcastTransaction + | Self::SendOpenChannel => vec![], Self::RecvAcceptChannel => AcceptChannelField::ALL .iter() @@ -769,7 +778,8 @@ impl Operation { | Self::BuildChannelAnnouncement | Self::SendMessage | Self::RecvAcceptChannel - | Self::BroadcastTransaction => false, + | Self::BroadcastTransaction + | Self::SendOpenChannel => false, } } } diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index e72c36b..0577f07 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -117,6 +117,10 @@ pub enum ExecuteError { #[error("invalid private key")] InvalidPrivateKey, + /// An affine variable is consumed more than once. + #[error("affine variable {index} consumed more than once")] + AffineOverUse { index: usize }, + /// Connection or send/receive failure. #[error("connection: {0}")] Connection(#[from] smite::noise::ConnectionError), @@ -140,6 +144,7 @@ pub enum ExecuteError { /// /// Returns an error if any instruction fails (type mismatch, connection error, /// decode error, etc.). +#[allow(clippy::too_many_lines)] pub fn execute( program: &Program, context: &ProgramContext, @@ -203,7 +208,7 @@ pub fn execute( Operation::BuildOpenChannel => { let oc = build_open_channel(&variables, &instr.inputs)?; let encoded = Message::OpenChannel(oc).encode(); - Some(Variable::Message(encoded)) + Some(Variable::OpenChannelMessage(encoded)) } Operation::BuildChannelAnnouncement => { @@ -231,7 +236,19 @@ pub fn execute( None } + Operation::SendOpenChannel => { + let bytes = resolve_open_channel_message(&variables, instr.inputs[0])?; + log::debug!( + "[{:?}] SendOpenChannel: {} bytes", + start.elapsed(), + bytes.len(), + ); + conn.send_message(bytes)?; + Some(Variable::SentOpenChannel) + } + Operation::RecvAcceptChannel => { + consume_sent_open_channel(&mut variables, instr.inputs[0])?; log::debug!("[{:?}] RecvAcceptChannel: waiting", start.elapsed()); let ac = recv_accept_channel(conn)?; log::debug!("[{:?}] RecvAcceptChannel: received", start.elapsed()); @@ -400,6 +417,17 @@ fn resolve_message(variables: &[Option], index: usize) -> Result<&[u8] } } +fn resolve_open_channel_message( + variables: &[Option], + index: usize, +) -> Result<&[u8], ExecuteError> { + let var = resolve(variables, index)?; + match var { + Variable::OpenChannelMessage(v) => Ok(v), + _ => Err(type_err(VariableType::OpenChannelMessage, var)), + } +} + fn resolve_accept_channel( variables: &[Option], index: usize, @@ -422,6 +450,28 @@ fn resolve_funding_transaction( } } +fn consume_sent_open_channel( + variables: &mut [Option], + index: usize, +) -> Result<(), ExecuteError> { + let var = match resolve(variables, index) { + Ok(v) => v, + // Map `VoidVariable` to `AffineOverUse` + Err(ExecuteError::VoidVariable { index }) => { + return Err(ExecuteError::AffineOverUse { index }); + } + Err(e) => return Err(e), + }; + match var { + Variable::SentOpenChannel => { + // Consume the affine `SentOpenChannel`. + variables[index] = None; + Ok(()) + } + _ => Err(type_err(VariableType::SentOpenChannel, var)), + } +} + // -- Operation handlers -- /// Create a funding transaction by querying the bitcoind for UTXOs and a @@ -913,6 +963,21 @@ mod tests { } } + 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 + } + // -- execute() tests -- #[test] @@ -924,7 +989,7 @@ mod tests { inputs: (0..20).collect(), }); instrs.push(Instruction { - operation: Operation::SendMessage, + operation: Operation::SendOpenChannel, inputs: vec![20], }); @@ -1133,7 +1198,7 @@ mod tests { inputs: (0..20).collect(), }); instrs.push(Instruction { - operation: Operation::SendMessage, + operation: Operation::SendOpenChannel, inputs: vec![20], }); @@ -1183,7 +1248,7 @@ mod tests { inputs: build_inputs, }); instrs.push(Instruction { - operation: Operation::SendMessage, + operation: Operation::SendOpenChannel, inputs: vec![base + 20], }); @@ -1232,14 +1297,17 @@ mod tests { AcceptChannelField::ChannelType, ]; - let mut instrs = vec![Instruction { + let mut instrs = send_open_channel_instructions(); + let sent_open_channel = instrs.len() - 1; + instrs.push(Instruction { operation: Operation::RecvAcceptChannel, - inputs: vec![], - }]; + inputs: vec![sent_open_channel], + }); + let accept_channel_idx = instrs.len() - 1; for field in fields { instrs.push(Instruction { operation: Operation::ExtractAcceptChannel(field), - inputs: vec![0], + inputs: vec![accept_channel_idx], }); } @@ -1266,10 +1334,12 @@ mod tests { fn execute_recv_unexpected_message() { let init_bytes = Message::Init(Init::empty()).encode(); - let instrs = vec![Instruction { + let mut instrs = send_open_channel_instructions(); + let sent_open_channel = instrs.len() - 1; + instrs.push(Instruction { operation: Operation::RecvAcceptChannel, - inputs: vec![], - }]; + inputs: vec![sent_open_channel], + }); let program = Program { instructions: instrs, @@ -1303,10 +1373,12 @@ mod tests { let ping_bytes = Message::Ping(ping).encode(); let ac_bytes = Message::AcceptChannel(sample_accept_channel()).encode(); - let instrs = vec![Instruction { + let mut instrs = send_open_channel_instructions(); + let sent_open_channel = instrs.len() - 1; + instrs.push(Instruction { operation: Operation::RecvAcceptChannel, - inputs: vec![], - }]; + inputs: vec![sent_open_channel], + }); let program = Program { instructions: instrs, @@ -1323,9 +1395,15 @@ mod tests { ) .unwrap(); - // Verify a correctly-sized pong was sent. - assert_eq!(conn.sent.len(), 1); - let pong = Message::decode(&conn.sent[0]).unwrap(); + // Verify exactly two messages were sent: `open_channel` and `pong`. + assert_eq!(conn.sent.len(), 2); + + // Verify the first message was `open_channel` (type 32). + let sent_type = u16::from_be_bytes([conn.sent[0][0], conn.sent[0][1]]); + assert_eq!(sent_type, msg_type::OPEN_CHANNEL); + + // Verify the second message was the pong. + let pong = Message::decode(&conn.sent[1]).unwrap(); let Message::Pong(pong) = pong else { panic!("expected Pong, got {:?}", pong.msg_type()); }; @@ -1397,7 +1475,7 @@ mod tests { #[test] fn execute_variable_out_of_bounds() { let instrs = vec![Instruction { - operation: Operation::SendMessage, + operation: Operation::SendOpenChannel, inputs: vec![99], }]; let program = Program { @@ -1445,23 +1523,19 @@ mod tests { #[test] fn execute_void_variable_reference() { - // SendMessage produces no output variable. Referencing it should fail. - let mut instrs = open_channel_instructions(); - // v20 = Message - instrs.push(Instruction { - operation: Operation::BuildOpenChannel, - inputs: (0..20).collect(), - }); - // v21 = void - instrs.push(Instruction { - operation: Operation::SendMessage, - inputs: vec![20], - }); - // Try to use the void variable. - instrs.push(Instruction { - operation: Operation::SendMessage, - inputs: vec![21], - }); + // MineBlocks produces no output variable. Referencing it should fail. + let instrs = vec![ + // v0 = void + Instruction { + operation: Operation::MineBlocks(1), + inputs: vec![], + }, + // Try to use the void variable. + Instruction { + operation: Operation::DerivePoint, + inputs: vec![0], + }, + ]; let program = Program { instructions: instrs, @@ -1475,7 +1549,7 @@ mod tests { std::time::Instant::now(), ) .unwrap_err(); - assert!(matches!(err, ExecuteError::VoidVariable { index: 21 })); + assert!(matches!(err, ExecuteError::VoidVariable { index: 0 })); } // MineBlocks should track calls to mine_blocks From 41acbf29a0a9881a8baf6a634513c20e5353f7f6 Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Fri, 22 May 2026 08:05:37 +0000 Subject: [PATCH 7/8] smite-ir: Wire affine state variables into `OpenChannelGenerator` Update `OpenChannelGenerator` to utilize the newly introduced strict topological operations: 1. Replace `SendMessage` with `SendOpenChannel`, which consumes the `Message` payload and outputs a `SentOpenChannel`. 2. Update `RecvAcceptChannel` to consume the `SentOpenChannel`. 3. Update tests to accommodate the non-void output of `SendOpenChannel`. --- smite-ir/src/generators/open_channel.rs | 6 +++--- smite-ir/src/tests.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/smite-ir/src/generators/open_channel.rs b/smite-ir/src/generators/open_channel.rs index f58f1bf..12a3539 100644 --- a/smite-ir/src/generators/open_channel.rs +++ b/smite-ir/src/generators/open_channel.rs @@ -48,7 +48,7 @@ impl Generator for OpenChannelGenerator { let channel_type = builder.append(Operation::LoadChannelType(variant), &[]); // Build and send open_channel. - let msg = builder.append( + let open_channel_msg = builder.append( Operation::BuildOpenChannel, &[ chain_hash, @@ -73,9 +73,9 @@ impl Generator for OpenChannelGenerator { channel_type, ], ); - builder.append(Operation::SendMessage, &[msg]); + let sent_open_channel = builder.append(Operation::SendOpenChannel, &[open_channel_msg]); // Receive accept_channel. - builder.append(Operation::RecvAcceptChannel, &[]); + builder.append(Operation::RecvAcceptChannel, &[sent_open_channel]); } } diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index a13f161..ad49378 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -956,10 +956,10 @@ fn generated_open_channel_program_structure() { let program = generate_open_channel_program(0); let ops: Vec<_> = program.instructions.iter().map(|i| &i.operation).collect(); - // Must end with SendMessage, RecvAcceptChannel. + // Must end with SendOpenChannel, RecvAcceptChannel. assert!( - matches!(ops[ops.len() - 2], Operation::SendMessage), - "second-to-last instruction should be SendMessage", + matches!(ops[ops.len() - 2], Operation::SendOpenChannel), + "second-to-last instruction should be SendOpenChannel", ); assert!( matches!(ops[ops.len() - 1], Operation::RecvAcceptChannel), From 5d3fd3cca32de6562c1fb9824800060987eaa973 Mon Sep 17 00:00:00 2001 From: Chandra Pratap Date: Mon, 25 May 2026 05:41:04 +0000 Subject: [PATCH 8/8] smite-ir/tests: Add tests for affine type constraints Add tests to verify the data-flow rules introduced for state tracking. --- smite-ir/src/tests.rs | 72 +++++++++++++++++++++++++++++++++ smite-scenarios/src/executor.rs | 67 ++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index ad49378..35cbdbf 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -1095,6 +1095,28 @@ fn pick_variable_reuses_existing() { ); } +#[test] +fn pick_variable_uses_unspent_affine() { + let mut rng = SmallRng::seed_from_u64(0); + let mut builder = ProgramBuilder::new(); + OpenChannelGenerator.generate(&mut builder, &mut rng); + + let msg_idx = builder.pick_variable(VariableType::OpenChannelMessage, &mut rng); + let unspent_sent_open_channel = builder.append(Operation::SendOpenChannel, &[msg_idx]); + + // pick_variable should prefer an unspent affine variable. + let idx = builder.pick_variable(VariableType::SentOpenChannel, &mut rng); + assert_eq!(idx, unspent_sent_open_channel); +} + +#[test] +#[should_panic(expected = "cannot generate fresh affine type")] +fn pick_variable_panics_on_empty_affine() { + let mut rng = SmallRng::seed_from_u64(0); + let mut builder = ProgramBuilder::new(); + builder.pick_variable(VariableType::SentOpenChannel, &mut rng); +} + #[test] #[should_panic(expected = "cannot generate fresh Message")] fn generate_fresh_message_panics() { @@ -1119,6 +1141,14 @@ fn generate_fresh_funding_transaction_panics() { builder.generate_fresh(VariableType::FundingTransaction, &mut rng); } +#[test] +#[should_panic(expected = "cannot generate fresh affine type")] +fn generate_fresh_sent_open_channel_panics() { + let mut rng = SmallRng::seed_from_u64(0); + let mut builder = ProgramBuilder::new(); + builder.generate_fresh(VariableType::SentOpenChannel, &mut rng); +} + #[test] #[should_panic(expected = "expected 1 inputs, got 0")] fn append_wrong_input_count_panics() { @@ -1165,6 +1195,20 @@ fn append_type_mismatch_panics() { builder.append(Operation::DerivePoint, &[amount]); } +#[test] +#[should_panic(expected = "RecvAcceptChannel input 0: affine SentOpenChannel already consumed")] +fn append_rejects_affine_overuse() { + let mut rng = SmallRng::seed_from_u64(0); + let mut builder = ProgramBuilder::new(); + OpenChannelGenerator.generate(&mut builder, &mut rng); + + let msg_idx = builder.pick_variable(VariableType::OpenChannelMessage, &mut rng); + let sent_open_channel = builder.append(Operation::SendOpenChannel, &[msg_idx]); + // Add consecutive `RecvAcceptChannel`s. + builder.append(Operation::RecvAcceptChannel, &[sent_open_channel]); + builder.append(Operation::RecvAcceptChannel, &[sent_open_channel]); +} + // -- Program::validate tests -- #[test] @@ -1376,6 +1420,34 @@ fn validate_rejects_oversized_features() { ); } +#[test] +fn validate_accepts_unused_affine() { + let mut program = generate_open_channel_program(0); + // Remove the `RecvAcceptChannel` instruction at the end. + program.instructions.pop(); + + // Validation should pass despite the unspent `SentOpenChannel`. + assert_eq!(program.validate(), Ok(())); +} + +#[test] +fn validate_rejects_affine_overuse() { + let mut program = generate_open_channel_program(0); + let sent_open_channel = program.instructions.len() - 2; + // Add another `RecvAcceptChannel` in addition to the existing one. + program.instructions.push(Instruction { + operation: Operation::RecvAcceptChannel, + inputs: vec![sent_open_channel], + }); + assert_eq!( + program.validate(), + Err(ValidateError::AffineOverUse { + index: sent_open_channel, + var_type: VariableType::SentOpenChannel, + }) + ); +} + // -- OperationParamMutator tests -- #[test] diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 0577f07..7db8d7a 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -1695,6 +1695,73 @@ mod tests { assert!(matches!(err, ExecuteError::InvalidPrivateKey)); } + #[test] + fn execute_send_open_channel_wrong_type() { + let instrs = vec![ + Instruction { + operation: Operation::LoadAmount(42), + inputs: vec![], + }, + Instruction { + operation: Operation::SendOpenChannel, + inputs: vec![0], + }, + ]; + + let program = Program { + instructions: instrs, + }; + + let mut conn = MockConnection::new(); + let err = execute( + &program, + &sample_context(), + &mut conn, + &mut MockBitcoinCli::default(), + std::time::Instant::now(), + ) + .unwrap_err(); + assert!(matches!( + err, + ExecuteError::TypeMismatch { + expected: VariableType::OpenChannelMessage, + got: VariableType::Amount, + } + )); + } + + #[test] + fn execute_affine_overuse() { + 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 program = Program { + instructions: instrs, + }; + let mut conn = MockConnection::new(); + let ac_bytes = Message::AcceptChannel(sample_accept_channel()).encode(); + conn.queue_recv(ac_bytes); + let err = execute( + &program, + &sample_context(), + &mut conn, + &mut MockBitcoinCli::default(), + std::time::Instant::now(), + ) + .unwrap_err(); + dbg!(&err); + assert!(matches!(err, ExecuteError::AffineOverUse { index } if index == sent_open_channel)); + } + // -- extract_field tests -- // TODO: Once we can actually construct and send accept_channel messages, it