diff --git a/smite/src/bitcoin.rs b/smite/src/bitcoin.rs index 250409aa..926b08c1 100644 --- a/smite/src/bitcoin.rs +++ b/smite/src/bitcoin.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use std::process::Command; use std::str::FromStr; -use bitcoin::consensus::encode::serialize_hex; +use bitcoin::consensus::encode::{deserialize, serialize_hex}; use bitcoin::{Address, Amount, Network, OutPoint, ScriptBuf, Transaction, Txid}; use serde::{Deserialize, Serialize}; @@ -49,6 +49,15 @@ pub struct TxBlockPosition { pub tx_index: u32, } +/// Parsed response from `signrawtransactionwithwallet `. +#[derive(Deserialize)] +struct SignRawTransactionResponse { + /// Consensus-serialized transaction with every signable input signed. + hex: String, + /// Whether every input now has a complete signature set. + complete: bool, +} + /// Parsed response from `getrawtransaction 1`. #[derive(Deserialize)] struct RawTransactionInfo { @@ -58,6 +67,8 @@ struct RawTransactionInfo { confirmations: u32, /// Omitted while the transaction is unconfirmed (in the mempool). blockhash: Option, + /// Consensus-serialized transaction, always present. + hex: String, } /// Connection info for invoking `bitcoin-cli` against the regtest `bitcoind` @@ -271,6 +282,50 @@ impl BitcoinCli { .expect("getnewaddress should return a valid address") } + /// Signs the wallet-owned inputs of `tx`, returning the partially or fully + /// signed transaction, or `None` if the node does not know how to sign any + /// of them. + /// + /// # Panics + /// + /// - If `bitcoin-cli signrawtransactionwithwallet` fails to execute. + /// - If the command succeeds but its output is not valid JSON, or its `hex` + /// field does not decode as a transaction. + #[must_use] + pub fn sign_tx(&self, tx: &Transaction) -> Option { + let signed = self + .sign_raw_transaction_with_wallet(tx) + .inspect_err(|stderr| { + log::debug!("bitcoin-cli signrawtransactionwithwallet failed: {stderr}"); + }) + .ok()?; + Some( + deserialize(&hex::decode(&signed.hex).expect("signing should return valid hex")) + .expect("signing should return a valid transaction"), + ) + } + + /// Runs `signrawtransactionwithwallet`, returning the raw response, or the + /// command's stderr if it exits non-zero. + fn sign_raw_transaction_with_wallet( + &self, + tx: &Transaction, + ) -> Result { + let signed_out = self + .run() + .arg("signrawtransactionwithwallet") + .arg(serialize_hex(tx)) + .output() + .expect("bitcoin-cli signrawtransactionwithwallet should not fail"); + + if !signed_out.status.success() { + return Err(String::from_utf8_lossy(&signed_out.stderr).into_owned()); + } + + Ok(serde_json::from_slice(&signed_out.stdout) + .expect("signrawtransactionwithwallet should return valid JSON")) + } + /// Signs and broadcasts a transaction, unless it is already confirmed. /// /// If the signed transaction is accepted by the mempool, it is broadcast @@ -278,29 +333,22 @@ impl BitcoinCli { /// the minimum relay feerate or creates a dust output), it is returned /// instead so the caller can mine it later, bypassing mempool policy. /// - /// Returns `None` if the transaction was already confirmed or was broadcast - /// successfully, or hex-encoded raw transaction if it was rejected by the - /// mempool. + /// Returns `None` if the transaction was already confirmed, could not be + /// fully signed, or was broadcast successfully; or the hex-encoded raw + /// transaction if it was rejected by the mempool. /// /// # Panics /// /// - If `bitcoin-cli signrawtransactionwithwallet` fails to execute or /// exits non-zero. /// - If the sign output is not valid JSON. - /// - If signing returns `complete=false`. /// - If `bitcoin-cli sendrawtransaction` fails to execute. - /// - If the broadcast is rejected for any reason other than a below-dust - /// output or a below-minimum relay feerate. + /// - If the broadcast is rejected for a reason other than a known mempool + /// policy rule or the transaction already being known to the node. /// - If a successful broadcast does not return a valid UTF-8 txid. /// - If the broadcasted txid does not match the given transaction's txid. #[must_use] pub fn sign_and_broadcast_tx(&self, tx: &Transaction) -> Option { - #[derive(Deserialize)] - struct SignRawTransactionResponse { - hex: String, - complete: bool, - } - // A confirmed transaction may be broadcast again by the fuzzer. Its // inputs are spent, so the wallet can no longer fully sign it, skip // signing and broadcasting it again. @@ -309,26 +357,18 @@ impl BitcoinCli { return None; } - let tx_hex = serialize_hex(tx); + let signed_tx = self + .sign_raw_transaction_with_wallet(tx) + .unwrap_or_else(|stderr| { + panic!("bitcoin-cli signrawtransactionwithwallet failed: {stderr}") + }); - let signed_out = self - .run() - .arg("signrawtransactionwithwallet") - .arg(&tx_hex) - .output() - .expect("bitcoin-cli signrawtransactionwithwallet should not fail"); - assert!( - signed_out.status.success(), - "bitcoin-cli signrawtransactionwithwallet failed: {}", - String::from_utf8_lossy(&signed_out.stderr) - ); - - let signed_tx: SignRawTransactionResponse = serde_json::from_slice(&signed_out.stdout) - .expect("signrawtransactionwithwallet should return valid JSON"); - assert!( - signed_tx.complete, - "signrawtransactionwithwallet returned complete=false" - ); + if !signed_tx.complete { + log::debug!( + "signrawtransactionwithwallet could not fully sign {txid}, not broadcasting" + ); + return None; + } let broadcast_out = self .run() @@ -347,6 +387,24 @@ impl BitcoinCli { if stderr.contains("tx with dust output") || stderr.contains("min relay fee not met") { return Some(signed_tx.hex); } + // The transaction may already be known to the node: the fuzzer can + // broadcast the same transaction twice before mining, and in + // channel establishment v2 the peer broadcasts the funding + // transaction as well. Either way it is already where we want it, + // nothing to mine privately. + if stderr.contains("txn-already-in-mempool") + || stderr.contains("txn-already-known") + || stderr.contains("Transaction already in block chain") + { + return None; + } + // A channel establishment v2 funding transaction takes its + // `nLockTime` from `open_channel2.locktime`, which the fuzzer picks + // freely, so it is routinely in the future. + if stderr.contains("non-final") { + log::debug!("{txid} is not final yet, not broadcasting"); + return None; + } panic!("bitcoin-cli sendrawtransaction failed: {stderr}"); } @@ -453,6 +511,20 @@ impl BitcoinCli { .map_or(0, |info| info.confirmations) } + /// Returns the consensus-serialized transaction with the given txid, or + /// `None` if it is unknown to the node. + /// + /// # Panics + /// + /// - If the `bitcoin-cli getrawtransaction` command fails to execute. + /// - If the command succeeds but its output is not valid JSON or its `hex` + /// field is not valid hex. + #[must_use] + pub fn get_raw_transaction(&self, txid: Txid) -> Option> { + let info = self.get_raw_transaction_info(txid)?; + Some(hex::decode(&info.hex).expect("getrawtransaction should return valid hex")) + } + /// Returns the position of the confirmed transaction with the given txid, /// or `None` if it is unconfirmed (in the mempool) or unknown to the node /// (e.g. not broadcast yet). diff --git a/smite/src/bolt.rs b/smite/src/bolt.rs index 5572124f..c3e15514 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -30,10 +30,12 @@ mod tlv; mod tx_abort; mod tx_ack_rbf; mod tx_add_input; +mod tx_add_output; mod tx_complete; mod tx_init_rbf; mod tx_remove_input; mod tx_remove_output; +mod tx_signatures; mod types; mod update_add_htlc; mod update_fail_htlc; @@ -69,10 +71,12 @@ pub use tlv::{TlvRecord, TlvStream}; pub use tx_abort::TxAbort; pub use tx_ack_rbf::{TxAckRbf, TxAckRbfTlvs}; pub use tx_add_input::{TxAddInput, TxAddInputTlvs}; +pub use tx_add_output::TxAddOutput; pub use tx_complete::TxComplete; pub use tx_init_rbf::{TxInitRbf, TxInitRbfTlvs}; pub use tx_remove_input::TxRemoveInput; pub use tx_remove_output::TxRemoveOutput; +pub use tx_signatures::{TxSignatures, TxSignaturesTlvs}; 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, @@ -175,12 +179,16 @@ impl MessageType { pub const ACCEPT_CHANNEL2: MessageType = MessageType(65); /// `tx_add_input` message (BOLT 2). pub const TX_ADD_INPUT: MessageType = MessageType(66); + /// `tx_add_output` message (BOLT 2). + pub const TX_ADD_OUTPUT: MessageType = MessageType(67); /// `tx_remove_input` message (BOLT 2). pub const TX_REMOVE_INPUT: MessageType = MessageType(68); /// `tx_remove_output` message (BOLT 2). pub const TX_REMOVE_OUTPUT: MessageType = MessageType(69); /// `tx_complete` message (BOLT 2). pub const TX_COMPLETE: MessageType = MessageType(70); + /// `tx_signatures` message (BOLT 2). + pub const TX_SIGNATURES: MessageType = MessageType(71); /// `tx_init_rbf` message (BOLT 2). pub const TX_INIT_RBF: MessageType = MessageType(72); /// `tx_ack_rbf` message (BOLT 2). @@ -243,9 +251,11 @@ impl MessageType { Self::OPEN_CHANNEL2 => "open_channel2", Self::ACCEPT_CHANNEL2 => "accept_channel2", Self::TX_ADD_INPUT => "tx_add_input", + Self::TX_ADD_OUTPUT => "tx_add_output", Self::TX_REMOVE_INPUT => "tx_remove_input", Self::TX_REMOVE_OUTPUT => "tx_remove_output", Self::TX_COMPLETE => "tx_complete", + Self::TX_SIGNATURES => "tx_signatures", Self::TX_INIT_RBF => "tx_init_rbf", Self::TX_ACK_RBF => "tx_ack_rbf", Self::TX_ABORT => "tx_abort", @@ -307,12 +317,16 @@ pub enum Message { AcceptChannel2(AcceptChannel2), /// `tx_add_input` message (type 66). TxAddInput(TxAddInput), + /// `tx_add_output` message (type 67). + TxAddOutput(TxAddOutput), /// `tx_remove_input` message (type 68). TxRemoveInput(TxRemoveInput), /// `tx_remove_output` message (type 69). TxRemoveOutput(TxRemoveOutput), /// `tx_complete` message (type 70). TxComplete(TxComplete), + /// `tx_signatures` message (type 71). + TxSignatures(TxSignatures), /// `tx_init_rbf` message (type 72). TxInitRbf(TxInitRbf), /// `tx_ack_rbf` message (type 73). @@ -380,9 +394,11 @@ impl Message { Self::OpenChannel2(_) => MessageType::OPEN_CHANNEL2, Self::AcceptChannel2(_) => MessageType::ACCEPT_CHANNEL2, Self::TxAddInput(_) => MessageType::TX_ADD_INPUT, + Self::TxAddOutput(_) => MessageType::TX_ADD_OUTPUT, Self::TxRemoveInput(_) => MessageType::TX_REMOVE_INPUT, Self::TxRemoveOutput(_) => MessageType::TX_REMOVE_OUTPUT, Self::TxComplete(_) => MessageType::TX_COMPLETE, + Self::TxSignatures(_) => MessageType::TX_SIGNATURES, Self::TxInitRbf(_) => MessageType::TX_INIT_RBF, Self::TxAckRbf(_) => MessageType::TX_ACK_RBF, Self::TxAbort(_) => MessageType::TX_ABORT, @@ -423,9 +439,11 @@ impl Message { Self::OpenChannel2(m) => out.extend(m.encode()), Self::AcceptChannel2(m) => out.extend(m.encode()), Self::TxAddInput(m) => out.extend(m.encode()), + Self::TxAddOutput(m) => out.extend(m.encode()), Self::TxRemoveInput(m) => out.extend(m.encode()), Self::TxRemoveOutput(m) => out.extend(m.encode()), Self::TxComplete(m) => out.extend(m.encode()), + Self::TxSignatures(m) => out.extend(m.encode()), Self::TxInitRbf(m) => out.extend(m.encode()), Self::TxAckRbf(m) => out.extend(m.encode()), Self::TxAbort(m) => out.extend(m.encode()), @@ -479,11 +497,13 @@ impl Message { Ok(Self::AcceptChannel2(AcceptChannel2::decode(cursor)?)) } MessageType::TX_ADD_INPUT => Ok(Self::TxAddInput(TxAddInput::decode(cursor)?)), + MessageType::TX_ADD_OUTPUT => Ok(Self::TxAddOutput(TxAddOutput::decode(cursor)?)), MessageType::TX_REMOVE_INPUT => Ok(Self::TxRemoveInput(TxRemoveInput::decode(cursor)?)), MessageType::TX_REMOVE_OUTPUT => { Ok(Self::TxRemoveOutput(TxRemoveOutput::decode(cursor)?)) } MessageType::TX_COMPLETE => Ok(Self::TxComplete(TxComplete::decode(cursor)?)), + MessageType::TX_SIGNATURES => Ok(Self::TxSignatures(TxSignatures::decode(cursor)?)), MessageType::TX_INIT_RBF => Ok(Self::TxInitRbf(TxInitRbf::decode(cursor)?)), MessageType::TX_ACK_RBF => Ok(Self::TxAckRbf(TxAckRbf::decode(cursor)?)), MessageType::TX_ABORT => Ok(Self::TxAbort(TxAbort::decode(cursor)?)), @@ -900,6 +920,25 @@ mod tests { assert_eq!(decoded, Message::TxAddInput(tx_add_input)); } + /// Valid `TxAddOutput` message for testing. + fn sample_tx_add_output() -> TxAddOutput { + TxAddOutput { + channel_id: ChannelId::new([0xab; CHANNEL_ID_SIZE]), + serial_id: 30, + sats: 49_999_845, + script: vec![0x00, 0x14, 0x1c, 0xa1], + } + } + + #[test] + fn message_tx_add_output_roundtrip() { + let tx_add_output = sample_tx_add_output(); + let msg = Message::TxAddOutput(tx_add_output.clone()); + let encoded = msg.encode(); + let decoded = Message::decode(&encoded).unwrap(); + assert_eq!(decoded, Message::TxAddOutput(tx_add_output)); + } + #[test] fn message_tx_remove_input_roundtrip() { let tx_remove_input = TxRemoveInput { @@ -935,6 +974,25 @@ mod tests { assert_eq!(decoded, Message::TxComplete(tx_complete)); } + /// Valid `TxSignatures` message for testing. + fn sample_tx_signatures() -> TxSignatures { + TxSignatures { + channel_id: ChannelId::new([0xab; CHANNEL_ID_SIZE]), + txid: Txid::from_byte_array([0xcd; TXID_SIZE]), + witnesses: vec![vec![0xde, 0xad, 0xbe, 0xef], vec![0x01, 0x02]], + tlvs: TxSignaturesTlvs::default(), + } + } + + #[test] + fn message_tx_signatures_roundtrip() { + let tx_signatures = sample_tx_signatures(); + let msg = Message::TxSignatures(tx_signatures.clone()); + let encoded = msg.encode(); + let decoded = Message::decode(&encoded).unwrap(); + assert_eq!(decoded, Message::TxSignatures(tx_signatures)); + } + #[test] fn message_tx_init_rbf_roundtrip() { let tx_init_rbf = TxInitRbf { @@ -1288,6 +1346,11 @@ mod tests { "tx_add_input", MessageType::TX_ADD_INPUT, ), + ( + Message::TxAddOutput(sample_tx_add_output()), + "tx_add_output", + MessageType::TX_ADD_OUTPUT, + ), ( Message::TxRemoveInput(TxRemoveInput { channel_id: ChannelId::new([0; CHANNEL_ID_SIZE]), @@ -1311,6 +1374,11 @@ mod tests { "tx_complete", MessageType::TX_COMPLETE, ), + ( + Message::TxSignatures(sample_tx_signatures()), + "tx_signatures", + MessageType::TX_SIGNATURES, + ), ( Message::TxInitRbf(TxInitRbf { channel_id: ChannelId::new([0; CHANNEL_ID_SIZE]), diff --git a/smite/src/bolt/tx_add_output.rs b/smite/src/bolt/tx_add_output.rs new file mode 100644 index 00000000..3b96950b --- /dev/null +++ b/smite/src/bolt/tx_add_output.rs @@ -0,0 +1,203 @@ +//! BOLT 2 `tx_add_output` message. + +use super::BoltError; +use super::types::ChannelId; +use super::wire::WireFormat; + +/// BOLT 2 `tx_add_output` message (type 67). +/// +/// Sent during interactive transaction construction to propose adding an +/// output to the shared transaction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TxAddOutput { + /// The channel ID. + pub channel_id: ChannelId, + /// Serial ID for this output. Must be even if sent by the initiator, + /// odd if sent by the non-initiator (BOLT 2 parity rule). + pub serial_id: u64, + /// The value of this output in satoshis. + pub sats: u64, + /// The `scriptPubKey` for the output. + pub script: Vec, +} + +impl TxAddOutput { + /// Encodes to wire format (without message type prefix). + #[must_use] + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + self.channel_id.write(&mut out); + self.serial_id.write(&mut out); + self.sats.write(&mut out); + self.script.write(&mut out); + out + } + + /// Decodes from wire format (without message type prefix). + /// + /// # Errors + /// + /// Returns `Truncated` if the payload is too short for any field. + pub fn decode(payload: &[u8]) -> Result { + let mut cursor = payload; + let channel_id = ChannelId::read(&mut cursor)?; + let serial_id = u64::read(&mut cursor)?; + let sats = u64::read(&mut cursor)?; + let script = Vec::::read(&mut cursor)?; + + Ok(Self { + channel_id, + serial_id, + sats, + script, + }) + } +} + +#[cfg(test)] +mod tests { + use super::super::CHANNEL_ID_SIZE; + use super::*; + + /// P2WPKH `scriptPubKey` from the BOLT 3 dual-funding test vectors. + const P2WPKH_SCRIPT: [u8; 22] = [ + 0x00, 0x14, 0x1c, 0xa1, 0xcc, 0xa8, 0x85, 0x5b, 0xad, 0x6b, 0xc1, 0xea, 0x54, 0x36, 0xed, + 0xd8, 0xcf, 0xf1, 0x0b, 0x7e, 0x44, 0x8b, + ]; + + fn sample_msg() -> TxAddOutput { + TxAddOutput { + channel_id: ChannelId::new([0xab; CHANNEL_ID_SIZE]), + serial_id: 30, + sats: 49_999_845, + script: P2WPKH_SCRIPT.to_vec(), + } + } + + #[test] + fn encode_field_sizes() { + let encoded = sample_msg().encode(); + // channel_id(32) + serial_id(8) + sats(8) + scriptlen(2) + script(22) + assert_eq!(encoded.len(), CHANNEL_ID_SIZE + 8 + 8 + 2 + 22); + assert_eq!( + &encoded[CHANNEL_ID_SIZE + 16..CHANNEL_ID_SIZE + 18], + &[0x00, 0x16] + ); + } + + #[test] + fn roundtrip() { + let original = sample_msg(); + let encoded = original.encode(); + let decoded = TxAddOutput::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn roundtrip_p2wsh_script() { + // 34-byte P2WSH funding output from the BOLT 3 dual-funding vectors. + let mut script = vec![0x00, 0x20]; + script.extend_from_slice(&[0x29; 32]); + let original = TxAddOutput { + serial_id: 44, + sats: 400_000_000, + script, + ..sample_msg() + }; + let encoded = original.encode(); + let decoded = TxAddOutput::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + /// A zero-length script and an out-of-range `sats` are negotiation + /// failures, not decode failures: the codec must round-trip both so that + /// they stay reachable as fuzzing inputs. + #[test] + fn roundtrip_empty_script_and_max_sats() { + let original = TxAddOutput { + sats: u64::MAX, + script: Vec::new(), + ..sample_msg() + }; + let encoded = original.encode(); + let decoded = TxAddOutput::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn decode_ignores_trailing_bytes() { + let original = sample_msg(); + let mut encoded = original.encode(); + encoded.extend_from_slice(&[0xff; 4]); + assert_eq!(TxAddOutput::decode(&encoded).unwrap(), original); + } + + #[test] + fn decode_truncated_channel_id() { + assert_eq!( + TxAddOutput::decode(&[0x00; 20]), + Err(BoltError::Truncated { + expected: CHANNEL_ID_SIZE, + actual: 20 + }) + ); + } + + #[test] + fn decode_truncated_serial_id() { + assert_eq!( + TxAddOutput::decode(&[0x00; CHANNEL_ID_SIZE + 4]), + Err(BoltError::Truncated { + expected: 8, + actual: 4 + }) + ); + } + + #[test] + fn decode_truncated_sats() { + assert_eq!( + TxAddOutput::decode(&[0x00; CHANNEL_ID_SIZE + 8 + 3]), + Err(BoltError::Truncated { + expected: 8, + actual: 3 + }) + ); + } + + #[test] + fn decode_truncated_scriptlen() { + assert_eq!( + TxAddOutput::decode(&[0x00; CHANNEL_ID_SIZE + 8 + 8 + 1]), + Err(BoltError::Truncated { + expected: 2, + actual: 1 + }) + ); + } + + #[test] + fn decode_truncated_script() { + let mut payload = vec![0x00u8; CHANNEL_ID_SIZE + 8 + 8]; + payload.extend_from_slice(&[0x00, 0x16]); // declare 22 bytes + payload.extend_from_slice(&[0x00; 5]); // only 5 provided + assert_eq!( + TxAddOutput::decode(&payload), + Err(BoltError::Truncated { + expected: 22, + actual: 5 + }) + ); + } + + #[test] + fn decode_empty() { + assert_eq!( + TxAddOutput::decode(&[]), + Err(BoltError::Truncated { + expected: CHANNEL_ID_SIZE, + actual: 0 + }) + ); + } +} diff --git a/smite/src/bolt/tx_signatures.rs b/smite/src/bolt/tx_signatures.rs new file mode 100644 index 00000000..287401d3 --- /dev/null +++ b/smite/src/bolt/tx_signatures.rs @@ -0,0 +1,390 @@ +//! BOLT 2 `tx_signatures` message. + +use bitcoin::Txid; +use bitcoin::secp256k1::ecdsa::Signature; + +use super::BoltError; +use super::tlv::TlvStream; +use super::types::ChannelId; +use super::wire::WireFormat; + +/// TLV type for the shared input signature. +const TLV_SHARED_INPUT_SIGNATURE: u64 = 0; + +/// BOLT 2 `tx_signatures` message (type 71). +/// +/// Sent once interactive transaction construction has completed, carrying the +/// sender's witnesses for the inputs it contributed to the shared transaction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TxSignatures { + /// The channel this message pertains to. + pub channel_id: ChannelId, + /// Transaction ID of the shared transaction being signed. + pub txid: Txid, + /// One entry per input added by the sender, ordered by that input's + /// `serial_id`. + /// + /// Each entry is bitcoin-wire-encoded witness data: a `CompactSize` + /// element count, then each element as a `CompactSize` length followed by + /// that many bytes. + pub witnesses: Vec>, + /// Optional TLV extensions. + pub tlvs: TxSignaturesTlvs, +} + +/// TLV extensions for the `tx_signatures` message. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct TxSignaturesTlvs { + /// Signature for the shared input, when one is being spent (splicing). + pub shared_input_signature: Option, +} + +impl TxSignaturesTlvs { + /// Extracts TLVs from a parsed TLV stream. + /// + /// # Errors + /// + /// Returns a `BoltError` if `shared_input_signature` has invalid length or + /// is not a canonical compact ECDSA signature. + fn from_stream(stream: &TlvStream) -> Result { + let shared_input_signature = stream.get_as::(TLV_SHARED_INPUT_SIGNATURE)?; + Ok(Self { + shared_input_signature, + }) + } +} + +impl TxSignatures { + /// Encodes to wire format (without message type prefix). + /// + /// # Panics + /// + /// Panics if the number of witnesses exceeds `u16::MAX`. + #[must_use] + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + self.channel_id.write(&mut out); + self.txid.write(&mut out); + u16::try_from(self.witnesses.len()) + .expect("number of witnesses must not exceed u16::MAX") + .write(&mut out); + for witness in &self.witnesses { + witness.write(&mut out); + } + + let mut tlv_stream = TlvStream::new(); + if let Some(signature) = &self.tlvs.shared_input_signature { + tlv_stream.add( + TLV_SHARED_INPUT_SIGNATURE, + signature.serialize_compact().to_vec(), + ); + } + out.extend(tlv_stream.encode()); + + out + } + + /// Decodes from wire format (without message type prefix). + /// + /// # Errors + /// + /// Returns `Truncated` if the payload is too short for any field, + /// `InvalidSignature` if `shared_input_signature` is not a valid compact + /// ECDSA signature, or a TLV error if the TLV stream is malformed. + pub fn decode(payload: &[u8]) -> Result { + let mut cursor = payload; + + let channel_id = WireFormat::read(&mut cursor)?; + let txid = WireFormat::read(&mut cursor)?; + let num_witnesses = u16::read(&mut cursor)?; + let mut witnesses = Vec::with_capacity(num_witnesses.into()); + for _ in 0..num_witnesses { + witnesses.push(Vec::::read(&mut cursor)?); + } + + let tlv_stream = TlvStream::decode_with_known(cursor, &[TLV_SHARED_INPUT_SIGNATURE])?; + let tlvs = TxSignaturesTlvs::from_stream(&tlv_stream)?; + + Ok(Self { + channel_id, + txid, + witnesses, + tlvs, + }) + } +} + +#[cfg(test)] +mod tests { + use super::super::{CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, TXID_SIZE}; + use super::*; + use bitcoin::secp256k1::hashes::Hash; + use bitcoin::secp256k1::{Message, Secp256k1, SecretKey}; + + /// Offset of `num_witnesses` within the encoded payload. + const NUM_WITNESSES_OFFSET: usize = CHANNEL_ID_SIZE + TXID_SIZE; + + fn sample_msg() -> TxSignatures { + TxSignatures { + channel_id: ChannelId::new([0xab; CHANNEL_ID_SIZE]), + txid: Txid::from_byte_array([0xcd; TXID_SIZE]), + witnesses: vec![vec![0xde, 0xad, 0xbe, 0xef], vec![0x01, 0x02]], + tlvs: TxSignaturesTlvs::default(), + } + } + + fn sample_signature() -> Signature { + let secp = Secp256k1::new(); + let sk = SecretKey::from_slice(&[0x11; 32]).unwrap(); + let msg = Message::from_digest([0xaa; 32]); + secp.sign_ecdsa(&msg, &sk) + } + + /// Dual-funding test vectors from BOLT 3, "Appendix G: Dual Funded + /// Transaction Test Vectors". `channel_id` is unspecified there, so the + /// sample value is used. `txid` is given in display order; the wire + /// encoding is its reverse, which is what `Txid` writes. + #[test] + fn encode_bolt3_dual_funding_vectors() { + const TXID_DISPLAY: &str = + "5ca4e657c1aa9d069ea4a5d712045d233a7d7c52738cb02993637289e6386057"; + let opener_witness = "022068656c6c6f2074686572652c2074686973206973206120626974636f6e21212127\ + 82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"; + let accepter_witness = "0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e90220\ + 69e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b786\ + 4c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"; + + let mut txid_bytes: [u8; TXID_SIZE] = + hex::decode(TXID_DISPLAY).unwrap().try_into().unwrap(); + txid_bytes.reverse(); + let txid = Txid::from_byte_array(txid_bytes); + assert_eq!(txid.to_string(), TXID_DISPLAY); + + // (witness hex, declared `len`, expected payload hex) + let cases = [ + ( + opener_witness, + 74, + "abababababababababababababababababababababababababababababababab\ + 576038e68972639329b08c73527c7d3a235d0412d7a5a49e069daac157e6a45c0001004a", + ), + ( + accepter_witness, + 107, + "abababababababababababababababababababababababababababababababab\ + 576038e68972639329b08c73527c7d3a235d0412d7a5a49e069daac157e6a45c0001006b", + ), + ]; + + for (witness_hex, len, prefix_hex) in cases { + let witness = hex::decode(witness_hex).unwrap(); + assert_eq!(witness.len(), len, "witness `len` field"); + + let msg = TxSignatures { + channel_id: ChannelId::new([0xab; CHANNEL_ID_SIZE]), + txid, + witnesses: vec![witness], + tlvs: TxSignaturesTlvs::default(), + }; + + let encoded = msg.encode(); + assert_eq!(hex::encode(&encoded), prefix_hex.to_owned() + witness_hex); + assert_eq!(TxSignatures::decode(&encoded).unwrap(), msg); + } + } + + #[test] + fn roundtrip() { + let original = sample_msg(); + let encoded = original.encode(); + // channel_id(32) + txid(32) + num_witnesses(2) + (2+4) + (2+2) = 76 + assert_eq!(encoded.len(), 76); + let decoded = TxSignatures::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn roundtrip_with_shared_input_signature() { + let mut original = sample_msg(); + original.tlvs.shared_input_signature = Some(sample_signature()); + let encoded = original.encode(); + let decoded = TxSignatures::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + /// A witness count of zero is a negotiation failure, not a parse failure. + #[test] + fn roundtrip_zero_witnesses() { + let original = TxSignatures { + witnesses: vec![], + ..sample_msg() + }; + let encoded = original.encode(); + assert_eq!(encoded.len(), NUM_WITNESSES_OFFSET + 2); + let decoded = TxSignatures::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + /// An empty witness is a negotiation failure, not a parse failure. + #[test] + fn roundtrip_empty_witness() { + let original = TxSignatures { + witnesses: vec![vec![]], + ..sample_msg() + }; + let encoded = original.encode(); + let decoded = TxSignatures::decode(&encoded).unwrap(); + assert_eq!(decoded.witnesses, vec![Vec::::new()]); + assert_eq!(original, decoded); + } + + #[test] + #[should_panic(expected = "number of witnesses must not exceed u16::MAX")] + fn encode_panics_on_oversized_witnesses() { + let msg = TxSignatures { + witnesses: vec![vec![0x00]; usize::from(u16::MAX) + 1], + ..sample_msg() + }; + let _ = msg.encode(); + } + + #[test] + fn decode_empty() { + assert_eq!( + TxSignatures::decode(&[]), + Err(BoltError::Truncated { + expected: CHANNEL_ID_SIZE, + actual: 0, + }) + ); + } + + #[test] + fn decode_truncated_channel_id() { + assert_eq!( + TxSignatures::decode(&[0x00; 5]), + Err(BoltError::Truncated { + expected: CHANNEL_ID_SIZE, + actual: 5, + }) + ); + } + + #[test] + fn decode_truncated_txid() { + assert_eq!( + TxSignatures::decode(&[0x00; CHANNEL_ID_SIZE + 20]), + Err(BoltError::Truncated { + expected: TXID_SIZE, + actual: 20, + }) + ); + } + + #[test] + fn decode_truncated_num_witnesses() { + assert_eq!( + TxSignatures::decode(&[0x00; NUM_WITNESSES_OFFSET + 1]), + Err(BoltError::Truncated { + expected: 2, + actual: 1, + }) + ); + } + + #[test] + fn decode_truncated_witness_len() { + // num_witnesses = 1, then a single byte of the 2-byte witness length. + let mut payload = vec![0x00u8; NUM_WITNESSES_OFFSET]; + payload.extend_from_slice(&[0x00, 0x01]); + payload.push(0x00); + assert_eq!( + TxSignatures::decode(&payload), + Err(BoltError::Truncated { + expected: 2, + actual: 1, + }) + ); + } + + #[test] + fn decode_truncated_witness_data() { + // num_witnesses = 1, witness declares 10 bytes but only 3 are present. + let mut payload = vec![0x00u8; NUM_WITNESSES_OFFSET]; + payload.extend_from_slice(&[0x00, 0x01]); + payload.extend_from_slice(&[0x00, 0x0a]); + payload.extend_from_slice(&[0x00; 3]); + assert_eq!( + TxSignatures::decode(&payload), + Err(BoltError::Truncated { + expected: 10, + actual: 3, + }) + ); + } + + #[test] + fn decode_missing_witness() { + // num_witnesses claims 2, but only the first witness is present. + let encoded = sample_msg().encode(); + let cutoff = NUM_WITNESSES_OFFSET + 2 + 2 + 4; + assert_eq!( + TxSignatures::decode(&encoded[..cutoff]), + Err(BoltError::Truncated { + expected: 2, + actual: 0, + }) + ); + } + + #[test] + fn decode_unknown_odd_tlv_ignored() { + let original = sample_msg(); + let mut encoded = original.encode(); + // Append unknown odd TLV: type 3, length 2, value [0xaa, 0xbb] + encoded.extend_from_slice(&[0x03, 0x02, 0xaa, 0xbb]); + let decoded = TxSignatures::decode(&encoded).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn decode_unknown_even_tlv_rejected() { + let mut encoded = sample_msg().encode(); + // Append unknown even TLV: type 2, length 1, value [0xff] + encoded.extend_from_slice(&[0x02, 0x01, 0xff]); + assert_eq!( + TxSignatures::decode(&encoded), + Err(BoltError::TlvUnknownEvenType(2)) + ); + } + + #[test] + fn decode_wrong_length_shared_input_signature() { + let mut encoded = sample_msg().encode(); + // Append TLV type 0 with only 32 bytes instead of 64. + encoded.push(0x00); // type 0 + encoded.push(0x20); // length 32 + encoded.extend_from_slice(&[0xaa; 32]); + assert_eq!( + TxSignatures::decode(&encoded), + Err(BoltError::Truncated { + expected: COMPACT_SIGNATURE_SIZE, + actual: 32, + }) + ); + } + + #[test] + fn decode_invalid_shared_input_signature() { + let mut encoded = sample_msg().encode(); + // r and s are both above the curve order. + let bad_sig = [0xff; COMPACT_SIGNATURE_SIZE]; + encoded.push(0x00); // type 0 + encoded.push(0x40); // length 64 + encoded.extend_from_slice(&bad_sig); + assert_eq!( + TxSignatures::decode(&encoded), + Err(BoltError::InvalidSignature(bad_sig)) + ); + } +} diff --git a/smite/src/bolt/types.rs b/smite/src/bolt/types.rs index c13925fc..34273ece 100644 --- a/smite/src/bolt/types.rs +++ b/smite/src/bolt/types.rs @@ -1,8 +1,9 @@ //! Fundamental types for BOLT message encoding. use bitcoin::OutPoint; -use bitcoin::hashes::Hash; +use bitcoin::hashes::{Hash, sha256}; use bitcoin::hex::DisplayHex; +use bitcoin::secp256k1::PublicKey; use std::fmt; /// Maximum Lightning message size (2-byte length prefix limit). @@ -71,6 +72,37 @@ impl ChannelId { res[31] ^= (outpoint.vout & 0xff) as u8; Self(res) } + + /// Creates a _v2_ channel ID from both peers' revocation basepoints. + #[must_use] + pub fn v2_from_revocation_basepoints(basepoint1: &PublicKey, basepoint2: &PublicKey) -> Self { + Self::v2_from_serialized_basepoints(basepoint1.serialize(), basepoint2.serialize()) + } + + /// Creates a _v2_ `temporary_channel_id` from the opener's revocation + /// basepoint. + #[must_use] + pub fn v2_temporary_from_revocation_basepoint( + opener_basepoint: &PublicKey, + ) -> TemporaryChannelId { + Self::v2_from_serialized_basepoints([0u8; PUBLIC_KEY_SIZE], opener_basepoint.serialize()) + } + + /// Hashes two already-serialized basepoints in BOLT 2's canonical order. + fn v2_from_serialized_basepoints( + basepoint1: [u8; PUBLIC_KEY_SIZE], + basepoint2: [u8; PUBLIC_KEY_SIZE], + ) -> Self { + let (lesser, greater) = if basepoint1 <= basepoint2 { + (basepoint1, basepoint2) + } else { + (basepoint2, basepoint1) + }; + let mut preimage = [0u8; PUBLIC_KEY_SIZE * 2]; + preimage[..PUBLIC_KEY_SIZE].copy_from_slice(&lesser); + preimage[PUBLIC_KEY_SIZE..].copy_from_slice(&greater); + Self(sha256::Hash::hash(&preimage).to_byte_array()) + } } impl fmt::Display for ChannelId { @@ -218,6 +250,17 @@ mod tests { use super::*; use bitcoin::{OutPoint, Txid}; + /// The two `funding_pubkey`s of the 2-of-2 output in the BOLT 3 + /// "Appendix G: Dual Funded Transaction Test Vectors". Used here only as a + /// convenient pair of known-valid compressed points. + const BASEPOINT_1: &str = "0292edb5f7bbf9e900f7e024be1c1339c6d149c11930e613af3a983d2565f4e41e"; + const BASEPOINT_2: &str = "02e16172a41e928cbd78f761bd1c657c4afc7495a1244f7f30166b654fbf7661e3"; + + fn pubkey(hex_str: &str) -> PublicKey { + let bytes = hex::decode(hex_str).expect("valid hex"); + PublicKey::from_slice(&bytes).expect("valid pubkey") + } + #[test] fn bigsize_new() { let bs = BigSize::new(42); @@ -308,6 +351,62 @@ mod tests { } } + #[test] + fn channel_id_v2_from_revocation_basepoints_matches_vector() { + let channel_id = + ChannelId::v2_from_revocation_basepoints(&pubkey(BASEPOINT_1), &pubkey(BASEPOINT_2)); + + assert_eq!( + channel_id.to_string(), + "59bc22f722836ce5095a37504f8ab87b1b2dbdc8aad638b77da3f5f3e8330edd", + ); + } + + #[test] + fn channel_id_v2_from_revocation_basepoints_is_order_independent() { + let basepoint_1 = pubkey(BASEPOINT_1); + let basepoint_2 = pubkey(BASEPOINT_2); + + assert_eq!( + ChannelId::v2_from_revocation_basepoints(&basepoint_1, &basepoint_2), + ChannelId::v2_from_revocation_basepoints(&basepoint_2, &basepoint_1), + ); + } + + #[test] + fn channel_id_v2_temporary_matches_vector() { + assert_eq!( + ChannelId::v2_temporary_from_revocation_basepoint(&pubkey(BASEPOINT_1)).to_string(), + "90fc2d0fcef3376e4c26de47e9c86a7362adecf87c8c1a01cdaa3263abd74c5a", + ); + assert_eq!( + ChannelId::v2_temporary_from_revocation_basepoint(&pubkey(BASEPOINT_2)).to_string(), + "270d4e8fa33e0c15f46c4978186d02c51a5307229ba5689c9f600c68360e25e3", + ); + } + + #[test] + fn channel_id_v2_temporary_differs_from_final() { + let basepoint_1 = pubkey(BASEPOINT_1); + let basepoint_2 = pubkey(BASEPOINT_2); + + assert_ne!( + ChannelId::v2_temporary_from_revocation_basepoint(&basepoint_1), + ChannelId::v2_from_revocation_basepoints(&basepoint_1, &basepoint_2), + ); + } + + #[test] + fn channel_id_v2_distinguishes_basepoints() { + let basepoint_1 = pubkey(BASEPOINT_1); + let basepoint_2 = pubkey(BASEPOINT_2); + + assert_ne!( + ChannelId::v2_from_revocation_basepoints(&basepoint_1, &basepoint_1), + ChannelId::v2_from_revocation_basepoints(&basepoint_1, &basepoint_2), + ); + } + #[test] fn short_channel_id_ord_matches_packed_u64() { let a = ShortChannelId::new(100, 0, 0); diff --git a/smite/src/channel_tx.rs b/smite/src/channel_tx.rs index 3e646154..45829a2d 100644 --- a/smite/src/channel_tx.rs +++ b/smite/src/channel_tx.rs @@ -1,13 +1,21 @@ //! BOLT 3 channel transaction construction. //! //! This module builds Lightning channel on-chain transactions: the funding -//! transaction and the commitment transaction. +//! transaction, the commitment transaction, and the shared transaction +//! negotiated by BOLT 2 interactive transaction construction. mod commitment; mod funding; +mod interactive_tx; pub use commitment::{ ChannelConfig, ChannelPartyConfig, ChannelState, CommitmentCost, CommitmentError, CommitmentPartyState, CommitmentState, HolderIdentity, Side, }; -pub use funding::{FundingTransaction, InsufficientFunds, build_funding_transaction}; +pub use funding::{ + FundingTransaction, InsufficientFunds, build_funding_transaction, build_funding_witness_script, +}; +pub use interactive_tx::{ + Contributor, MAX_INPUTS, MAX_OUTPUTS, MAX_SEQUENCE, SharedInput, SharedOutput, + SharedTransaction, signs_first, +}; diff --git a/smite/src/channel_tx/funding.rs b/smite/src/channel_tx/funding.rs index 3fb89429..2b5bd9ad 100644 --- a/smite/src/channel_tx/funding.rs +++ b/smite/src/channel_tx/funding.rs @@ -187,6 +187,7 @@ impl FundingTransaction { } /// Builds the funding output witness script per BOLT 3. +#[must_use] pub fn build_funding_witness_script(pubkey1: &PublicKey, pubkey2: &PublicKey) -> ScriptBuf { let key1_bytes = pubkey1.serialize(); let key2_bytes = pubkey2.serialize(); diff --git a/smite/src/channel_tx/interactive_tx.rs b/smite/src/channel_tx/interactive_tx.rs new file mode 100644 index 00000000..ddab042b --- /dev/null +++ b/smite/src/channel_tx/interactive_tx.rs @@ -0,0 +1,792 @@ +//! BOLT 2 interactive transaction construction. +//! +//! Two peers collaboratively build one transaction by exchanging `tx_add_input` +//! / `tx_add_output` / `tx_remove_input` / `tx_remove_output` messages, each +//! carrying a `serial_id`. [`SharedTransaction`] accumulates those +//! contributions and assembles the transaction both peers must agree on. + +use std::collections::BTreeMap; + +use bitcoin::absolute::LockTime; +use bitcoin::consensus::encode::deserialize; +use bitcoin::hashes::Hash; +use bitcoin::transaction::Version; +use bitcoin::{Amount, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid}; +use bitcoin::{Witness, secp256k1::PublicKey}; + +use super::funding::FundingTransaction; + +/// Maximum inputs in the constructed transaction (BOLT 2). +pub const MAX_INPUTS: usize = 252; + +/// Weight of the transaction fields the initiator alone pays for (BOLT 2): +/// `(input_count + output_count + version + locktime) * 4 + segwit marker and +/// flag`. +const COMMON_FIELDS_WEIGHT: u64 = (1 + 1 + 4 + 4) * 4 + 2; + +/// Weight of one input's non-witness fields: `txid + vout + scriptSig length + +/// sequence`, all outside the witness and so multiplied by four. +const INPUT_WEIGHT: u64 = (32 + 4 + 1 + 4) * 4; + +/// Weight of one output's fixed fields: `value + script length`. +const OUTPUT_BASE_WEIGHT: u64 = (8 + 1) * 4; + +/// Witness weight charged per input we contribute. +/// +/// BOLT 3 Appendix G charges `max(num_inputs * 107, actual witness weight)`, +/// where 107 is the minimum witness weight. Our wallet inputs are P2WPKH, whose +/// witness is `1` element count `+ 1 + sig + 1 + 33` pubkey; Bitcoin Core +/// grinds for a low-R signature, so `sig` is 71 bytes and the actual weight is +/// the same 107 the floor already charges. +/// +/// Charging 108 is a deliberate one-unit overpay, sized for the 72-byte +/// signature Core does not normally produce. It keeps the estimate on the +/// paying side of the requirement: the peer fails the negotiation when our +/// feerate falls short, never when it exceeds. +const WITNESS_WEIGHT_PER_INPUT: u64 = 108; + +/// Maximum outputs in the constructed transaction (BOLT 2). +pub const MAX_OUTPUTS: usize = 252; + +/// Largest `sequence` a `tx_add_input` may carry (BOLT 2): every input must +/// signal replaceability. +pub const MAX_SEQUENCE: u32 = 0xffff_fffd; + +/// Which peer contributed an input or output to the shared transaction. +/// +/// Only [`Contributor::Local`] contributions are ours to sign and to remove. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Contributor { + /// We contributed it. + Local, + /// The peer contributed it. + Remote, +} + +/// An input contributed to the shared transaction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SharedInput { + /// The outpoint being spent. + pub outpoint: OutPoint, + /// `nSequence` for this input. + pub sequence: u32, + /// Which peer contributed it. + pub contributor: Contributor, + /// The output being spent, when known. Always known for our own inputs; + /// known for the peer's only when its `prevtx` parsed and `prevtx_vout` was + /// within range. + pub prevout: Option, +} + +impl SharedInput { + /// Builds an input from a `tx_add_input`'s serialized previous transaction. + /// + /// A `prevtx` that does not parse, or a `prevtx_vout` past the end of it, + /// yields an all-zero txid and an unknown `prevout` rather than an error: + /// the peer is free to send nonsense, and it is the peer that must then + /// fail the negotiation. + #[must_use] + pub fn from_prevtx( + prevtx: &[u8], + prevtx_vout: u32, + sequence: u32, + contributor: Contributor, + ) -> Self { + let prev: Option = deserialize(prevtx).ok(); + let prevout = prev + .as_ref() + .and_then(|tx| tx.output.get(prevtx_vout as usize)) + .cloned(); + let txid = prev.as_ref().map_or_else( + || Txid::from_byte_array([0u8; 32]), + Transaction::compute_txid, + ); + + Self { + outpoint: OutPoint { + txid, + vout: prevtx_vout, + }, + sequence, + contributor, + prevout, + } + } + + /// Value of the output being spent, or `0` when it is unknown. + #[must_use] + pub fn value(&self) -> u64 { + self.prevout.as_ref().map_or(0, |o| o.value.to_sat()) + } +} + +/// An output contributed to the shared transaction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SharedOutput { + /// Output value in satoshis. + pub value: u64, + /// Output `scriptPubKey`. + pub script_pubkey: ScriptBuf, + /// Which peer contributed it. + pub contributor: Contributor, +} + +/// The transaction being built by an interactive construction session. +/// +/// Contributions are keyed by `serial_id`, so iteration is already in the +/// ascending order BOLT 2 requires for the assembled transaction. A repeated +/// `serial_id` replaces the previous entry, mirroring what a peer that failed +/// to enforce uniqueness would end up with. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SharedTransaction { + /// `nLockTime` of the transaction, from `open_channel2`. + pub locktime: u32, + inputs: BTreeMap, + outputs: BTreeMap, +} + +impl SharedTransaction { + /// Creates an empty session for a transaction with the given `nLockTime`. + #[must_use] + pub fn new(locktime: u32) -> Self { + Self { + locktime, + inputs: BTreeMap::new(), + outputs: BTreeMap::new(), + } + } + + /// Adds or replaces the input with `serial_id`. + /// + /// Returns `false` when the transaction already holds [`MAX_INPUTS`] + /// distinct serial ids and the input was dropped. + pub fn add_input(&mut self, serial_id: u64, input: SharedInput) -> bool { + if !self.inputs.contains_key(&serial_id) && self.inputs.len() >= MAX_INPUTS { + return false; + } + self.inputs.insert(serial_id, input); + true + } + + /// Adds or replaces the output with `serial_id`. + /// + /// Returns `false` when the transaction already holds [`MAX_OUTPUTS`] + /// distinct serial ids and the output was dropped. + pub fn add_output(&mut self, serial_id: u64, output: SharedOutput) -> bool { + if !self.outputs.contains_key(&serial_id) && self.outputs.len() >= MAX_OUTPUTS { + return false; + } + self.outputs.insert(serial_id, output); + true + } + + /// Removes the input with `serial_id`, returning it when it was present. + pub fn remove_input(&mut self, serial_id: u64) -> Option { + self.inputs.remove(&serial_id) + } + + /// Removes the output with `serial_id`, returning it when it was present. + pub fn remove_output(&mut self, serial_id: u64) -> Option { + self.outputs.remove(&serial_id) + } + + /// Inputs in ascending `serial_id` order. + pub fn inputs(&self) -> impl Iterator { + self.inputs.iter().map(|(id, input)| (*id, input)) + } + + /// Outputs in ascending `serial_id` order. + pub fn outputs(&self) -> impl Iterator { + self.outputs.iter().map(|(id, output)| (*id, output)) + } + + /// Positions in the assembled transaction of the inputs `contributor` + /// contributed. + /// + /// [`Self::build`] emits inputs in ascending `serial_id` order, so these + /// are also the positions BOLT 2's "order the `witnesses` by the + /// `serial_id` of the input they correspond to" maps a `tx_signatures`'s + /// witnesses onto, in either direction. + #[must_use] + pub fn input_positions(&self, contributor: Contributor) -> Vec { + self.inputs + .values() + .enumerate() + .filter(|(_, input)| input.contributor == contributor) + .map(|(position, _)| position) + .collect() + } + + /// Total value of the inputs contributed by `contributor`, saturating. + /// + /// Inputs whose `prevout` is unknown count as zero. + #[must_use] + pub fn contributed_input_value(&self, contributor: Contributor) -> u64 { + self.inputs + .values() + .filter(|i| i.contributor == contributor) + .fold(0u64, |acc, i| acc.saturating_add(i.value())) + } + + /// Fee we are responsible for at `feerate_per_kw`, in satoshis, **as the + /// initiator**. + /// + /// BOLT 2 splits fee responsibility: the initiator pays for the common + /// transaction fields, and each peer pays for the inputs and outputs it + /// contributed. This unconditionally charges both halves, which is correct + /// only while we are the initiator -- true for every caller today, since we + /// reach interactive construction by sending `open_channel2`. It stops + /// being true if `tx_init_rbf` is ever implemented, since an accepter that + /// initiates an RBF attempt becomes the initiator and takes the common + /// fields with it; splitting the two halves is the change to make then. + /// + /// `pending_output_script_lens` covers outputs we are about to add but have + /// not added yet, which is what makes a change output's value computable + /// before it exists. + /// + /// Rounds up. BOLT 3 Appendix G's worked example has weight 609 at 253 + /// sat/kw and states a fee of 155, not the 154 that truncating would give; + /// underpaying by a single satoshi makes the peer fail the negotiation. + #[must_use] + pub fn local_fee_sat(&self, feerate_per_kw: u32, pending_output_script_lens: &[usize]) -> u64 { + let local_inputs = self + .inputs + .values() + .filter(|i| i.contributor == Contributor::Local) + .count() as u64; + + let output_weight = self + .outputs + .values() + .filter(|o| o.contributor == Contributor::Local) + .map(|o| o.script_pubkey.len() as u64) + .chain(pending_output_script_lens.iter().map(|len| *len as u64)) + .map(|script_len| OUTPUT_BASE_WEIGHT + script_len * 4) + .sum::(); + + let weight = COMMON_FIELDS_WEIGHT + + local_inputs * INPUT_WEIGHT + + output_weight + + local_inputs * WITNESS_WEIGHT_PER_INPUT; + + weight + .saturating_mul(u64::from(feerate_per_kw)) + .div_ceil(1000) + } + + /// Assembles the transaction both peers must agree on. + /// + /// Per BOLT 2 the inputs and outputs are sorted by ascending `serial_id`; + /// `nVersion` is 2 and `nLockTime` comes from `open_channel2`. + #[must_use] + pub fn build(&self) -> Transaction { + Transaction { + version: Version::TWO, + lock_time: LockTime::from_consensus(self.locktime), + input: self + .inputs + .values() + .map(|i| TxIn { + previous_output: i.outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence(i.sequence), + witness: Witness::new(), + }) + .collect(), + output: self + .outputs + .values() + .map(|o| TxOut { + value: Amount::from_sat(o.value), + script_pubkey: o.script_pubkey.clone(), + }) + .collect(), + } + } + + /// Index of the channel funding output, identified by its script and value. + #[must_use] + pub fn funding_vout(&self, funding_script: &Script, funding_satoshis: u64) -> Option { + let index = self + .outputs + .values() + .position(|o| o.script_pubkey == *funding_script && o.value == funding_satoshis)?; + u32::try_from(index).ok() + } + + /// Assembles the transaction and locates its funding output. + #[must_use] + pub fn build_funding( + &self, + funding_script: &Script, + funding_satoshis: u64, + ) -> FundingTransaction { + FundingTransaction { + tx: self.build(), + vout: self + .funding_vout(funding_script, funding_satoshis) + .unwrap_or(0), + } + } +} + +/// Returns whether we must send `tx_signatures` first. +/// +/// Per BOLT 2 the peer contributing the lowest total input value signs first, +/// with the lexicographically lower `node_id` breaking a tie. The strict +/// ordering is what stops both peers waiting on each other. +#[must_use] +pub fn signs_first( + local_input_value: u64, + remote_input_value: u64, + local_node_id: &PublicKey, + remote_node_id: &PublicKey, +) -> bool { + match local_input_value.cmp(&remote_input_value) { + std::cmp::Ordering::Less => true, + std::cmp::Ordering::Greater => false, + std::cmp::Ordering::Equal => local_node_id.serialize() < remote_node_id.serialize(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The transaction whose outputs both peers spend in the BOLT 3 + /// "Appendix G: Dual Funded Transaction Test Vectors". + const APPENDIX_G_PREVTX: &str = "02000000000101f86fd1d0db3ac5a72df968622f31e6b5e6566a09e2920\ +6d7c7a55df90e181de800000000171600141fb9623ffd0d422eacc450fd1e967efc477b83ccffffffff0580b2e60e0000\ +0000220020fd89acf65485df89797d9ba7ba7a33624ac4452f00db08107f34257d33e5b94680b2e60e0000000017a9146\ +a235d064786b49e7043e4a042d4cc429f7eb6948780b2e60e00000000160014fbb4db9d85fba5e301f4399e3038928e44\ +e37d3280b2e60e0000000017a9147ecd1b519326bc13b0ec716e469b58ed02b112a087f0006bee0000000017a914f856a\ +70093da3a5b5c4302ade033d4c2171705d387024730440220696f6cee2929f1feb3fd6adf024ca0f9aa2f4920ed6d35fb\ +9ec5b78c8408475302201641afae11242160101c6f9932aeb4fcd1f13a9c6df5d1386def000ea259a35001210381d7d5b\ +1bc0d7600565d827242576d9cb793bfe0754334af82289ee8b65d137600000000"; + + /// The `Unsigned Funding Transaction` of BOLT 3 Appendix G. + const APPENDIX_G_UNSIGNED_TX: &str = "0200000002b932b0669cd0394d0d5bcc27e01ab8c511f1662a679992\ +5b346c0cf18fca03430200000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca0\ +3430000000000fdffffff03e5effa02000000001600141ca1cca8855bad6bc1ea5436edd8cff10b7e448b1cf0fa020000\ +000016001444cb0c39f93ecc372b5851725bd29d865d333b100084d71700000000220020297b92c238163e820b8248608\ +4634b4846b86a3c658d87b9384192e6bea98ec578000000"; + + /// The 2-of-2 funding `scriptPubKey` of Appendix G. + const APPENDIX_G_FUNDING_SPK: &str = + "0020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec5"; + /// Appendix G's opener change `scriptPubKey`. + const APPENDIX_G_OPENER_CHANGE_SPK: &str = "00141ca1cca8855bad6bc1ea5436edd8cff10b7e448b"; + /// Appendix G's accepter change `scriptPubKey`. + const APPENDIX_G_ACCEPTER_CHANGE_SPK: &str = "001444cb0c39f93ecc372b5851725bd29d865d333b10"; + + /// Appendix G's `nLockTime`. + const APPENDIX_G_LOCKTIME: u32 = 120; + /// Appendix G's funding output value: 2 x 2,000,000,000 sat. + const APPENDIX_G_FUNDING_SATS: u64 = 400_000_000; + + fn script(hex_str: &str) -> ScriptBuf { + ScriptBuf::from(hex::decode(hex_str).expect("valid hex")) + } + + fn pubkey(hex_str: &str) -> PublicKey { + PublicKey::from_slice(&hex::decode(hex_str).expect("valid hex")).expect("valid pubkey") + } + + /// Rebuilds Appendix G's funding transaction from the `tx_add_input` and + /// `tx_add_output` messages the appendix says each peer sends. Note that + /// the contributions are added out of serial order on purpose. + fn appendix_g() -> SharedTransaction { + let prevtx = hex::decode(APPENDIX_G_PREVTX).expect("valid hex"); + let mut shared = SharedTransaction::new(APPENDIX_G_LOCKTIME); + + // Opener's input, serial_id 20, spending the parent's output 0. + assert!(shared.add_input( + 20, + SharedInput::from_prevtx(&prevtx, 0, MAX_SEQUENCE, Contributor::Local), + )); + // Accepter's input, serial_id 11, spending the parent's output 2. + assert!(shared.add_input( + 11, + SharedInput::from_prevtx(&prevtx, 2, MAX_SEQUENCE, Contributor::Remote), + )); + + // Opener's change, serial_id 30. + assert!(shared.add_output( + 30, + SharedOutput { + value: 49_999_845, + script_pubkey: script(APPENDIX_G_OPENER_CHANGE_SPK), + contributor: Contributor::Local, + }, + )); + // Opener's funding output, serial_id 44. + assert!(shared.add_output( + 44, + SharedOutput { + value: APPENDIX_G_FUNDING_SATS, + script_pubkey: script(APPENDIX_G_FUNDING_SPK), + contributor: Contributor::Local, + }, + )); + // Accepter's change, serial_id 33. + assert!(shared.add_output( + 33, + SharedOutput { + value: 49_999_900, + script_pubkey: script(APPENDIX_G_ACCEPTER_CHANGE_SPK), + contributor: Contributor::Remote, + }, + )); + + shared + } + + #[test] + fn build_matches_bolt3_appendix_g() { + let tx = appendix_g().build(); + + assert_eq!( + bitcoin::consensus::encode::serialize_hex(&tx), + APPENDIX_G_UNSIGNED_TX, + ); + } + + #[test] + fn build_sorts_by_serial_id_not_insertion_order() { + let tx = appendix_g().build(); + + // Inputs: serial 11 (parent vout 2) before serial 20 (parent vout 0), + // even though serial 20 was added first. + assert_eq!( + tx.input + .iter() + .map(|i| i.previous_output.vout) + .collect::>(), + vec![2, 0], + ); + // Outputs: serials 30, 33, 44, even though 44 was added before 33. + assert_eq!( + tx.output + .iter() + .map(|o| o.value.to_sat()) + .collect::>(), + vec![49_999_845, 49_999_900, APPENDIX_G_FUNDING_SATS], + ); + } + + #[test] + fn build_uses_version_two_and_negotiated_locktime() { + let tx = appendix_g().build(); + + assert_eq!(tx.version, Version::TWO); + assert_eq!(tx.lock_time, LockTime::from_consensus(APPENDIX_G_LOCKTIME)); + assert!( + tx.input + .iter() + .all(|i| i.sequence == Sequence(MAX_SEQUENCE)) + ); + } + + #[test] + fn funding_vout_locates_the_two_of_two_output() { + let shared = appendix_g(); + + assert_eq!( + shared.funding_vout(&script(APPENDIX_G_FUNDING_SPK), APPENDIX_G_FUNDING_SATS), + Some(2), + ); + assert_eq!( + shared + .build_funding(&script(APPENDIX_G_FUNDING_SPK), APPENDIX_G_FUNDING_SATS) + .vout, + 2, + ); + } + + #[test] + fn funding_vout_rejects_a_wrong_value_or_script() { + let shared = appendix_g(); + + assert_eq!( + shared.funding_vout(&script(APPENDIX_G_FUNDING_SPK), APPENDIX_G_FUNDING_SATS - 1), + None, + ); + assert_eq!( + shared.funding_vout( + &script(APPENDIX_G_OPENER_CHANGE_SPK), + APPENDIX_G_FUNDING_SATS + ), + None, + ); + } + + #[test] + fn build_funding_falls_back_to_vout_zero_without_a_funding_output() { + let mut shared = appendix_g(); + shared.remove_output(44); + + let funding = + shared.build_funding(&script(APPENDIX_G_FUNDING_SPK), APPENDIX_G_FUNDING_SATS); + + assert_eq!(funding.vout, 0); + } + + #[test] + fn contributed_input_value_splits_by_contributor() { + let shared = appendix_g(); + + // Each peer spends one 2.5 BTC output of the parent transaction. + assert_eq!( + shared.contributed_input_value(Contributor::Local), + 250_000_000 + ); + assert_eq!( + shared.contributed_input_value(Contributor::Remote), + 250_000_000 + ); + } + + #[test] + fn input_positions_follow_serial_order_not_insertion_order() { + let shared = appendix_g(); + + // Serial 11 is the accepter's and was added second, but sorts first. + assert_eq!(shared.input_positions(Contributor::Remote), vec![0]); + assert_eq!(shared.input_positions(Contributor::Local), vec![1]); + } + + #[test] + fn input_positions_is_empty_without_contributions() { + assert!( + SharedTransaction::new(0) + .input_positions(Contributor::Local) + .is_empty() + ); + } + + #[test] + fn from_prevtx_with_unparsable_prevtx_is_not_an_error() { + let input = SharedInput::from_prevtx(&[0xde, 0xad], 0, MAX_SEQUENCE, Contributor::Remote); + + assert_eq!(input.outpoint.txid, Txid::from_byte_array([0u8; 32])); + assert_eq!(input.prevout, None); + assert_eq!(input.value(), 0); + } + + #[test] + fn from_prevtx_with_out_of_range_vout_has_no_prevout() { + let prevtx = hex::decode(APPENDIX_G_PREVTX).expect("valid hex"); + + let input = SharedInput::from_prevtx(&prevtx, 99, MAX_SEQUENCE, Contributor::Remote); + + // The txid is still known, so the outpoint is well-formed and the peer + // is the one that must fail the negotiation. + assert_ne!(input.outpoint.txid, Txid::from_byte_array([0u8; 32])); + assert_eq!(input.outpoint.vout, 99); + assert_eq!(input.prevout, None); + assert_eq!(input.value(), 0); + } + + #[test] + fn add_replaces_a_duplicate_serial_id() { + let mut shared = appendix_g(); + let outputs_before = shared.outputs().count(); + + assert!(shared.add_output( + 30, + SharedOutput { + value: 1, + script_pubkey: script(APPENDIX_G_ACCEPTER_CHANGE_SPK), + contributor: Contributor::Remote, + }, + )); + + assert_eq!(shared.outputs().count(), outputs_before); + assert_eq!(shared.build().output[0].value.to_sat(), 1); + } + + #[test] + fn add_input_stops_at_the_maximum() { + let prevtx = hex::decode(APPENDIX_G_PREVTX).expect("valid hex"); + let mut shared = SharedTransaction::new(0); + for serial_id in 0..MAX_INPUTS as u64 { + assert!(shared.add_input( + serial_id, + SharedInput::from_prevtx(&prevtx, 0, MAX_SEQUENCE, Contributor::Remote), + )); + } + + // A new serial id is dropped, but replacing an existing one still works. + assert!(!shared.add_input( + MAX_INPUTS as u64, + SharedInput::from_prevtx(&prevtx, 0, MAX_SEQUENCE, Contributor::Remote), + )); + assert!(shared.add_input( + 0, + SharedInput::from_prevtx(&prevtx, 1, MAX_SEQUENCE, Contributor::Remote), + )); + assert_eq!(shared.inputs().count(), MAX_INPUTS); + } + + #[test] + fn add_output_stops_at_the_maximum() { + let mut shared = SharedTransaction::new(0); + let output = SharedOutput { + value: 1000, + script_pubkey: script(APPENDIX_G_ACCEPTER_CHANGE_SPK), + contributor: Contributor::Remote, + }; + for serial_id in 0..MAX_OUTPUTS as u64 { + assert!(shared.add_output(serial_id, output.clone())); + } + + assert!(!shared.add_output(MAX_OUTPUTS as u64, output.clone())); + assert!(shared.add_output(0, output)); + assert_eq!(shared.outputs().count(), MAX_OUTPUTS); + } + + #[test] + fn remove_reports_whether_the_serial_id_was_present() { + let mut shared = appendix_g(); + + assert!(shared.remove_input(20).is_some()); + assert!(shared.remove_input(20).is_none()); + assert!(shared.remove_output(44).is_some()); + assert!(shared.remove_output(9999).is_none()); + } + + // -- Fee responsibility -- + + #[test] + fn local_fee_matches_bolt3_appendix_g_opener() { + // Appendix G's opener contributes one input, the funding output and a + // change output, at 253 sat/kw, and owes 155 sat. + let prevtx = hex::decode(APPENDIX_G_PREVTX).expect("valid hex"); + let mut shared = SharedTransaction::new(APPENDIX_G_LOCKTIME); + shared.add_input( + 20, + SharedInput::from_prevtx(&prevtx, 0, MAX_SEQUENCE, Contributor::Local), + ); + shared.add_output( + 44, + SharedOutput { + value: APPENDIX_G_FUNDING_SATS, + script_pubkey: script(APPENDIX_G_FUNDING_SPK), + contributor: Contributor::Local, + }, + ); + + // The change output is not added yet; its script length is what makes + // its own value computable. + let change_script = script(APPENDIX_G_OPENER_CHANGE_SPK); + assert_eq!(shared.local_fee_sat(253, &[change_script.len()]), 155); + } + + #[test] + fn local_fee_rounds_up() { + let prevtx = hex::decode(APPENDIX_G_PREVTX).expect("valid hex"); + let mut shared = SharedTransaction::new(0); + shared.add_input( + 0, + SharedInput::from_prevtx(&prevtx, 0, MAX_SEQUENCE, Contributor::Local), + ); + + // Weight 42 + 164 + 108 = 314. At 1 sat/kw that is 0.314 sat, which + // must round up to 1 rather than down to 0. + assert_eq!(shared.local_fee_sat(1, &[]), 1); + // And 314 * 1000 / 1000 divides exactly. + assert_eq!(shared.local_fee_sat(1000, &[]), 314); + } + + #[test] + fn local_fee_ignores_the_peers_contributions() { + let prevtx = hex::decode(APPENDIX_G_PREVTX).expect("valid hex"); + let mut shared = SharedTransaction::new(0); + shared.add_input( + 0, + SharedInput::from_prevtx(&prevtx, 0, MAX_SEQUENCE, Contributor::Local), + ); + let ours = shared.local_fee_sat(253, &[]); + + // Each peer pays for what it contributed, so adding theirs must not + // change what we owe. + shared.add_input( + 1, + SharedInput::from_prevtx(&prevtx, 1, MAX_SEQUENCE, Contributor::Remote), + ); + shared.add_output( + 3, + SharedOutput { + value: 10_000, + script_pubkey: script(APPENDIX_G_ACCEPTER_CHANGE_SPK), + contributor: Contributor::Remote, + }, + ); + + assert_eq!(shared.local_fee_sat(253, &[]), ours); + } + + #[test] + fn local_fee_covers_the_common_fields_with_no_contributions() { + // The initiator pays for version, locktime and the two counts even + // when it contributes nothing else: weight 42 at 1000 sat/kw. + assert_eq!(SharedTransaction::new(0).local_fee_sat(1000, &[]), 42); + } + + #[test] + fn local_fee_grows_with_each_input_and_output() { + let prevtx = hex::decode(APPENDIX_G_PREVTX).expect("valid hex"); + let mut shared = SharedTransaction::new(0); + let base = shared.local_fee_sat(1000, &[]); + + shared.add_input( + 0, + SharedInput::from_prevtx(&prevtx, 0, MAX_SEQUENCE, Contributor::Local), + ); + let with_input = shared.local_fee_sat(1000, &[]); + assert_eq!(with_input - base, INPUT_WEIGHT + WITNESS_WEIGHT_PER_INPUT); + + let change_script = script(APPENDIX_G_OPENER_CHANGE_SPK); + let with_output = shared.local_fee_sat(1000, &[change_script.len()]); + assert_eq!( + with_output - with_input, + OUTPUT_BASE_WEIGHT + change_script.len() as u64 * 4, + ); + } + + // -- tx_signatures ordering -- + + /// Two valid compressed points, ordered so that `LOW` sorts first. + const NODE_ID_LOW: &str = "0292edb5f7bbf9e900f7e024be1c1339c6d149c11930e613af3a983d2565f4e41e"; + const NODE_ID_HIGH: &str = "02e16172a41e928cbd78f761bd1c657c4afc7495a1244f7f30166b654fbf7661e3"; + + #[test] + fn signs_first_follows_the_lowest_contribution() { + let low = pubkey(NODE_ID_LOW); + let high = pubkey(NODE_ID_HIGH); + + // We contributed less, so we sign first regardless of node id. + assert!(signs_first(1, 2, &high, &low)); + // We contributed more, so the peer signs first. + assert!(!signs_first(2, 1, &low, &high)); + } + + #[test] + fn signs_first_breaks_an_equal_contribution_by_node_id() { + let low = pubkey(NODE_ID_LOW); + let high = pubkey(NODE_ID_HIGH); + + assert!(signs_first(5, 5, &low, &high)); + assert!(!signs_first(5, 5, &high, &low)); + } + + #[test] + fn signs_first_when_the_peer_contributes_nothing_is_the_peer() { + let low = pubkey(NODE_ID_LOW); + let high = pubkey(NODE_ID_HIGH); + + // The opener-funds-everything case: the peer's total is 0, so the peer + // signs first and we must receive tx_signatures before sending ours. + assert!(!signs_first(250_000_000, 0, &low, &high)); + } +}