From 5c4b3233cbf1792cb067216b0cbebea17815b348 Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Wed, 16 Sep 2026 15:25:54 -0300 Subject: [PATCH 1/4] smite-ir: add SendError operation --- smite-ir/src/mutators/operation_param.rs | 1 + smite-ir/src/operation.rs | 18 ++++++++++--- smite-ir/src/tests.rs | 33 ++++++++++++++++++++++++ smite-scenarios/src/executor.rs | 16 ++++++++++++ smite-scenarios/src/executor/tests.rs | 21 +++++++++++++++ 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/smite-ir/src/mutators/operation_param.rs b/smite-ir/src/mutators/operation_param.rs index 745aa415..7820d348 100644 --- a/smite-ir/src/mutators/operation_param.rs +++ b/smite-ir/src/mutators/operation_param.rs @@ -119,6 +119,7 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool { | Operation::SendOpenChannel | Operation::SendFundingCreated | Operation::SendShutdown + | Operation::SendError | Operation::RecvAcceptChannel | Operation::RecvFundingSigned | Operation::RecvChannelReady diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 22a17899..051bb878 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -214,6 +214,12 @@ pub enum Operation { /// 0: `channel_id` (`ChannelId`) /// 1: `scriptpubkey` (`Bytes`) SendShutdown, + /// Build and send an `error` message (BOLT 1, type 17). + /// + /// Inputs (2): + /// 0: `channel_id` (`ChannelId`, all zeros = all channels) + /// 1: `data` (`Bytes`) + SendError, /// Receive and parse an `accept_channel` response. /// Produces an `AcceptChannel` compound variable. RecvAcceptChannel, @@ -553,6 +559,7 @@ impl fmt::Display for Operation { write!(f, "SendChannelReady{{include_alias={include_alias}}}") } Self::SendShutdown => write!(f, "SendShutdown"), + Self::SendError => write!(f, "SendError"), Self::RecvAcceptChannel => write!(f, "RecvAcceptChannel"), Self::RecvFundingSigned => write!(f, "RecvFundingSigned"), Self::RecvChannelReady => write!(f, "RecvChannelReady()"), @@ -594,6 +601,7 @@ impl Operation { | Self::BuildAnnouncementSignatures => Some(VariableType::Message), Self::SendMessage | Self::SendChannelReady { .. } + | Self::SendError | Self::RecvChannelReady | Self::MineBlocks(_) | Self::BroadcastTransaction => None, @@ -712,9 +720,9 @@ impl Operation { VariableType::Point, // second_per_commitment_point VariableType::ShortChannelId, // short_channel_id (alias) ], - Self::SendShutdown => vec![ + Self::SendShutdown | Self::SendError => vec![ VariableType::ChannelId, // channel_id - VariableType::Bytes, // scriptpubkey + VariableType::Bytes, // scriptpubkey (shutdown) or data (error) ], Self::RecvAcceptChannel => vec![VariableType::SentOpenChannel], Self::RecvFundingSigned => vec![VariableType::SentFundingCreated], @@ -761,6 +769,7 @@ impl Operation { | Self::SendFundingCreated | Self::SendChannelReady { .. } | Self::SendShutdown + | Self::SendError | Self::RecvFundingSigned | Self::RecvChannelReady | Self::MineBlocks(_) @@ -809,6 +818,7 @@ impl Operation { | Self::SendFundingCreated | Self::SendChannelReady { .. } | Self::SendShutdown + | Self::SendError | Self::RecvAcceptChannel | Self::RecvFundingSigned | Self::RecvChannelReady @@ -853,7 +863,8 @@ impl Operation { | Self::SendMessage | Self::SendOpenChannel | Self::SendChannelReady { .. } - | Self::SendShutdown => true, + | Self::SendShutdown + | Self::SendError => true, // `CreateFundingTransaction` selects coins from the wallet, whose // contents change as transactions are created and broadcast. // `SendFundingCreated` builds its message from the recorded @@ -916,6 +927,7 @@ impl Operation { | Self::SendOpenChannel | Self::SendFundingCreated | Self::SendShutdown + | Self::SendError | Self::RecvAcceptChannel | Self::RecvFundingSigned | Self::RecvChannelReady diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index 2cb77285..31a4785b 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -603,6 +603,39 @@ fn display_send_shutdown_program() { } } +#[test] +fn display_send_error_program() { + let instructions = vec![ + Instruction { + operation: Operation::LoadChannelId([0x00; 32]), + inputs: vec![], + }, + Instruction { + operation: Operation::LoadBytes(vec![0x6f, 0x6f, 0x70, 0x73]), + inputs: vec![], + }, + Instruction { + operation: Operation::SendError, + inputs: vec![0, 1], + }, + ]; + + let program = Program { instructions }; + let text = program.to_string(); + let lines: Vec<&str> = text.lines().collect(); + + let cid_hex = "00".repeat(32); + let expected: Vec = vec![ + format!("v0 = LoadChannelId(0x{cid_hex})"), + "v1 = LoadBytes(0x6f6f7073)".into(), + "SendError(v0, v1)".into(), + ]; + assert_eq!(lines.len(), expected.len(), "line count mismatch"); + for (i, (got, want)) in lines.iter().zip(expected.iter()).enumerate() { + assert_eq!(got, want, "line {i} mismatch"); + } +} + #[test] fn postcard_roundtrip() { let program = Program { diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 28fb7bc6..788418c5 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -492,6 +492,14 @@ impl Executor { Some(Variable::SentShutdown) } + Operation::SendError => { + let err = build_error(&variables, &instr.inputs); + let encoded = Message::Error(err).encode(); + log::debug!("[{:?}] SendError: {} bytes", start.elapsed(), encoded.len()); + self.conn.send_message(&encoded)?; + None + } + Operation::RecvAcceptChannel => { consume_affine( &mut variables, @@ -939,6 +947,14 @@ fn build_shutdown(variables: &[Option], inputs: &[usize]) -> Shutdown Shutdown::for_channel(channel_id, scriptpubkey) } +/// Builds an `Error` message from 2 input variables (wire order). +fn build_error(variables: &[Option], inputs: &[usize]) -> smite::bolt::Error { + smite::bolt::Error { + channel_id: resolve_channel_id(variables, inputs[0]), + data: resolve_bytes(variables, inputs[1]).to_vec(), + } +} + /// Builds a signed `ChannelAnnouncement` from 7 input variables. fn build_channel_announcement( variables: &[Option], diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 92bbc936..1b42c338 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -1132,6 +1132,27 @@ fn execute_send_shutdown_empty_scriptpubkey() { assert!(sd.scriptpubkey.is_empty()); } +#[test] +fn execute_send_error() { + let channel_id = ChannelId::new([0x7a; 32]); + // Non-printable bytes are allowed so the fuzzer can probe the target's + // handling of data that violates BOLT 1's printable-ASCII requirement. + let data = vec![0x00, b'b', b'a', b'd', 0xff]; + + let mut b = ProgramBuilder::new(); + let channel_id_var = b.append(Operation::LoadChannelId(channel_id.0), &[]); + let data_var = b.append(Operation::LoadBytes(data.clone()), &[]); + b.append(Operation::SendError, &[channel_id_var, data_var]); + + let mut fx = Fixture::new(); + fx.run(&b.build()); + + assert_eq!(fx.sent_len(), 1); + let err: smite::bolt::Error = fx.sent(0); + assert_eq!(err.channel_id, channel_id); + assert_eq!(err.data, data); +} + #[test] fn execute_recv_channel_ready_invalid_funding_outpoint_is_noop() { // Corrupt the negotiated opener funding pubkey so the broadcast funding From 5f6136206880f619612877e86cc5a98893224cff Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Wed, 16 Sep 2026 15:26:08 -0300 Subject: [PATCH 2/4] smite-ir: add SendError generator --- smite-ir/src/generators.rs | 5 ++ smite-ir/src/generators/error.rs | 30 ++++++++++++ smite-ir/src/tests.rs | 84 +++++++++++++++++++++++++++++++- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 smite-ir/src/generators/error.rs diff --git a/smite-ir/src/generators.rs b/smite-ir/src/generators.rs index 58d1b2df..2fba90aa 100644 --- a/smite-ir/src/generators.rs +++ b/smite-ir/src/generators.rs @@ -8,6 +8,7 @@ mod channel_announcement; mod channel_ready; mod channel_update; +mod error; mod funding_created; mod funding_flow; mod node_announcement; @@ -16,6 +17,7 @@ mod open_channel; pub use channel_announcement::ChannelAnnouncementGenerator; pub use channel_ready::ChannelReadyGenerator; pub use channel_update::ChannelUpdateGenerator; +pub use error::SendErrorGenerator; pub use funding_created::FundingCreatedGenerator; pub use funding_flow::FundingFlowGenerator; pub use node_announcement::NodeAnnouncementGenerator; @@ -42,6 +44,7 @@ pub enum AnyGenerator { FundingCreated(FundingCreatedGenerator), ChannelReady(ChannelReadyGenerator), FundingFlow(FundingFlowGenerator), + SendError(SendErrorGenerator), } impl AnyGenerator { @@ -54,6 +57,7 @@ impl AnyGenerator { Self::FundingCreated(FundingCreatedGenerator), Self::ChannelReady(ChannelReadyGenerator), Self::FundingFlow(FundingFlowGenerator), + Self::SendError(SendErrorGenerator), ]; } @@ -67,6 +71,7 @@ impl Generator for AnyGenerator { Self::FundingCreated(generator) => generator.generate(builder, rng), Self::ChannelReady(generator) => generator.generate(builder, rng), Self::FundingFlow(generator) => generator.generate(builder, rng), + Self::SendError(generator) => generator.generate(builder, rng), } } } diff --git a/smite-ir/src/generators/error.rs b/smite-ir/src/generators/error.rs new file mode 100644 index 00000000..8047986c --- /dev/null +++ b/smite-ir/src/generators/error.rs @@ -0,0 +1,30 @@ +//! Generator for the BOLT 1 `error` message. + +use rand::{Rng, RngExt}; +use smite::bolt::ChannelId; + +use super::Generator; +use crate::builder::ProgramBuilder; +use crate::{Operation, VariableType}; + +/// Generates an unsolicited `error` send. +/// +/// The target fails the referenced channel, so inserting this after a funding +/// flow exercises its force-close path. +#[derive(Clone, Copy)] +pub struct SendErrorGenerator; + +impl Generator for SendErrorGenerator { + fn generate(&self, builder: &mut ProgramBuilder, rng: &mut impl Rng) { + // The all-zero id fails every channel at once (BOLT 1), a path the + // mutator rarely reaches by flipping bits in a real channel_id. + let channel_id = if rng.random_ratio(1, 4) { + builder.append(Operation::LoadChannelId(ChannelId::ALL.0), &[]) + } else { + builder.pick_variable(VariableType::ChannelId, rng) + }; + let data = builder.pick_variable(VariableType::Bytes, rng); + + builder.append(Operation::SendError, &[channel_id, data]); + } +} diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index 31a4785b..22fdc39b 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -10,6 +10,7 @@ use super::*; use generators::{ AnyGenerator, ChannelAnnouncementGenerator, ChannelReadyGenerator, ChannelUpdateGenerator, FundingCreatedGenerator, FundingFlowGenerator, NodeAnnouncementGenerator, OpenChannelGenerator, + SendErrorGenerator, }; use minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer}; use mutators::{ @@ -948,7 +949,8 @@ fn any_generator_all_is_complete() { | AnyGenerator::OpenChannel(_) | AnyGenerator::FundingCreated(_) | AnyGenerator::ChannelReady(_) - | AnyGenerator::FundingFlow(_) => 7, + | AnyGenerator::FundingFlow(_) + | AnyGenerator::SendError(_) => 8, } }; assert_eq!(AnyGenerator::ALL.len(), variant_count(AnyGenerator::ALL[0])); @@ -1635,6 +1637,78 @@ fn generated_channel_update_program_structure() { assert_eq!(build_count, 1, "expected exactly one BuildChannelUpdate"); } +fn generate_send_error_program(seed: u64) -> Program { + let mut rng = SmallRng::seed_from_u64(seed); + let mut builder = ProgramBuilder::new(); + SendErrorGenerator.generate(&mut builder, &mut rng); + builder.build() +} + +// If SendErrorGenerator completes without panicking, every instruction has +// correct input types (enforced by ProgramBuilder::append). +#[test] +fn generated_send_error_program_is_type_correct() { + for seed in 0..100 { + generate_send_error_program(seed); + } +} + +/// Asserts that `program` ends with exactly one send matching `is_send`, and +/// that its `channel_id` and `data` inputs are literal loads. +fn assert_send_error_like_structure(program: &Program, is_send: fn(&Operation) -> bool) { + let ops: Vec<_> = program.instructions.iter().map(|i| &i.operation).collect(); + assert!( + is_send(ops[ops.len() - 1]), + "last instruction should be the send" + ); + assert_eq!( + ops.iter().filter(|op| is_send(op)).count(), + 1, + "expected exactly one send" + ); + + let send = program.instructions.last().expect("non-empty"); + assert!( + matches!(ops[send.inputs[0]], Operation::LoadChannelId(_)), + "channel_id input should be a LoadChannelId", + ); + assert!( + matches!(ops[send.inputs[1]], Operation::LoadBytes(_)), + "data input should be a LoadBytes", + ); +} + +#[test] +fn generated_send_error_program_structure() { + let program = generate_send_error_program(0); + assert_send_error_like_structure(&program, |op| matches!(op, Operation::SendError)); +} + +/// Returns whether the last instruction's `channel_id` input is the all-zero +/// "all channels" id. +fn targets_all_channels(program: &Program) -> bool { + let send = program.instructions.last().expect("non-empty"); + matches!( + program.instructions[send.inputs[0]].operation, + Operation::LoadChannelId(id) if id == [0u8; 32], + ) +} + +// Both the all-channels id and a specific id must show up across seeds, so +// the "fail everything" path gets fresh coverage without relying on mutation. +#[test] +fn generated_send_error_program_varies_channel_id_scope() { + let programs: Vec<_> = (0..100).map(generate_send_error_program).collect(); + assert!( + programs.iter().any(targets_all_channels), + "never targets all channels" + ); + assert!( + !programs.iter().all(targets_all_channels), + "always targets all channels" + ); +} + #[test] fn generated_open_channel_program_postcard_roundtrip() { let program = generate_open_channel_program(42); @@ -1691,6 +1765,14 @@ fn generated_channel_update_program_postcard_roundtrip() { assert_eq!(program, decoded); } +#[test] +fn generated_send_error_program_postcard_roundtrip() { + let program = generate_send_error_program(42); + let bytes = postcard::to_allocvec(&program).expect("postcard serialization"); + let decoded: Program = postcard::from_bytes(&bytes).expect("postcard deserialization"); + assert_eq!(program, decoded); +} + #[test] fn generate_fresh_produces_distinct_indices() { let mut rng = SmallRng::seed_from_u64(0); From 3b619f306e11e657fbe9d3d1b43a146ada034ab7 Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Wed, 16 Sep 2026 15:26:18 -0300 Subject: [PATCH 3/4] smite-ir: add SendWarning operation --- smite-ir/src/mutators/operation_param.rs | 1 + smite-ir/src/operation.rs | 18 ++++++++++--- smite-ir/src/tests.rs | 33 ++++++++++++++++++++++++ smite-scenarios/src/executor.rs | 22 +++++++++++++++- smite-scenarios/src/executor/tests.rs | 19 ++++++++++++++ 5 files changed, 89 insertions(+), 4 deletions(-) diff --git a/smite-ir/src/mutators/operation_param.rs b/smite-ir/src/mutators/operation_param.rs index 7820d348..c5a43d81 100644 --- a/smite-ir/src/mutators/operation_param.rs +++ b/smite-ir/src/mutators/operation_param.rs @@ -120,6 +120,7 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool { | Operation::SendFundingCreated | Operation::SendShutdown | Operation::SendError + | Operation::SendWarning | Operation::RecvAcceptChannel | Operation::RecvFundingSigned | Operation::RecvChannelReady diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 051bb878..9d416187 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -220,6 +220,12 @@ pub enum Operation { /// 0: `channel_id` (`ChannelId`, all zeros = all channels) /// 1: `data` (`Bytes`) SendError, + /// Build and send a `warning` message (BOLT 1, type 1). + /// + /// Inputs (2): + /// 0: `channel_id` (`ChannelId`, all zeros = not channel-specific) + /// 1: `data` (`Bytes`) + SendWarning, /// Receive and parse an `accept_channel` response. /// Produces an `AcceptChannel` compound variable. RecvAcceptChannel, @@ -560,6 +566,7 @@ impl fmt::Display for Operation { } Self::SendShutdown => write!(f, "SendShutdown"), Self::SendError => write!(f, "SendError"), + Self::SendWarning => write!(f, "SendWarning"), Self::RecvAcceptChannel => write!(f, "RecvAcceptChannel"), Self::RecvFundingSigned => write!(f, "RecvFundingSigned"), Self::RecvChannelReady => write!(f, "RecvChannelReady()"), @@ -602,6 +609,7 @@ impl Operation { Self::SendMessage | Self::SendChannelReady { .. } | Self::SendError + | Self::SendWarning | Self::RecvChannelReady | Self::MineBlocks(_) | Self::BroadcastTransaction => None, @@ -720,9 +728,9 @@ impl Operation { VariableType::Point, // second_per_commitment_point VariableType::ShortChannelId, // short_channel_id (alias) ], - Self::SendShutdown | Self::SendError => vec![ + Self::SendShutdown | Self::SendError | Self::SendWarning => vec![ VariableType::ChannelId, // channel_id - VariableType::Bytes, // scriptpubkey (shutdown) or data (error) + VariableType::Bytes, // scriptpubkey (shutdown) or data (error, warning) ], Self::RecvAcceptChannel => vec![VariableType::SentOpenChannel], Self::RecvFundingSigned => vec![VariableType::SentFundingCreated], @@ -770,6 +778,7 @@ impl Operation { | Self::SendChannelReady { .. } | Self::SendShutdown | Self::SendError + | Self::SendWarning | Self::RecvFundingSigned | Self::RecvChannelReady | Self::MineBlocks(_) @@ -819,6 +828,7 @@ impl Operation { | Self::SendChannelReady { .. } | Self::SendShutdown | Self::SendError + | Self::SendWarning | Self::RecvAcceptChannel | Self::RecvFundingSigned | Self::RecvChannelReady @@ -864,7 +874,8 @@ impl Operation { | Self::SendOpenChannel | Self::SendChannelReady { .. } | Self::SendShutdown - | Self::SendError => true, + | Self::SendError + | Self::SendWarning => true, // `CreateFundingTransaction` selects coins from the wallet, whose // contents change as transactions are created and broadcast. // `SendFundingCreated` builds its message from the recorded @@ -928,6 +939,7 @@ impl Operation { | Self::SendFundingCreated | Self::SendShutdown | Self::SendError + | Self::SendWarning | Self::RecvAcceptChannel | Self::RecvFundingSigned | Self::RecvChannelReady diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index 22fdc39b..c7ee60a1 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -637,6 +637,39 @@ fn display_send_error_program() { } } +#[test] +fn display_send_warning_program() { + let instructions = vec![ + Instruction { + operation: Operation::LoadChannelId([0xcd; 32]), + inputs: vec![], + }, + Instruction { + operation: Operation::LoadBytes(vec![0x6f, 0x6f, 0x70, 0x73]), + inputs: vec![], + }, + Instruction { + operation: Operation::SendWarning, + inputs: vec![0, 1], + }, + ]; + + let program = Program { instructions }; + let text = program.to_string(); + let lines: Vec<&str> = text.lines().collect(); + + let cid_hex = "cd".repeat(32); + let expected: Vec = vec![ + format!("v0 = LoadChannelId(0x{cid_hex})"), + "v1 = LoadBytes(0x6f6f7073)".into(), + "SendWarning(v0, v1)".into(), + ]; + assert_eq!(lines.len(), expected.len(), "line count mismatch"); + for (i, (got, want)) in lines.iter().zip(expected.iter()).enumerate() { + assert_eq!(got, want, "line {i} mismatch"); + } +} + #[test] fn postcard_roundtrip() { let program = Program { diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 788418c5..5283bee5 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -11,7 +11,7 @@ use smite::bolt::{ AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelId, ChannelReady, ChannelReadyTlvs, ChannelUpdate, Features, FromMessage, FundingCreated, FundingSigned, Message, MessageType, NodeAnnouncement, OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, - TemporaryChannelId, + TemporaryChannelId, Warning, }; use smite::channel_tx::{ ChannelConfig, ChannelPartyConfig, ChannelState, FundingTransaction, HolderIdentity, Side, @@ -500,6 +500,18 @@ impl Executor { None } + Operation::SendWarning => { + let warning = build_warning(&variables, &instr.inputs); + let encoded = Message::Warning(warning).encode(); + log::debug!( + "[{:?}] SendWarning: {} bytes", + start.elapsed(), + encoded.len() + ); + self.conn.send_message(&encoded)?; + None + } + Operation::RecvAcceptChannel => { consume_affine( &mut variables, @@ -955,6 +967,14 @@ fn build_error(variables: &[Option], inputs: &[usize]) -> smite::bolt: } } +/// Builds a `Warning` message from 2 input variables (wire order). +fn build_warning(variables: &[Option], inputs: &[usize]) -> Warning { + Warning { + channel_id: resolve_channel_id(variables, inputs[0]), + data: resolve_bytes(variables, inputs[1]).to_vec(), + } +} + /// Builds a signed `ChannelAnnouncement` from 7 input variables. fn build_channel_announcement( variables: &[Option], diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 1b42c338..243bbf73 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -1153,6 +1153,25 @@ fn execute_send_error() { assert_eq!(err.data, data); } +#[test] +fn execute_send_warning() { + let channel_id = ChannelId::ALL; + let data = b"not channel-specific".to_vec(); + + let mut b = ProgramBuilder::new(); + let channel_id_var = b.append(Operation::LoadChannelId(channel_id.0), &[]); + let data_var = b.append(Operation::LoadBytes(data.clone()), &[]); + b.append(Operation::SendWarning, &[channel_id_var, data_var]); + + let mut fx = Fixture::new(); + fx.run(&b.build()); + + assert_eq!(fx.sent_len(), 1); + let warning: Warning = fx.sent(0); + assert_eq!(warning.channel_id, channel_id); + assert_eq!(warning.data, data); +} + #[test] fn execute_recv_channel_ready_invalid_funding_outpoint_is_noop() { // Corrupt the negotiated opener funding pubkey so the broadcast funding From fcbdd07c49ba0093953d6fb1b34ac7294cf5c533 Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Wed, 16 Sep 2026 15:26:32 -0300 Subject: [PATCH 4/4] smite-ir: add SendWarning generator --- smite-ir/src/generators.rs | 5 ++++ smite-ir/src/generators/warning.rs | 30 +++++++++++++++++++ smite-ir/src/tests.rs | 48 ++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 smite-ir/src/generators/warning.rs diff --git a/smite-ir/src/generators.rs b/smite-ir/src/generators.rs index 2fba90aa..3ea2c4a6 100644 --- a/smite-ir/src/generators.rs +++ b/smite-ir/src/generators.rs @@ -13,6 +13,7 @@ mod funding_created; mod funding_flow; mod node_announcement; mod open_channel; +mod warning; pub use channel_announcement::ChannelAnnouncementGenerator; pub use channel_ready::ChannelReadyGenerator; @@ -22,6 +23,7 @@ pub use funding_created::FundingCreatedGenerator; pub use funding_flow::FundingFlowGenerator; pub use node_announcement::NodeAnnouncementGenerator; pub use open_channel::OpenChannelGenerator; +pub use warning::SendWarningGenerator; use rand::Rng; @@ -45,6 +47,7 @@ pub enum AnyGenerator { ChannelReady(ChannelReadyGenerator), FundingFlow(FundingFlowGenerator), SendError(SendErrorGenerator), + SendWarning(SendWarningGenerator), } impl AnyGenerator { @@ -58,6 +61,7 @@ impl AnyGenerator { Self::ChannelReady(ChannelReadyGenerator), Self::FundingFlow(FundingFlowGenerator), Self::SendError(SendErrorGenerator), + Self::SendWarning(SendWarningGenerator), ]; } @@ -72,6 +76,7 @@ impl Generator for AnyGenerator { Self::ChannelReady(generator) => generator.generate(builder, rng), Self::FundingFlow(generator) => generator.generate(builder, rng), Self::SendError(generator) => generator.generate(builder, rng), + Self::SendWarning(generator) => generator.generate(builder, rng), } } } diff --git a/smite-ir/src/generators/warning.rs b/smite-ir/src/generators/warning.rs new file mode 100644 index 00000000..4ed9c4a3 --- /dev/null +++ b/smite-ir/src/generators/warning.rs @@ -0,0 +1,30 @@ +//! Generator for the BOLT 1 `warning` message. + +use rand::{Rng, RngExt}; +use smite::bolt::ChannelId; + +use super::Generator; +use crate::builder::ProgramBuilder; +use crate::{Operation, VariableType}; + +/// Generates an unsolicited `warning` send. +/// +/// Targets only log warnings, so this mainly covers their parsing and logging +/// of untrusted `data` for both known and unknown channels. +#[derive(Clone, Copy)] +pub struct SendWarningGenerator; + +impl Generator for SendWarningGenerator { + fn generate(&self, builder: &mut ProgramBuilder, rng: &mut impl Rng) { + // The all-zero id marks a warning as not channel-specific (BOLT 1), a + // path the mutator rarely reaches by flipping bits in a real channel_id. + let channel_id = if rng.random_ratio(1, 4) { + builder.append(Operation::LoadChannelId(ChannelId::ALL.0), &[]) + } else { + builder.pick_variable(VariableType::ChannelId, rng) + }; + let data = builder.pick_variable(VariableType::Bytes, rng); + + builder.append(Operation::SendWarning, &[channel_id, data]); + } +} diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index c7ee60a1..ecf25543 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -10,7 +10,7 @@ use super::*; use generators::{ AnyGenerator, ChannelAnnouncementGenerator, ChannelReadyGenerator, ChannelUpdateGenerator, FundingCreatedGenerator, FundingFlowGenerator, NodeAnnouncementGenerator, OpenChannelGenerator, - SendErrorGenerator, + SendErrorGenerator, SendWarningGenerator, }; use minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer}; use mutators::{ @@ -983,7 +983,8 @@ fn any_generator_all_is_complete() { | AnyGenerator::FundingCreated(_) | AnyGenerator::ChannelReady(_) | AnyGenerator::FundingFlow(_) - | AnyGenerator::SendError(_) => 8, + | AnyGenerator::SendError(_) + | AnyGenerator::SendWarning(_) => 9, } }; assert_eq!(AnyGenerator::ALL.len(), variant_count(AnyGenerator::ALL[0])); @@ -1742,6 +1743,41 @@ fn generated_send_error_program_varies_channel_id_scope() { ); } +fn generate_send_warning_program(seed: u64) -> Program { + let mut rng = SmallRng::seed_from_u64(seed); + let mut builder = ProgramBuilder::new(); + SendWarningGenerator.generate(&mut builder, &mut rng); + builder.build() +} + +// If SendWarningGenerator completes without panicking, every instruction has +// correct input types (enforced by ProgramBuilder::append). +#[test] +fn generated_send_warning_program_is_type_correct() { + for seed in 0..100 { + generate_send_warning_program(seed); + } +} + +#[test] +fn generated_send_warning_program_structure() { + let program = generate_send_warning_program(0); + assert_send_error_like_structure(&program, |op| matches!(op, Operation::SendWarning)); +} + +#[test] +fn generated_send_warning_program_varies_channel_id_scope() { + let programs: Vec<_> = (0..100).map(generate_send_warning_program).collect(); + assert!( + programs.iter().any(targets_all_channels), + "never targets all channels" + ); + assert!( + !programs.iter().all(targets_all_channels), + "always targets all channels" + ); +} + #[test] fn generated_open_channel_program_postcard_roundtrip() { let program = generate_open_channel_program(42); @@ -1806,6 +1842,14 @@ fn generated_send_error_program_postcard_roundtrip() { assert_eq!(program, decoded); } +#[test] +fn generated_send_warning_program_postcard_roundtrip() { + let program = generate_send_warning_program(42); + let bytes = postcard::to_allocvec(&program).expect("postcard serialization"); + let decoded: Program = postcard::from_bytes(&bytes).expect("postcard deserialization"); + assert_eq!(program, decoded); +} + #[test] fn generate_fresh_produces_distinct_indices() { let mut rng = SmallRng::seed_from_u64(0);