From 601b6165091a1f0140f3f370d9f87c101710c352 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 | 1 + smite-scenarios/src/scenarios/setup.rs | 3 +- 7 files changed, 80 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 28fb7bc6..84b2d980 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -140,6 +140,9 @@ pub struct ProgramContext { /// feature bits are treated equivalently, and the distinction carries no /// meaning here. pub negotiated_features: Features, + /// 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 @@ -361,6 +364,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 92bbc936..c513a157 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -116,6 +116,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 64ca233a..ab7e9f2f 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -260,6 +260,7 @@ pub fn sample_context() -> ProgramContext { Features::OPTION_STATIC_REMOTEKEY, Features::OPTION_ANCHORS, ]), + local_node_secret: [0xee; 32], } } diff --git a/smite-scenarios/src/scenarios/setup.rs b/smite-scenarios/src/scenarios/setup.rs index c626279d..8068923b 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}; @@ -91,6 +91,7 @@ impl SnapshotSetup for PostInitSetup { // flow and avoid unrelated noise, negotiated features are just the // features we sent in our init. negotiated_features: Features::from(our_init.features), + local_node_secret: STATIC_KEY, }; Ok((conn, context)) From 7734233dd7f8547b812b6214b7dc56177a36e738 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 | 41 +++++++++++++++++----- 3 files changed, 71 insertions(+), 17 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..08c7e729 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::{ @@ -1133,8 +1133,9 @@ fn generated_open_channel_program_structure() { // Asserts that the channel parameters of the `open_channel` message built by // `program` are within the bounds the generators are supposed to respect. +// `announce` is whether the generator should have announced the channel, and // `seed` only labels the failure message. -fn assert_open_channel_params_are_bounded(program: &Program, seed: u64) { +fn assert_open_channel_params_are_bounded(program: &Program, announce: bool, seed: u64) { let build = &program.instructions[find_operation!(program, Operation::BuildOpenChannel)]; let open_channel_input = |i: usize| match &program.instructions[build.inputs[i]].operation { Operation::LoadAmount(v) => *v, @@ -1211,21 +1212,45 @@ fn assert_open_channel_params_are_bounded(program: &Program, seed: u64) { OpenChannelGenerator::MIN_MAX_ACCEPTED_HTLCS, OpenChannelGenerator::MAX_MAX_ACCEPTED_HTLCS, ); + let expected_flags = if announce { + u64::from(OpenChannelGenerator::ANNOUNCE_CHANNEL_FLAG) + } else { + 0 + }; assert_eq!( - channel_flags, - u64::from(OpenChannelGenerator::CHANNEL_FLAGS), - "seed {seed}: channel_flags should be {} but got {channel_flags}", - OpenChannelGenerator::CHANNEL_FLAGS, + channel_flags, expected_flags, + "seed {seed}: channel_flags should be {expected_flags} but got {channel_flags}", ); } #[test] fn generated_open_channel_params_are_bounded() { for seed in 0..100 { - assert_open_channel_params_are_bounded(&generate_open_channel_program(seed), seed); + assert_open_channel_params_are_bounded(&generate_open_channel_program(seed), false, seed); } } +// 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(); @@ -1333,7 +1358,7 @@ fn generated_funding_flow_program_is_type_correct() { #[test] fn generated_funding_flow_params_are_bounded() { for seed in 0..100 { - assert_open_channel_params_are_bounded(&generate_funding_flow_program(seed), seed); + assert_open_channel_params_are_bounded(&generate_funding_flow_program(seed), false, seed); } } From ff69d86b8a108b25b3abd51f05cb4ad2e1a7e96f 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 flow is shared with FundingFlowGenerator through append_funding_flow, which takes an announce flag that opens an announced channel, mines it deep enough to be announced, and sends channel_ready without an alias. 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 | 74 ++++++ smite-ir/src/generators/funding_flow.rs | 168 +++++++++----- smite-ir/src/tests.rs | 212 +++++++++++++++++- 4 files changed, 394 insertions(+), 65 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..9cd03cce --- /dev/null +++ b/smite-ir/src/generators/announcement_signatures.rs @@ -0,0 +1,74 @@ +//! Generator for `announcement_signatures` message flow. + +use rand::Rng; + +use super::Generator; +use super::funding_flow::append_funding_flow; +use crate::builder::ProgramBuilder; +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 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) { + // Open, fund, and confirm the channel. It is announced, since targets + // ignore `announcement_signatures` for a private one. The funding + // secret signs the announcement as `bitcoin_key_1`, and the target + // announces the acceptor's funding key as `bitcoin_key_2`. + let funding = append_funding_flow(builder, rng, true); + 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.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, + &[ + funding.channel_id, + features, + chain_hash, + short_channel_id, + node_sk, + target_node_id, + funding.funding_privkey, + funding.acceptor_funding_pubkey, + ], + ); + builder.append(Operation::SendMessage, &[msg]); + } +} diff --git a/smite-ir/src/generators/funding_flow.rs b/smite-ir/src/generators/funding_flow.rs index 037f5e91..669c4aa1 100644 --- a/smite-ir/src/generators/funding_flow.rs +++ b/smite-ir/src/generators/funding_flow.rs @@ -18,68 +18,114 @@ use crate::{Operation, VariableType}; #[derive(Clone, Copy)] pub struct FundingFlowGenerator; +impl FundingFlowGenerator { + /// Blocks mined to confirm an announced channel's funding transaction. + /// 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 ANNOUNCED_MIN_DEPTH_BLOCKS: u8 = 8; +} + +/// Variables produced by [`append_funding_flow`] that callers may reuse. +pub struct FundingFlowVars { + /// Our funding private key, committed to by the funding output. + pub funding_privkey: usize, + /// The acceptor's funding public key from `accept_channel`. + pub acceptor_funding_pubkey: usize, + /// The confirmed funding transaction. + pub funding_transaction: usize, + /// The channel id returned by `funding_signed`. + pub channel_id: usize, +} + +/// Appends the complete v1 outbound channel funding flow to `builder`. +/// +/// If `announce` is set, the channel is opened as announced, its funding +/// transaction is mined deep enough to be announced, and `channel_ready` +/// carries no alias, since an announced channel is reached by its real +/// `short_channel_id`. +pub fn append_funding_flow( + builder: &mut ProgramBuilder, + rng: &mut impl Rng, + announce: bool, +) -> FundingFlowVars { + // The funding key pair is generated fresh so the funding transaction + // can later be signed with the key `open_channel` commits to. + let funding_privkey = builder.generate_fresh(VariableType::PrivateKey, rng); + let funding_pubkey = builder.append(Operation::DerivePoint, &[funding_privkey]); + + // Build and send open_channel. + let open_channel = append_open_channel(builder, rng, funding_pubkey, announce); + + // Receive accept_channel. + let accept_channel = builder.append( + Operation::RecvAcceptChannel, + &[open_channel.sent_open_channel], + ); + 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, + funding_privkey, + open_channel.temporary_channel_id, + ], + ); + + // Receive funding_signed. + let channel_id = builder.append(Operation::RecvFundingSigned, &[sent_funding_created]); + + // Broadcast the funding transaction. + builder.append(Operation::BroadcastTransaction, &[funding_transaction]); + + // Mine blocks to confirm the funding transaction. + let blocks = if announce { + FundingFlowGenerator::ANNOUNCED_MIN_DEPTH_BLOCKS + } else { + rng.random_range(1..=16) + }; + builder.append(Operation::MineBlocks(blocks), &[]); + + // Channel ready parameters. + let second_per_commitment_point = builder.generate_fresh(VariableType::Point, rng); + let short_channel_id = builder.generate_fresh(VariableType::ShortChannelId, rng); + let include_alias = !announce && rng.random(); + + // Build and send channel_ready. + builder.append( + Operation::SendChannelReady { include_alias }, + &[channel_id, second_per_commitment_point, short_channel_id], + ); + + // Receive channel_ready. + builder.append(Operation::RecvChannelReady, &[]); + + FundingFlowVars { + funding_privkey, + acceptor_funding_pubkey, + funding_transaction, + channel_id, + } +} + impl Generator for FundingFlowGenerator { 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. - let funding_privkey = builder.generate_fresh(VariableType::PrivateKey, rng); - let funding_pubkey = builder.append(Operation::DerivePoint, &[funding_privkey]); - - // Build and send open_channel. - let open_channel = append_open_channel(builder, rng, funding_pubkey, false); - - // Receive accept_channel. - let accept_channel = builder.append( - Operation::RecvAcceptChannel, - &[open_channel.sent_open_channel], - ); - 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, - funding_privkey, - open_channel.temporary_channel_id, - ], - ); - - // Receive funding_signed. - let channel_id = builder.append(Operation::RecvFundingSigned, &[sent_funding_created]); - - // Broadcast the funding transaction. - builder.append(Operation::BroadcastTransaction, &[funding_transaction]); - - // Mine blocks to confirm the funding transaction. - builder.append(Operation::MineBlocks(rng.random_range(1..=16)), &[]); - - // Channel ready parameters. - let second_per_commitment_point = builder.generate_fresh(VariableType::Point, rng); - let short_channel_id = builder.generate_fresh(VariableType::ShortChannelId, rng); - let include_alias = rng.random(); - - // Build and send channel_ready. - builder.append( - Operation::SendChannelReady { include_alias }, - &[channel_id, second_per_commitment_point, short_channel_id], - ); - - // Receive channel_ready. - builder.append(Operation::RecvChannelReady, &[]); + append_funding_flow(builder, rng, false); } } diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index 08c7e729..598bd230 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])); @@ -1629,6 +1631,208 @@ 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), + true, + 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);