Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 104 additions & 32 deletions smite/src/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -49,6 +49,15 @@ pub struct TxBlockPosition {
pub tx_index: u32,
}

/// Parsed response from `signrawtransactionwithwallet <hex>`.
#[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 <txid> 1`.
#[derive(Deserialize)]
struct RawTransactionInfo {
Expand All @@ -58,6 +67,8 @@ struct RawTransactionInfo {
confirmations: u32,
/// Omitted while the transaction is unconfirmed (in the mempool).
blockhash: Option<String>,
/// Consensus-serialized transaction, always present.
hex: String,
}

/// Connection info for invoking `bitcoin-cli` against the regtest `bitcoind`
Expand Down Expand Up @@ -271,36 +282,73 @@ 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<Transaction> {
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<SignRawTransactionResponse, String> {
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
/// normally. If the mempool rejects it (for example, because it is below
/// 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<String> {
#[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.
Expand All @@ -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()
Expand All @@ -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}");
}

Expand Down Expand Up @@ -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<Vec<u8>> {
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).
Expand Down
68 changes: 68 additions & 0 deletions smite/src/bolt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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)?)),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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]),
Expand All @@ -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]),
Expand Down
Loading