From 280aa53a267677f68e454107f6a94e0ae5db9aea Mon Sep 17 00:00:00 2001 From: Devansh Vashisht Date: Sun, 13 Sep 2026 21:42:38 +0530 Subject: [PATCH 1/3] smite-ir: add LoadLocalNodeSecretFromContext operation Loads our node's secret key from the program context, so that programs can sign gossip as the identity the target knows us by. The Noise static key is our node id on the wire, and it is what targets verify our gossip signatures against. Signing with a LoadPrivateKey literal instead can never produce a valid signature, so announcement_signatures has no way to get past signature verification today. The key is not exposed as a mutable parameter. OperationParamMutator can already invalidate a signature by mutating the message body, and letting it corrupt our identity as well would just collapse every mutation of the instruction onto the same path. The executor test fixture holds a different key than the scenarios use, so a passing test proves the executor read the context. --- smite-ir/src/mutators/operation_param.rs | 1 + smite-ir/src/operation.rs | 16 +++++- smite-ir/src/tests.rs | 6 ++- smite-scenarios/src/executor.rs | 6 +++ smite-scenarios/src/executor/tests.rs | 51 +++++++++++++++++++ smite-scenarios/src/executor/tests/harness.rs | 3 ++ smite-scenarios/src/scenarios/setup.rs | 3 +- 7 files changed, 82 insertions(+), 4 deletions(-) diff --git a/smite-ir/src/mutators/operation_param.rs b/smite-ir/src/mutators/operation_param.rs index 745aa415..e3f04b3b 100644 --- a/smite-ir/src/mutators/operation_param.rs +++ b/smite-ir/src/mutators/operation_param.rs @@ -111,6 +111,7 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool { | Operation::CreateFundingTransaction | Operation::LoadTargetPubkeyFromContext | Operation::LoadChainHashFromContext + | Operation::LoadLocalNodeSecretFromContext | Operation::BuildOpenChannel | Operation::BuildChannelAnnouncement | Operation::BuildChannelUpdate diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 22a17899..78c49795 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -63,6 +63,10 @@ pub enum Operation { LoadTargetPubkeyFromContext, /// Load the chain hash from the program context. LoadChainHashFromContext, + /// Load our node's secret key from the program context. This is the Noise + /// static key the connection was established with, so it is the identity + /// targets verify our gossip signatures against. + LoadLocalNodeSecretFromContext, // -- Compute: derive a variable from inputs -- /// Derive a compressed public key from a private key. The executor @@ -532,6 +536,9 @@ impl fmt::Display for Operation { Self::LoadChannelType(v) => write!(f, "LoadChannelType({v})"), Self::LoadTargetPubkeyFromContext => write!(f, "LoadTargetPubkeyFromContext()"), Self::LoadChainHashFromContext => write!(f, "LoadChainHashFromContext()"), + Self::LoadLocalNodeSecretFromContext => { + write!(f, "LoadLocalNodeSecretFromContext()") + } // Operations with inputs: parens added by Program::Display. Self::DerivePoint => write!(f, "DerivePoint"), Self::ExtractAcceptChannel(field) => write!(f, "Extract{field}"), @@ -581,7 +588,9 @@ impl Operation { Self::LoadU8(_) => Some(VariableType::U8), Self::LoadBytes(_) | Self::LoadShutdownScript(_) => Some(VariableType::Bytes), Self::LoadFeatures(_) | Self::LoadChannelType(_) => Some(VariableType::Features), - Self::LoadPrivateKey(_) => Some(VariableType::PrivateKey), + Self::LoadPrivateKey(_) | Self::LoadLocalNodeSecretFromContext => { + Some(VariableType::PrivateKey) + } Self::LoadChannelId(_) | Self::RecvFundingSigned => Some(VariableType::ChannelId), Self::LoadTargetPubkeyFromContext | Self::DerivePoint => Some(VariableType::Point), Self::LoadChainHashFromContext => Some(VariableType::ChainHash), @@ -625,6 +634,7 @@ impl Operation { | Self::LoadChannelType(_) | Self::LoadTargetPubkeyFromContext | Self::LoadChainHashFromContext + | Self::LoadLocalNodeSecretFromContext | Self::RecvChannelReady | Self::MineBlocks(_) => vec![], @@ -748,6 +758,7 @@ impl Operation { | Self::LoadChannelType(_) | Self::LoadTargetPubkeyFromContext | Self::LoadChainHashFromContext + | Self::LoadLocalNodeSecretFromContext | Self::DerivePoint | Self::ExtractAcceptChannel(_) | Self::CreateFundingTransaction @@ -795,6 +806,7 @@ impl Operation { | Self::LoadChannelType(_) | Self::LoadTargetPubkeyFromContext | Self::LoadChainHashFromContext + | Self::LoadLocalNodeSecretFromContext | Self::DerivePoint | Self::ExtractAcceptChannel(_) | Self::BuildOpenChannel @@ -843,6 +855,7 @@ impl Operation { | Self::LoadChannelType(_) | Self::LoadTargetPubkeyFromContext | Self::LoadChainHashFromContext + | Self::LoadLocalNodeSecretFromContext | Self::DerivePoint | Self::ExtractAcceptChannel(_) | Self::BuildOpenChannel @@ -906,6 +919,7 @@ impl Operation { Self::LoadTargetPubkeyFromContext | Self::LoadChainHashFromContext + | Self::LoadLocalNodeSecretFromContext | Self::DerivePoint | Self::CreateFundingTransaction | Self::BuildOpenChannel diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index 2cb77285..e9a49dcf 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -457,8 +457,10 @@ fn display_build_announcement_signatures_program() { operation: Operation::LoadShortChannelId(scid.as_u64()), inputs: vec![], }, + // Our node secret key (input 4 to BuildAnnouncementSignatures): the + // Noise static key, which is the identity the target verifies against. Instruction { - operation: Operation::LoadPrivateKey(key(1)), + operation: Operation::LoadLocalNodeSecretFromContext, inputs: vec![], }, // Target's node public key (input 5 to BuildAnnouncementSignatures). @@ -499,7 +501,7 @@ fn display_build_announcement_signatures_program() { "v1 = LoadFeatures(0x0102)".into(), "v2 = LoadChainHashFromContext()".into(), format!("v3 = LoadShortChannelId({scid})"), - format!("v4 = LoadPrivateKey(0x{z31}01)"), + "v4 = LoadLocalNodeSecretFromContext()".into(), "v5 = LoadTargetPubkeyFromContext()".into(), format!("v6 = LoadPrivateKey(0x{z31}02)"), "v7 = LoadTargetPubkeyFromContext()".into(), diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index a63eb3b3..44be3eee 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -138,6 +138,9 @@ pub struct ProgramContext { pub block_height: u32, /// Target's advertised feature bits from init message. pub target_features: Vec, + /// Our node's secret key: the Noise static key used for the handshake, and + /// so the identity the target knows us by. + pub local_node_secret: [u8; 32], } /// Abstraction over a Noise-encrypted connection, allowing mock implementations @@ -359,6 +362,9 @@ impl Executor { Operation::LoadChainHashFromContext => { Some(Variable::ChainHash(self.context.chain_hash)) } + Operation::LoadLocalNodeSecretFromContext => { + Some(Variable::PrivateKey(self.context.local_node_secret)) + } // -- Compute operations -- Operation::DerivePoint => { diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 93eb76e8..5a97673d 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -182,6 +182,57 @@ fn execute_build_node_announcement() { assert!(na.verify()); } +// The node secret must come from the context rather than the program, so that +// gossip is signed with the identity the target knows us by. +#[test] +fn execute_load_local_node_secret_from_context() { + let instrs = vec![ + Instruction { + operation: Operation::LoadLocalNodeSecretFromContext, + inputs: vec![], + }, + Instruction { + operation: Operation::LoadFeatures(vec![]), + inputs: vec![], + }, + Instruction { + operation: Operation::LoadTimestamp(1_700_000_000), + inputs: vec![], + }, + Instruction { + operation: Operation::LoadBytes(vec![]), + inputs: vec![], + }, + Instruction { + operation: Operation::BuildNodeAnnouncement { + rgb_color: [0; 3], + alias: [0; 32], + }, + inputs: vec![0, 1, 2, 3], + }, + Instruction { + operation: Operation::SendMessage, + inputs: vec![4], + }, + ]; + + let mut fx = Fixture::new(); + fx.run(&Program { + instructions: instrs, + }); + + assert_eq!(fx.sent_len(), 1); + let na: NodeAnnouncement = fx.sent(0); + + let secp = Secp256k1::new(); + let expected_node_id = PublicKey::from_secret_key( + &secp, + &SecretKey::from_slice(&sample_context().local_node_secret).unwrap(), + ); + assert_eq!(na.node_id, expected_node_id); + assert!(na.verify()); +} + #[test] fn execute_build_channel_update() { let mut sk_bytes = [0u8; 32]; diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index a76226fa..a4d21fe6 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -257,6 +257,9 @@ pub fn sample_context() -> ProgramContext { chain_hash: [0xcc; 32], block_height: 800_000, target_features: vec![], + // Deliberately not the real Noise static key, so a test can tell the + // executor read the context rather than a constant. + local_node_secret: [0xee; 32], } } diff --git a/smite-scenarios/src/scenarios/setup.rs b/smite-scenarios/src/scenarios/setup.rs index 1daf89e9..b26a7674 100644 --- a/smite-scenarios/src/scenarios/setup.rs +++ b/smite-scenarios/src/scenarios/setup.rs @@ -6,7 +6,7 @@ use smite::bolt::{FeatureBit, Features, Init, InitTlvs, Message}; use smite::noise::NoiseConnection; use smite::scenarios::ScenarioError; -use super::{handshake_with_target, ping_pong}; +use super::{STATIC_KEY, handshake_with_target, ping_pong}; use crate::executor::ProgramContext; use crate::targets::{INITIAL_BLOCKS, Target}; @@ -87,6 +87,7 @@ impl SnapshotSetup for PostInitSetup { // later. block_height: u32::try_from(INITIAL_BLOCKS).expect("fits in u32"), target_features: target_init.features, + local_node_secret: STATIC_KEY, }; Ok((conn, context)) From 48232976203443048aae2ebaf8fb167b37944858 Mon Sep 17 00:00:00 2001 From: Devansh Vashisht Date: Sun, 13 Sep 2026 21:37:23 +0530 Subject: [PATCH 2/3] smite-ir: let open_channel flows announce the channel announcement_signatures is only legal on an announced channel, so append_open_channel takes an announce flag that sets channel_flags bit 0. Announcing also restricts the channel type: LDK and LND reject an announced channel that negotiates option_scid_alias or option_zeroconf, neither of which has an announceable short_channel_id. A test pins the list to that property so a new variant fails rather than silently changing what announced channels negotiate. Both existing call sites pass false, so the flows they generate are unchanged. --- smite-ir/src/generators/funding_flow.rs | 2 +- smite-ir/src/generators/open_channel.rs | 45 ++++++++++++++++++++----- smite-ir/src/tests.rs | 30 ++++++++++++++--- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/smite-ir/src/generators/funding_flow.rs b/smite-ir/src/generators/funding_flow.rs index b07f6791..037f5e91 100644 --- a/smite-ir/src/generators/funding_flow.rs +++ b/smite-ir/src/generators/funding_flow.rs @@ -26,7 +26,7 @@ impl Generator for FundingFlowGenerator { let funding_pubkey = builder.append(Operation::DerivePoint, &[funding_privkey]); // Build and send open_channel. - let open_channel = append_open_channel(builder, rng, funding_pubkey); + let open_channel = append_open_channel(builder, rng, funding_pubkey, false); // Receive accept_channel. let accept_channel = builder.append( diff --git a/smite-ir/src/generators/open_channel.rs b/smite-ir/src/generators/open_channel.rs index 581071fe..2705d197 100644 --- a/smite-ir/src/generators/open_channel.rs +++ b/smite-ir/src/generators/open_channel.rs @@ -62,10 +62,25 @@ impl OpenChannelGenerator { /// and CLN allow up to 483, while LDK and Eclair cap 0FC channels at 114 /// due to the v3 package size limit. pub const MAX_MAX_ACCEPTED_HTLCS: u16 = 114; - /// Keep channels unannounced: clearing `announce_channel` keeps - /// `option_scid_alias` valid, while LDK and LND reject announced channels - /// that negotiate it. - pub const CHANNEL_FLAGS: u8 = 0; + /// BOLT 2 `channel_flags` bit 0. Set, the channel is announced to the + /// network, which every target requires before it acts on + /// `announcement_signatures`. Clear, the channel stays private, which keeps + /// `option_scid_alias` valid: LDK and LND reject announced channels that + /// negotiate it. All other bits are undefined and must stay zero. + pub const ANNOUNCE_CHANNEL_FLAG: u8 = 0b0000_0001; + + /// Channel types an announced channel may negotiate. LDK and LND reject an + /// announced channel whose type includes `option_scid_alias` (bit 46) or + /// `option_zeroconf` (bit 50), since neither has an announceable + /// `short_channel_id`. + pub const ANNOUNCEABLE_CHANNEL_TYPES: &[ChannelTypeVariant] = &[ + ChannelTypeVariant::StaticRemoteKey, + ChannelTypeVariant::Anchors, + ChannelTypeVariant::ZeroFeeCommitments, + ChannelTypeVariant::SimpleTaproot, + ChannelTypeVariant::SimpleTaprootStaging, + ChannelTypeVariant::ScriptEnforcedLease, + ]; } /// Instruction indices produced by [`append_open_channel`], for later @@ -84,10 +99,14 @@ pub struct OpenChannelVars { /// Appends the instructions that generate bounded channel parameters, then /// build and send `open_channel` using `funding_pubkey`. +/// +/// When `announce` is set the channel is opened as an announced one, which the +/// gossip flows need and which restricts the channel types it may negotiate. pub fn append_open_channel( builder: &mut ProgramBuilder, rng: &mut impl Rng, funding_pubkey: usize, + announce: bool, ) -> OpenChannelVars { type Bounds = OpenChannelGenerator; @@ -146,13 +165,23 @@ pub fn append_open_channel( ), &[], ); - let channel_flags = builder.append(Operation::LoadU8(Bounds::CHANNEL_FLAGS), &[]); + let flags = if announce { + Bounds::ANNOUNCE_CHANNEL_FLAG + } else { + 0 + }; + let channel_flags = builder.append(Operation::LoadU8(flags), &[]); let shutdown_script_variant = ShutdownScriptVariant::random(rng); let upfront_shutdown_script = builder.append(Operation::LoadShutdownScript(shutdown_script_variant), &[]); - let variant = *ChannelTypeVariant::ALL + let channel_types = if announce { + Bounds::ANNOUNCEABLE_CHANNEL_TYPES + } else { + ChannelTypeVariant::ALL + }; + let variant = *channel_types .choose(rng) - .expect("ChannelTypeVariant::ALL is non-empty"); + .expect("channel type list is non-empty"); let channel_type = builder.append(Operation::LoadChannelType(variant), &[]); // Build and send open_channel. @@ -198,7 +227,7 @@ impl Generator for OpenChannelGenerator { let funding_pubkey = builder.generate_fresh(VariableType::Point, rng); // Build and send open_channel. - let open_channel = append_open_channel(builder, rng, funding_pubkey); + let open_channel = append_open_channel(builder, rng, funding_pubkey, false); // Receive accept_channel. builder.append( diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index e9a49dcf..fd9f79d9 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -4,7 +4,7 @@ use bitcoin::secp256k1::SecretKey; use rand::SeedableRng; use rand::rngs::SmallRng; use rand::{Rng, RngExt}; -use smite::bolt::{MAX_MESSAGE_SIZE, ShortChannelId}; +use smite::bolt::{ChannelTypeVariant, Features, MAX_MESSAGE_SIZE, ShortChannelId}; use super::*; use generators::{ @@ -1211,11 +1211,10 @@ fn assert_open_channel_params_are_bounded(program: &Program, seed: u64) { OpenChannelGenerator::MIN_MAX_ACCEPTED_HTLCS, OpenChannelGenerator::MAX_MAX_ACCEPTED_HTLCS, ); + let undefined_bits = channel_flags & !u64::from(OpenChannelGenerator::ANNOUNCE_CHANNEL_FLAG); assert_eq!( - channel_flags, - u64::from(OpenChannelGenerator::CHANNEL_FLAGS), - "seed {seed}: channel_flags should be {} but got {channel_flags}", - OpenChannelGenerator::CHANNEL_FLAGS, + undefined_bits, 0, + "seed {seed}: channel_flags should leave undefined bits clear but got {channel_flags}", ); } @@ -1226,6 +1225,27 @@ fn generated_open_channel_params_are_bounded() { } } +// Ensure ANNOUNCEABLE_CHANNEL_TYPES stays in sync with ChannelTypeVariant. It +// must hold exactly the variants negotiating neither option_scid_alias nor +// option_zeroconf, so that adding a variant fails here rather than silently +// changing what announced channels may negotiate. +#[test] +fn announceable_channel_types_is_complete() { + let announceable: Vec = ChannelTypeVariant::ALL + .iter() + .filter(|variant| { + let bits = variant.bits(); + !bits.contains(&Features::OPTION_SCID_ALIAS) + && !bits.contains(&Features::OPTION_ZEROCONF) + }) + .copied() + .collect(); + assert_eq!( + OpenChannelGenerator::ANNOUNCEABLE_CHANNEL_TYPES, + announceable + ); +} + fn generate_funding_created_program(seed: u64) -> Program { let mut rng = SmallRng::seed_from_u64(seed); let mut builder = ProgramBuilder::new(); From 977c9da8848bae5930a9c9262509356b79d9dc9e Mon Sep 17 00:00:00 2001 From: Devansh Vashisht Date: Sun, 13 Sep 2026 21:41:05 +0530 Subject: [PATCH 3/3] smite-ir: implement AnnouncementSignaturesGenerator Generates programs that open, fund, and confirm an announced channel, then sign and send announcement_signatures for it. BuildAnnouncementSignatures has had no producer since it landed, so no generated program could reach it. The message only means anything on a channel the target has already opened with us, so the generator emits the funding flow itself rather than relying on a preceding one: the channel id, the funding keys, and the short_channel_id all have to come from the same channel, and pick_variable cannot promise that. The signatures cover the channel_announcement body the target rebuilds for itself, so every field has to match what it already knows. That is why the features are empty, the scid is looked up from the confirmed funding output rather than loaded, and the node secret comes from the context. --- smite-ir/src/generators.rs | 5 + .../src/generators/announcement_signatures.rs | 138 ++++++++++++ smite-ir/src/tests.rs | 211 +++++++++++++++++- 3 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 smite-ir/src/generators/announcement_signatures.rs diff --git a/smite-ir/src/generators.rs b/smite-ir/src/generators.rs index 58d1b2df..c8e86c9a 100644 --- a/smite-ir/src/generators.rs +++ b/smite-ir/src/generators.rs @@ -5,6 +5,7 @@ //! protocol flow but delegates value selection and variable reuse to //! `ProgramBuilder`. +mod announcement_signatures; mod channel_announcement; mod channel_ready; mod channel_update; @@ -13,6 +14,7 @@ mod funding_flow; mod node_announcement; mod open_channel; +pub use announcement_signatures::AnnouncementSignaturesGenerator; pub use channel_announcement::ChannelAnnouncementGenerator; pub use channel_ready::ChannelReadyGenerator; pub use channel_update::ChannelUpdateGenerator; @@ -35,6 +37,7 @@ pub trait Generator { /// here may be used by the custom mutator library. #[derive(Clone, Copy)] pub enum AnyGenerator { + AnnouncementSignatures(AnnouncementSignaturesGenerator), ChannelAnnouncement(ChannelAnnouncementGenerator), ChannelUpdate(ChannelUpdateGenerator), NodeAnnouncement(NodeAnnouncementGenerator), @@ -47,6 +50,7 @@ pub enum AnyGenerator { impl AnyGenerator { /// All variants. Keep in sync with the enum definition. pub const ALL: &[Self] = &[ + Self::AnnouncementSignatures(AnnouncementSignaturesGenerator), Self::ChannelAnnouncement(ChannelAnnouncementGenerator), Self::ChannelUpdate(ChannelUpdateGenerator), Self::NodeAnnouncement(NodeAnnouncementGenerator), @@ -60,6 +64,7 @@ impl AnyGenerator { impl Generator for AnyGenerator { fn generate(&self, builder: &mut ProgramBuilder, rng: &mut impl Rng) { match self { + Self::AnnouncementSignatures(generator) => generator.generate(builder, rng), Self::ChannelAnnouncement(generator) => generator.generate(builder, rng), Self::ChannelUpdate(generator) => generator.generate(builder, rng), Self::NodeAnnouncement(generator) => generator.generate(builder, rng), diff --git a/smite-ir/src/generators/announcement_signatures.rs b/smite-ir/src/generators/announcement_signatures.rs new file mode 100644 index 00000000..ddd77b23 --- /dev/null +++ b/smite-ir/src/generators/announcement_signatures.rs @@ -0,0 +1,138 @@ +//! Generator for `announcement_signatures` message flow. + +use rand::Rng; + +use super::Generator; +use super::open_channel::append_open_channel; +use crate::builder::ProgramBuilder; +use crate::operation::AcceptChannelField; +use crate::{Operation, VariableType}; + +/// Generates an announced channel and signs its `channel_announcement`. +/// +/// Emits instructions to: +/// 1. Open, fund, and confirm an announced channel +/// 2. Complete the `channel_ready` exchange +/// 3. Look up the `short_channel_id` of the confirmed funding output +/// 4. Build and send `announcement_signatures` +/// +/// The signatures cover the `channel_announcement` body the target rebuilds +/// for itself, so they only verify if every field matches what the target +/// already knows: the channel's real `short_channel_id`, our node identity, +/// and the funding keys the channel was opened with. +#[derive(Clone, Copy)] +pub struct AnnouncementSignaturesGenerator; + +impl AnnouncementSignaturesGenerator { + /// Blocks mined before the `channel_ready` exchange. Eight is the deepest + /// default `minimum_depth` across the targets, which is what gates their + /// `channel_ready`, and it also clears the six confirmations BOLT 7 + /// requires before a channel may be announced. + pub const MIN_DEPTH_BLOCKS: u8 = 8; + + /// Blocks mined after the `channel_ready` exchange. Targets re-check + /// whether a channel may be announced as new blocks arrive, so give them + /// one while the channel is usable. + pub const POST_READY_BLOCKS: u8 = 1; +} + +impl Generator for AnnouncementSignaturesGenerator { + fn generate(&self, builder: &mut ProgramBuilder, rng: &mut impl Rng) { + // The funding key pair is generated fresh so the funding transaction + // can later be signed with the key `open_channel` commits to. Its + // secret signs the announcement as well, as `bitcoin_key_1`. + let bitcoin_sk = builder.generate_fresh(VariableType::PrivateKey, rng); + let funding_pubkey = builder.append(Operation::DerivePoint, &[bitcoin_sk]); + + // Build and send open_channel. The channel is announced, since targets + // ignore `announcement_signatures` for a private one. + let open_channel = append_open_channel(builder, rng, funding_pubkey, true); + + // Receive accept_channel. + let accept_channel = builder.append( + Operation::RecvAcceptChannel, + &[open_channel.sent_open_channel], + ); + + // The target announces this key as `bitcoin_key_2`, so the funding + // output has to be the one committing to it. + let acceptor_funding_pubkey = builder.append( + Operation::ExtractAcceptChannel(AcceptChannelField::FundingPubkey), + &[accept_channel], + ); + + // Create the BOLT 3 funding transaction. + let funding_transaction = builder.append( + Operation::CreateFundingTransaction, + &[ + funding_pubkey, + acceptor_funding_pubkey, + open_channel.funding_satoshis, + open_channel.feerate_per_kw, + ], + ); + + // Build and send funding_created. + let sent_funding_created = builder.append( + Operation::SendFundingCreated, + &[ + funding_transaction, + bitcoin_sk, + open_channel.temporary_channel_id, + ], + ); + + // Receive funding_signed. + let channel_id = builder.append(Operation::RecvFundingSigned, &[sent_funding_created]); + + // Broadcast the funding transaction and confirm it. + builder.append(Operation::BroadcastTransaction, &[funding_transaction]); + builder.append(Operation::MineBlocks(Self::MIN_DEPTH_BLOCKS), &[]); + + // Build and send channel_ready. An announced channel is reached by its + // real short_channel_id, so no alias is sent. + let second_per_commitment_point = builder.generate_fresh(VariableType::Point, rng); + let alias = builder.generate_fresh(VariableType::ShortChannelId, rng); + builder.append( + Operation::SendChannelReady { + include_alias: false, + }, + &[channel_id, second_per_commitment_point, alias], + ); + + // Receive channel_ready. + builder.append(Operation::RecvChannelReady, &[]); + builder.append(Operation::MineBlocks(Self::POST_READY_BLOCKS), &[]); + + // The announcement covers the channel's real short_channel_id. + let short_channel_id = + builder.append(Operation::LookupShortChannelId, &[funding_transaction]); + + // Targets rebuild the announcement body with empty channel features, + // so any other value changes the digest and fails every signature + // check. + let features = builder.append(Operation::LoadFeatures(Vec::new()), &[]); + let chain_hash = builder.pick_variable(VariableType::ChainHash, rng); + + // Our node secret comes from the context because the target verifies + // `node_signature` against the identity we handshook with. + let node_sk = builder.append(Operation::LoadLocalNodeSecretFromContext, &[]); + let target_node_id = builder.append(Operation::LoadTargetPubkeyFromContext, &[]); + + // Build and send announcement_signatures. + let msg = builder.append( + Operation::BuildAnnouncementSignatures, + &[ + channel_id, + features, + chain_hash, + short_channel_id, + node_sk, + target_node_id, + bitcoin_sk, + acceptor_funding_pubkey, + ], + ); + builder.append(Operation::SendMessage, &[msg]); + } +} diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index fd9f79d9..a9ce55f0 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -8,8 +8,9 @@ use smite::bolt::{ChannelTypeVariant, Features, MAX_MESSAGE_SIZE, ShortChannelId use super::*; use generators::{ - AnyGenerator, ChannelAnnouncementGenerator, ChannelReadyGenerator, ChannelUpdateGenerator, - FundingCreatedGenerator, FundingFlowGenerator, NodeAnnouncementGenerator, OpenChannelGenerator, + AnnouncementSignaturesGenerator, AnyGenerator, ChannelAnnouncementGenerator, + ChannelReadyGenerator, ChannelUpdateGenerator, FundingCreatedGenerator, FundingFlowGenerator, + NodeAnnouncementGenerator, OpenChannelGenerator, }; use minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer}; use mutators::{ @@ -911,13 +912,14 @@ fn accept_channel_field_all_is_complete() { fn any_generator_all_is_complete() { let variant_count = |f: AnyGenerator| -> usize { match f { - AnyGenerator::ChannelAnnouncement(_) + AnyGenerator::AnnouncementSignatures(_) + | AnyGenerator::ChannelAnnouncement(_) | AnyGenerator::ChannelUpdate(_) | AnyGenerator::NodeAnnouncement(_) | AnyGenerator::OpenChannel(_) | AnyGenerator::FundingCreated(_) | AnyGenerator::ChannelReady(_) - | AnyGenerator::FundingFlow(_) => 7, + | AnyGenerator::FundingFlow(_) => 8, } }; assert_eq!(AnyGenerator::ALL.len(), variant_count(AnyGenerator::ALL[0])); @@ -1624,6 +1626,207 @@ fn generated_channel_update_program_structure() { assert_eq!(build_count, 1, "expected exactly one BuildChannelUpdate"); } +fn generate_announcement_signatures_program(seed: u64) -> Program { + let mut rng = SmallRng::seed_from_u64(seed); + let mut builder = ProgramBuilder::new(); + AnnouncementSignaturesGenerator.generate(&mut builder, &mut rng); + builder.build() +} + +// If AnnouncementSignaturesGenerator completes without panicking, every +// instruction has correct input types (enforced by ProgramBuilder::append). +#[test] +fn generated_announcement_signatures_program_is_type_correct() { + for seed in 0..100 { + generate_announcement_signatures_program(seed); + } +} + +#[test] +fn generated_announcement_signatures_params_are_bounded() { + for seed in 0..100 { + assert_open_channel_params_are_bounded( + &generate_announcement_signatures_program(seed), + seed, + ); + } +} + +#[test] +fn generated_announcement_signatures_program_structure() { + let program = generate_announcement_signatures_program(0); + let ops: Vec<_> = program.instructions.iter().map(|i| &i.operation).collect(); + + assert!( + matches!(ops[ops.len() - 1], Operation::SendMessage), + "last instruction should be SendMessage", + ); + let build_count = ops + .iter() + .filter(|op| matches!(op, Operation::BuildAnnouncementSignatures)) + .count(); + assert_eq!( + build_count, 1, + "expected exactly one BuildAnnouncementSignatures" + ); + + // The channel must be open and confirmed before it can be announced. + let recv_accept_channel = find_operation!(program, Operation::RecvAcceptChannel); + let send_funding_created = find_operation!(program, Operation::SendFundingCreated); + let recv_funding_signed = find_operation!(program, Operation::RecvFundingSigned); + let broadcast = find_operation!(program, Operation::BroadcastTransaction); + let send_channel_ready = find_operation!(program, Operation::SendChannelReady { .. }); + let recv_channel_ready = find_operation!(program, Operation::RecvChannelReady); + let lookup = find_operation!(program, Operation::LookupShortChannelId); + let build = find_operation!(program, Operation::BuildAnnouncementSignatures); + + assert!( + recv_accept_channel < send_funding_created, + "RecvAcceptChannel should precede SendFundingCreated", + ); + assert!( + recv_funding_signed < broadcast, + "RecvFundingSigned should precede BroadcastTransaction", + ); + assert!( + broadcast < send_channel_ready, + "BroadcastTransaction should precede SendChannelReady", + ); + assert!( + send_channel_ready < recv_channel_ready, + "SendChannelReady should precede RecvChannelReady", + ); + assert!( + recv_channel_ready < lookup && lookup < build, + "the announced scid should be looked up after channel_ready and before the build, got {lookup} between {recv_channel_ready} and {build}", + ); +} + +// BOLT 7 only allows announcing a channel that both peers agreed to announce, +// and only once its funding transaction has six confirmations. +#[test] +fn generated_announcement_signatures_announces_an_announceable_channel() { + for seed in 0..100 { + let program = generate_announcement_signatures_program(seed); + let build = &program.instructions[find_operation!(program, Operation::BuildOpenChannel)]; + + match &program.instructions[build.inputs[17]].operation { + Operation::LoadU8(flags) => assert_eq!( + *flags & OpenChannelGenerator::ANNOUNCE_CHANNEL_FLAG, + OpenChannelGenerator::ANNOUNCE_CHANNEL_FLAG, + "seed {seed}: channel should be announced, got channel_flags {flags}", + ), + op => panic!("seed {seed}: expected LoadU8, got {op}"), + } + match &program.instructions[build.inputs[19]].operation { + Operation::LoadChannelType(variant) => assert!( + OpenChannelGenerator::ANNOUNCEABLE_CHANNEL_TYPES.contains(variant), + "seed {seed}: {variant} cannot be announced", + ), + op => panic!("seed {seed}: expected LoadChannelType, got {op}"), + } + + let mined: u32 = program + .instructions + .iter() + .filter_map(|i| match i.operation { + Operation::MineBlocks(blocks) => Some(u32::from(blocks)), + _ => None, + }) + .sum(); + assert!( + mined >= 6, + "seed {seed}: funding needs six confirmations to be announced, mined {mined}", + ); + } +} + +// The signatures only verify if they cover the channel_announcement body the +// target rebuilds for itself, which means our node identity, the funding keys +// the channel was opened with, and its real short_channel_id. +#[test] +fn generated_announcement_signatures_sign_the_announced_channel() { + let program = generate_announcement_signatures_program(0); + + let create_idx = find_operation!(program, Operation::CreateFundingTransaction); + let lookup_idx = find_operation!(program, Operation::LookupShortChannelId); + let recv_funding_signed = find_operation!(program, Operation::RecvFundingSigned); + let send_funding_created = find_operation!(program, Operation::SendFundingCreated); + let build_idx = find_operation!(program, Operation::BuildAnnouncementSignatures); + + let create = &program.instructions[create_idx]; + let build = &program.instructions[build_idx]; + + assert_eq!( + build.inputs[0], recv_funding_signed, + "the message should carry the channel_id funding_signed assigned", + ); + match &program.instructions[build.inputs[1]].operation { + Operation::LoadFeatures(features) => assert!( + features.is_empty(), + "channel features must be empty to match the target's body, got {features:?}", + ), + op => panic!("expected LoadFeatures, got {op}"), + } + assert_eq!( + program.instructions[lookup_idx].inputs[0], create_idx, + "the announced scid should come from the funding transaction just created", + ); + assert_eq!( + build.inputs[3], lookup_idx, + "the message should carry the scid looked up from the funding transaction", + ); + assert!( + matches!( + program.instructions[build.inputs[4]].operation, + Operation::LoadLocalNodeSecretFromContext + ), + "node_signature must be made with the identity the target knows us by", + ); + assert!( + matches!( + program.instructions[build.inputs[5]].operation, + Operation::LoadTargetPubkeyFromContext + ), + "node_id_2 should be the target's node id", + ); + + // bitcoin_key_1 is the secret behind the funding pubkey we opened with and + // signed funding_created with; bitcoin_key_2 is the target's own. + let derive = &program.instructions[create.inputs[0]]; + assert!( + matches!(derive.operation, Operation::DerivePoint), + "our funding pubkey should be a DerivePoint", + ); + assert_eq!( + derive.inputs[0], build.inputs[6], + "bitcoin_key_1 must be the private key behind our funding pubkey", + ); + assert_eq!( + program.instructions[send_funding_created].inputs[1], build.inputs[6], + "bitcoin_key_1 must be the key that signed funding_created", + ); + assert_eq!( + create.inputs[1], build.inputs[7], + "bitcoin_key_2 must be the funding pubkey from accept_channel", + ); + assert!( + matches!( + program.instructions[build.inputs[7]].operation, + Operation::ExtractAcceptChannel(AcceptChannelField::FundingPubkey) + ), + "bitcoin_key_2 should be extracted from accept_channel", + ); +} + +#[test] +fn generated_announcement_signatures_program_postcard_roundtrip() { + let program = generate_announcement_signatures_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_open_channel_program_postcard_roundtrip() { let program = generate_open_channel_program(42);