diff --git a/Cargo.lock b/Cargo.lock index c0b34f53..0056b0a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base58ck" version = "0.1.0" @@ -98,7 +104,7 @@ dependencies = [ "bitcoin_hashes", "hex-conservative", "hex_lit", - "secp256k1", + "secp256k1 0.29.1", ] [[package]] @@ -138,6 +144,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "cc" version = "1.2.51" @@ -269,6 +284,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -364,6 +390,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -419,6 +454,20 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "musig2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183312a1f782d0e27c2e9aa4d68c151301caa39b38f48ea125c1d55ab1ff29f0" +dependencies = [ + "base16ct", + "hmac", + "secp", + "secp256k1 0.31.1", + "sha2", + "subtle", +] + [[package]] name = "nix" version = "0.30.1" @@ -472,6 +521,15 @@ dependencies = [ "serde", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -496,13 +554,42 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", ] [[package]] @@ -524,6 +611,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "secp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ef0ffc3b1b2720b00b919f6c697b1e64aca7bba54b93e0ef4b8560fac6f946" +dependencies = [ + "base16ct", + "secp256k1 0.31.1", + "subtle", +] + [[package]] name = "secp256k1" version = "0.29.1" @@ -531,7 +629,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "secp256k1-sys", + "secp256k1-sys 0.10.1", +] + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.5", + "secp256k1-sys 0.11.0", ] [[package]] @@ -543,6 +652,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + [[package]] name = "serde" version = "1.0.228" @@ -595,6 +713,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -619,6 +748,7 @@ dependencies = [ "chacha20poly1305", "hex", "log", + "musig2", "nix", "serde", "serde_json", @@ -634,7 +764,7 @@ version = "0.0.0" dependencies = [ "bitcoin", "postcard", - "rand", + "rand 0.10.0", "serde", "smite", "thiserror", @@ -646,7 +776,7 @@ version = "0.0.0" dependencies = [ "log", "postcard", - "rand", + "rand 0.10.0", "simple_logger", "smite-ir", ] @@ -864,6 +994,26 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zeroize" version = "1.8.2" diff --git a/Cargo.toml b/Cargo.toml index bf448bc9..051808bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ rand = { version = "0.10", default-features = false } simple_logger = { version = "5", default-features = false } postcard = { version = "1.1", default-features = false, features = ["alloc"] } bitcoin = "0.32" +musig2 = { version = "0.4", default-features = false, features = ["secp256k1"] } serde = { version = "1", features = ["derive"] } thiserror = "2" serde_json = "1" diff --git a/smite-ir/src/generators/funding_created.rs b/smite-ir/src/generators/funding_created.rs index eb05b2ae..4b6f9a32 100644 --- a/smite-ir/src/generators/funding_created.rs +++ b/smite-ir/src/generators/funding_created.rs @@ -26,6 +26,7 @@ impl Generator for FundingCreatedGenerator { let funding_satoshis = builder.pick_variable(VariableType::Amount, rng); let feerate_per_kw = builder.pick_variable(VariableType::FeeratePerKw, rng); let temporary_channel_id = builder.pick_variable(VariableType::ChannelId, rng); + let channel_type = builder.pick_variable(VariableType::Features, rng); // Create the BOLT 3 funding transaction. let funding_transaction = builder.append( @@ -35,6 +36,7 @@ impl Generator for FundingCreatedGenerator { acceptor_funding_pubkey, funding_satoshis, feerate_per_kw, + channel_type, ], ); diff --git a/smite-ir/src/generators/funding_flow.rs b/smite-ir/src/generators/funding_flow.rs index 47489122..99a2f613 100644 --- a/smite-ir/src/generators/funding_flow.rs +++ b/smite-ir/src/generators/funding_flow.rs @@ -42,7 +42,6 @@ impl Generator for FundingFlowGenerator { let feerate_per_kw = builder.pick_variable(VariableType::FeeratePerKw, rng); let to_self_delay = builder.pick_variable(VariableType::U16, rng); let max_accepted_htlcs = builder.pick_variable(VariableType::U16, rng); - let channel_flags = builder.pick_variable(VariableType::U8, rng); let shutdown_script_variant = ShutdownScriptVariant::random(rng); let upfront_shutdown_script = builder.append(Operation::LoadShutdownScript(shutdown_script_variant), &[]); @@ -51,6 +50,16 @@ impl Generator for FundingFlowGenerator { .expect("ChannelTypeVariant::ALL is non-empty"); let channel_type = builder.append(Operation::LoadChannelType(variant), &[]); + // Taproot channels cannot be announced, so a random `channel_flags` + // would leave the happy path unreachable for a third of the channel + // types. Mutators can still flip the bit afterwards to exercise how + // targets reject a public taproot channel. + let channel_flags = if variant.requires_unannounced_channel() { + builder.append(Operation::LoadU8(0), &[]) + } else { + builder.pick_variable(VariableType::U8, rng) + }; + // Build and send open_channel. let open_channel_msg = builder.append( Operation::BuildOpenChannel, @@ -94,6 +103,9 @@ impl Generator for FundingFlowGenerator { acceptor_funding_pubkey, funding_satoshis, feerate_per_kw, + // Must match the type sent in open_channel, or the funding + // output will not be the one the target negotiated. + channel_type, ], ); diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 20b1f64b..fef238a6 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -14,7 +14,7 @@ use std::fmt::Write; use bitcoin::{opcodes::all as opcodes, script::Builder, script::PushBytes}; use rand::{Rng, RngExt}; use serde::{Deserialize, Serialize}; -use smite::bolt::ShortChannelId; +use smite::bolt::{FeatureBit, Features, ShortChannelId}; use super::VariableType; @@ -75,11 +75,18 @@ pub enum Operation { ExtractAcceptChannel(AcceptChannelField), /// Create a BOLT 3 funding transaction for the channel funding flow. /// - /// Inputs (4): + /// The `channel_type` selects the funding output format: simple taproot + /// channels pay to a single P2TR key aggregated from both funding pubkeys, + /// every other type to a 2-of-2 P2WSH. It must match the `channel_type` + /// given to `BuildOpenChannel`, or the target will never see the funding + /// output it negotiated. + /// + /// Inputs (5): /// 0: `opener_funding_pubkey` (`Point`) /// 1: `acceptor_funding_pubkey` (`Point`) /// 2: `funding_satoshis` (`Amount`) /// 3: `feerate_per_kw` (`FeeratePerKw`) + /// 4: `channel_type` (`Features`, empty = 2-of-2 P2WSH) CreateFundingTransaction, // -- Build: construct a BOLT message from inputs -- @@ -518,58 +525,107 @@ impl ChannelTypeVariant { Self::ScriptEnforcedLeaseScidAliasZeroConf, ]; + /// Returns whether this type requires the channel to be unannounced. + /// + /// Taproot channels cannot be gossiped, so both lnd and eclair reject an + /// `open_channel` that sets the `announce_channel` bit alongside one of + /// these types. + #[must_use] + pub fn requires_unannounced_channel(self) -> bool { + self.bits().iter().any(|&bit| { + bit == Features::OPTION_SIMPLE_TAPROOT || bit == Features::OPTION_SIMPLE_TAPROOT_STAGING + }) + } + /// The feature bits (even/required) contained in this channel type. #[must_use] - pub fn bits(self) -> &'static [usize] { - // BOLT 9 feature bits: - // 12 = option_static_remotekey - // 22 = option_anchors - // 40 = zero_fee_commitments - // 46 = option_scid_alias - // 50 = option_zeroconf - // 80 = option_simple_taproot - // 180 = option_simple_taproot_staging - // 2022 = option_script_enforced_lease + pub fn bits(self) -> &'static [FeatureBit] { + use Features as F; match self { - Self::StaticRemoteKey => &[12], - Self::StaticRemoteKeyScidAlias => &[12, 46], - Self::StaticRemoteKeyZeroConf => &[12, 50], - Self::StaticRemoteKeyScidAliasZeroConf => &[12, 46, 50], - Self::Anchors => &[12, 22], - Self::AnchorsScidAlias => &[12, 22, 46], - Self::AnchorsZeroConf => &[12, 22, 50], - Self::AnchorsScidAliasZeroConf => &[12, 22, 46, 50], - Self::ZeroFeeCommitments => &[40], - Self::ZeroFeeCommitmentsScidAlias => &[40, 46], - Self::ZeroFeeCommitmentsZeroConf => &[40, 50], - Self::ZeroFeeCommitmentsScidAliasZeroConf => &[40, 46, 50], - Self::SimpleTaproot => &[80], - Self::SimpleTaprootScidAlias => &[80, 46], - Self::SimpleTaprootZeroConf => &[80, 50], - Self::SimpleTaprootScidAliasZeroConf => &[80, 46, 50], - Self::SimpleTaprootStaging => &[180], - Self::SimpleTaprootStagingScidAlias => &[180, 46], - Self::SimpleTaprootStagingZeroConf => &[180, 50], - Self::SimpleTaprootStagingScidAliasZeroConf => &[180, 46, 50], - Self::ScriptEnforcedLease => &[12, 22, 2022], - Self::ScriptEnforcedLeaseScidAlias => &[12, 22, 2022, 46], - Self::ScriptEnforcedLeaseZeroConf => &[12, 22, 2022, 50], - Self::ScriptEnforcedLeaseScidAliasZeroConf => &[12, 22, 2022, 46, 50], + Self::StaticRemoteKey => &[F::OPTION_STATIC_REMOTEKEY], + Self::StaticRemoteKeyScidAlias => &[F::OPTION_STATIC_REMOTEKEY, F::OPTION_SCID_ALIAS], + Self::StaticRemoteKeyZeroConf => &[F::OPTION_STATIC_REMOTEKEY, F::OPTION_ZEROCONF], + Self::StaticRemoteKeyScidAliasZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::Anchors => &[F::OPTION_STATIC_REMOTEKEY, F::OPTION_ANCHORS], + Self::AnchorsScidAlias => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCID_ALIAS, + ], + Self::AnchorsZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_ZEROCONF, + ], + Self::AnchorsScidAliasZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::ZeroFeeCommitments => &[F::ZERO_FEE_COMMITMENTS], + Self::ZeroFeeCommitmentsScidAlias => &[F::ZERO_FEE_COMMITMENTS, F::OPTION_SCID_ALIAS], + Self::ZeroFeeCommitmentsZeroConf => &[F::ZERO_FEE_COMMITMENTS, F::OPTION_ZEROCONF], + Self::ZeroFeeCommitmentsScidAliasZeroConf => &[ + F::ZERO_FEE_COMMITMENTS, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::SimpleTaproot => &[F::OPTION_SIMPLE_TAPROOT], + Self::SimpleTaprootScidAlias => &[F::OPTION_SIMPLE_TAPROOT, F::OPTION_SCID_ALIAS], + Self::SimpleTaprootZeroConf => &[F::OPTION_SIMPLE_TAPROOT, F::OPTION_ZEROCONF], + Self::SimpleTaprootScidAliasZeroConf => &[ + F::OPTION_SIMPLE_TAPROOT, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::SimpleTaprootStaging => &[F::OPTION_SIMPLE_TAPROOT_STAGING], + Self::SimpleTaprootStagingScidAlias => { + &[F::OPTION_SIMPLE_TAPROOT_STAGING, F::OPTION_SCID_ALIAS] + } + Self::SimpleTaprootStagingZeroConf => { + &[F::OPTION_SIMPLE_TAPROOT_STAGING, F::OPTION_ZEROCONF] + } + Self::SimpleTaprootStagingScidAliasZeroConf => &[ + F::OPTION_SIMPLE_TAPROOT_STAGING, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::ScriptEnforcedLease => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCRIPT_ENFORCED_LEASE, + ], + Self::ScriptEnforcedLeaseScidAlias => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCRIPT_ENFORCED_LEASE, + F::OPTION_SCID_ALIAS, + ], + Self::ScriptEnforcedLeaseZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCRIPT_ENFORCED_LEASE, + F::OPTION_ZEROCONF, + ], + Self::ScriptEnforcedLeaseScidAliasZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCRIPT_ENFORCED_LEASE, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], } } /// Encodes the channel type as a BOLT feature bitmap (big-endian bytes). #[must_use] - #[allow(clippy::missing_panics_doc)] // bits() is always non-empty pub fn encode(self) -> Vec { - let bits = self.bits(); - let max_bit = *bits.iter().max().expect("non-empty bits"); - let num_bytes = max_bit / 8 + 1; - let mut out = vec![0u8; num_bytes]; - for &bit in bits { - out[num_bytes - 1 - bit / 8] |= 1 << (bit % 8); - } - out + Features::from_bits(self.bits()).into_bytes() } } @@ -791,6 +847,7 @@ impl Operation { VariableType::Point, // acceptor_funding_pubkey VariableType::Amount, // funding_satoshis VariableType::FeeratePerKw, // feerate_per_kw + VariableType::Features, // channel_type ], Self::SendMessage => vec![VariableType::Message], Self::SendOpenChannel => vec![VariableType::OpenChannelMessage], diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index bb092617..8fac34b6 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -637,8 +637,9 @@ fn postcard_roundtrip() { inputs: vec![], }, Instruction { + // v6 is LoadFeatures, standing in for the channel type. operation: Operation::CreateFundingTransaction, - inputs: vec![1, 9, 5, 10], + inputs: vec![1, 9, 5, 10, 6], }, Instruction { operation: Operation::BroadcastTransaction, @@ -692,6 +693,7 @@ fn create_and_broadcast_tx_operation() { VariableType::Point, VariableType::Amount, VariableType::FeeratePerKw, + VariableType::Features, ], ); assert_eq!(op.output_type(), Some(VariableType::FundingTransaction)); @@ -741,13 +743,17 @@ fn create_and_broadcast_tx_instructions() -> Vec { operation: Operation::LoadFeeratePerKw(15_000), inputs: vec![], }, + Instruction { + operation: Operation::LoadChannelType(ChannelTypeVariant::StaticRemoteKey), + inputs: vec![], + }, Instruction { operation: Operation::CreateFundingTransaction, - inputs: vec![1, 3, 4, 5], + inputs: vec![1, 3, 4, 5, 6], }, Instruction { operation: Operation::BroadcastTransaction, - inputs: vec![6], + inputs: vec![7], }, ] } @@ -769,8 +775,9 @@ fn displays_create_and_broadcast_tx_program() { "v3 = DerivePoint(v2)".into(), "v4 = LoadAmount(10000000)".into(), "v5 = LoadFeeratePerKw(15000)".into(), - "v6 = CreateFundingTransaction(v1, v3, v4, v5)".into(), - "BroadcastTransaction(v6)".into(), + "v6 = LoadChannelType(StaticRemoteKey)".into(), + "v7 = CreateFundingTransaction(v1, v3, v4, v5, v6)".into(), + "BroadcastTransaction(v7)".into(), ]; assert_eq!(lines, expected); } diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 1c01b145..08e7ec40 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -8,14 +8,16 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use bitcoin::{OutPoint, ScriptBuf, Txid}; use smite::bitcoin::{BitcoinCli, TxBlockPosition, Utxo}; use smite::bolt::{ - AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelId, ChannelReady, - ChannelReadyTlvs, ChannelUpdate, FundingCreated, FundingSigned, Message, NodeAnnouncement, - OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, msg_type, + AcceptChannel, AnnouncementSignatures, COMPACT_SIGNATURE_SIZE, ChannelAnnouncement, ChannelId, + ChannelReady, ChannelReadyTlvs, ChannelUpdate, Features, FundingCreated, FundingCreatedTlvs, + FundingSigned, Message, NodeAnnouncement, OpenChannel, OpenChannelTlvs, + PartialSignatureWithNonce, Pong, PublicNonce, ShortChannelId, Shutdown, msg_type, }; use smite::channel_tx::{ - ChannelConfig, ChannelPartyConfig, ChannelState, FundingTransaction, HolderIdentity, Side, - build_funding_transaction, + ChannelConfig, ChannelPartyConfig, ChannelState, CommitmentState, FundingTransaction, + HolderIdentity, Side, build_funding_transaction, }; +use smite::musig::{PartialSignature, derive_nonce}; use smite::noise::{ConnectionError, NoiseConnection}; use smite::oracles::{AcceptChannelContext, AcceptChannelOracle, Oracle}; use smite::pending_channel::PendingChannel; @@ -210,9 +212,13 @@ pub enum ExecuteError { #[error("peer error on {:?}: {}", .0.channel_id, .0.message().unwrap_or(""))] PeerError(smite::bolt::Error), - /// Wallet UTXOs could not cover the funding amount and fees. + /// The funding transaction could not be built. #[error("funding: {0}")] - InsufficientFunds(#[from] smite::channel_tx::InsufficientFunds), + Funding(#[from] smite::channel_tx::FundingError), + + /// A `MuSig2` partial signature could not be produced or checked. + #[error("musig2: {0}")] + Musig(#[from] smite::musig::MusigError), /// Failed to construct the initial commitment state. #[error("commitment: {0}")] @@ -798,6 +804,7 @@ fn create_funding_transaction( let acceptor_pubkey = resolve_pubkey(variables, inputs[1]); let funding_satoshis = resolve_amount(variables, inputs[2]); let feerate_per_kw = resolve_feerate(variables, inputs[3]); + let channel_type = Features::from(resolve_features(variables, inputs[4])); // Query wallet state from bitcoind for coin selection and change. let utxos = cli.get_utxos(); @@ -809,6 +816,7 @@ fn create_funding_transaction( &acceptor_pubkey, funding_satoshis, feerate_per_kw, + &channel_type, utxos, change_spk, )?; @@ -826,11 +834,41 @@ fn create_funding_transaction( Ok(funding) } +/// Returns the holder's own `funding_pubkey`, which is the key its `MuSig2` +/// nonces are bound to. smite always opens channels today, but reading the side +/// keeps this correct if it ever accepts one. +fn holder_funding_pubkey(state: &ChannelState) -> PublicKey { + match state.holder.side { + Side::Opener => state.config.opener.funding_pubkey, + Side::Acceptor => state.config.acceptor.funding_pubkey, + } +} + /// Builds an `OpenChannel` from 20 input variables (wire order). +/// +/// Simple taproot channels additionally carry a `MuSig2` verification nonce, +/// which the peer signs our first commitment against. `channel_flags` is left +/// exactly as the program specifies, including the `announce_channel` bit that +/// taproot channels must not set, so mutators can still exercise how targets +/// reject a public taproot channel. fn build_open_channel(variables: &[Option], inputs: &[usize]) -> OpenChannel { + let channel_type = Features::from(resolve_features(variables, inputs[19])); + let funding_pubkey = resolve_pubkey(variables, inputs[11]); + let temporary_channel_id = resolve_channel_id(variables, inputs[1]); + + let next_local_nonce = channel_type + .supports_feature(Features::OPTION_SIMPLE_TAPROOT) + .then(|| { + derive_nonce( + &[b"verification", temporary_channel_id.as_bytes()], + &funding_pubkey, + ) + .public_nonce() + }); + OpenChannel { chain_hash: resolve_chain_hash(variables, inputs[0]), - temporary_channel_id: resolve_channel_id(variables, inputs[1]), + temporary_channel_id, funding_satoshis: resolve_amount(variables, inputs[2]), push_msat: resolve_amount(variables, inputs[3]), dust_limit_satoshis: resolve_amount(variables, inputs[4]), @@ -840,7 +878,7 @@ fn build_open_channel(variables: &[Option], inputs: &[usize]) -> OpenC feerate_per_kw: resolve_feerate(variables, inputs[8]), to_self_delay: resolve_u16(variables, inputs[9]), max_accepted_htlcs: resolve_u16(variables, inputs[10]), - funding_pubkey: resolve_pubkey(variables, inputs[11]), + funding_pubkey, revocation_basepoint: resolve_pubkey(variables, inputs[12]), payment_basepoint: resolve_pubkey(variables, inputs[13]), delayed_payment_basepoint: resolve_pubkey(variables, inputs[14]), @@ -853,7 +891,8 @@ fn build_open_channel(variables: &[Option], inputs: &[usize]) -> OpenC // Omitting it is a protocol violation in that case. Including if // not negotiated is not. upfront_shutdown_script: Some(resolve_bytes(variables, inputs[18]).to_vec()), - channel_type: nonempty_or_none(resolve_features(variables, inputs[19])), + channel_type: nonempty_or_none(&channel_type.into_bytes()), + next_local_nonce, }, } } @@ -895,6 +934,7 @@ fn build_funding_created( funding_output_index, signature: Signature::from_compact(&[0u8; 64]) .expect("zero bytes parse as a signature"), + tlvs: FundingCreatedTlvs::default(), }); }; let open_channel = &pending.open_channel; @@ -905,6 +945,7 @@ fn build_funding_created( funding_output_index, signature: Signature::from_compact(&[0u8; 64]) .expect("zero bytes parse as a signature"), + tlvs: FundingCreatedTlvs::default(), }); }; @@ -913,30 +954,12 @@ fn build_funding_created( let secp = Secp256k1::new(); let opener_funding_pubkey = PublicKey::from_secret_key(&secp, &opener_funding_privkey); - let opener = ChannelPartyConfig { - funding_pubkey: opener_funding_pubkey, - payment_basepoint: open_channel.payment_basepoint, - revocation_basepoint: open_channel.revocation_basepoint, - delayed_payment_basepoint: open_channel.delayed_payment_basepoint, - dust_limit_satoshis: open_channel.dust_limit_satoshis, - to_self_delay: open_channel.to_self_delay, - }; - let acceptor = ChannelPartyConfig { - funding_pubkey: accept_channel.funding_pubkey, - payment_basepoint: accept_channel.payment_basepoint, - revocation_basepoint: accept_channel.revocation_basepoint, - delayed_payment_basepoint: accept_channel.delayed_payment_basepoint, - dust_limit_satoshis: accept_channel.dust_limit_satoshis, - to_self_delay: accept_channel.to_self_delay, - }; - let config = ChannelConfig { + let config = negotiated_channel_config( + open_channel, + accept_channel, funding_outpoint, - funding_satoshis: open_channel.funding_satoshis, - channel_type: open_channel.tlvs.channel_type.clone().unwrap_or_default(), - opener, - acceptor, - minimum_depth: accept_channel.minimum_depth, - }; + opener_funding_pubkey, + ); let state = config.new_initial_commitment( open_channel.push_msat, @@ -948,7 +971,15 @@ fn build_funding_created( side: Side::Opener, funding_privkey: opener_funding_privkey, }; - let signature = config.sign_counterparty_commitment(&state, &holder); + + let (signature, tlvs) = sign_initial_commitment( + &config, + &state, + &holder, + &temporary_channel_id, + &opener_funding_pubkey, + accept_channel.tlvs.next_local_nonce, + )?; let channel_id = ChannelId::v1_from_funding_outpoint(config.funding_outpoint); @@ -959,8 +990,13 @@ fn build_funding_created( &open_channel.funding_pubkey, &accept_channel.funding_pubkey, open_channel.funding_satoshis, + &config.channel_type, ); + // The nonce we published in `open_channel` is what the acceptor signs our + // commitment against, so keep it to verify the incoming `funding_signed`. + let holder_verification_nonce = open_channel.tlvs.next_local_nonce; + // Building the same message again must not clobber a channel whose state // has already been established (and possibly advanced). channel_states.entry(channel_id).or_insert_with(|| { @@ -970,6 +1006,7 @@ fn build_funding_created( state, is_funding_outpoint_valid, mined_txids.contains(&funding_outpoint.txid), + holder_verification_nonce, ) }); @@ -986,9 +1023,108 @@ fn build_funding_created( funding_txid: funding_outpoint.txid, funding_output_index, signature, + tlvs, }) } +/// Builds the channel configuration from the values actually exchanged in +/// `open_channel` and `accept_channel`. +/// +/// The opener's funding pubkey comes from the private key the program supplied +/// to `SendFundingCreated` rather than from `open_channel`, since that is the +/// key we can actually sign with. A mutator can make the two disagree, which +/// then shows up as a funding output the target does not recognize. +fn negotiated_channel_config( + open_channel: &OpenChannel, + accept_channel: &AcceptChannel, + funding_outpoint: OutPoint, + opener_funding_pubkey: PublicKey, +) -> ChannelConfig { + ChannelConfig { + funding_outpoint, + funding_satoshis: open_channel.funding_satoshis, + channel_type: Features::from(open_channel.tlvs.channel_type.clone().unwrap_or_default()), + opener: ChannelPartyConfig { + funding_pubkey: opener_funding_pubkey, + payment_basepoint: open_channel.payment_basepoint, + revocation_basepoint: open_channel.revocation_basepoint, + delayed_payment_basepoint: open_channel.delayed_payment_basepoint, + dust_limit_satoshis: open_channel.dust_limit_satoshis, + to_self_delay: open_channel.to_self_delay, + }, + acceptor: ChannelPartyConfig { + funding_pubkey: accept_channel.funding_pubkey, + payment_basepoint: accept_channel.payment_basepoint, + revocation_basepoint: accept_channel.revocation_basepoint, + delayed_payment_basepoint: accept_channel.delayed_payment_basepoint, + dust_limit_satoshis: accept_channel.dust_limit_satoshis, + to_self_delay: accept_channel.to_self_delay, + }, + minimum_depth: accept_channel.minimum_depth, + } +} + +/// Signs the acceptor's first commitment transaction. +/// +/// Simple taproot channels zero the fixed `signature` field and carry a +/// `MuSig2` partial signature in TLV 2 instead. Signing needs the acceptor's +/// verification nonce from `accept_channel`; without it we can only send the +/// unsigned form and let the target fail the channel, which is what the spec +/// tells it to do. +/// +/// # Errors +/// +/// Returns [`ExecuteError::Musig`] if the acceptor's nonce is malformed or the +/// funding keys cannot be aggregated. +fn sign_initial_commitment( + config: &ChannelConfig, + state: &CommitmentState, + holder: &HolderIdentity, + temporary_channel_id: &ChannelId, + opener_funding_pubkey: &PublicKey, + acceptor_verification_nonce: Option, +) -> Result<(Signature, FundingCreatedTlvs), ExecuteError> { + if !config.is_simple_taproot() { + return Ok(( + config.sign_counterparty_commitment(state, holder), + FundingCreatedTlvs::default(), + )); + } + + let Some(acceptor_nonce) = acceptor_verification_nonce else { + return Ok((zero_signature(), FundingCreatedTlvs::default())); + }; + + let signing_nonce = derive_nonce( + &[b"signing", temporary_channel_id.as_bytes()], + opener_funding_pubkey, + ); + let public_nonce = signing_nonce.public_nonce(); + let partial = config.partial_sign_counterparty_commitment( + state, + holder, + signing_nonce, + &acceptor_nonce, + )?; + + Ok(( + zero_signature(), + FundingCreatedTlvs { + partial_signature_with_nonce: Some(PartialSignatureWithNonce { + partial_signature: partial.0, + public_nonce, + }), + }, + )) +} + +/// The all-zero signature the taproot channel spec requires in the fixed +/// `signature` field of `funding_created` and `funding_signed`. +fn zero_signature() -> Signature { + Signature::from_compact(&[0u8; COMPACT_SIGNATURE_SIZE]) + .expect("zero bytes parse as a signature") +} + /// Builds a `ChannelReady` from 3 input variables (wire order). fn build_channel_ready( variables: &[Option], @@ -1015,10 +1151,26 @@ fn build_channel_ready( } } + // Simple taproot channels must publish a fresh verification nonce here, + // replacing the one the funding flow consumed. + let next_local_nonce = channel_states + .get(&channel_id) + .filter(|state| state.config.is_simple_taproot()) + .map(|state| { + derive_nonce( + &[b"channel-ready", channel_id.as_bytes()], + &holder_funding_pubkey(state), + ) + .public_nonce() + }); + ChannelReady { channel_id, second_per_commitment_point, - tlvs: ChannelReadyTlvs { short_channel_id }, + tlvs: ChannelReadyTlvs { + short_channel_id, + next_local_nonce, + }, } } @@ -1332,13 +1484,45 @@ fn verify_funding_signed( .get(&fs.channel_id) .ok_or(Violation::UnknownChannel(fs.channel_id))?; - state - .config - .verify_counterparty_signature(&state.commitment, &state.holder, &fs.signature) + let valid = if state.config.is_simple_taproot() { + verify_taproot_funding_signed(fs, state) + } else { + state + .config + .verify_counterparty_signature(&state.commitment, &state.holder, &fs.signature) + }; + + valid .then_some(()) .ok_or(Violation::InvalidCounterpartySignature(fs.channel_id)) } +/// Returns whether the peer's `MuSig2` partial signature over our first +/// commitment is valid. +/// +/// A missing TLV, a malformed nonce, or a nonce we never published all mean the +/// peer did not sign what we asked it to, so all of them read as invalid rather +/// than as an executor error. +fn verify_taproot_funding_signed(fs: &FundingSigned, state: &ChannelState) -> bool { + let (Some(partial), Some(holder_nonce)) = ( + fs.tlvs.partial_signature_with_nonce, + state.holder_verification_nonce, + ) else { + return false; + }; + + state + .config + .verify_counterparty_partial_signature( + &state.commitment, + &state.holder, + &PartialSignature(partial.partial_signature), + &partial.public_nonce, + &holder_nonce, + ) + .unwrap_or(false) +} + /// Records a sent `open_channel`, keyed by `temporary_channel_id`, so the /// funding flow can build commitments from the values actually put on the wire. /// @@ -1433,9 +1617,9 @@ mod tests { use super::*; use bitcoin::secp256k1::{Secp256k1, SecretKey}; use bitcoin::{Amount, Transaction}; - use smite::bolt::{AcceptChannelTlvs, GossipTimestampFilter, Init, Ping}; + use smite::bolt::{AcceptChannelTlvs, FundingSignedTlvs, GossipTimestampFilter, Init, Ping}; use smite_ir::Instruction; - use smite_ir::operation::ShutdownScriptVariant; + use smite_ir::operation::{ChannelTypeVariant, ShutdownScriptVariant}; // -- MockConnection -- @@ -1601,6 +1785,7 @@ mod tests { htlc_basepoint: sample_pubkey(5), first_per_commitment_point: sample_pubkey(6), tlvs: AcceptChannelTlvs { + next_local_nonce: None, upfront_shutdown_script: Some(vec![0xde, 0xad]), channel_type: Some(vec![0x40, 0x10, 0x00]), }, @@ -1728,13 +1913,17 @@ mod tests { operation: Operation::LoadFeeratePerKw(15_000), inputs: vec![], }, + Instruction { + operation: Operation::LoadChannelType(ChannelTypeVariant::StaticRemoteKey), + inputs: vec![], + }, Instruction { operation: Operation::CreateFundingTransaction, - inputs: vec![1, 3, 4, 5], + inputs: vec![1, 3, 4, 5, 6], }, Instruction { operation: Operation::BroadcastTransaction, - inputs: vec![6], + inputs: vec![7], }, ] } @@ -2317,6 +2506,52 @@ mod tests { assert_eq!(oc.tlvs.channel_type, Some(vec![0x01, 0x02])); } + /// A simple taproot `channel_type` makes `open_channel` carry a `MuSig2` + /// verification nonce, which every other channel type must omit. + #[test] + fn execute_build_open_channel_adds_taproot_nonce() { + let taproot_nonce = |channel_type: ChannelTypeVariant| { + let mut instrs = open_channel_instructions(); + instrs[19] = Instruction { + operation: Operation::LoadChannelType(channel_type), + inputs: vec![], + }; + instrs.push(Instruction { + operation: Operation::BuildOpenChannel, + inputs: (0..20).collect(), + }); + instrs.push(Instruction { + operation: Operation::SendOpenChannel, + inputs: vec![20], + }); + + let mut executor = Executor::new( + MockConnection::new(), + MockBitcoinCli::default(), + sample_context(), + ); + executor + .execute( + &Program { + instructions: instrs, + }, + std::time::Instant::now(), + ) + .unwrap(); + + decode_open_channel(&executor.conn.sent[0]) + .tlvs + .next_local_nonce + }; + + let nonce = taproot_nonce(ChannelTypeVariant::SimpleTaproot) + .expect("taproot channels must publish a verification nonce"); + assert!(smite::musig::is_valid_public_nonce(&nonce)); + + assert_eq!(taproot_nonce(ChannelTypeVariant::Anchors), None); + assert_eq!(taproot_nonce(ChannelTypeVariant::StaticRemoteKey), None); + } + #[test] fn execute_derive_point() { let mut instrs = vec![ @@ -3101,13 +3336,16 @@ mod tests { }); instrs.push(Instruction { // Feed the FundingTransaction produced by - // CreateFundingTransaction (instruction 6) into the lookup. The - // resulting ShortChannelId is variable 9. + // CreateFundingTransaction (instruction 7) into the lookup. The + // resulting ShortChannelId is variable 10. operation: Operation::LookupShortChannelId, - inputs: vec![6], + inputs: vec![7], }); // Build and send a channel_announcement carrying the looked-up SCID. - instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 9)); + instrs.extend(channel_announcement_from_scid_instructions( + instrs.len(), + 10, + )); let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); executor @@ -3173,17 +3411,21 @@ mod tests { operation: Operation::LoadFeeratePerKw(15_000), inputs: vec![], }, + Instruction { + operation: Operation::LoadChannelType(ChannelTypeVariant::StaticRemoteKey), + inputs: vec![], + }, Instruction { operation: Operation::CreateFundingTransaction, - inputs: vec![1, 3, 4, 5], + inputs: vec![1, 3, 4, 5, 6], }, - // The looked-up SCID is variable 7. + // The looked-up SCID is variable 8. Instruction { operation: Operation::LookupShortChannelId, - inputs: vec![6], + inputs: vec![7], }, ]; - instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 7)); + instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 8)); let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); executor @@ -3273,7 +3515,9 @@ mod tests { std::time::Instant::now(), ) .unwrap_err(); - let ExecuteError::InsufficientFunds(funds_err) = err else { + let ExecuteError::Funding(smite::channel_tx::FundingError::InsufficientFunds(funds_err)) = + err + else { panic!("expected InsufficientFunds, got {err:?}"); }; assert_eq!(funds_err.available, Amount::from_sat(1_000)); @@ -3344,11 +3588,11 @@ mod tests { }, Instruction { operation: Operation::SendFundingCreated, - inputs: vec![6, 0, 8], + inputs: vec![7, 0, 9], }, Instruction { operation: Operation::RecvFundingSigned, - inputs: vec![9], + inputs: vec![10], }, ]); instrs @@ -3376,6 +3620,7 @@ mod tests { let fs_bytes = Message::FundingSigned(FundingSigned { channel_id, signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + tlvs: FundingSignedTlvs::default(), }) .encode(); @@ -3581,6 +3826,7 @@ mod tests { let fs_bytes = Message::FundingSigned(FundingSigned { channel_id, signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + tlvs: FundingSignedTlvs::default(), }) .encode(); @@ -3621,6 +3867,7 @@ mod tests { channel_id, signature: Signature::from_compact(&[0u8; 64]) .expect("zero bytes parse as a signature"), + tlvs: FundingSignedTlvs::default(), }) .encode(); @@ -3668,13 +3915,13 @@ mod tests { operation: Operation::SendChannelReady { include_alias: false, }, - inputs: vec![10, 1, 11], + inputs: vec![11, 1, 12], }, Instruction { operation: Operation::SendChannelReady { include_alias: true, }, - inputs: vec![10, 3, 11], + inputs: vec![11, 3, 12], }, ]); @@ -3689,6 +3936,7 @@ mod tests { let fs_bytes = Message::FundingSigned(FundingSigned { channel_id, signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + tlvs: FundingSignedTlvs::default(), }) .encode(); let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); @@ -3843,6 +4091,7 @@ mod tests { let fs_bytes = Message::FundingSigned(FundingSigned { channel_id, signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), + tlvs: FundingSignedTlvs::default(), }) .encode(); @@ -4004,11 +4253,11 @@ mod tests { }, Instruction { operation: Operation::SendFundingCreated, - inputs: vec![6, 0, 9], + inputs: vec![7, 0, 10], }, Instruction { operation: Operation::RecvFundingSigned, - inputs: vec![10], + inputs: vec![11], }, Instruction { operation: Operation::RecvChannelReady, diff --git a/smite-scenarios/src/scenarios/ir.rs b/smite-scenarios/src/scenarios/ir.rs index f4926618..7771ca02 100644 --- a/smite-scenarios/src/scenarios/ir.rs +++ b/smite-scenarios/src/scenarios/ir.rs @@ -94,10 +94,16 @@ impl> Scenario for IrScenario { // the spec doesn't allow. return ScenarioResult::Fail(format!("decode error: {e}")); } - Err(ExecuteError::InsufficientFunds(e)) => { + Err(ExecuteError::Funding(e)) => { // The mutator generated a funding amount/feerate combination // the available UTXOs can't cover. Not a bug in the target. - log::debug!("[{:?}] insufficient funds: {e}", start.elapsed()); + log::debug!("[{:?}] funding transaction: {e}", start.elapsed()); + } + Err(ExecuteError::Musig(e)) => { + // The mutator produced a taproot negotiation we cannot sign, + // e.g. a malformed peer nonce. Not a bug in the target: the + // oracles report a peer that sends one. + log::debug!("[{:?}] musig2: {e}", start.elapsed()); } Err(ExecuteError::Commitment(e)) => { // The mutator generated a funding amount/push_msat combination diff --git a/smite-scenarios/src/scenarios/setup.rs b/smite-scenarios/src/scenarios/setup.rs index 08422d86..1daf89e9 100644 --- a/smite-scenarios/src/scenarios/setup.rs +++ b/smite-scenarios/src/scenarios/setup.rs @@ -2,7 +2,7 @@ use std::time::Duration; -use smite::bolt::{Init, InitTlvs, Message}; +use smite::bolt::{FeatureBit, Features, Init, InitTlvs, Message}; use smite::noise::NoiseConnection; use smite::scenarios::ScenarioError; @@ -30,49 +30,34 @@ pub trait SnapshotSetup { fn setup(target: &T) -> Result<(NoiseConnection, ProgramContext), ScenarioError>; } -/// Clears a feature bit from a feature vector. -/// -/// Feature vectors are encoded as big-endian byte arrays where bit N lives in -/// byte `features[len - 1 - N/8]` at position `N % 8`. -fn clear_feature_bit(features: &mut [u8], bit: usize) { - let byte_index = features.len().checked_sub(1 + bit / 8); - if let Some(i) = byte_index { - features[i] &= !(1 << (bit % 8)); - } -} - -/// Gossip-related feature bits (BOLT 9): `gossip_queries` (6/7), -/// `gossip_queries_ex` (10/11). Stripped so the target doesn't send -/// `gossip_timestamp_filter` or other gossip noise during execution. -const GOSSIP_FEATURE_BITS: &[usize] = &[6, 7, 10, 11]; - -/// Feature bits that force a dual-funded flow when both peers support them: -/// `option_dual_fund` (28/29). Eclair in particular will not allow -/// single-funded flows if either of these feature bits is set, so we strip them -/// when fuzzing the single-funded flow. -const DUAL_FUNDING_FEATURE_BITS: &[usize] = &[28, 29]; - -/// Peer storage feature bits: `option_provide_storage` (42/43). When enabled, -/// peers may send `peer_storage` and `peer_storage_retrieval` messages at -/// arbitrary times. Disabling these bits eliminates peer storage noise. -const PEER_STORAGE_FEATURE_BITS: &[usize] = &[42, 43]; +/// Features stripped from our echoed `init` so the target stays on the single +/// funded flow and doesn't emit unrelated noise: +/// - `gossip_queries` (6/7), `gossip_queries_ex` (10/11): Stripped so the +/// target doesn't send `gossip_timestamp_filter` or other gossip noise during +/// execution. +/// - `option_dual_fund` (28/29): Eclair in particular will not allow +/// single-funded flows if either of these feature bits is set. +/// - `option_provide_storage` (42/43): When enabled, peers may send +/// `peer_storage` and `peer_storage_retrieval` messages at arbitrary times. +const STRIPPED_FEATURES: &[FeatureBit] = &[ + Features::GOSSIP_QUERIES, + Features::GOSSIP_QUERIES_EX, + Features::OPTION_DUAL_FUND, + Features::OPTION_PROVIDE_STORAGE, +]; /// Creates an `init` that echoes the received features with bits stripped that /// would steer the target away from the single-funded `open_channel` flow. fn init_for_single_funded(received: &Init) -> Init { - let mut globalfeatures = received.globalfeatures.clone(); - let mut features = received.features.clone(); - for &bit in GOSSIP_FEATURE_BITS - .iter() - .chain(DUAL_FUNDING_FEATURE_BITS) - .chain(PEER_STORAGE_FEATURE_BITS) - { - clear_feature_bit(&mut globalfeatures, bit); - clear_feature_bit(&mut features, bit); + let mut globalfeatures = Features::from(received.globalfeatures.clone()); + let mut features = Features::from(received.features.clone()); + for &bit in STRIPPED_FEATURES { + globalfeatures.clear_feature(bit); + features.clear_feature(bit); } Init { - globalfeatures, - features, + globalfeatures: globalfeatures.into_bytes(), + features: features.into_bytes(), tlvs: InitTlvs::default(), } } diff --git a/smite-scenarios/src/targets/lnd.rs b/smite-scenarios/src/targets/lnd.rs index f42f2a0c..9b370935 100644 --- a/smite-scenarios/src/targets/lnd.rs +++ b/smite-scenarios/src/targets/lnd.rs @@ -112,6 +112,7 @@ impl LndTarget { let mut cmd = Command::new("lnd"); cmd.arg("--noseedbackup") .arg("--debuglevel=info") + .arg("--protocol.simple-taproot-chans") .arg("--bitcoin.active") .arg("--bitcoin.regtest") .arg("--bitcoin.node=bitcoind") diff --git a/smite/Cargo.toml b/smite/Cargo.toml index 66a524f3..65575e28 100644 --- a/smite/Cargo.toml +++ b/smite/Cargo.toml @@ -14,6 +14,7 @@ workspace = true log.workspace = true simple_logger.workspace = true bitcoin.workspace = true +musig2.workspace = true thiserror.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/smite/src/bolt.rs b/smite/src/bolt.rs index 69eda957..41eef5af 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -14,6 +14,7 @@ mod closing_complete; mod closing_sig; mod commitment_signed; mod error; +mod features; mod funding_created; mod funding_signed; mod gossip_timestamp_filter; @@ -52,8 +53,9 @@ pub use closing_complete::{ClosingComplete, ClosingTlvs}; pub use closing_sig::ClosingSig; pub use commitment_signed::{CommitmentSigned, CommitmentSignedTlvs}; pub use error::Error; -pub use funding_created::FundingCreated; -pub use funding_signed::FundingSigned; +pub use features::{FeatureBit, Features}; +pub use funding_created::{FundingCreated, FundingCreatedTlvs}; +pub use funding_signed::{FundingSigned, FundingSignedTlvs}; pub use gossip_timestamp_filter::GossipTimestampFilter; pub use init::{Init, InitTlvs}; pub use node_announcement::NodeAnnouncement; @@ -73,8 +75,9 @@ pub use tx_remove_input::TxRemoveInput; pub use tx_remove_output::TxRemoveOutput; pub use types::{ BigSize, CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, ChannelId, MAX_MESSAGE_SIZE, - PAYMENT_ONION_PACKET_SIZE, PER_COMMITMENT_SECRET_SIZE, PUBLIC_KEY_SIZE, SHA256_HASH_SIZE, - SHORT_CHANNEL_ID_SIZE, ShortChannelId, TXID_SIZE, Tu32, Tu64, + PARTIAL_SIGNATURE_SIZE, PARTIAL_SIGNATURE_WITH_NONCE_SIZE, PAYMENT_ONION_PACKET_SIZE, + PER_COMMITMENT_SECRET_SIZE, PUBLIC_KEY_SIZE, PUBLIC_NONCE_SIZE, PartialSignatureWithNonce, + PublicNonce, SHA256_HASH_SIZE, SHORT_CHANNEL_ID_SIZE, ShortChannelId, TXID_SIZE, Tu32, Tu64, }; pub use update_add_htlc::{UpdateAddHtlc, UpdateAddHtlcTlvs}; pub use update_fail_htlc::{UpdateFailHtlc, UpdateFailHtlcTlvs}; @@ -601,6 +604,7 @@ mod tests { funding_txid: Txid::from_byte_array([0xcc; TXID_SIZE]), funding_output_index: 0, signature: sig, + tlvs: FundingCreatedTlvs::default(), } } @@ -623,6 +627,7 @@ mod tests { FundingSigned { channel_id: ChannelId::new([0xbb; CHANNEL_ID_SIZE]), signature: sig, + tlvs: FundingSignedTlvs::default(), } } diff --git a/smite/src/bolt/accept_channel.rs b/smite/src/bolt/accept_channel.rs index 506aac75..cd4808d7 100644 --- a/smite/src/bolt/accept_channel.rs +++ b/smite/src/bolt/accept_channel.rs @@ -2,7 +2,7 @@ use super::BoltError; use super::tlv::TlvStream; -use super::types::ChannelId; +use super::types::{ChannelId, PublicNonce}; use super::wire::WireFormat; use bitcoin::secp256k1::PublicKey; @@ -12,6 +12,9 @@ const TLV_UPFRONT_SHUTDOWN_SCRIPT: u64 = 0; /// TLV type for channel type. const TLV_CHANNEL_TYPE: u64 = 1; +/// TLV type for the `MuSig2` verification nonce of simple taproot channels. +const TLV_NEXT_LOCAL_NONCE: u64 = 4; + /// BOLT 2 `accept_channel` message (type 33). /// /// Sent by the channel acceptor in response to `open_channel` to continue the v1 channel @@ -57,6 +60,9 @@ pub struct AcceptChannelTlvs { pub upfront_shutdown_script: Option>, /// The channel type represented as feature bits pub channel_type: Option>, + /// The `MuSig2` nonce the acceptor will use to verify incoming commitment + /// signatures. Required for simple taproot channels. + pub next_local_nonce: Option, } impl AcceptChannel { @@ -87,6 +93,11 @@ impl AcceptChannel { if let Some(channel_type) = &self.tlvs.channel_type { tlv_stream.add(TLV_CHANNEL_TYPE, channel_type.clone()); } + if let Some(next_local_nonce) = &self.tlvs.next_local_nonce { + let mut value = Vec::new(); + next_local_nonce.write(&mut value); + tlv_stream.add(TLV_NEXT_LOCAL_NONCE, value); + } out.extend(tlv_stream.encode()); out @@ -117,10 +128,14 @@ impl AcceptChannel { let first_per_commitment_point = WireFormat::read(&mut cursor)?; // Decode TLVs (remaining bytes) - // Type 0 (`upfront_shutdown_script`) is an even type defined by BOLT 2, - // so we must whitelist it as known. - let tlv_stream = TlvStream::decode_with_known(cursor, &[TLV_UPFRONT_SHUTDOWN_SCRIPT])?; - let tlvs = AcceptChannelTlvs::from_stream(&tlv_stream); + // Types 0 (`upfront_shutdown_script`) and 4 (`next_local_nonce`) are + // even types defined by BOLT 2 and the simple taproot channels + // extension, so we must whitelist them as known. + let tlv_stream = TlvStream::decode_with_known( + cursor, + &[TLV_UPFRONT_SHUTDOWN_SCRIPT, TLV_NEXT_LOCAL_NONCE], + )?; + let tlvs = AcceptChannelTlvs::from_stream(&tlv_stream)?; Ok(Self { temporary_channel_id, @@ -144,20 +159,27 @@ impl AcceptChannel { impl AcceptChannelTlvs { /// Extracts accept channel TLVs from a parsed TLV stream. - fn from_stream(stream: &TlvStream) -> Self { + /// + /// # Errors + /// + /// Returns a `BoltError` if the `next_local_nonce` TLV has an invalid + /// length. + fn from_stream(stream: &TlvStream) -> Result { let upfront_shutdown_script = stream.get(TLV_UPFRONT_SHUTDOWN_SCRIPT).map(Vec::from); let channel_type = stream.get(TLV_CHANNEL_TYPE).map(Vec::from); + let next_local_nonce = stream.get_as::(TLV_NEXT_LOCAL_NONCE)?; - Self { + Ok(Self { upfront_shutdown_script, channel_type, - } + next_local_nonce, + }) } } #[cfg(test)] mod tests { - use super::super::PUBLIC_KEY_SIZE; + use super::super::{PUBLIC_KEY_SIZE, PUBLIC_NONCE_SIZE}; use super::*; use bitcoin::secp256k1::{Secp256k1, SecretKey}; @@ -408,6 +430,7 @@ mod tests { let original = sample_accept_channel(Some(AcceptChannelTlvs { upfront_shutdown_script: Some(vec![0xab; 22]), channel_type: Some(vec![0x01, 0x02]), + next_local_nonce: Some(PublicNonce([0xcd; PUBLIC_NONCE_SIZE])), })); let encoded = original.encode(); @@ -418,8 +441,8 @@ mod tests { #[test] fn encode_with_channel_type() { let accept = sample_accept_channel(Some(AcceptChannelTlvs { - upfront_shutdown_script: None, channel_type: Some(vec![0x01, 0x02]), + ..Default::default() })); let encoded = accept.encode(); @@ -438,6 +461,7 @@ mod tests { // P2WPKH like script upfront_shutdown_script: Some(script), channel_type: Some(vec![0x01]), + ..Default::default() })); let encoded = accept.encode(); @@ -486,5 +510,26 @@ mod tests { let tlvs = AcceptChannelTlvs::default(); assert!(tlvs.upfront_shutdown_script.is_none()); assert!(tlvs.channel_type.is_none()); + assert!(tlvs.next_local_nonce.is_none()); + } + + /// Simple taproot channels require the acceptor's `MuSig2` verification + /// nonce in TLV type 4. + #[test] + fn encode_with_next_local_nonce() { + let accept = sample_accept_channel(Some(AcceptChannelTlvs { + next_local_nonce: Some(PublicNonce([0xcd; PUBLIC_NONCE_SIZE])), + ..Default::default() + })); + + let encoded = accept.encode(); + // 270 fixed + TLV: type(1) + len(1) + value(66) = 68 + assert_eq!(encoded.len(), 270 + 68); + + let decoded = AcceptChannel::decode(&encoded).unwrap(); + assert_eq!( + decoded.tlvs.next_local_nonce, + Some(PublicNonce([0xcd; PUBLIC_NONCE_SIZE])) + ); } } diff --git a/smite/src/bolt/channel_ready.rs b/smite/src/bolt/channel_ready.rs index 4025a67b..13f67330 100644 --- a/smite/src/bolt/channel_ready.rs +++ b/smite/src/bolt/channel_ready.rs @@ -2,13 +2,16 @@ use super::BoltError; use super::tlv::TlvStream; -use super::types::{ChannelId, ShortChannelId}; +use super::types::{ChannelId, PublicNonce, ShortChannelId}; use super::wire::WireFormat; use bitcoin::secp256k1::PublicKey; /// TLV type for short channel ID alias. const TLV_SHORT_CHANNEL_ID: u64 = 1; +/// TLV type for the `MuSig2` verification nonce of simple taproot channels. +const TLV_NEXT_LOCAL_NONCE: u64 = 4; + /// BOLT 2 `channel_ready` message (type 36). /// /// Sent by each side once the funding transaction has reached the agreed-upon @@ -29,6 +32,9 @@ pub struct ChannelReadyTlvs { /// An alias SCID for this channel, used for forwarding before confirmation /// and for private channels instead of the real `short_channel_id`. pub short_channel_id: Option, + /// A fresh `MuSig2` verification nonce, replacing the one consumed by the + /// funding flow. Required for simple taproot channels. + pub next_local_nonce: Option, } impl ChannelReady { @@ -46,6 +52,11 @@ impl ChannelReady { scid.write(&mut value); tlv_stream.add(TLV_SHORT_CHANNEL_ID, value); } + if let Some(next_local_nonce) = &self.tlvs.next_local_nonce { + let mut value = Vec::new(); + next_local_nonce.write(&mut value); + tlv_stream.add(TLV_NEXT_LOCAL_NONCE, value); + } out.extend(tlv_stream.encode()); out @@ -64,7 +75,9 @@ impl ChannelReady { let second_per_commitment_point = WireFormat::read(&mut cursor)?; // Decode TLVs (remaining bytes) - let tlv_stream = TlvStream::decode(cursor)?; + // Type 4 (`next_local_nonce`) is an even type defined by the simple + // taproot channels extension, so we must whitelist it as known. + let tlv_stream = TlvStream::decode_with_known(cursor, &[TLV_NEXT_LOCAL_NONCE])?; let tlvs = ChannelReadyTlvs::from_stream(&tlv_stream)?; Ok(Self { @@ -80,16 +93,21 @@ impl ChannelReadyTlvs { /// /// # Errors /// - /// Returns a `BoltError` if the short channel ID TLV has invalid length. + /// Returns a `BoltError` if the short channel ID or `next_local_nonce` + /// TLV has an invalid length. fn from_stream(stream: &TlvStream) -> Result { let short_channel_id = stream.get_as::(TLV_SHORT_CHANNEL_ID)?; - Ok(Self { short_channel_id }) + let next_local_nonce = stream.get_as::(TLV_NEXT_LOCAL_NONCE)?; + Ok(Self { + short_channel_id, + next_local_nonce, + }) } } #[cfg(test)] mod tests { - use super::super::{CHANNEL_ID_SIZE, PUBLIC_KEY_SIZE}; + use super::super::{CHANNEL_ID_SIZE, PUBLIC_KEY_SIZE, PUBLIC_NONCE_SIZE}; use super::*; use bitcoin::secp256k1::{Secp256k1, SecretKey}; @@ -160,6 +178,7 @@ mod tests { fn roundtrip_with_tlvs() { let original = sample_channel_ready(Some(ChannelReadyTlvs { short_channel_id: Some(ShortChannelId::from_u64(1_029_637_663_919_046_661)), + next_local_nonce: Some(PublicNonce([0xcd; PUBLIC_NONCE_SIZE])), })); let encoded = original.encode(); @@ -171,6 +190,7 @@ mod tests { fn encode_with_short_channel_id() { let msg = sample_channel_ready(Some(ChannelReadyTlvs { short_channel_id: Some(ShortChannelId::from_u64(1_029_637_663_919_046_661)), + ..Default::default() })); let encoded = msg.encode(); diff --git a/smite/src/bolt/features.rs b/smite/src/bolt/features.rs new file mode 100644 index 00000000..2e18c361 --- /dev/null +++ b/smite/src/bolt/features.rs @@ -0,0 +1,324 @@ +//! BOLT 9 feature bitfield primitives. + +/// BOLT 9 feature bit index. Even bits are required (reject if unknown); odd +/// bits are optional (safe to ignore if unknown): "it's ok to be odd" rule. +pub type FeatureBit = usize; + +/// BOLT 9 feature bitfield, encoded as big-endian bytes. +#[derive(Debug, Clone, Default)] +pub struct Features(Vec); + +impl Features { + /// `gossip_queries` (bits 6/7). + pub const GOSSIP_QUERIES: FeatureBit = 6; + /// `gossip_queries_ex` (bits 10/11). + pub const GOSSIP_QUERIES_EX: FeatureBit = 10; + /// `option_static_remotekey` (bits 12/13). + pub const OPTION_STATIC_REMOTEKEY: FeatureBit = 12; + /// `option_anchors` (bits 22/23). + pub const OPTION_ANCHORS: FeatureBit = 22; + /// `option_dual_fund` (bits 28/29). + pub const OPTION_DUAL_FUND: FeatureBit = 28; + /// `zero_fee_commitments` (bits 40/41). + pub const ZERO_FEE_COMMITMENTS: FeatureBit = 40; + /// `option_provide_storage` (bits 42/43). + pub const OPTION_PROVIDE_STORAGE: FeatureBit = 42; + /// `option_scid_alias` (bits 46/47). + pub const OPTION_SCID_ALIAS: FeatureBit = 46; + /// `option_zeroconf` (bits 50/51). + pub const OPTION_ZEROCONF: FeatureBit = 50; + /// `option_simple_taproot` (bits 80/81). + pub const OPTION_SIMPLE_TAPROOT: FeatureBit = 80; + /// `option_simple_taproot_staging` (bits 180/181). + pub const OPTION_SIMPLE_TAPROOT_STAGING: FeatureBit = 180; + /// `option_script_enforced_lease` (bits 2022/2023). + /// Note: Currently, this feature is LND-specific and is not defined in BOLT 9. + pub const OPTION_SCRIPT_ENFORCED_LEASE: FeatureBit = 2022; + + /// Creates an empty set of features. + #[must_use] + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Creates features with the given bits set. + #[must_use] + pub fn from_bits(bits: &[FeatureBit]) -> Self { + let mut features = Self::new(); + for &bit in bits { + features.set_bit(bit); + } + features + } + + /// Consumes the features into their underlying bytes. + #[must_use] + pub fn into_bytes(self) -> Vec { + self.0 + } + + /// Sets the bit, extending the features with leading zero bytes if needed. + /// + /// # Panics + /// + /// Panics if bit exceeds the BOLT 9 feature bitfield's u16-byte length + /// limit. + pub fn set_bit(&mut self, bit: FeatureBit) { + let byte_offset = bit / 8; + assert!( + byte_offset < u16::MAX as usize, + "feature bit {bit} exceeds the feature bitfield length limit" + ); + + let mut len = self.0.len(); + if len <= byte_offset { + let new_len = byte_offset + 1; + let mut new_features = vec![0u8; new_len]; + new_features[(new_len - len)..].copy_from_slice(&self.0); + self.0 = new_features; + len = new_len; + } + + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] |= mask; + } + + /// Clears the bit, no-op if beyond the feature's length. + pub fn clear_bit(&mut self, bit: FeatureBit) { + let byte_offset = bit / 8; + let len = self.0.len(); + if byte_offset < len { + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] &= !mask; + } + } + + /// Returns whether the bit is set. + #[must_use] + pub fn is_bit_set(&self, bit: FeatureBit) -> bool { + let byte_offset = bit / 8; + let len = self.0.len(); + if len <= byte_offset { + return false; + } + + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] & mask != 0 + } + + /// Returns whether the feature is supported by checking its required or + /// optional bit. + #[must_use] + pub fn supports_feature(&self, bit: FeatureBit) -> bool { + self.is_bit_set(bit) || self.is_bit_set(bit ^ 1) + } + + /// Clears the feature's required (even) and optional (odd) bits. + pub fn clear_feature(&mut self, bit: FeatureBit) { + self.clear_bit(bit); + self.clear_bit(bit ^ 1); + } +} + +impl PartialEq for Features { + /// Returns true if this set and other have the same feature bits set. + fn eq(&self, other: &Self) -> bool { + for bit in 0..(self.0.len().max(other.0.len()) * 8) { + if self.is_bit_set(bit) != other.is_bit_set(bit) { + return false; + } + } + true + } +} + +impl Eq for Features {} + +impl From> for Features { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From<&[u8]> for Features { + fn from(bytes: &[u8]) -> Self { + Self(bytes.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_features_support_no_features() { + let features = Features::new(); + assert!(!features.supports_feature(Features::OPTION_ANCHORS)); + assert!(!features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + assert!(!features.supports_feature(Features::GOSSIP_QUERIES)); + } + + #[test] + fn from_bits_sets_requested_bits() { + assert_eq!(Features::from_bits(&[]), Features::new()); + assert_eq!( + Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY]).into_bytes(), + vec![0x10, 0x00] + ); + assert_eq!( + Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY, Features::OPTION_ANCHORS]), + Features::from(vec![0x40, 0x10, 0x00]) + ); + } + + #[test] + fn supports_multiple_set_features() { + let features = Features::from_bits(&[ + Features::OPTION_ANCHORS, + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_DUAL_FUND, + ]); + + assert!(features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + assert!(features.supports_feature(Features::OPTION_DUAL_FUND)); + assert!(!features.supports_feature(Features::OPTION_ZEROCONF)); + } + + #[test] + fn set_bit_within_existing_length() { + let mut fv = Features::from(vec![0x00, 0x00]); + fv.set_bit(0); + assert_eq!(fv, Features::from(vec![0x00, 0x01])); + fv.set_bit(8); + assert_eq!(fv, Features::from(vec![0x01, 0x01])); + } + + #[test] + fn set_bit_grows_and_preserves_existing_bits() { + let mut fv = Features::from(vec![0x01]); + fv.set_bit(12); + assert_eq!(fv, Features::from(vec![0x10, 0x01])); + assert!(fv.is_bit_set(0)); + assert!(fv.is_bit_set(12)); + } + + #[test] + fn set_bit_accepts_the_highest_bit_within_the_length_limit() { + // The bitfield is length-prefixed with a `u16`, so `u16::MAX` bytes + // hold bits 0 through `u16::MAX * 8 - 1`. + let highest = u16::MAX as usize * 8 - 1; + let mut fv = Features::new(); + fv.set_bit(highest); + assert!(fv.is_bit_set(highest)); + assert_eq!(fv.into_bytes().len(), u16::MAX as usize); + } + + #[test] + #[should_panic(expected = "exceeds the feature bitfield length limit")] + fn set_bit_panics_beyond_the_length_limit() { + Features::new().set_bit(u16::MAX as usize * 8); + } + + #[test] + fn clear_bit_within_bounds_and_noop_out_of_bounds() { + let mut fv = Features::from(vec![0xff, 0xff]); + fv.clear_bit(0); + assert_eq!(fv, Features::from(vec![0xff, 0xfe])); + // Out of range: no-op. + fv.clear_bit(100); + assert_eq!(fv, Features::from(vec![0xff, 0xfe])); + } + + #[test] + fn is_bit_set_uses_big_endian_bit_order() { + let fv = Features::from(vec![0x00, 0x01]); + assert!(fv.is_bit_set(0)); + assert!(!fv.is_bit_set(1)); + + let fv = Features::from(vec![0x01, 0x00]); + assert!(fv.is_bit_set(8)); + assert!(!fv.is_bit_set(0)); + } + + #[test] + fn is_bit_set_out_of_bounds_returns_false() { + assert!(!Features::new().is_bit_set(0)); + assert!(!Features::from(vec![0xff]).is_bit_set(8)); + } + + #[test] + fn supports_feature_uses_big_endian_bit_order() { + // Required (bit 22), optional (bit 23). + assert!(Features::from(vec![0x40, 0x00, 0x00]).supports_feature(Features::OPTION_ANCHORS)); + assert!(Features::from(vec![0x80, 0x00, 0x00]).supports_feature(Features::OPTION_ANCHORS)); + // No support. + assert!(!Features::from(vec![0x00, 0x00, 0x40]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0x00, 0x00, 0x80]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0xff, 0xff]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0x00, 0x10]).supports_feature(Features::OPTION_ANCHORS)); + } + + #[test] + fn supports_feature_matches_either_bit_set() { + // Only the required (even) bit set. + let required_only = Features::from_bits(&[22]); + assert!(required_only.supports_feature(Features::OPTION_ANCHORS)); + assert!(required_only.supports_feature(23)); + // Only the optional (odd) bit set. + let optional_only = Features::from_bits(&[23]); + assert!(optional_only.supports_feature(Features::OPTION_ANCHORS)); + assert!(optional_only.supports_feature(23)); + // Neither bit set. + assert!( + !Features::from_bits(&[Features::OPTION_ZEROCONF]) + .supports_feature(Features::OPTION_ANCHORS) + ); + } + + #[test] + fn clear_feature_clears_both_bits() { + // Passing the even bit clears the pair. + let mut fv = Features::from_bits(&[22, 23]); + fv.clear_feature(Features::OPTION_ANCHORS); + assert!(!fv.supports_feature(Features::OPTION_ANCHORS)); + // Passing the odd bit clears the pair. + let mut fv = Features::from_bits(&[22, 23]); + fv.clear_feature(23); + assert!(!fv.supports_feature(Features::OPTION_ANCHORS)); + } + + #[test] + fn clear_feature_removes_support() { + let mut features = + Features::from_bits(&[Features::OPTION_ANCHORS, Features::OPTION_STATIC_REMOTEKEY]); + + assert!(features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + + features.clear_feature(Features::OPTION_ANCHORS); + assert!(!features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + } + + #[test] + fn equality_with_same_and_different_bits() { + let lease = Features::from_bits(&[Features::OPTION_SCRIPT_ENFORCED_LEASE]); + assert_eq!(lease, Features::from_bits(&[2022])); + assert_eq!( + Features::from(vec![0x02, 0x01]), + Features::from(vec![0x02, 0x01]) + ); + + // equality ignores leading zero bytes. + assert_eq!(Features::from(vec![0x01]), Features::from(vec![0x00, 0x01])); + assert_eq!(Features::new(), Features::from(vec![0x00, 0x00])); + + assert_ne!(Features::from(vec![0x01]), Features::from(vec![0x02])); + assert_ne!(Features::from(vec![0x01]), Features::from(vec![0x01, 0x00])); + assert_ne!(Features::from(vec![0x01]), Features::from(vec![0x01, 0x01])); + assert_ne!(Features::from(vec![0x01, 0x00]), Features::from(vec![0x00])); + assert_ne!(lease, Features::from_bits(&[2023])); + } +} diff --git a/smite/src/bolt/funding_created.rs b/smite/src/bolt/funding_created.rs index e7a4bbb9..de27d4a8 100644 --- a/smite/src/bolt/funding_created.rs +++ b/smite/src/bolt/funding_created.rs @@ -1,11 +1,15 @@ //! BOLT 2 funding created message. use super::BoltError; -use super::types::ChannelId; +use super::tlv::TlvStream; +use super::types::{ChannelId, PartialSignatureWithNonce}; use super::wire::WireFormat; use bitcoin::Txid; use bitcoin::secp256k1::ecdsa::Signature; +/// TLV type for the `MuSig2` partial signature of simple taproot channels. +const TLV_PARTIAL_SIGNATURE_WITH_NONCE: u64 = 2; + /// BOLT 2 `funding_created` message (type 34). /// /// Sent by the channel initiator after receiving `accept_channel` to provide the @@ -19,8 +23,23 @@ pub struct FundingCreated { pub funding_txid: Txid, /// The specific output index funding this channel pub funding_output_index: u16, - /// The channel initiator's signature for the counterparty's first commitment transaction + /// The channel initiator's signature for the counterparty's first commitment transaction. + /// + /// Simple taproot channels carry the real signature in + /// [`FundingCreatedTlvs::partial_signature_with_nonce`] and require this + /// field to be 64 zero bytes. pub signature: Signature, + /// Optional TLV extensions. + pub tlvs: FundingCreatedTlvs, +} + +/// TLV extensions for the `funding_created` message. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct FundingCreatedTlvs { + /// The `MuSig2` partial signature over the counterparty's first commitment + /// transaction, with the nonce it was produced with. Required for simple + /// taproot channels. + pub partial_signature_with_nonce: Option, } impl FundingCreated { @@ -32,6 +51,16 @@ impl FundingCreated { self.funding_txid.write(&mut out); self.funding_output_index.write(&mut out); self.signature.write(&mut out); + + // Encode TLVs + let mut tlv_stream = TlvStream::new(); + if let Some(partial_signature_with_nonce) = &self.tlvs.partial_signature_with_nonce { + let mut value = Vec::new(); + partial_signature_with_nonce.write(&mut value); + tlv_stream.add(TLV_PARTIAL_SIGNATURE_WITH_NONCE, value); + } + out.extend(tlv_stream.encode()); + out } @@ -49,18 +78,45 @@ impl FundingCreated { let funding_output_index = WireFormat::read(&mut cursor)?; let signature = WireFormat::read(&mut cursor)?; + // Decode TLVs (remaining bytes) + // Type 2 (`partial_signature_with_nonce`) is an even type defined by + // the simple taproot channels extension, so we must whitelist it as + // known. + let tlv_stream = TlvStream::decode_with_known(cursor, &[TLV_PARTIAL_SIGNATURE_WITH_NONCE])?; + let tlvs = FundingCreatedTlvs::from_stream(&tlv_stream)?; + Ok(Self { temporary_channel_id, funding_txid, funding_output_index, signature, + tlvs, + }) + } +} + +impl FundingCreatedTlvs { + /// Extracts funding created TLVs from a parsed TLV stream. + /// + /// # Errors + /// + /// Returns a `BoltError` if the `partial_signature_with_nonce` TLV has an + /// invalid length. + fn from_stream(stream: &TlvStream) -> Result { + let partial_signature_with_nonce = + stream.get_as::(TLV_PARTIAL_SIGNATURE_WITH_NONCE)?; + Ok(Self { + partial_signature_with_nonce, }) } } #[cfg(test)] mod tests { - use super::super::{CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, TXID_SIZE}; + use super::super::{ + CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, PARTIAL_SIGNATURE_SIZE, PUBLIC_NONCE_SIZE, + PublicNonce, TXID_SIZE, + }; use super::*; use bitcoin::hashes::Hash; use bitcoin::secp256k1::{Message, Secp256k1, SecretKey}; @@ -77,6 +133,7 @@ mod tests { funding_txid: Txid::from_byte_array([0xcc; TXID_SIZE]), funding_output_index: 0, signature: sig, + tlvs: FundingCreatedTlvs::default(), } } @@ -165,4 +222,42 @@ mod tests { Err(BoltError::InvalidSignature(bad_sig)) ); } + + /// Simple taproot channels zero the fixed `signature` field and carry the + /// real signature in TLV type 2. + #[test] + fn encode_with_partial_signature_with_nonce() { + let mut msg = sample_funding_created(); + msg.signature = Signature::from_compact(&[0u8; COMPACT_SIGNATURE_SIZE]) + .expect("zero bytes parse as a signature"); + msg.tlvs.partial_signature_with_nonce = Some(PartialSignatureWithNonce { + partial_signature: [0xab; PARTIAL_SIGNATURE_SIZE], + public_nonce: PublicNonce([0xcd; PUBLIC_NONCE_SIZE]), + }); + + let encoded = msg.encode(); + // 130 fixed + TLV: type(1) + len(1) + value(98) = 100 + assert_eq!(encoded.len(), 130 + 100); + + assert_eq!(FundingCreated::decode(&encoded), Ok(msg)); + } + + /// A `partial_signature_with_nonce` of the wrong length must be rejected + /// rather than silently truncated. + #[test] + fn decode_rejects_partial_signature_of_wrong_length() { + let msg = sample_funding_created(); + let mut encoded = msg.encode(); + + // TLV type 2, length 2 -- too short for a 98-byte value. + encoded.extend_from_slice(&[0x02, 0x02, 0xaa, 0xbb]); + + assert_eq!( + FundingCreated::decode(&encoded), + Err(BoltError::Truncated { + expected: PARTIAL_SIGNATURE_SIZE, + actual: 2 + }) + ); + } } diff --git a/smite/src/bolt/funding_signed.rs b/smite/src/bolt/funding_signed.rs index 4713a879..60c84cc4 100644 --- a/smite/src/bolt/funding_signed.rs +++ b/smite/src/bolt/funding_signed.rs @@ -1,10 +1,14 @@ //! BOLT 2 funding signed message. use super::BoltError; -use super::types::ChannelId; +use super::tlv::TlvStream; +use super::types::{ChannelId, PartialSignatureWithNonce}; use super::wire::WireFormat; use bitcoin::secp256k1::ecdsa::Signature; +/// TLV type for the `MuSig2` partial signature of simple taproot channels. +const TLV_PARTIAL_SIGNATURE_WITH_NONCE: u64 = 2; + /// BOLT 2 `funding_signed` message (type 35). /// /// Sent by the channel acceptor in response to `funding_created` to provide @@ -13,8 +17,23 @@ use bitcoin::secp256k1::ecdsa::Signature; pub struct FundingSigned { /// The channel ID derived from the funding transaction outpoint pub channel_id: ChannelId, - /// The channel acceptor's signature for the counterparty's first commitment transaction + /// The channel acceptor's signature for the counterparty's first commitment transaction. + /// + /// Simple taproot channels carry the real signature in + /// [`FundingSignedTlvs::partial_signature_with_nonce`] and require this + /// field to be 64 zero bytes. pub signature: Signature, + /// Optional TLV extensions. + pub tlvs: FundingSignedTlvs, +} + +/// TLV extensions for the `funding_signed` message. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct FundingSignedTlvs { + /// The `MuSig2` partial signature over the counterparty's first commitment + /// transaction, with the nonce it was produced with. Required for simple + /// taproot channels. + pub partial_signature_with_nonce: Option, } impl FundingSigned { @@ -24,6 +43,16 @@ impl FundingSigned { let mut out = Vec::new(); self.channel_id.write(&mut out); self.signature.write(&mut out); + + // Encode TLVs + let mut tlv_stream = TlvStream::new(); + if let Some(partial_signature_with_nonce) = &self.tlvs.partial_signature_with_nonce { + let mut value = Vec::new(); + partial_signature_with_nonce.write(&mut value); + tlv_stream.add(TLV_PARTIAL_SIGNATURE_WITH_NONCE, value); + } + out.extend(tlv_stream.encode()); + out } @@ -39,16 +68,43 @@ impl FundingSigned { let channel_id = WireFormat::read(&mut cursor)?; let signature = WireFormat::read(&mut cursor)?; + // Decode TLVs (remaining bytes) + // Type 2 (`partial_signature_with_nonce`) is an even type defined by + // the simple taproot channels extension, so we must whitelist it as + // known. + let tlv_stream = TlvStream::decode_with_known(cursor, &[TLV_PARTIAL_SIGNATURE_WITH_NONCE])?; + let tlvs = FundingSignedTlvs::from_stream(&tlv_stream)?; + Ok(Self { channel_id, signature, + tlvs, + }) + } +} + +impl FundingSignedTlvs { + /// Extracts funding signed TLVs from a parsed TLV stream. + /// + /// # Errors + /// + /// Returns a `BoltError` if the `partial_signature_with_nonce` TLV has an + /// invalid length. + fn from_stream(stream: &TlvStream) -> Result { + let partial_signature_with_nonce = + stream.get_as::(TLV_PARTIAL_SIGNATURE_WITH_NONCE)?; + Ok(Self { + partial_signature_with_nonce, }) } } #[cfg(test)] mod tests { - use super::super::{CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE}; + use super::super::{ + CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, PARTIAL_SIGNATURE_SIZE, PUBLIC_NONCE_SIZE, + PublicNonce, + }; use super::*; use bitcoin::secp256k1::{Message, Secp256k1, SecretKey}; @@ -62,6 +118,7 @@ mod tests { FundingSigned { channel_id: ChannelId::new([0xbb; CHANNEL_ID_SIZE]), signature: sig, + tlvs: FundingSignedTlvs::default(), } } @@ -122,4 +179,23 @@ mod tests { Err(BoltError::InvalidSignature(bad_sig)) ); } + + /// Simple taproot channels zero the fixed `signature` field and carry the + /// real signature in TLV type 2. + #[test] + fn encode_with_partial_signature_with_nonce() { + let mut msg = sample_funding_signed(); + msg.signature = Signature::from_compact(&[0u8; COMPACT_SIGNATURE_SIZE]) + .expect("zero bytes parse as a signature"); + msg.tlvs.partial_signature_with_nonce = Some(PartialSignatureWithNonce { + partial_signature: [0xab; PARTIAL_SIGNATURE_SIZE], + public_nonce: PublicNonce([0xcd; PUBLIC_NONCE_SIZE]), + }); + + let encoded = msg.encode(); + // 96 fixed + TLV: type(1) + len(1) + value(98) = 100 + assert_eq!(encoded.len(), 96 + 100); + + assert_eq!(FundingSigned::decode(&encoded), Ok(msg)); + } } diff --git a/smite/src/bolt/open_channel.rs b/smite/src/bolt/open_channel.rs index 0a61c50b..4c0480e8 100644 --- a/smite/src/bolt/open_channel.rs +++ b/smite/src/bolt/open_channel.rs @@ -2,7 +2,7 @@ use super::BoltError; use super::tlv::TlvStream; -use super::types::{CHAIN_HASH_SIZE, ChannelId}; +use super::types::{CHAIN_HASH_SIZE, ChannelId, PublicNonce}; use super::wire::WireFormat; use bitcoin::secp256k1::PublicKey; @@ -12,6 +12,9 @@ const TLV_UPFRONT_SHUTDOWN_SCRIPT: u64 = 0; /// TLV type for channel type. const TLV_CHANNEL_TYPE: u64 = 1; +/// TLV type for the `MuSig2` verification nonce of simple taproot channels. +const TLV_NEXT_LOCAL_NONCE: u64 = 4; + /// BOLT 2 `open_channel` message (type 32). /// /// Sent by the channel initiator to begin the v1 channel establishment flow. @@ -64,6 +67,9 @@ pub struct OpenChannelTlvs { pub upfront_shutdown_script: Option>, /// The channel type represented as feature bits pub channel_type: Option>, + /// The `MuSig2` nonce the initiator will use to verify incoming commitment + /// signatures. Required for simple taproot channels. + pub next_local_nonce: Option, } impl OpenChannel { @@ -98,6 +104,11 @@ impl OpenChannel { if let Some(channel_type) = &self.tlvs.channel_type { tlv_stream.add(TLV_CHANNEL_TYPE, channel_type.clone()); } + if let Some(next_local_nonce) = &self.tlvs.next_local_nonce { + let mut value = Vec::new(); + next_local_nonce.write(&mut value); + tlv_stream.add(TLV_NEXT_LOCAL_NONCE, value); + } out.extend(tlv_stream.encode()); out @@ -132,10 +143,14 @@ impl OpenChannel { let channel_flags = WireFormat::read(&mut cursor)?; // Decode TLVs (remaining bytes) - // Type 0 (`upfront_shutdown_script`) is an even type defined by BOLT 2, - // so we must whitelist it as known. - let tlv_stream = TlvStream::decode_with_known(cursor, &[TLV_UPFRONT_SHUTDOWN_SCRIPT])?; - let tlvs = OpenChannelTlvs::from_stream(&tlv_stream); + // Types 0 (`upfront_shutdown_script`) and 4 (`next_local_nonce`) are + // even types defined by BOLT 2 and the simple taproot channels + // extension, so we must whitelist them as known. + let tlv_stream = TlvStream::decode_with_known( + cursor, + &[TLV_UPFRONT_SHUTDOWN_SCRIPT, TLV_NEXT_LOCAL_NONCE], + )?; + let tlvs = OpenChannelTlvs::from_stream(&tlv_stream)?; Ok(Self { chain_hash, @@ -163,20 +178,27 @@ impl OpenChannel { impl OpenChannelTlvs { /// Extracts open channel TLVs from a parsed TLV stream. - fn from_stream(stream: &TlvStream) -> Self { + /// + /// # Errors + /// + /// Returns a `BoltError` if the `next_local_nonce` TLV has an invalid + /// length. + fn from_stream(stream: &TlvStream) -> Result { let upfront_shutdown_script = stream.get(TLV_UPFRONT_SHUTDOWN_SCRIPT).map(Vec::from); let channel_type = stream.get(TLV_CHANNEL_TYPE).map(Vec::from); + let next_local_nonce = stream.get_as::(TLV_NEXT_LOCAL_NONCE)?; - Self { + Ok(Self { upfront_shutdown_script, channel_type, - } + next_local_nonce, + }) } } #[cfg(test)] mod tests { - use super::super::PUBLIC_KEY_SIZE; + use super::super::{PUBLIC_KEY_SIZE, PUBLIC_NONCE_SIZE}; use super::*; use bitcoin::secp256k1::{Secp256k1, SecretKey}; @@ -459,6 +481,7 @@ mod tests { let original = sample_open_channel(Some(OpenChannelTlvs { upfront_shutdown_script: Some(vec![0xab; 22]), channel_type: Some(vec![0x01, 0x02]), + next_local_nonce: Some(PublicNonce([0xcd; PUBLIC_NONCE_SIZE])), })); let encoded = original.encode(); @@ -469,8 +492,8 @@ mod tests { #[test] fn encode_with_channel_type() { let open = sample_open_channel(Some(OpenChannelTlvs { - upfront_shutdown_script: None, channel_type: Some(vec![0x01, 0x02]), + ..Default::default() })); let encoded = open.encode(); @@ -489,6 +512,7 @@ mod tests { // P2WPKH like script upfront_shutdown_script: Some(script), channel_type: Some(vec![0x01]), + ..Default::default() })); let encoded = open.encode(); @@ -537,5 +561,45 @@ mod tests { let tlvs = OpenChannelTlvs::default(); assert!(tlvs.upfront_shutdown_script.is_none()); assert!(tlvs.channel_type.is_none()); + assert!(tlvs.next_local_nonce.is_none()); + } + + /// Simple taproot channels require the initiator's `MuSig2` verification + /// nonce in TLV type 4. + #[test] + fn encode_with_next_local_nonce() { + let open = sample_open_channel(Some(OpenChannelTlvs { + next_local_nonce: Some(PublicNonce([0xcd; PUBLIC_NONCE_SIZE])), + ..Default::default() + })); + + let encoded = open.encode(); + // 319 fixed + TLV: type(1) + len(1) + value(66) = 68 + assert_eq!(encoded.len(), 319 + 68); + + let decoded = OpenChannel::decode(&encoded).unwrap(); + assert_eq!( + decoded.tlvs.next_local_nonce, + Some(PublicNonce([0xcd; PUBLIC_NONCE_SIZE])) + ); + } + + /// The nonce TLV is an even type, so it must be whitelisted as known + /// rather than rejected as an unknown even type. + #[test] + fn decode_rejects_next_local_nonce_of_wrong_length() { + let open = sample_open_channel(None); + let mut encoded = open.encode(); + + // TLV type 4, length 2 -- too short for a 66-byte nonce. + encoded.extend_from_slice(&[0x04, 0x02, 0xaa, 0xbb]); + + assert_eq!( + OpenChannel::decode(&encoded), + Err(BoltError::Truncated { + expected: PUBLIC_NONCE_SIZE, + actual: 2 + }) + ); } } diff --git a/smite/src/bolt/types.rs b/smite/src/bolt/types.rs index d3851b86..e1e7ede4 100644 --- a/smite/src/bolt/types.rs +++ b/smite/src/bolt/types.rs @@ -35,6 +35,43 @@ pub const PAYMENT_ONION_PACKET_SIZE: usize = 1366; /// Size of a per-commitment secret in bytes. pub const PER_COMMITMENT_SECRET_SIZE: usize = 32; +/// Size of a `MuSig2` public nonce: two compressed secp256k1 points. +pub const PUBLIC_NONCE_SIZE: usize = 2 * PUBLIC_KEY_SIZE; + +/// Size of a `MuSig2` partial signature: the `s` scalar. +pub const PARTIAL_SIGNATURE_SIZE: usize = 32; + +/// Size of a `partial_signature_with_nonce`: `partial_signature || public_nonce`. +pub const PARTIAL_SIGNATURE_WITH_NONCE_SIZE: usize = PARTIAL_SIGNATURE_SIZE + PUBLIC_NONCE_SIZE; + +/// A `MuSig2` public nonce as it appears on the wire: `point_1 || point_2`. +/// +/// Deliberately an opaque byte container rather than a parsed pair of points. +/// Decoding must not reject a malformed nonce: a target that sends one is +/// violating the taproot channel spec, and smite reports that as a finding +/// instead of failing to parse the message. [`crate::musig`] interprets these +/// bytes when they are actually used for signing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PublicNonce(pub [u8; PUBLIC_NONCE_SIZE]); + +/// A `MuSig2` partial signature paired with the public nonce used to produce +/// it, carried in `funding_created`, `funding_signed` and `commitment_signed`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PartialSignatureWithNonce { + /// The `s` scalar of the signer's partial signature. + pub partial_signature: [u8; PARTIAL_SIGNATURE_SIZE], + /// The public nonce the signature was produced with. + pub public_nonce: PublicNonce, +} + +impl PublicNonce { + /// Returns the wire encoding. + #[must_use] + pub fn as_bytes(&self) -> &[u8; PUBLIC_NONCE_SIZE] { + &self.0 + } +} + /// A 32-byte channel identifier. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] pub struct ChannelId(pub [u8; CHANNEL_ID_SIZE]); diff --git a/smite/src/bolt/wire.rs b/smite/src/bolt/wire.rs index 4181e9d0..81f103dd 100644 --- a/smite/src/bolt/wire.rs +++ b/smite/src/bolt/wire.rs @@ -2,7 +2,8 @@ use crate::bolt::BoltError; use crate::bolt::types::{ - BigSize, CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, ChannelId, PUBLIC_KEY_SIZE, SHA256_HASH_SIZE, + BigSize, CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, ChannelId, PARTIAL_SIGNATURE_SIZE, + PUBLIC_KEY_SIZE, PUBLIC_NONCE_SIZE, PartialSignatureWithNonce, PublicNonce, SHA256_HASH_SIZE, ShortChannelId, TXID_SIZE, Tu32, Tu64, }; use bitcoin::Txid; @@ -139,6 +140,38 @@ impl WireFormat for PublicKey { } } +impl WireFormat for PublicNonce { + /// Reads a 66-byte `MuSig2` public nonce. + /// + /// The bytes are not checked to be two valid points: a peer sending a + /// malformed nonce is a spec violation to report, not a decode failure. + fn read(data: &mut &[u8]) -> Result { + let bytes: [u8; PUBLIC_NONCE_SIZE] = WireFormat::read(data)?; + Ok(Self(bytes)) + } + + fn write(&self, out: &mut Vec) { + self.as_bytes().write(out); + } +} + +impl WireFormat for PartialSignatureWithNonce { + /// Reads a 98-byte `partial_signature || public_nonce`. + fn read(data: &mut &[u8]) -> Result { + let partial_signature: [u8; PARTIAL_SIGNATURE_SIZE] = WireFormat::read(data)?; + let public_nonce = PublicNonce::read(data)?; + Ok(Self { + partial_signature, + public_nonce, + }) + } + + fn write(&self, out: &mut Vec) { + self.partial_signature.write(out); + self.public_nonce.write(out); + } +} + impl WireFormat for ChannelId { fn read(data: &mut &[u8]) -> Result { let bytes: [u8; CHANNEL_ID_SIZE] = WireFormat::read(data)?; diff --git a/smite/src/channel_tx.rs b/smite/src/channel_tx.rs index 3e646154..f87750e9 100644 --- a/smite/src/channel_tx.rs +++ b/smite/src/channel_tx.rs @@ -5,9 +5,13 @@ mod commitment; mod funding; +mod taproot; pub use commitment::{ ChannelConfig, ChannelPartyConfig, ChannelState, CommitmentCost, CommitmentError, CommitmentPartyState, CommitmentState, HolderIdentity, Side, }; -pub use funding::{FundingTransaction, InsufficientFunds, build_funding_transaction}; +pub use funding::{ + FundingError, FundingTransaction, InsufficientFunds, build_funding_scriptpubkey, + build_funding_transaction, +}; diff --git a/smite/src/channel_tx/commitment.rs b/smite/src/channel_tx/commitment.rs index 0b451b60..d27e852d 100644 --- a/smite/src/channel_tx/commitment.rs +++ b/smite/src/channel_tx/commitment.rs @@ -1,6 +1,9 @@ //! BOLT 3 commitment transaction construction and signing. -use super::funding::build_funding_witness_script; +use super::funding::{build_funding_scriptpubkey, build_funding_witness_script}; +use super::taproot; +use crate::bolt::{Features, PublicNonce}; +use crate::musig::{FundingKeys, MusigError, PartialSignature, SecretNonce}; use bitcoin::absolute::LockTime; use bitcoin::hashes::sha256::Hash as Sha256; @@ -9,7 +12,7 @@ use bitcoin::opcodes::all as opcodes; use bitcoin::script::Builder; use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::{Message, PublicKey, Scalar, Secp256k1, SecretKey}; -use bitcoin::sighash::{EcdsaSighashType, SighashCache}; +use bitcoin::sighash::{EcdsaSighashType, Prevouts, SighashCache, TapSighashType}; use bitcoin::transaction::Version; use bitcoin::{ Amount, CompressedPublicKey, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, @@ -24,8 +27,8 @@ const COMMITMENT_TX_BASE_WEIGHT_NON_ANCHOR: u64 = 724; /// Weight of an anchor commitment transaction without HTLCs. const COMMITMENT_TX_BASE_WEIGHT_ANCHOR: u64 = 1124; -/// `option_anchors` feature bits (BOLT 9, bits 22/23). -const OPTION_ANCHORS_FEATURE_BITS: &[usize] = &[22, 23]; +/// Weight of a simple taproot commitment transaction without HTLCs. +const COMMITMENT_TX_BASE_WEIGHT_TAPROOT: u64 = 968; /// Errors that can occur when constructing or validating commitment transactions. #[derive(Debug, thiserror::Error)] @@ -77,7 +80,7 @@ pub struct ChannelConfig { pub funding_satoshis: u64, /// Channel type feature bits. The commitment format (anchor / legacy) is /// derived from the bits set here. - pub channel_type: Vec, + pub channel_type: Features, /// Opener's static keys and parameters. pub opener: ChannelPartyConfig, /// Acceptor's static keys and parameters. @@ -150,6 +153,11 @@ pub struct ChannelState { /// after the block height at which they receive `funding_created`, so they /// may never observe it and never send `channel_ready`. pub was_funding_mined_prematurely: bool, + /// The public `MuSig2` nonce the holder gave the counterparty to sign + /// against, for simple taproot channels. Needed to verify the counterparty's + /// partial signature over the holder's commitment. `None` for every other + /// channel type. + pub holder_verification_nonce: Option, } impl Side { @@ -179,6 +187,7 @@ impl ChannelState { commitment: CommitmentState, is_funding_outpoint_valid: bool, was_funding_mined_prematurely: bool, + holder_verification_nonce: Option, ) -> Self { Self { config, @@ -188,6 +197,7 @@ impl ChannelState { acceptor_next_per_commitment_point: None, is_funding_outpoint_valid, was_funding_mined_prematurely, + holder_verification_nonce, } } @@ -228,6 +238,14 @@ impl ChannelState { } impl ChannelConfig { + /// Returns whether this is a simple taproot channel, which funds a P2TR + /// output and commits with `MuSig2` rather than a 2-of-2 P2WSH. + #[must_use] + pub fn is_simple_taproot(&self) -> bool { + self.channel_type + .supports_feature(Features::OPTION_SIMPLE_TAPROOT) + } + /// Returns the config for the given channel side. fn party(&self, side: &Side) -> &ChannelPartyConfig { match side { @@ -286,6 +304,69 @@ impl ChannelConfig { sign(&sighash, &holder.funding_privkey) } + /// Builds the `MuSig2` partial signature for the counterparty's commitment + /// transaction, for a simple taproot channel. + /// + /// `signing_nonce` is a fresh nonce that must be sent alongside the + /// signature; `counterparty_verification_nonce` is the nonce the peer sent + /// in its `accept_channel` (or `open_channel`, when we are the acceptor). + /// + /// # Errors + /// + /// Returns [`MusigError`] if either nonce is malformed or the funding keys + /// cannot be aggregated. + pub fn partial_sign_counterparty_commitment( + &self, + state: &CommitmentState, + holder: &HolderIdentity, + signing_nonce: SecretNonce, + counterparty_verification_nonce: &PublicNonce, + ) -> Result { + let sighash = self.build_commitment_sighash(state, holder.counterparty_side()); + self.funding_keys()?.partial_sign( + &sighash, + &holder.funding_privkey, + signing_nonce, + counterparty_verification_nonce, + ) + } + + /// Verifies a `MuSig2` partial signature received from the counterparty for + /// the holder's commitment transaction. + /// + /// `counterparty_signing_nonce` is the nonce carried alongside the + /// signature; `holder_verification_nonce` is the nonce we sent earlier. + /// + /// # Errors + /// + /// Returns [`MusigError`] if either nonce is malformed or the funding keys + /// cannot be aggregated. A well-formed but incorrect signature yields + /// `Ok(false)`. + pub fn verify_counterparty_partial_signature( + &self, + state: &CommitmentState, + holder: &HolderIdentity, + signature: &PartialSignature, + counterparty_signing_nonce: &PublicNonce, + holder_verification_nonce: &PublicNonce, + ) -> Result { + let sighash = self.build_commitment_sighash(state, &holder.side); + let counterparty = self.party(holder.counterparty_side()); + + self.funding_keys()?.verify_partial( + &sighash, + signature, + &counterparty.funding_pubkey, + counterparty_signing_nonce, + holder_verification_nonce, + ) + } + + /// Builds the `MuSig2` key aggregation context for the funding output. + fn funding_keys(&self) -> Result { + FundingKeys::new(&self.opener.funding_pubkey, &self.acceptor.funding_pubkey) + } + /// Verifies a signature received from the counterparty for the holder's /// commitment transaction. Returns `true` if the signature is valid. #[must_use] @@ -359,6 +440,31 @@ impl ChannelConfig { output: outputs, }; + // Simple taproot channels spend the funding output through the taproot + // key path, so the digest is the BIP 341 one over the funding prevout + // rather than the BIP 143 one over a witness script. + if self.is_simple_taproot() { + let funding_spk = build_funding_scriptpubkey( + &self.opener.funding_pubkey, + &self.acceptor.funding_pubkey, + &self.channel_type, + ) + .expect("two valid funding pubkeys always aggregate"); + let funding_output = TxOut { + value: Amount::from_sat(self.funding_satoshis), + script_pubkey: funding_spk, + }; + + return SighashCache::new(&tx) + .taproot_key_spend_signature_hash( + 0, + &Prevouts::All(&[funding_output]), + TapSighashType::Default, + ) + .expect("input index 0 is always in bounds for a single input transaction") + .to_byte_array(); + } + // Funding output witness script. let funding_witness_script = build_funding_witness_script( &self.opener.funding_pubkey, @@ -383,7 +489,8 @@ impl ChannelConfig { /// `local_side` selects whose commitment outputs are built: the /// opener's or the acceptor's. fn build_commitment_outputs(&self, state: &CommitmentState, local_side: &Side) -> Vec { - let anchor = supports_option_anchors(&self.channel_type); + let anchor = has_anchor_outputs(&self.channel_type); + let taproot = self.is_simple_taproot(); // Fee and balances. let commitment_cost = CommitmentCost::new(state.feerate_per_kw, &self.channel_type); @@ -410,11 +517,19 @@ impl ChannelConfig { let revocationpubkey = derive_revocation_pubkey(&remote.revocation_basepoint, &local_per_commitment_point); - let to_local_spk = build_to_local_scriptpubkey( - &local_delayedpubkey, - &revocationpubkey, - remote.to_self_delay, - ); + let to_local_spk = if taproot { + taproot::to_local_scriptpubkey( + &local_delayedpubkey, + &revocationpubkey, + remote.to_self_delay, + ) + } else { + build_to_local_scriptpubkey( + &local_delayedpubkey, + &revocationpubkey, + remote.to_self_delay, + ) + }; outputs.push(TxOut { value: Amount::from_sat(to_local_value), @@ -422,14 +537,26 @@ impl ChannelConfig { }); if anchor { + // Taproot anchors key on the party's main output key, which is + // revealed when the commitment is spent; `MuSig2` no longer + // reveals the funding key that segwit v0 anchors use. + let anchor_spk = if taproot { + taproot::anchor_scriptpubkey(&local_delayedpubkey) + } else { + build_anchor_scriptpubkey(&local.funding_pubkey) + }; outputs.push(TxOut { value: Amount::from_sat(ANCHOR_OUTPUT_VALUE), - script_pubkey: build_anchor_scriptpubkey(&local.funding_pubkey), + script_pubkey: anchor_spk, }); } } if to_remote_value >= local.dust_limit_satoshis { - let to_remote_spk = build_to_remote_scriptpubkey(&remote.payment_basepoint, anchor); + let to_remote_spk = if taproot { + taproot::to_remote_scriptpubkey(&remote.payment_basepoint) + } else { + build_to_remote_scriptpubkey(&remote.payment_basepoint, anchor) + }; outputs.push(TxOut { value: Amount::from_sat(to_remote_value), @@ -437,9 +564,14 @@ impl ChannelConfig { }); if anchor { + let anchor_spk = if taproot { + taproot::anchor_scriptpubkey(&remote.payment_basepoint) + } else { + build_anchor_scriptpubkey(&remote.funding_pubkey) + }; outputs.push(TxOut { value: Amount::from_sat(ANCHOR_OUTPUT_VALUE), - script_pubkey: build_anchor_scriptpubkey(&remote.funding_pubkey), + script_pubkey: anchor_spk, }); } } @@ -471,7 +603,7 @@ impl CommitmentState { impl CommitmentCost { /// Calculates the total cost of a commitment transaction. #[must_use] - pub fn new(feerate_per_kw: u32, channel_type: &[u8]) -> CommitmentCost { + pub fn new(feerate_per_kw: u32, channel_type: &Features) -> CommitmentCost { CommitmentCost { fee_sat: commit_tx_fee_sat(feerate_per_kw, channel_type), anchor_cost_sat: total_anchors_sat(channel_type), @@ -485,9 +617,20 @@ impl CommitmentCost { } } +/// Returns whether the commitment carries anchor outputs. +/// +/// Simple taproot channels inherit anchor semantics without setting the anchor +/// bits, so the taproot bit implies them. +fn has_anchor_outputs(channel_type: &Features) -> bool { + channel_type.supports_feature(Features::OPTION_ANCHORS) + || channel_type.supports_feature(Features::OPTION_SIMPLE_TAPROOT) +} + /// Get the fee cost of a commitment tx in satoshis. -fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &[u8]) -> u64 { - let commitment_weight = if supports_option_anchors(channel_type) { +fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &Features) -> u64 { + let commitment_weight = if channel_type.supports_feature(Features::OPTION_SIMPLE_TAPROOT) { + COMMITMENT_TX_BASE_WEIGHT_TAPROOT + } else if has_anchor_outputs(channel_type) { COMMITMENT_TX_BASE_WEIGHT_ANCHOR } else { COMMITMENT_TX_BASE_WEIGHT_NON_ANCHOR @@ -497,8 +640,11 @@ fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &[u8]) -> u64 { } /// Get the anchor cost of a commitment tx in satoshis. -fn total_anchors_sat(channel_type: &[u8]) -> u64 { - if supports_option_anchors(channel_type) { +/// +/// This must follow [`has_anchor_outputs`]: whenever the commitment carries +/// anchors, the opener pays for them. +fn total_anchors_sat(channel_type: &Features) -> u64 { + if has_anchor_outputs(channel_type) { ANCHOR_OUTPUT_VALUE * 2 } else { 0 @@ -528,24 +674,6 @@ fn compute_obscuring_factor( u64::from_be_bytes(buf) } -/// Checks whether `option_anchors` (BOLT 9, bits 22/23) is set in a -/// big-endian `channel_type` feature bitfield. -/// -/// Per BOLT 9, even bit (22) = required, odd bit (23) = optional. -/// Either bit indicates anchor support. -fn supports_option_anchors(channel_type: &[u8]) -> bool { - let byte_offset = OPTION_ANCHORS_FEATURE_BITS[0] / 8; - let len = channel_type.len(); - if len <= byte_offset { - return false; - } - - let required_mask = 1 << (OPTION_ANCHORS_FEATURE_BITS[0] % 8); - let optional_mask = 1 << (OPTION_ANCHORS_FEATURE_BITS[1] % 8); - - channel_type[len - 1 - byte_offset] & (required_mask | optional_mask) != 0 -} - /// Derives a public key from a basepoint and per-commitment point per BOLT 3. fn derive_pubkey(basepoint: &PublicKey, per_commitment_point: &PublicKey) -> PublicKey { let secp = Secp256k1::new(); @@ -688,25 +816,12 @@ mod tests { assert_eq!(factor, 0x2bb0_3852_1914); } - #[test] - fn supports_option_anchors_detection() { - // Required (bit 22), optional (bit 23). - assert!(supports_option_anchors(&[0x40, 0x00, 0x00])); - assert!(supports_option_anchors(&[0x80, 0x00, 0x00])); - // No support. - assert!(!supports_option_anchors(&[0x00, 0x00, 0x40])); - assert!(!supports_option_anchors(&[0x00, 0x00, 0x80])); - assert!(!supports_option_anchors(&[])); - assert!(!supports_option_anchors(&[0xff, 0xff])); - assert!(!supports_option_anchors(&[0x00, 0x10])); - } - fn bolt3_commitment_params( feerate_per_kw: u32, to_opener_msat: u64, to_acceptor_msat: u64, dust_limit_satoshis: u64, - channel_type: Vec, + channel_type: Features, ) -> ( ChannelConfig, CommitmentState, @@ -794,7 +909,7 @@ mod tests { #[test] fn simple_commitment_tx_with_no_htlcs_legacy() { let (chan_config, commitment_params, opener_holder, acceptor_holder) = - bolt3_commitment_params(15_000, 7_000_000_000, 3_000_000_000, 546, vec![]); + bolt3_commitment_params(15_000, 7_000_000_000, 3_000_000_000, 546, Features::new()); // Opener signs own commitment. assert_eq!( @@ -830,7 +945,7 @@ mod tests { #[test] fn commitment_tx_with_two_outputs_untrimmed_minimum_feerate_legacy() { let (chan_config, commitment_params, opener_holder, acceptor_holder) = - bolt3_commitment_params(4_915, 6_988_000_000, 3_000_000_000, 546, vec![]); + bolt3_commitment_params(4_915, 6_988_000_000, 3_000_000_000, 546, Features::new()); // Opener signs own commitment. assert_eq!( @@ -866,7 +981,13 @@ mod tests { #[test] fn commitment_tx_with_two_outputs_untrimmed_maximum_feerate_legacy() { let (chan_config, commitment_params, opener_holder, acceptor_holder) = - bolt3_commitment_params(9_651_180, 6_988_000_000, 3_000_000_000, 546, vec![]); + bolt3_commitment_params( + 9_651_180, + 6_988_000_000, + 3_000_000_000, + 546, + Features::new(), + ); // Opener signs own commitment. assert_eq!( @@ -902,7 +1023,13 @@ mod tests { #[test] fn commitment_tx_with_one_output_untrimmed_minimum_feerate_legacy() { let (chan_config, commitment_params, opener_holder, acceptor_holder) = - bolt3_commitment_params(9_651_181, 6_988_000_000, 3_000_000_000, 546, vec![]); + bolt3_commitment_params( + 9_651_181, + 6_988_000_000, + 3_000_000_000, + 546, + Features::new(), + ); // Opener signs own commitment. assert_eq!( @@ -938,7 +1065,13 @@ mod tests { #[test] fn commitment_tx_with_fee_greater_than_funder_amount_legacy() { let (chan_config, commitment_params, opener_holder, acceptor_holder) = - bolt3_commitment_params(9_651_936, 6_988_000_000, 3_000_000_000, 546, vec![]); + bolt3_commitment_params( + 9_651_936, + 6_988_000_000, + 3_000_000_000, + 546, + Features::new(), + ); // Opener signs own commitment. assert_eq!( @@ -976,7 +1109,7 @@ mod tests { #[test] fn commitment_tx_with_balance_msat_not_multiple_of_1000_legacy() { let (chan_config, commitment_params, opener_holder, acceptor_holder) = - bolt3_commitment_params(15_000, 6_999_999_000, 3_000_000_123, 546, vec![]); + bolt3_commitment_params(15_000, 6_999_999_000, 3_000_000_123, 546, Features::new()); // Opener signs own commitment. assert_eq!( @@ -1014,7 +1147,7 @@ mod tests { #[test] fn commitment_tx_with_equal_output_values_orders_by_script_pubkey_legacy() { let (chan_config, commitment_params, opener_holder, acceptor_holder) = - bolt3_commitment_params(15_000, 5_005_430_000, 4_994_570_000, 546, vec![]); + bolt3_commitment_params(15_000, 5_005_430_000, 4_994_570_000, 546, Features::new()); // Opener signs own commitment. assert_eq!( @@ -1058,7 +1191,7 @@ mod tests { 7_000_000_000, 3_000_000_000, 546, - vec![0x40, 0x00, 0x00], + Features::from_bits(&[Features::OPTION_ANCHORS]), ); // Opener signs own commitment. @@ -1095,7 +1228,13 @@ mod tests { #[test] fn simple_commitment_tx_with_no_htlc_and_single_anchor() { let (chan_config, commitment_params, opener_holder, acceptor_holder) = - bolt3_commitment_params(15_000, 10_000_000_000, 0, 546, vec![0x40, 0x00, 0x00]); + bolt3_commitment_params( + 15_000, + 10_000_000_000, + 0, + 546, + Features::from_bits(&[Features::OPTION_ANCHORS]), + ); // Opener signs own commitment. assert_eq!( @@ -1136,7 +1275,7 @@ mod tests { 6_988_000_000, 3_000_000_000, 4_001, - vec![0x40, 0x00, 0x00], + Features::from_bits(&[Features::OPTION_ANCHORS]), ); // Opener signs own commitment. @@ -1178,7 +1317,7 @@ mod tests { 6_988_000_000, 3_000_000_000, 4_001, - vec![0x40, 0x00, 0x00], + Features::from_bits(&[Features::OPTION_ANCHORS]), ); // Opener signs own commitment. @@ -1222,7 +1361,7 @@ mod tests { 6_999_999_000, 3_000_000_123, 546, - vec![0x40, 0x00, 0x00], + Features::from_bits(&[Features::OPTION_ANCHORS]), ); // Opener signs own commitment. @@ -1266,7 +1405,7 @@ mod tests { 5_008_760_000, 4_991_240_000, 546, - vec![0x40, 0x00, 0x00], + Features::from_bits(&[Features::OPTION_ANCHORS]), ); // Opener signs own commitment. @@ -1329,7 +1468,7 @@ mod tests { ); } - fn sample_chan_config(funding_satoshis: u64, channel_type: Vec) -> ChannelConfig { + fn sample_chan_config(funding_satoshis: u64, channel_type: Features) -> ChannelConfig { let sample_key = pubkey("03b28f7c5a9d1e4f8c6a7b2d3e9f1048576a1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e"); let sample_party = || ChannelPartyConfig { @@ -1360,7 +1499,7 @@ mod tests { fn new_initial_from_funding_msat_overflow() { let sample_key = pubkey("03b28f7c5a9d1e4f8c6a7b2d3e9f1048576a1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e"); - let chan_config = sample_chan_config(u64::MAX, vec![]); + let chan_config = sample_chan_config(u64::MAX, Features::new()); let result = chan_config.new_initial_commitment(0, 15_000, sample_key, sample_key); assert!(matches!(result, Err(CommitmentError::FundingMsatOverflow))); } @@ -1369,7 +1508,7 @@ mod tests { fn new_initial_from_funding_push_exceeds_funding() { let sample_key = pubkey("03b28f7c5a9d1e4f8c6a7b2d3e9f1048576a1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e"); - let chan_config = sample_chan_config(1_000, vec![]); + let chan_config = sample_chan_config(1_000, Features::new()); let result = chan_config.new_initial_commitment(2_000_000, 15_000, sample_key, sample_key); assert!(matches!(result, Err(CommitmentError::PushExceedsFunding))); } @@ -1377,28 +1516,32 @@ mod tests { #[test] fn opener_balance_after_commitment_cost_total_sat_checks() { let feerate_per_kw: u32 = 15_000; - let anchor_channel_type = [0x40, 0x00, 0x00]; + let legacy = Features::new(); + let anchor = Features::from_bits(&[Features::OPTION_ANCHORS]); // Legacy fee: 15000 * 724 / 1000 = 10_860 sat // Anchor fee: 15000 * 1124 / 1000 = 16_860 sat; anchor_cost = 660 sat // Comfortably affordable let opener_balance_sat: u64 = 20_000; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), Some(9_140), ); // Exact zero opener balance let opener_balance_sat: u64 = 10_860; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), Some(0), ); // Balance does not cover the fee let opener_balance_sat: u64 = 10_000; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), None ); @@ -1406,8 +1549,132 @@ mod tests { let opener_balance_sat: u64 = 17_500; assert_eq!( opener_balance_sat - .checked_sub(CommitmentCost::new(feerate_per_kw, &anchor_channel_type).total_sat()), + .checked_sub(CommitmentCost::new(feerate_per_kw, &anchor).total_sat()), None, ); } + + /// A `channel_type` negotiating `option_simple_taproot`. + fn simple_taproot_channel_type() -> Features { + Features::from_bits(&[Features::OPTION_SIMPLE_TAPROOT]) + } + + /// The opener's partial signature over the acceptor's commitment verifies + /// from the acceptor's side of the same `MuSig2` session. This exercises the + /// whole taproot path: taproot commitment outputs, the BIP 341 key-spend + /// sighash, and `MuSig2` signing. + #[test] + fn taproot_partial_signature_round_trips_between_both_sides() { + let (chan_config, commitment_params, opener_holder, acceptor_holder) = + bolt3_commitment_params( + 2_500, + 7_000_000_000, + 3_000_000_000, + 546, + simple_taproot_channel_type(), + ); + + // The acceptor publishes a verification nonce in `accept_channel`; the + // opener signs with a fresh nonce sent alongside the signature. + let acceptor_verification = + crate::musig::derive_nonce(&[b"acceptor"], &chan_config.acceptor.funding_pubkey); + let opener_signing = + crate::musig::derive_nonce(&[b"opener"], &chan_config.opener.funding_pubkey); + let opener_signing_public = opener_signing.public_nonce(); + + let partial = chan_config + .partial_sign_counterparty_commitment( + &commitment_params, + &opener_holder, + opener_signing, + &acceptor_verification.public_nonce(), + ) + .expect("signing succeeds"); + + assert!( + chan_config + .verify_counterparty_partial_signature( + &commitment_params, + &acceptor_holder, + &partial, + &opener_signing_public, + &acceptor_verification.public_nonce(), + ) + .expect("nonces parse") + ); + } + + /// Simple taproot channels carry anchors without setting the anchor bits. + /// The staging bits (180/181) are a different channel type using different + /// tapscript leaves, so they must not imply anchors. + #[test] + fn anchor_outputs_detection() { + let taproot = Features::from_bits(&[Features::OPTION_SIMPLE_TAPROOT]); + let anchors = + Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY, Features::OPTION_ANCHORS]); + let staging = Features::from_bits(&[Features::OPTION_SIMPLE_TAPROOT_STAGING]); + let static_remotekey = Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY]); + + assert!(has_anchor_outputs(&taproot)); + assert!(has_anchor_outputs(&anchors)); + + assert!(!has_anchor_outputs(&staging)); + assert!(!has_anchor_outputs(&static_remotekey)); + assert!(!has_anchor_outputs(&Features::new())); + } + + /// A taproot commitment must not be signable with the segwit v0 digest: + /// the two formats produce different sighashes, so a signature made under + /// one channel type must not verify under the other. + #[test] + fn taproot_and_segwit_commitments_have_different_sighashes() { + let (taproot_config, state, opener_holder, _) = bolt3_commitment_params( + 2_500, + 7_000_000_000, + 3_000_000_000, + 546, + simple_taproot_channel_type(), + ); + let (segwit_config, _, _, _) = + bolt3_commitment_params(2_500, 7_000_000_000, 3_000_000_000, 546, Features::new()); + + assert_ne!( + taproot_config.build_commitment_sighash(&state, &opener_holder.side), + segwit_config.build_commitment_sighash(&state, &opener_holder.side) + ); + } + + /// Taproot commitments carry anchors and use the 968 weight rather than the + /// segwit v0 anchor weight, so the fee both peers compute must differ. + #[test] + fn taproot_commitment_cost_uses_the_taproot_weight() { + let taproot = CommitmentCost::new(2_500, &simple_taproot_channel_type()); + assert_eq!(taproot.fee_sat, 2_500 * 968 / 1000); + assert_eq!(taproot.anchor_cost_sat, ANCHOR_OUTPUT_VALUE * 2); + } + + /// Every output of a taproot commitment is a P2TR output. + #[test] + fn taproot_commitment_outputs_are_all_p2tr() { + let (chan_config, state, _, _) = bolt3_commitment_params( + 2_500, + 7_000_000_000, + 3_000_000_000, + 546, + simple_taproot_channel_type(), + ); + + let outputs = chan_config.build_commitment_outputs(&state, &Side::Opener); + + // to_local, to_remote and both anchors. + assert_eq!(outputs.len(), 4); + assert!(outputs.iter().all(|out| out.script_pubkey.is_p2tr())); + assert_eq!( + outputs + .iter() + .filter(|out| out.value == Amount::from_sat(ANCHOR_OUTPUT_VALUE)) + .count(), + 2 + ); + } } diff --git a/smite/src/channel_tx/funding.rs b/smite/src/channel_tx/funding.rs index 3fb89429..b62129a4 100644 --- a/smite/src/channel_tx/funding.rs +++ b/smite/src/channel_tx/funding.rs @@ -7,7 +7,10 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::transaction::{InputWeightPrediction, Version, predict_weight}; use bitcoin::{Amount, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; +use super::taproot; use crate::bitcoin::Utxo; +use crate::bolt::Features; +use crate::musig::{FundingKeys, MusigError}; /// Error returned when available UTXOs cannot cover the funding amount plus /// estimated miner fee. @@ -22,6 +25,19 @@ pub struct InsufficientFunds { pub available: Amount, } +/// Errors that can occur while building a funding transaction. +#[derive(Debug, thiserror::Error)] +pub enum FundingError { + /// The selected UTXOs cannot cover the funding amount and fees. + #[error(transparent)] + InsufficientFunds(#[from] InsufficientFunds), + + /// The taproot funding key could not be derived from the two + /// `funding_pubkey`s. + #[error("taproot funding output: {0}")] + Musig(#[from] MusigError), +} + /// A constructed funding transaction along with the index of the 2-of-2 /// funding output within it. #[derive(Debug, Clone, PartialEq, Eq)] @@ -32,15 +48,19 @@ pub struct FundingTransaction { pub vout: u32, } -/// Builds a funding transaction with a 2-of-2 P2WSH output between the opener -/// and acceptor. +/// Builds a funding transaction paying to the channel's funding output. +/// +/// The output is a 2-of-2 P2WSH, or a single-key P2TR when `channel_type` +/// negotiates `option_simple_taproot`. /// /// Coins are selected for spending in the order the `utxos` are provided. /// /// # Errors /// -/// Returns [`InsufficientFunds`] if the provided inputs do not contain enough -/// value to cover `funding_satoshis` and the required transaction fees. +/// Returns [`FundingError::InsufficientFunds`] if the provided inputs do not +/// contain enough value to cover `funding_satoshis` and the required +/// transaction fees, or [`FundingError::Musig`] if a taproot funding key +/// cannot be derived from the two `funding_pubkey`s. /// /// # Panics /// @@ -51,9 +71,10 @@ pub fn build_funding_transaction( acceptor_funding_pubkey: &PublicKey, funding_satoshis: u64, feerate_per_kw: u32, + channel_type: &Features, utxos: Vec, change_spk: ScriptBuf, -) -> Result { +) -> Result { // Amounts exceeding Bitcoin's maximum supply can never be funded. The // error's `available` field reports Bitcoin's total supply cap. let funding_amt = Amount::from_sat(funding_satoshis); @@ -61,7 +82,8 @@ pub fn build_funding_transaction( return Err(InsufficientFunds { required: funding_amt, available: Amount::MAX_MONEY, - }); + } + .into()); } // Return early if no UTXOs are available, since the funding transaction @@ -70,11 +92,12 @@ pub fn build_funding_transaction( return Err(InsufficientFunds { required: funding_amt, available: Amount::ZERO, - }); + } + .into()); } let funding_spk = - build_funding_witness_script(opener_funding_pubkey, acceptor_funding_pubkey).to_p2wsh(); + build_funding_scriptpubkey(opener_funding_pubkey, acceptor_funding_pubkey, channel_type)?; let mut inputs = Vec::new(); let mut input_weights = Vec::new(); @@ -123,7 +146,8 @@ pub fn build_funding_transaction( return Err(InsufficientFunds { required: funding_amt + expected_fee_no_change, available: total, - }); + } + .into()); } // Add remaining funds after accounting for fees as a change output, @@ -171,21 +195,53 @@ fn predict_tx_fee( impl FundingTransaction { /// Returns whether the referenced funding output matches the negotiated /// output script and amount. + /// + /// A mismatch is not an error here: mutators can make the transaction + /// disagree with the negotiation, and the caller records that so it knows + /// the target will never see the channel confirm. #[must_use] pub fn matches_funding_output( &self, opener_funding_pubkey: &PublicKey, acceptor_funding_pubkey: &PublicKey, funding_satoshis: u64, + channel_type: &Features, ) -> bool { - let expected_spk = - build_funding_witness_script(opener_funding_pubkey, acceptor_funding_pubkey).to_p2wsh(); + let Ok(expected_spk) = build_funding_scriptpubkey( + opener_funding_pubkey, + acceptor_funding_pubkey, + channel_type, + ) else { + return false; + }; self.tx.output.get(self.vout as usize).is_some_and(|out| { out.script_pubkey == expected_spk && out.value.to_sat() == funding_satoshis }) } } +/// Builds the funding output `script_pubkey` for the negotiated channel type. +/// +/// Simple taproot channels pay to a single P2TR key aggregated from both +/// `funding_pubkey`s; every other channel type pays to a 2-of-2 P2WSH. +/// +/// # Errors +/// +/// Returns [`MusigError`] if the taproot funding key cannot be derived, which +/// cannot happen for two valid public keys. +pub fn build_funding_scriptpubkey( + opener_funding_pubkey: &PublicKey, + acceptor_funding_pubkey: &PublicKey, + channel_type: &Features, +) -> Result { + if channel_type.supports_feature(Features::OPTION_SIMPLE_TAPROOT) { + let keys = FundingKeys::new(opener_funding_pubkey, acceptor_funding_pubkey)?; + Ok(taproot::funding_scriptpubkey(keys.aggregate_pubkey())) + } else { + Ok(build_funding_witness_script(opener_funding_pubkey, acceptor_funding_pubkey).to_p2wsh()) + } +} + /// Builds the funding output witness script per BOLT 3. pub fn build_funding_witness_script(pubkey1: &PublicKey, pubkey2: &PublicKey) -> ScriptBuf { let key1_bytes = pubkey1.serialize(); @@ -213,6 +269,22 @@ mod tests { use bitcoin::secp256k1::{Secp256k1, SecretKey}; use bitcoin::sighash::{EcdsaSighashType, SighashCache}; + /// Unwraps the [`FundingError::InsufficientFunds`] variant, failing the + /// test on any other error. + fn expect_insufficient_funds(err: FundingError) -> InsufficientFunds { + match err { + FundingError::InsufficientFunds(e) => e, + other @ FundingError::Musig(_) => { + panic!("expected InsufficientFunds, got {other:?}") + } + } + } + + /// A `channel_type` negotiating `option_simple_taproot`. + fn simple_taproot_channel_type() -> Features { + Features::from_bits(&[Features::OPTION_SIMPLE_TAPROOT]) + } + fn pubkey(hex_str: &str) -> PublicKey { let bytes = hex::decode(hex_str).expect("valid hex"); PublicKey::from_slice(&bytes).expect("valid pubkey") @@ -296,6 +368,7 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), 10_000_000, 15_000, + &Features::new(), utxos.clone(), change_spk, ) @@ -362,6 +435,7 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), 10_000_000, 15_000, + &Features::new(), utxos.clone(), change_spk, ) @@ -423,6 +497,7 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), 10_000_000, 15_000, + &Features::new(), utxos.clone(), change_spk, ) @@ -512,6 +587,7 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), 15_000_000, 15_000, + &Features::new(), utxos.clone(), change_spk, ) @@ -558,11 +634,12 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), 10_000_000, 15_000, + &Features::new(), vec![], change_spk, ) .unwrap_err(); - assert!(matches!(err, InsufficientFunds { .. })); + let err = expect_insufficient_funds(err); assert_eq!(err.required, Amount::from_sat(10_000_000)); assert_eq!(err.available, Amount::from_sat(0)); } @@ -581,11 +658,12 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), 0, 0, + &Features::new(), vec![], change_spk, ) .unwrap_err(); - assert!(matches!(err, InsufficientFunds { .. })); + let err = expect_insufficient_funds(err); assert_eq!(err.required, Amount::from_sat(0)); assert_eq!(err.available, Amount::from_sat(0)); } @@ -617,11 +695,12 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), 10_000_000, 15_000, + &Features::new(), utxos, change_spk, ) .unwrap_err(); - assert!(matches!(err, InsufficientFunds { .. })); + let err = expect_insufficient_funds(err); assert_eq!(err.required, Amount::from_sat(10_012_060)); assert_eq!(err.available, Amount::from_sat(1_000)); } @@ -655,11 +734,12 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), u64::MAX, 15_000, + &Features::new(), utxos, change_spk, ) .unwrap_err(); - assert!(matches!(err, InsufficientFunds { .. })); + let err = expect_insufficient_funds(err); assert_eq!(err.required, Amount::from_sat(u64::MAX)); assert_eq!(err.available, Amount::MAX_MONEY); } @@ -694,6 +774,7 @@ mod tests { &pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"), 10_000_000, 15_000, + &Features::new(), utxos, change_spk, ); @@ -751,6 +832,7 @@ mod tests { &acceptor_funding_pubkey, funding_satoshis, 15_000, + &Features::new(), utxos, change_spk, ) @@ -762,6 +844,7 @@ mod tests { &opener_funding_pubkey, &acceptor_funding_pubkey, funding_satoshis, + &Features::new(), )); // A different funding pubkey yields a different script and does not match. @@ -771,6 +854,7 @@ mod tests { &opener_funding_pubkey, &other_pubkey, funding_satoshis, + &Features::new(), )); // A mismatched funding amount does not match. @@ -778,7 +862,82 @@ mod tests { assert!(!funding.matches_funding_output( &opener_funding_pubkey, &acceptor_funding_pubkey, - other_amount + other_amount, + &Features::new(), )); } + + /// The spec's funding test vector: a simple taproot channel funds a P2TR + /// output keyed on the `MuSig2` aggregate of both `funding_pubkey`s, not a + /// 2-of-2 P2WSH. + #[test] + fn taproot_channel_type_funds_a_p2tr_output() { + let local = pubkey("03b7203dec7c13896b6ff1f58b24f84458c441720a12b5a57426397e22f0a8c78b"); + let remote = pubkey("02956e6845a6f346f97c5e028c0f8ab38a76b0124fd7184deab60f682b3e657fdb"); + + // `option_simple_taproot` alone: BOLT 9 bit 80. + let taproot_type = simple_taproot_channel_type(); + let spk = build_funding_scriptpubkey(&local, &remote, &taproot_type) + .expect("valid funding pubkeys"); + + assert!(spk.is_p2tr()); + assert_eq!( + hex::encode(spk.as_bytes()), + "5120d0ebb4909d563a7ae1213fddede4ae54132fba0ef0b97ee3f8469191fecd348e" + ); + + // Without the taproot bit the output stays a 2-of-2 P2WSH. + assert!( + build_funding_scriptpubkey(&local, &remote, &Features::new()) + .expect("valid funding pubkeys") + .is_p2wsh() + ); + } + + /// The funding transaction pays to the taproot script, and + /// `matches_funding_output` only agrees when given the same channel type. + #[test] + fn taproot_funding_output_matches_only_its_own_channel_type() { + let local = pubkey("023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb"); + let remote = pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1"); + let taproot_type = simple_taproot_channel_type(); + + let utxos = vec![Utxo { + amount: Amount::from_sat(5_000_000_000), + outpoint: OutPoint { + txid: "fd2105607605d2302994ffea703b09f66b6351816ee737a93e42a841ea20bbad" + .parse() + .expect("valid txid"), + vout: 0, + }, + script_pubkey: ScriptBuf::from( + hex::decode("0014a10d9257489e685dda030662390dc177852faf13") + .expect("valid P2WPKH scriptpubkey hex"), + ), + }]; + let change_spk = ScriptBuf::from( + hex::decode("00143ca33c2e4446f4a305f23c80df8ad1afdcf652f9") + .expect("valid P2WPKH scriptpubkey hex"), + ); + + let funding = build_funding_transaction( + &local, + &remote, + 10_000_000, + 15_000, + &taproot_type, + utxos, + change_spk, + ) + .expect("inputs should cover funding amount and fees"); + + assert!( + funding.tx.output[funding.vout as usize] + .script_pubkey + .is_p2tr() + ); + assert!(funding.matches_funding_output(&local, &remote, 10_000_000, &taproot_type)); + // The same transaction does not satisfy a non-taproot negotiation. + assert!(!funding.matches_funding_output(&local, &remote, 10_000_000, &Features::new())); + } } diff --git a/smite/src/channel_tx/taproot.rs b/smite/src/channel_tx/taproot.rs new file mode 100644 index 00000000..29cd5bcd --- /dev/null +++ b/smite/src/channel_tx/taproot.rs @@ -0,0 +1,307 @@ +//! Simple taproot channel output scripts. +//! +//! Every commitment output of a simple taproot channel is a P2TR output whose +//! key commits to a tapscript tree. This module builds those `script_pubkey`s. +//! The forms here are the ones negotiated by the `option_simple_taproot` +//! channel type (BOLT 9 bit 80), which is what lnd calls its "final" taproot +//! commitment and what the spec's test vectors encode. + +use bitcoin::ScriptBuf; +use bitcoin::opcodes::all as opcodes; +use bitcoin::script::Builder; +use bitcoin::secp256k1::{PublicKey, Secp256k1, XOnlyPublicKey}; +use bitcoin::taproot::{TaprootBuilder, TaprootSpendInfo}; + +/// The "Nothing Up My Sleeve" point used as the internal key of the `to_local` +/// and `to_remote` outputs, so that the script path must always be taken and +/// the keys inside it are revealed on chain. +/// +/// Generated with the seed phrase "Lightning Simple Taproot". +const NUMS_POINT: [u8; 33] = [ + 0x02, 0xdc, 0xa0, 0x94, 0x75, 0x11, 0x09, 0xd0, 0xbd, 0x05, 0x5d, 0x03, 0x56, 0x58, 0x74, 0xe8, + 0x27, 0x6d, 0xd5, 0x3e, 0x92, 0x6b, 0x44, 0xe3, 0xbd, 0x1b, 0xb6, 0xbf, 0x4b, 0xc1, 0x30, 0xa2, + 0x79, +]; + +/// Returns the NUMS internal key. +fn nums_point() -> XOnlyPublicKey { + PublicKey::from_slice(&NUMS_POINT) + .expect("the NUMS constant is a valid compressed pubkey") + .x_only_public_key() + .0 +} + +/// Builds the funding output `script_pubkey` for a simple taproot channel. +/// +/// `aggregate_funding_key` is the `MuSig2` aggregate of both `funding_pubkey`s +/// with the BIP 86 tweak already applied, so the output commits to no script +/// path and is spent with a single aggregated Schnorr signature. +#[must_use] +pub fn funding_scriptpubkey(aggregate_funding_key: XOnlyPublicKey) -> ScriptBuf { + // `dangerous_assume_tweaked` is correct here: the `MuSig2` key aggregation + // already applied the BIP 86 taptweak, so tweaking again would produce a + // key neither peer can sign for. + ScriptBuf::new_p2tr_tweaked(bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked( + aggregate_funding_key, + )) +} + +/// Builds the `to_local` output `script_pubkey`. +/// +/// The tree has two leaves: the owner sweeps after `to_self_delay` blocks, or +/// the counterparty sweeps immediately with the revocation key. The revocation +/// leaf pushes the delayed key and drops it, which reveals that key on chain so +/// the anchor output stays spendable. +#[must_use] +pub fn to_local_scriptpubkey( + local_delayedpubkey: &PublicKey, + revocationpubkey: &PublicKey, + to_self_delay: u16, +) -> ScriptBuf { + let delay_script = to_local_delay_script(local_delayedpubkey, to_self_delay); + let revoke_script = to_local_revoke_script(local_delayedpubkey, revocationpubkey); + + let spend_info = TaprootBuilder::new() + .add_leaf(1, delay_script) + .and_then(|builder| builder.add_leaf(1, revoke_script)) + .expect("a two-leaf tree at depth 1 is always valid") + .finalize(&Secp256k1::verification_only(), nums_point()) + .expect("a complete two-leaf tree always finalizes"); + + p2tr(&spend_info) +} + +/// Builds the `to_remote` output `script_pubkey`. +/// +/// A single leaf letting the counterparty sweep after a 1-block delay. The +/// internal key is the NUMS point so the delay cannot be bypassed, and because +/// it is a constant the counterparty can scan the chain for this output. +#[must_use] +pub fn to_remote_scriptpubkey(remotepubkey: &PublicKey) -> ScriptBuf { + let spend_info = TaprootBuilder::new() + .add_leaf(0, to_remote_script(remotepubkey)) + .expect("a single-leaf tree is always valid") + .finalize(&Secp256k1::verification_only(), nums_point()) + .expect("a complete single-leaf tree always finalizes"); + + p2tr(&spend_info) +} + +/// Builds an anchor output `script_pubkey`. +/// +/// The owner spends it via the key path; anyone may sweep it via the script +/// path after 16 blocks. Unlike the segwit v0 anchors, the internal key is the +/// owner's main output key (`local_delayedpubkey` or `remotepubkey`) rather +/// than the funding key, which `MuSig2` no longer reveals. +#[must_use] +pub fn anchor_scriptpubkey(anchor_internal_key: &PublicKey) -> ScriptBuf { + let spend_info = TaprootBuilder::new() + .add_leaf(0, anchor_script()) + .expect("a single-leaf tree is always valid") + .finalize( + &Secp256k1::verification_only(), + anchor_internal_key.x_only_public_key().0, + ) + .expect("a complete single-leaf tree always finalizes"); + + p2tr(&spend_info) +} + +/// ` OP_CHECKSIGVERIFY OP_CHECKSEQUENCEVERIFY` +fn to_local_delay_script(local_delayedpubkey: &PublicKey, to_self_delay: u16) -> ScriptBuf { + Builder::new() + .push_slice(local_delayedpubkey.x_only_public_key().0.serialize()) + .push_opcode(opcodes::OP_CHECKSIGVERIFY) + .push_int(i64::from(to_self_delay)) + .push_opcode(opcodes::OP_CSV) + .into_script() +} + +/// ` OP_DROP OP_CHECKSIG` +fn to_local_revoke_script( + local_delayedpubkey: &PublicKey, + revocationpubkey: &PublicKey, +) -> ScriptBuf { + Builder::new() + .push_slice(local_delayedpubkey.x_only_public_key().0.serialize()) + .push_opcode(opcodes::OP_DROP) + .push_slice(revocationpubkey.x_only_public_key().0.serialize()) + .push_opcode(opcodes::OP_CHECKSIG) + .into_script() +} + +/// ` OP_CHECKSIGVERIFY OP_1 OP_CHECKSEQUENCEVERIFY` +fn to_remote_script(remotepubkey: &PublicKey) -> ScriptBuf { + Builder::new() + .push_slice(remotepubkey.x_only_public_key().0.serialize()) + .push_opcode(opcodes::OP_CHECKSIGVERIFY) + .push_opcode(opcodes::OP_PUSHNUM_1) + .push_opcode(opcodes::OP_CSV) + .into_script() +} + +/// `OP_16 OP_CHECKSEQUENCEVERIFY` +fn anchor_script() -> ScriptBuf { + Builder::new() + .push_opcode(opcodes::OP_PUSHNUM_16) + .push_opcode(opcodes::OP_CSV) + .into_script() +} + +/// Returns the P2TR `script_pubkey` for a finalized tapscript tree. +fn p2tr(spend_info: &TaprootSpendInfo) -> ScriptBuf { + ScriptBuf::new_p2tr_tweaked(spend_info.output_key()) +} + +/// Returns the tapscript merkle root of a `to_local` output, exposed so tests +/// can check it against the spec vectors. +#[cfg(test)] +fn to_local_merkle_root( + local_delayedpubkey: &PublicKey, + revocationpubkey: &PublicKey, + to_self_delay: u16, +) -> bitcoin::TapNodeHash { + TaprootBuilder::new() + .add_leaf(1, to_local_delay_script(local_delayedpubkey, to_self_delay)) + .and_then(|builder| { + builder.add_leaf( + 1, + to_local_revoke_script(local_delayedpubkey, revocationpubkey), + ) + }) + .expect("a two-leaf tree at depth 1 is always valid") + .finalize(&Secp256k1::verification_only(), nums_point()) + .expect("a complete two-leaf tree always finalizes") + .merkle_root() + .expect("a tree with leaves has a merkle root") +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::hashes::Hash; + + fn pubkey(hex_str: &str) -> PublicKey { + let bytes = hex::decode(hex_str).expect("valid hex"); + PublicKey::from_slice(&bytes).expect("valid pubkey") + } + + // Test vectors from the simple taproot channels spec: + // bolt-simple-taproot.md, "Test Vectors" appendix. + // + // `csv_delay` is 144 throughout. + + const DELAYED_PUBKEY: &str = + "0315ec0138eb42f1ab4603042123988d53c854e89d1d87aa4dbb97a57482029c05"; + const REVOCATION_PUBKEY: &str = + "03d4c77088d346bce67c13bbbf82ca112588f4b1c9595a1f8af3be9b2f95a109a0"; + const REMOTE_PAYMENT_PUBKEY: &str = + "03595f2ef2a51d2250a21077dbea4a7fc3ce550f10676996bf63719e2a71d1f4c9"; + const CSV_DELAY: u16 = 144; + + #[test] + fn nums_point_matches_spec_vector() { + assert_eq!( + hex::encode(nums_point().serialize()), + "dca094751109d0bd055d03565874e8276dd53e926b44e3bd1bb6bf4bc130a279" + ); + } + + #[test] + fn to_local_leaf_scripts_match_spec_vectors() { + let delayed = pubkey(DELAYED_PUBKEY); + let revocation = pubkey(REVOCATION_PUBKEY); + + assert_eq!( + hex::encode(to_local_delay_script(&delayed, CSV_DELAY).as_bytes()), + "2015ec0138eb42f1ab4603042123988d53c854e89d1d87aa4dbb97a57482029c05ad029000b2" + ); + assert_eq!( + hex::encode(to_local_revoke_script(&delayed, &revocation).as_bytes()), + "2015ec0138eb42f1ab4603042123988d53c854e89d1d87aa4dbb97a57482029c057520d4c77088d346bce67c13bbbf82ca112588f4b1c9595a1f8af3be9b2f95a109a0ac" + ); + } + + /// BIP 341 sorts each `TapBranch`'s children, so the root is independent of + /// the order the leaves were added in. This pins the root against the spec + /// so a change in tree assembly cannot go unnoticed. + #[test] + fn to_local_merkle_root_matches_spec_vector() { + assert_eq!( + hex::encode( + to_local_merkle_root( + &pubkey(DELAYED_PUBKEY), + &pubkey(REVOCATION_PUBKEY), + CSV_DELAY + ) + .to_byte_array() + ), + "b8b76c2e893ca785072f0d7393e35d5bd72adf8b7ff2a53538aa664378a38a36" + ); + } + + #[test] + fn to_local_scriptpubkey_matches_spec_vector() { + assert_eq!( + hex::encode( + to_local_scriptpubkey( + &pubkey(DELAYED_PUBKEY), + &pubkey(REVOCATION_PUBKEY), + CSV_DELAY + ) + .as_bytes() + ), + "51203e1fcbbd06c8a7414704612c72be9834a75d86ed85b29f0ef0c52e1950afaff3" + ); + } + + #[test] + fn to_remote_script_matches_spec_vector() { + assert_eq!( + hex::encode(to_remote_script(&pubkey(REMOTE_PAYMENT_PUBKEY)).as_bytes()), + "20595f2ef2a51d2250a21077dbea4a7fc3ce550f10676996bf63719e2a71d1f4c9ad51b2" + ); + } + + #[test] + fn to_remote_scriptpubkey_matches_spec_vector() { + assert_eq!( + hex::encode(to_remote_scriptpubkey(&pubkey(REMOTE_PAYMENT_PUBKEY)).as_bytes()), + "51203609bb705034e5629aa6ec05c5ca906ac89ac08b34c4583c259521ec30174408" + ); + } + + #[test] + fn anchor_script_matches_spec_vector() { + assert_eq!(hex::encode(anchor_script().as_bytes()), "60b2"); + } + + /// The local anchor's internal key is the delayed payment key, and the + /// remote anchor's is the remote payment key: both are revealed on chain + /// when the commitment is spent. + #[test] + fn anchor_scriptpubkeys_match_spec_vectors() { + assert_eq!( + hex::encode(anchor_scriptpubkey(&pubkey(DELAYED_PUBKEY)).as_bytes()), + "5120f67ab012701705f3203d132f909a6810ef18c5da4c11d986cb50818803b8344e" + ); + assert_eq!( + hex::encode(anchor_scriptpubkey(&pubkey(REMOTE_PAYMENT_PUBKEY)).as_bytes()), + "51201249c50576fdf914caa14f9221370b986df520bdbc73f57d5056a86ee03e5ac4" + ); + } + + #[test] + fn funding_scriptpubkey_matches_spec_vector() { + let aggregate = XOnlyPublicKey::from_slice( + &hex::decode("d0ebb4909d563a7ae1213fddede4ae54132fba0ef0b97ee3f8469191fecd348e") + .expect("valid hex"), + ) + .expect("valid x-only pubkey"); + + assert_eq!( + hex::encode(funding_scriptpubkey(aggregate).as_bytes()), + "5120d0ebb4909d563a7ae1213fddede4ae54132fba0ef0b97ee3f8469191fecd348e" + ); + } +} diff --git a/smite/src/lib.rs b/smite/src/lib.rs index cee57d9a..7a409ae1 100644 --- a/smite/src/lib.rs +++ b/smite/src/lib.rs @@ -9,6 +9,7 @@ //! - [`bitcoin`] - Utilities for interacting with `bitcoind` instances via `bitcoin-cli`. //! - [`bolt`] - BOLT message encoding and decoding. //! - [`channel_tx`] - BOLT 3 channel transaction construction (funding and commitment). +//! - [`musig`] - BIP 327 `MuSig2` helpers for simple taproot channels. //! - [`noise`] - BOLT 8 `Noise_XK` encrypted transport. //! - [`oracles`] - Protocol invariant checks. //! - [`pending_channel`] - BOLT 2 channel negotiation state. @@ -20,6 +21,7 @@ pub mod bitcoin; pub mod bolt; pub mod channel_tx; +pub mod musig; pub mod noise; #[cfg(feature = "nyx")] pub mod nyx_log; diff --git a/smite/src/musig.rs b/smite/src/musig.rs new file mode 100644 index 00000000..8fe41b13 --- /dev/null +++ b/smite/src/musig.rs @@ -0,0 +1,372 @@ +//! BIP-327 `MuSig2` helpers for simple taproot channels. +//! +//! Simple taproot channels replace the 2-of-2 P2WSH funding output with a +//! single P2TR output whose key is the `MuSig2` aggregate of both +//! `funding_pubkey`s, and replace the commitment ECDSA signature with a `MuSig2` +//! partial signature exchanged in `funding_created` / `funding_signed`. +//! +//! This module is the only place that talks to the `musig2` crate. That crate +//! pulls its own `secp256k1` version, distinct from the one `bitcoin` +//! re-exports, so every key crosses the boundary as serialized bytes and the +//! rest of smite only ever sees `bitcoin::secp256k1` types. +//! +//! The `musig2` dependency is removable once `bitcoin` 0.33 is stable: +//! `secp256k1` 0.32 ships `secp256k1::musig`, and `bitcoin` 0.33 re-exports +//! that version. + +use bitcoin::secp256k1::{PublicKey, SecretKey, XOnlyPublicKey}; +use musig2::secp256k1::PublicKey as MusigPublicKey; +use musig2::{AggNonce, BinaryEncoding, KeyAggContext, PubNonce, SecNonce, SecNonceBuilder}; + +use crate::bolt::{PARTIAL_SIGNATURE_SIZE, PublicNonce}; + +/// Errors produced while building or using a `MuSig2` funding session. +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum MusigError { + /// The two `funding_pubkey`s could not be aggregated. + #[error("failed to aggregate funding pubkeys: {0}")] + KeyAggregation(String), + + /// The BIP 86 taproot tweak could not be applied to the aggregate key. + #[error("failed to apply taproot tweak to aggregate funding key: {0}")] + Tweak(String), + + /// A public nonce received from the peer is not two compressed points. + #[error("public nonce is not two valid compressed secp256k1 points")] + InvalidPublicNonce, + + /// Producing our own partial signature failed. + #[error("failed to produce partial signature: {0}")] + Signing(String), +} + +/// A `MuSig2` secret nonce. +/// +/// Wraps `musig2::SecNonce` so that type stays inside this module +#[derive(Debug)] +pub struct SecretNonce(SecNonce); + +/// A `MuSig2` partial signature: the `s` scalar of one signer's contribution. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PartialSignature(pub [u8; PARTIAL_SIGNATURE_SIZE]); + +/// Re-parses a secret key into the `musig2` crate's `secp256k1` version. +/// +/// Both crates validate the same 32-byte range, so a key valid in one is valid +/// in the other. +fn musig_seckey(funding_privkey: &SecretKey) -> musig2::secp256k1::SecretKey { + musig2::secp256k1::SecretKey::from_byte_array(funding_privkey.secret_bytes()) + .expect("a valid secp256k1 secret key is valid in either crate version") +} + +/// The `MuSig2` signing context for a channel's funding output. +/// +/// Holds the key aggregation context built from both `funding_pubkey`s, sorted +/// with `KeySort` and tweaked per BIP 86 as the taproot channel spec requires. +pub struct FundingKeys { + ctx: KeyAggContext, +} + +/// Derives a secret nonce deterministically from `context` and the signer's +/// `funding_pubkey`. +/// +/// # Panics +/// +/// Panics if `funding_pubkey` does not round-trip between the two `secp256k1` +/// versions, which cannot happen for a valid public key. +#[must_use] +pub fn derive_nonce(context: &[&[u8]], funding_pubkey: &PublicKey) -> SecretNonce { + let pubkey = MusigPublicKey::from_slice(&funding_pubkey.serialize()) + .expect("a valid secp256k1 pubkey is valid in either crate version"); + + let builder = context.iter().fold( + SecNonce::build_with_pubkey([0u8; 32], pubkey), + SecNonceBuilder::with_extra_input, + ); + + SecretNonce(builder.build()) +} + +/// Returns whether a nonce received from the peer is two valid compressed +/// secp256k1 points. +/// +/// The wire layer accepts any 66 bytes so that a malformed nonce reaches the +/// oracles instead of failing to decode; this is the check the spec requires +/// before the nonce is used. +#[must_use] +pub fn is_valid_public_nonce(nonce: &PublicNonce) -> bool { + PubNonce::from_bytes(nonce.as_bytes()).is_ok() +} + +/// Converts a wire nonce to the `musig2` crate's representation. +fn to_pub_nonce(nonce: &PublicNonce) -> Result { + PubNonce::from_bytes(nonce.as_bytes()).map_err(|_| MusigError::InvalidPublicNonce) +} + +impl SecretNonce { + /// Returns the matching public nonce, safe to send to the peer. + #[must_use] + pub fn public_nonce(&self) -> PublicNonce { + PublicNonce(self.0.public_nonce().to_bytes()) + } +} + +impl FundingKeys { + /// Builds the funding signing context from both `funding_pubkey`s. + /// + /// The keys are sorted with `KeySort` and aggregated with `KeyAgg`, then + /// tweaked with the BIP 86 unspendable-script-path tweak. The resulting key + /// is what the funding output pays to, so the argument order does not + /// matter. + /// + /// # Errors + /// + /// Returns [`MusigError::KeyAggregation`] or [`MusigError::Tweak`] if the + /// aggregate key cannot be formed, which cannot happen for two valid + /// distinct public keys. + pub fn new(pubkey1: &PublicKey, pubkey2: &PublicKey) -> Result { + // `KeySort` from BIP 327: lexicographic over the 33-byte compressed + // encodings. Sorting here is what lets both peers derive the same + // aggregate key without exchanging ordering information. + let mut serialized = [pubkey1.serialize(), pubkey2.serialize()]; + serialized.sort_unstable(); + + let keys: Vec = serialized + .iter() + .map(|bytes| { + MusigPublicKey::from_slice(bytes) + .map_err(|e| MusigError::KeyAggregation(e.to_string())) + }) + .collect::>()?; + + let ctx = KeyAggContext::new(keys) + .map_err(|e| MusigError::KeyAggregation(e.to_string()))? + .with_unspendable_taproot_tweak() + .map_err(|e| MusigError::Tweak(e.to_string()))?; + + Ok(Self { ctx }) + } + + /// Returns the tweaked aggregate key the funding output pays to. + /// + /// # Panics + /// + /// Panics if the aggregate key does not round-trip between the two + /// `secp256k1` versions, which cannot happen for a key `KeyAgg` produced. + #[must_use] + pub fn aggregate_pubkey(&self) -> XOnlyPublicKey { + let aggregate: MusigPublicKey = self.ctx.aggregated_pubkey(); + XOnlyPublicKey::from_slice(&aggregate.x_only_public_key().0.serialize()) + .expect("musig2 aggregate key is a valid x-only pubkey") + } + + /// Produces our partial signature over `sighash` for the counterparty's + /// commitment. + /// + /// `our_nonce` is the fresh signing nonce sent alongside the signature; + /// `their_nonce` is the peer's verification nonce from `open_channel` or + /// `accept_channel`. + /// + /// # Errors + /// + /// Returns [`MusigError::InvalidPublicNonce`] if either nonce cannot be + /// parsed, or [`MusigError::Signing`] if signing fails. + pub fn partial_sign( + &self, + sighash: &[u8; 32], + funding_privkey: &SecretKey, + our_nonce: SecretNonce, + their_nonce: &PublicNonce, + ) -> Result { + let aggregated_nonce = aggregate_nonce(&our_nonce.public_nonce(), their_nonce)?; + + let signature: musig2::PartialSignature = musig2::sign_partial( + &self.ctx, + musig_seckey(funding_privkey), + our_nonce.0, + &aggregated_nonce, + sighash, + ) + .map_err(|e| MusigError::Signing(e.to_string()))?; + + Ok(PartialSignature(signature.serialize())) + } + + /// Returns whether the peer's partial signature over `sighash` is valid. + /// + /// `their_nonce` is the signing nonce the peer sent alongside the + /// signature; `our_nonce` is the verification nonce we sent earlier. + /// + /// # Errors + /// + /// Returns [`MusigError::InvalidPublicNonce`] if either nonce cannot be + /// parsed. An otherwise well-formed but incorrect signature yields + /// `Ok(false)` rather than an error. + pub fn verify_partial( + &self, + sighash: &[u8; 32], + signature: &PartialSignature, + their_pubkey: &PublicKey, + their_nonce: &PublicNonce, + our_nonce: &PublicNonce, + ) -> Result { + let aggregated_nonce = aggregate_nonce(our_nonce, their_nonce)?; + let Ok(signature) = musig2::PartialSignature::from_slice(&signature.0) else { + return Ok(false); + }; + let pubkey = MusigPublicKey::from_slice(&their_pubkey.serialize()) + .map_err(|e| MusigError::KeyAggregation(e.to_string()))?; + + Ok(musig2::verify_partial( + &self.ctx, + signature, + &aggregated_nonce, + pubkey, + &to_pub_nonce(their_nonce)?, + sighash, + ) + .is_ok()) + } +} + +/// Combines both public nonces with `NonceAgg`. Point addition is commutative, +/// so the argument order does not matter. +fn aggregate_nonce(nonce1: &PublicNonce, nonce2: &PublicNonce) -> Result { + Ok(AggNonce::sum([ + to_pub_nonce(nonce1)?, + to_pub_nonce(nonce2)?, + ])) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pubkey(hex_str: &str) -> PublicKey { + let bytes = hex::decode(hex_str).expect("valid hex"); + PublicKey::from_slice(&bytes).expect("valid pubkey") + } + + fn secret(hex_str: &str) -> SecretKey { + let bytes = hex::decode(hex_str).expect("valid hex"); + SecretKey::from_slice(&bytes).expect("valid secret key") + } + + // Test vectors from the simple taproot channels spec: + // bolt-simple-taproot.md, "Test Vectors" appendix. + + /// The spec's `funding` vector: the aggregate of the two `funding_pubkey`s, + /// sorted and BIP 86 tweaked, is the key the funding output pays to. + #[test] + fn aggregate_funding_key_matches_spec_vector() { + let local = pubkey("03b7203dec7c13896b6ff1f58b24f84458c441720a12b5a57426397e22f0a8c78b"); + let remote = pubkey("02956e6845a6f346f97c5e028c0f8ab38a76b0124fd7184deab60f682b3e657fdb"); + + let keys = FundingKeys::new(&local, &remote).expect("valid funding pubkeys"); + + assert_eq!( + hex::encode(keys.aggregate_pubkey().serialize()), + "d0ebb4909d563a7ae1213fddede4ae54132fba0ef0b97ee3f8469191fecd348e" + ); + } + + /// `KeySort` runs before `KeyAgg`, so the caller's argument order cannot + /// change the funding output. + #[test] + fn aggregate_funding_key_is_argument_order_independent() { + let local = pubkey("03b7203dec7c13896b6ff1f58b24f84458c441720a12b5a57426397e22f0a8c78b"); + let remote = pubkey("02956e6845a6f346f97c5e028c0f8ab38a76b0124fd7184deab60f682b3e657fdb"); + + let one = FundingKeys::new(&local, &remote).expect("valid funding pubkeys"); + let other = FundingKeys::new(&remote, &local).expect("valid funding pubkeys"); + + assert_eq!(one.aggregate_pubkey(), other.aggregate_pubkey()); + } + + /// A partial signature produced by one side verifies against the other + /// side's view of the same session. + #[test] + fn partial_signature_round_trip() { + let local_sk = secret("20ae2d254ab29afd3dcbf8744a5b88d06070f55a4bd5532483a093ac4db91277"); + let local = pubkey("03b7203dec7c13896b6ff1f58b24f84458c441720a12b5a57426397e22f0a8c78b"); + let remote = pubkey("02956e6845a6f346f97c5e028c0f8ab38a76b0124fd7184deab60f682b3e657fdb"); + + let keys = FundingKeys::new(&local, &remote).expect("valid funding pubkeys"); + let sighash = [0x42u8; 32]; + + // The verifier's nonce is sent first, in `open_channel`. + let verification = derive_nonce(&[b"verification"], &remote); + // The signer's nonce accompanies the signature in `funding_created`. + let signing = derive_nonce(&[b"signing"], &local); + let signing_public = signing.public_nonce(); + + let signature = keys + .partial_sign(&sighash, &local_sk, signing, &verification.public_nonce()) + .expect("signing succeeds"); + + assert!( + keys.verify_partial( + &sighash, + &signature, + &local, + &signing_public, + &verification.public_nonce(), + ) + .expect("nonces parse") + ); + } + + /// A signature over a different commitment must not verify, otherwise + /// `funding_signed` verification would accept anything. + #[test] + fn partial_signature_over_wrong_sighash_is_rejected() { + let local_sk = secret("20ae2d254ab29afd3dcbf8744a5b88d06070f55a4bd5532483a093ac4db91277"); + let local = pubkey("03b7203dec7c13896b6ff1f58b24f84458c441720a12b5a57426397e22f0a8c78b"); + let remote = pubkey("02956e6845a6f346f97c5e028c0f8ab38a76b0124fd7184deab60f682b3e657fdb"); + + let keys = FundingKeys::new(&local, &remote).expect("valid funding pubkeys"); + + let verification = derive_nonce(&[b"verification"], &remote); + let signing = derive_nonce(&[b"signing"], &local); + let signing_public = signing.public_nonce(); + + let signature = keys + .partial_sign( + &[0x42u8; 32], + &local_sk, + signing, + &verification.public_nonce(), + ) + .expect("signing succeeds"); + + assert!( + !keys + .verify_partial( + &[0x43u8; 32], + &signature, + &local, + &signing_public, + &verification.public_nonce(), + ) + .expect("nonces parse") + ); + } + + /// Peer nonces arrive as untrusted bytes; the spec requires failing the + /// channel when they are not two compressed points. + #[test] + fn malformed_public_nonce_is_not_valid() { + assert!(!is_valid_public_nonce(&PublicNonce( + [0x00; crate::bolt::PUBLIC_NONCE_SIZE] + ))); + } + + #[test] + fn generated_public_nonce_is_valid() { + let key = pubkey("03b7203dec7c13896b6ff1f58b24f84458c441720a12b5a57426397e22f0a8c78b"); + + assert!(is_valid_public_nonce( + &derive_nonce(&[b"ctx"], &key).public_nonce() + )); + } +} diff --git a/smite/src/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index 3ab16320..545e70af 100644 --- a/smite/src/oracles/accept_channel.rs +++ b/smite/src/oracles/accept_channel.rs @@ -1,8 +1,9 @@ //! BOLT 2 `accept_channel` oracle, for the v1 outbound channel funding flow. use super::Oracle; -use crate::bolt::{AcceptChannel, OpenChannel}; +use crate::bolt::{AcceptChannel, Features, OpenChannel, PublicNonce}; use crate::channel_tx::CommitmentCost; +use crate::musig::is_valid_public_nonce; use crate::pending_channel::PendingChannel; use crate::violation::Violation; @@ -13,6 +14,9 @@ use bitcoin::Amount; const MAX_ACCEPTED_HTLCS_LIMIT: u16 = 483; const MIN_DUST_LIMIT_SATOSHIS: u64 = 354; +/// `announce_channel` bit of `open_channel.channel_flags` (BOLT 2). +const ANNOUNCE_CHANNEL_FLAG: u8 = 0b0000_0001; + /// Context for `AcceptChannelOracle` pub struct AcceptChannelContext<'a> { /// The `accept_channel` received from the peer. @@ -105,7 +109,12 @@ fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String // Check that the channel type was included. // TODO: Check option_channel_type in negotiated features since it is // assumed to be supported. - let Some(channel_type) = open_channel.tlvs.channel_type.as_deref() else { + let Some(channel_type) = open_channel + .tlvs + .channel_type + .as_deref() + .map(Features::from) + else { return Err("open_channel does not include a channel_type".to_string()); }; @@ -128,14 +137,48 @@ fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String )); } + // Simple taproot channels cannot be gossiped and need a `MuSig2` nonce for + // the acceptor to sign the first commitment against, so a target accepting + // an `open_channel` without them has skipped a required check. + if channel_type.supports_feature(Features::OPTION_SIMPLE_TAPROOT) { + if open_channel.channel_flags & ANNOUNCE_CHANNEL_FLAG != 0 { + return Err( + "open_channel announces a simple taproot channel, which cannot be gossiped" + .to_string(), + ); + } + verify_next_local_nonce("open_channel", open_channel.tlvs.next_local_nonce)?; + } + // Check the initial commitment satisfies the channel reserve. verify_initial_commitment( open_channel, - channel_type, + &channel_type, open_channel.channel_reserve_satoshis, ) } +/// Verifies that a simple taproot channel message carries a usable `MuSig2` +/// verification nonce. +fn verify_next_local_nonce( + message: &str, + next_local_nonce: Option, +) -> Result<(), String> { + let Some(nonce) = next_local_nonce else { + return Err(format!( + "{message} negotiates a simple taproot channel but omits next_local_nonce" + )); + }; + + if !is_valid_public_nonce(&nonce) { + return Err(format!( + "{message} next_local_nonce is not two valid compressed secp256k1 points" + )); + } + + Ok(()) +} + /// Verifies the `accept_channel` against the BOLT 2 requirements it must meet, /// returning an error if it breaches one, or `Ok(())` if it meets them all. /// @@ -150,7 +193,12 @@ fn verify_accept_channel( open_channel: &OpenChannel, ) -> Result<(), String> { // Check that the channel type was included. - let Some(channel_type) = accept_channel.tlvs.channel_type.as_deref() else { + let Some(channel_type) = accept_channel + .tlvs + .channel_type + .as_deref() + .map(Features::from) + else { return Err("accept_channel does not include a channel_type".to_string()); }; @@ -159,6 +207,11 @@ fn verify_accept_channel( return Err("accept_channel channel_type does not match open_channel".to_string()); } + // The acceptor must publish the nonce we sign its commitment against. + if channel_type.supports_feature(Features::OPTION_SIMPLE_TAPROOT) { + verify_next_local_nonce("accept_channel", accept_channel.tlvs.next_local_nonce)?; + } + // Check the acceptor's channel reserve covers the opener's dust limit. if accept_channel.channel_reserve_satoshis < open_channel.dust_limit_satoshis { return Err(format!( @@ -197,7 +250,7 @@ fn verify_accept_channel( // Check the initial commitment satisfies the channel reserve. verify_initial_commitment( open_channel, - channel_type, + &channel_type, accept_channel.channel_reserve_satoshis, ) } @@ -206,20 +259,17 @@ fn verify_accept_channel( /// channel reserve requirement, returning an error if it breaches either, or /// `Ok(())` if both are met. /// -/// NOTE: This check is safe from false positives for `zero_fee_commitments` -/// and `option_simple_taproot`, although the reported error may be misleading: +/// NOTE: This check is safe from false positives for `zero_fee_commitments`, +/// although the reported error may be misleading: /// /// - `zero_fee_commitments` requires `feerate_per_kw == 0`, which we currently /// do not enforce. A non-zero feerate may cause the error to be reported here /// even though it is invalid for this channel type. -/// - `option_simple_taproot` has a different commitment fee (968-byte weight), -/// but we calculate it using the lower 724-byte weight. This may allow some -/// invalid cases through, but cannot cause a false positive. -/// - Anchor costs are only included when `option_anchors` is negotiated, so -/// they are not unnecessarily subtracted for these channel types. +/// - Anchor costs are only included when the channel type carries anchor +/// outputs, so they are not unnecessarily subtracted for other types. fn verify_initial_commitment( open_channel: &OpenChannel, - channel_type: &[u8], + channel_type: &Features, channel_reserve_satoshis: u64, ) -> Result<(), String> { // Check that the opener can afford the proposed feerate. @@ -286,8 +336,8 @@ mod tests { first_per_commitment_point: key, channel_flags: 1, tlvs: OpenChannelTlvs { - upfront_shutdown_script: None, channel_type: Some(vec![0x10, 0x00]), + ..Default::default() }, } } @@ -311,6 +361,7 @@ mod tests { htlc_basepoint: key, first_per_commitment_point: key, tlvs: AcceptChannelTlvs { + next_local_nonce: None, upfront_shutdown_script: None, channel_type: Some(vec![0x10, 0x00]), }, @@ -577,4 +628,86 @@ mod tests { assert_pass(&accept_channel(), Some(&negotiation)); } + + /// A `channel_type` negotiating `option_simple_taproot`, as wire bytes. + fn simple_taproot_channel_type() -> Vec { + Features::from_bits(&[Features::OPTION_SIMPLE_TAPROOT]).into_bytes() + } + + /// A well-formed taproot negotiation: both sides carry a valid nonce and + /// the channel is unannounced. + fn taproot_negotiation() -> (OpenChannel, AcceptChannel) { + let nonce = crate::musig::derive_nonce(&[b"test"], &pubkey(1)).public_nonce(); + + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(simple_taproot_channel_type()); + oc.tlvs.next_local_nonce = Some(nonce); + oc.channel_flags = 0; + // Taproot commitments are heavier, so keep the feerate affordable. + oc.feerate_per_kw = 2_500; + + let mut ac = accept_channel(); + ac.tlvs.channel_type = Some(simple_taproot_channel_type()); + ac.tlvs.next_local_nonce = Some(nonce); + + (oc, ac) + } + + #[test] + fn conforming_taproot_negotiation_passes() { + let (oc, ac) = taproot_negotiation(); + assert_pass(&ac, Some(&pending_negotiation(oc))); + } + + #[test] + fn taproot_open_channel_without_a_nonce() { + let (mut oc, ac) = taproot_negotiation(); + oc.tlvs.next_local_nonce = None; + assert_fail( + &ac, + Some(&pending_negotiation(oc)), + "open_channel negotiates a simple taproot channel but omits next_local_nonce", + ); + } + + #[test] + fn taproot_open_channel_with_a_malformed_nonce() { + let (mut oc, ac) = taproot_negotiation(); + oc.tlvs.next_local_nonce = Some(PublicNonce([0x00; crate::bolt::PUBLIC_NONCE_SIZE])); + assert_fail( + &ac, + Some(&pending_negotiation(oc)), + "open_channel next_local_nonce is not two valid compressed secp256k1 points", + ); + } + + /// Taproot channels cannot be gossiped, so a target must reject an + /// `open_channel` that sets `announce_channel`. + #[test] + fn taproot_open_channel_that_announces_the_channel() { + let (mut oc, ac) = taproot_negotiation(); + oc.channel_flags |= ANNOUNCE_CHANNEL_FLAG; + assert_fail(&ac, Some(&pending_negotiation(oc)), "cannot be gossiped"); + } + + #[test] + fn taproot_accept_channel_without_a_nonce() { + let (oc, mut ac) = taproot_negotiation(); + ac.tlvs.next_local_nonce = None; + assert_fail( + &ac, + Some(&pending_negotiation(oc)), + "accept_channel negotiates a simple taproot channel but omits next_local_nonce", + ); + } + + /// A non-taproot channel must not be required to carry a nonce. + #[test] + fn non_taproot_negotiation_does_not_require_a_nonce() { + let oc = open_channel(); + let ac = accept_channel(); + assert!(oc.tlvs.next_local_nonce.is_none()); + assert!(ac.tlvs.next_local_nonce.is_none()); + assert_pass(&ac, Some(&pending_negotiation(oc))); + } }