From b9204de2792b42832c7aefd6cd876076eba41567 Mon Sep 17 00:00:00 2001 From: Devansh Vashisht Date: Tue, 9 Jun 2026 12:27:24 +0530 Subject: [PATCH] smite-ir: implement BuildChannelUpdate operation --- smite-ir/src/mutators/operation_param.rs | 1 + smite-ir/src/operation.rs | 45 ++++++- smite-ir/src/tests.rs | 84 +++++++++++++ smite-scenarios/src/executor.rs | 153 ++++++++++++++++++++++- 4 files changed, 277 insertions(+), 6 deletions(-) diff --git a/smite-ir/src/mutators/operation_param.rs b/smite-ir/src/mutators/operation_param.rs index 6e4df53..9cf95ab 100644 --- a/smite-ir/src/mutators/operation_param.rs +++ b/smite-ir/src/mutators/operation_param.rs @@ -102,6 +102,7 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool { | Operation::LoadChainHashFromContext | Operation::BuildOpenChannel | Operation::BuildChannelAnnouncement + | Operation::BuildChannelUpdate | Operation::SendMessage | Operation::SendOpenChannel | Operation::RecvAcceptChannel diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 1da815c..08ee7c3 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -136,6 +136,25 @@ pub enum Operation { /// 32-byte node alias, zero-padded. alias: [u8; 32], }, + /// Build a `channel_update` message (BOLT 7, type 258). + /// + /// The signature is computed internally over the double-SHA256 of the + /// message body following the signature field, using the supplied node + /// secret key (per BOLT 7). + /// + /// Inputs (11): + /// 0: `node_sk` (`PrivateKey`) -- signs the body + /// 1: `chain_hash` (`ChainHash`) + /// 2: `short_channel_id` (`ShortChannelId`) + /// 3: `timestamp` (`Timestamp`) + /// 4: `message_flags` (`U8`) + /// 5: `channel_flags` (`U8`) + /// 6: `cltv_expiry_delta` (`U16`) + /// 7: `htlc_minimum_msat` (`Amount`) + /// 8: `fee_base_msat` (`ForwardingFee`) + /// 9: `fee_proportional_millionths` (`ForwardingFee`) + /// 10: `htlc_maximum_msat` (`Amount`) + BuildChannelUpdate, // -- Act: side effects against the target -- /// Send an encoded message over the connection. @@ -597,6 +616,7 @@ impl fmt::Display for Operation { format_hex(rgb_color), format_hex(alias), ), + Self::BuildChannelUpdate => write!(f, "BuildChannelUpdate"), Self::SendMessage => write!(f, "SendMessage"), Self::SendOpenChannel => write!(f, "SendOpenChannel"), } @@ -626,9 +646,9 @@ impl Operation { Self::ExtractAcceptChannel(field) => Some(field.output_type()), Self::CreateFundingTransaction => Some(VariableType::FundingTransaction), Self::BuildOpenChannel => Some(VariableType::OpenChannelMessage), - Self::BuildChannelAnnouncement | Self::BuildNodeAnnouncement { .. } => { - Some(VariableType::Message) - } + Self::BuildChannelAnnouncement + | Self::BuildNodeAnnouncement { .. } + | Self::BuildChannelUpdate => Some(VariableType::Message), Self::SendMessage | Self::MineBlocks(_) | Self::BroadcastTransaction => None, Self::SendOpenChannel => Some(VariableType::SentOpenChannel), Self::RecvAcceptChannel => Some(VariableType::AcceptChannel), @@ -709,6 +729,20 @@ impl Operation { VariableType::Timestamp, // timestamp VariableType::Bytes, // addresses ], + + Self::BuildChannelUpdate => vec![ + VariableType::PrivateKey, // node_sk + VariableType::ChainHash, // chain_hash + VariableType::ShortChannelId, // short_channel_id + VariableType::Timestamp, // timestamp + VariableType::U8, // message_flags + VariableType::U8, // channel_flags + VariableType::U16, // cltv_expiry_delta + VariableType::Amount, // htlc_minimum_msat + VariableType::ForwardingFee, // fee_base_msat + VariableType::ForwardingFee, // fee_proportional_millionths + VariableType::Amount, // htlc_maximum_msat + ], } } @@ -742,6 +776,7 @@ impl Operation { | Self::BuildOpenChannel | Self::BuildChannelAnnouncement | Self::BuildNodeAnnouncement { .. } + | Self::BuildChannelUpdate | Self::SendMessage | Self::SendOpenChannel | Self::MineBlocks(_) @@ -785,7 +820,8 @@ impl Operation { | Self::DerivePoint | Self::ExtractAcceptChannel(_) | Self::BuildOpenChannel - | Self::BuildNodeAnnouncement { .. } => false, + | Self::BuildNodeAnnouncement { .. } + | Self::BuildChannelUpdate => false, } } @@ -818,6 +854,7 @@ impl Operation { | Self::CreateFundingTransaction | Self::BuildOpenChannel | Self::BuildChannelAnnouncement + | Self::BuildChannelUpdate | Self::SendMessage | Self::SendOpenChannel | Self::RecvAcceptChannel diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index ccb722b..ad41364 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -330,6 +330,90 @@ fn display_build_node_announcement_program() { } } +#[test] +fn display_build_channel_update_program() { + let scid = ShortChannelId::new(538_532, 845, 1); + let instructions = vec![ + Instruction { + operation: Operation::LoadPrivateKey(key(1)), + 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 + 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 program = Program { instructions }; + let text = program.to_string(); + let lines: Vec<&str> = text.lines().collect(); + + let z31 = "00".repeat(31); + let expected: Vec = vec![ + format!("v0 = LoadPrivateKey(0x{z31}01)"), + "v1 = LoadChainHashFromContext()".into(), + "v2 = LoadShortChannelId(538532x845x1)".into(), + "v3 = LoadTimestamp(1715000000)".into(), + "v4 = LoadU8(1)".into(), + "v5 = LoadU8(0)".into(), + "v6 = LoadU16(144)".into(), + "v7 = LoadAmount(1000)".into(), + "v8 = LoadForwardingFee(1000)".into(), + "v9 = LoadForwardingFee(100)".into(), + "v10 = LoadAmount(99000000)".into(), + "v11 = BuildChannelUpdate(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)".into(), + "SendMessage(v11)".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 dd663aa..f3b257e 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -8,8 +8,8 @@ use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use smite::bitcoin::{BitcoinCli, Utxo}; use smite::bolt::{ - AcceptChannel, ChannelAnnouncement, ChannelId, Message, NodeAnnouncement, OpenChannel, - OpenChannelTlvs, Pong, ShortChannelId, msg_type, + AcceptChannel, ChannelAnnouncement, ChannelId, ChannelUpdate, Message, NodeAnnouncement, + OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, msg_type, }; use smite::channel_tx::{FundingTransaction, build_funding_transaction}; use smite::noise::{ConnectionError, NoiseConnection}; @@ -215,6 +215,12 @@ pub fn execute( Some(Variable::Message(encoded)) } + Operation::BuildChannelUpdate => { + let cu = build_channel_update(&variables, &instr.inputs); + let encoded = Message::ChannelUpdate(cu).encode(); + Some(Variable::Message(encoded)) + } + // -- Act operations -- Operation::SendMessage => { let bytes = resolve_message(&variables, instr.inputs[0]); @@ -306,6 +312,16 @@ fn resolve_feerate(variables: &[Option], index: usize) -> u32 { } } +fn resolve_forwarding_fee(variables: &[Option], index: usize) -> u32 { + match resolve(variables, index) { + Variable::ForwardingFee(v) => *v, + other => panic!( + "variable {index}: expected ForwardingFee, got {:?}", + other.var_type(), + ), + } +} + fn resolve_timestamp(variables: &[Option], index: usize) -> u32 { match resolve(variables, index) { Variable::Timestamp(v) => *v, @@ -592,6 +608,41 @@ fn build_node_announcement( na } +/// Builds a signed `ChannelUpdate` from 11 input variables. +fn build_channel_update(variables: &[Option], inputs: &[usize]) -> ChannelUpdate { + let sk_bytes = resolve_private_key(variables, inputs[0]); + let chain_hash = resolve_chain_hash(variables, inputs[1]); + let short_channel_id = resolve_short_channel_id(variables, inputs[2]); + let timestamp = resolve_timestamp(variables, inputs[3]); + let message_flags = resolve_u8(variables, inputs[4]); + let channel_flags = resolve_u8(variables, inputs[5]); + let cltv_expiry_delta = resolve_u16(variables, inputs[6]); + let htlc_minimum_msat = resolve_amount(variables, inputs[7]); + let fee_base_msat = resolve_forwarding_fee(variables, inputs[8]); + let fee_proportional_millionths = resolve_forwarding_fee(variables, inputs[9]); + let htlc_maximum_msat = resolve_amount(variables, inputs[10]); + + let sk = SecretKey::from_slice(&sk_bytes).expect("valid private key"); + + let mut cu = ChannelUpdate { + signature: bitcoin::secp256k1::ecdsa::Signature::from_compact(&[0u8; 64]) + .expect("zero bytes parse as a signature"), + 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, + extra: Vec::new(), + }; + cu.sign(&sk); + cu +} + /// Receives the next message of interest, auto-responding to pings and silently /// skipping unknown odd-type messages. #[allow(clippy::similar_names)] // ping and pong are canonical names @@ -1159,6 +1210,104 @@ mod tests { assert!(na.verify()); } + #[test] + fn execute_build_channel_update() { + let mut sk_bytes = [0u8; 32]; + 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 program = Program { + instructions: instrs, + }; + let mut conn = MockConnection::new(); + execute( + &program, + &sample_context(), + &mut conn, + &mut MockBitcoinCli::default(), + std::time::Instant::now(), + ) + .unwrap(); + + assert_eq!(conn.sent.len(), 1); + let cu = match Message::decode(&conn.sent[0]).expect("valid message") { + Message::ChannelUpdate(cu) => cu, + other => panic!("expected ChannelUpdate, got type {}", other.msg_type()), + }; + + assert_eq!(cu.chain_hash, sample_context().chain_hash); + assert_eq!(cu.short_channel_id, scid); + assert_eq!(cu.timestamp, 1_715_000_000); + assert_eq!(cu.message_flags, 0x01); + assert_eq!(cu.channel_flags, 0x00); + assert_eq!(cu.cltv_expiry_delta, 144); + assert_eq!(cu.htlc_minimum_msat, 1_000); + assert_eq!(cu.fee_base_msat, 1_000); + assert_eq!(cu.fee_proportional_millionths, 100); + assert_eq!(cu.htlc_maximum_msat, 99_000_000); + assert!(cu.extra.is_empty()); + + let secp = Secp256k1::new(); + let expected_node_id = + PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&sk_bytes).unwrap()); + assert!(cu.verify(&expected_node_id)); + } + #[test] fn execute_build_open_channel_with_tlvs() { let mut instrs = open_channel_instructions();