diff --git a/smite-ir/src/generators.rs b/smite-ir/src/generators.rs index 58d1b2df..3ea2c4a6 100644 --- a/smite-ir/src/generators.rs +++ b/smite-ir/src/generators.rs @@ -8,18 +8,22 @@ mod channel_announcement; mod channel_ready; mod channel_update; +mod error; mod funding_created; mod funding_flow; mod node_announcement; mod open_channel; +mod warning; 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; pub use open_channel::OpenChannelGenerator; +pub use warning::SendWarningGenerator; use rand::Rng; @@ -42,6 +46,8 @@ pub enum AnyGenerator { FundingCreated(FundingCreatedGenerator), ChannelReady(ChannelReadyGenerator), FundingFlow(FundingFlowGenerator), + SendError(SendErrorGenerator), + SendWarning(SendWarningGenerator), } impl AnyGenerator { @@ -54,6 +60,8 @@ impl AnyGenerator { Self::FundingCreated(FundingCreatedGenerator), Self::ChannelReady(ChannelReadyGenerator), Self::FundingFlow(FundingFlowGenerator), + Self::SendError(SendErrorGenerator), + Self::SendWarning(SendWarningGenerator), ]; } @@ -67,6 +75,8 @@ 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), + Self::SendWarning(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/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/mutators/operation_param.rs b/smite-ir/src/mutators/operation_param.rs index 745aa415..c5a43d81 100644 --- a/smite-ir/src/mutators/operation_param.rs +++ b/smite-ir/src/mutators/operation_param.rs @@ -119,6 +119,8 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool { | Operation::SendOpenChannel | 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 22a17899..9d416187 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -214,6 +214,18 @@ 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, + /// 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, @@ -553,6 +565,8 @@ impl fmt::Display for Operation { write!(f, "SendChannelReady{{include_alias={include_alias}}}") } 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()"), @@ -594,6 +608,8 @@ impl Operation { | Self::BuildAnnouncementSignatures => Some(VariableType::Message), Self::SendMessage | Self::SendChannelReady { .. } + | Self::SendError + | Self::SendWarning | Self::RecvChannelReady | Self::MineBlocks(_) | Self::BroadcastTransaction => None, @@ -712,9 +728,9 @@ impl Operation { VariableType::Point, // second_per_commitment_point VariableType::ShortChannelId, // short_channel_id (alias) ], - Self::SendShutdown => vec![ + Self::SendShutdown | Self::SendError | Self::SendWarning => vec![ VariableType::ChannelId, // channel_id - VariableType::Bytes, // scriptpubkey + VariableType::Bytes, // scriptpubkey (shutdown) or data (error, warning) ], Self::RecvAcceptChannel => vec![VariableType::SentOpenChannel], Self::RecvFundingSigned => vec![VariableType::SentFundingCreated], @@ -761,6 +777,8 @@ impl Operation { | Self::SendFundingCreated | Self::SendChannelReady { .. } | Self::SendShutdown + | Self::SendError + | Self::SendWarning | Self::RecvFundingSigned | Self::RecvChannelReady | Self::MineBlocks(_) @@ -809,6 +827,8 @@ impl Operation { | Self::SendFundingCreated | Self::SendChannelReady { .. } | Self::SendShutdown + | Self::SendError + | Self::SendWarning | Self::RecvAcceptChannel | Self::RecvFundingSigned | Self::RecvChannelReady @@ -853,7 +873,9 @@ impl Operation { | Self::SendMessage | Self::SendOpenChannel | Self::SendChannelReady { .. } - | Self::SendShutdown => true, + | Self::SendShutdown + | 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 @@ -916,6 +938,8 @@ impl Operation { | Self::SendOpenChannel | 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 2cb77285..ecf25543 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, SendWarningGenerator, }; use minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer}; use mutators::{ @@ -603,6 +604,72 @@ 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 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 { @@ -915,7 +982,9 @@ fn any_generator_all_is_complete() { | AnyGenerator::OpenChannel(_) | AnyGenerator::FundingCreated(_) | AnyGenerator::ChannelReady(_) - | AnyGenerator::FundingFlow(_) => 7, + | AnyGenerator::FundingFlow(_) + | AnyGenerator::SendError(_) + | AnyGenerator::SendWarning(_) => 9, } }; assert_eq!(AnyGenerator::ALL.len(), variant_count(AnyGenerator::ALL[0])); @@ -1602,6 +1671,113 @@ 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" + ); +} + +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); @@ -1658,6 +1834,22 @@ 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 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); diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 28fb7bc6..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, @@ -492,6 +492,26 @@ 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::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, @@ -939,6 +959,22 @@ 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 `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 92bbc936..243bbf73 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -1132,6 +1132,46 @@ 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_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