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/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/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..a9ce55f0 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -4,12 +4,13 @@ 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::{ - AnyGenerator, ChannelAnnouncementGenerator, ChannelReadyGenerator, ChannelUpdateGenerator, - FundingCreatedGenerator, FundingFlowGenerator, NodeAnnouncementGenerator, OpenChannelGenerator, + AnnouncementSignaturesGenerator, AnyGenerator, ChannelAnnouncementGenerator, + ChannelReadyGenerator, ChannelUpdateGenerator, FundingCreatedGenerator, FundingFlowGenerator, + NodeAnnouncementGenerator, OpenChannelGenerator, }; use minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer}; use mutators::{ @@ -457,8 +458,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 +502,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(), @@ -909,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])); @@ -1209,11 +1213,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}", ); } @@ -1224,6 +1227,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(); @@ -1602,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); 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))