Skip to content
Merged
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ postcard = { version = "1.1", default-features = false, features = ["alloc"] }
bitcoin = "0.32"
serde = { version = "1", features = ["derive"] }
thiserror = "2"
serde_json = "1"
2 changes: 1 addition & 1 deletion smite-scenarios/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ bitcoin.workspace = true
hex = "0.4"
libc = "0.2"
serde.workspace = true
serde_json = "1"
serde_json.workspace = true
tempfile = "3"
2 changes: 2 additions & 0 deletions smite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ log.workspace = true
simple_logger.workspace = true
bitcoin.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true

# Noise protocol crypto dependencies (BOLT 8)
chacha20poly1305 = { version = "0.10", default-features = false, features = [
Expand Down
178 changes: 178 additions & 0 deletions smite/src/bitcoin.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
//! This module implements utilities for interacting with regtest
//! `bitcoind` instances via `bitcoin-cli`.

use std::cmp::Ordering;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;

use bitcoin::consensus::encode::serialize_hex;
use bitcoin::{Address, Amount, Network, OutPoint, ScriptBuf, Transaction, Txid};
use serde::Deserialize;

/// A spendable UTXO used as a transaction input.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Utxo {
/// The value of the UTXO.
pub amount: Amount,
/// The transaction outpoint identifying the UTXO.
pub outpoint: OutPoint,
/// The script pubkey of the UTXO being spent.
pub script_pubkey: ScriptBuf,
}

impl Ord for Utxo {
fn cmp(&self, other: &Self) -> Ordering {
// Sort in decreasing order of amount to support largest-first coin
// selection (as used in `bdk_wallet`) and ensure deterministic ordering.
other
.amount
.cmp(&self.amount)
.then_with(|| other.script_pubkey.cmp(&self.script_pubkey))
.then_with(|| other.outpoint.cmp(&self.outpoint))
}
}

impl PartialOrd for Utxo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

/// Connection info for invoking `bitcoin-cli` against the regtest `bitcoind`
/// started by a target.
Expand Down Expand Up @@ -48,4 +83,147 @@ impl BitcoinCli {
String::from_utf8_lossy(&mine_out.stderr)
);
}

/// Returns the wallet's spendable UTXOs, sorted deterministically.
///
/// # Panics
///
/// - If `bitcoin-cli listunspent` fails to execute or exits non-zero.
/// - If the output is not valid JSON, or any entry has an invalid amount,
/// txid, or hex scriptPubKey.
#[must_use]
pub fn get_utxos(&self) -> Vec<Utxo> {
#[derive(Deserialize)]
struct UnspentOutput {
txid: String,
vout: u32,
amount: f64,
#[serde(rename = "scriptPubKey")]
script_pubkey: String,
spendable: bool,
}

let utxo_out = self
.run()
.arg("listunspent")
.output()
.expect("bitcoin-cli listunspent should not fail");
assert!(
utxo_out.status.success(),
"bitcoin-cli listunspent failed: {}",
String::from_utf8_lossy(&utxo_out.stderr)
);

let utxos: Vec<UnspentOutput> =
serde_json::from_slice(&utxo_out.stdout).expect("listunspent should return valid JSON");

let mut spendable: Vec<Utxo> = utxos
.into_iter()
.filter(|u| u.spendable)
.map(|u| Utxo {
amount: Amount::from_btc(u.amount).expect("listunspent amount should be valid BTC"),
outpoint: OutPoint::new(
Txid::from_str(&u.txid).expect("listunspent should return valid txid"),
u.vout,
),
script_pubkey: ScriptBuf::from(
hex::decode(&u.script_pubkey)
.expect("listunspent should return valid hex scriptPubKey"),
),
})
.collect();
// Sorted for determinism and to support largest-first coin selection
// during transaction construction.
spendable.sort();

spendable
}

/// Returns the scriptPubKey for a newly generated wallet address.
///
/// # Panics
///
/// - If `bitcoin-cli getnewaddress` fails to execute or exits non-zero.
/// - If the output is not valid UTF-8 or not a valid regtest address.
#[must_use]
pub fn get_new_address_script_pubkey(&self) -> ScriptBuf {
let addr_out = self
.run()
.arg("getnewaddress")
.output()
.expect("bitcoin-cli getnewaddress should not fail");
assert!(
addr_out.status.success(),
"bitcoin-cli getnewaddress failed: {}",
String::from_utf8_lossy(&addr_out.stderr)
);

let addr_str = String::from_utf8(addr_out.stdout).expect("bitcoin address is valid UTF-8");
Address::from_str(addr_str.trim())
.and_then(|a| a.require_network(Network::Regtest))
.expect("getnewaddress should return a valid address")
.script_pubkey()
}

/// Signs and broadcasts a transaction.
///
/// # Panics
///
/// - If `bitcoin-cli signrawtransactionwithwallet` or `sendrawtransaction`
/// fails to execute or exits non-zero.
/// - If the sign output is not valid JSON.
/// - If signing returns `complete=false`.
/// - If `sendrawtransaction` does not return a valid UTF-8 txid.
/// - If the broadcasted txid does not match the given transaction's txid.
pub fn sign_and_broadcast_tx(&self, tx: &Transaction) {
#[derive(Deserialize)]
struct SignRawTransactionResponse {
hex: String,
complete: bool,
}

let tx_hex = serialize_hex(tx);

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"
);

let broadcast_out = self
.run()
.arg("sendrawtransaction")
.arg(&signed_tx.hex)
.output()
.expect("bitcoin-cli sendrawtransaction should not fail");
assert!(
broadcast_out.status.success(),
"bitcoin-cli sendrawtransaction failed: {}",
String::from_utf8_lossy(&broadcast_out.stderr)
);

// Safe because bitcoind descriptor wallets currently default to native
// SegWit, so signing does not alter the txid computed from the unsigned
// Transaction.
let broadcast_txid = String::from_utf8(broadcast_out.stdout)
.expect("sendrawtransaction should return a valid UTF-8 txid");
assert_eq!(
broadcast_txid.trim(),
tx.compute_txid().to_string(),
"sendrawtransaction returned unexpected txid"
);
Comment on lines +218 to +227
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we were to ever provide a transaction to this function that did not have only segwit inputs, this assertion would fail, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so if the tx has non-segwit or non-taproot inputs i.e legacy inputs, this will fail. I think this is safe, since the current and subsequent versions of Bitcoin Core do not generate legacy addresses by default

}
}
2 changes: 2 additions & 0 deletions smite/src/bolt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod attribution_data;
mod channel_ready;
mod commitment;
mod error;
mod funding;
mod funding_created;
mod funding_signed;
mod gossip_timestamp_filter;
Expand Down Expand Up @@ -42,6 +43,7 @@ pub use commitment::{
HolderIdentity, Side,
};
pub use error::Error;
pub use funding::{FundingTransaction, InsufficientFunds, build_funding_transaction};
pub use funding_created::FundingCreated;
pub use funding_signed::FundingSigned;
pub use gossip_timestamp_filter::GossipTimestampFilter;
Expand Down
54 changes: 8 additions & 46 deletions smite/src/bolt/commitment.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! BOLT 3 commitment transaction construction and signing.

use super::funding::build_funding_witness_script;

use bitcoin::absolute::LockTime;
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::{Hash, HashEngine};
Expand Down Expand Up @@ -271,15 +273,17 @@ impl ChannelConfig {
output: outputs,
};

// Funding output redeem script.
let funding_redeemscript =
build_funding_redeemscript(&self.opener.funding_pubkey, &self.acceptor.funding_pubkey);
// Funding output witness script.
let funding_witness_script = build_funding_witness_script(
&self.opener.funding_pubkey,
&self.acceptor.funding_pubkey,
);

// Compute the BIP143 sighash
let sighash = SighashCache::new(&tx)
.p2wsh_signature_hash(
0,
&funding_redeemscript,
&funding_witness_script,
Amount::from_sat(self.funding_satoshis),
EcdsaSighashType::All,
)
Expand Down Expand Up @@ -539,24 +543,6 @@ fn build_anchor_scriptpubkey(funding_pubkey: &PublicKey) -> ScriptBuf {
.to_p2wsh()
}

/// Builds the funding output redeem script per BOLT 3.
fn build_funding_redeemscript(pubkey1: &PublicKey, pubkey2: &PublicKey) -> ScriptBuf {
let key1_bytes = pubkey1.serialize();
let key2_bytes = pubkey2.serialize();
let (lesser, greater) = if key1_bytes < key2_bytes {
(&key1_bytes, &key2_bytes)
} else {
(&key2_bytes, &key1_bytes)
};
Builder::new()
.push_opcode(opcodes::OP_PUSHNUM_2)
.push_slice(lesser)
.push_slice(greater)
.push_opcode(opcodes::OP_PUSHNUM_2)
.push_opcode(opcodes::OP_CHECKMULTISIG)
.into_script()
}

/// Signs a commitment sighash with the given funding private key.
fn sign(sighash: &[u8; 32], funding_privkey: &SecretKey) -> Signature {
let secp = Secp256k1::new();
Expand Down Expand Up @@ -615,30 +601,6 @@ mod tests {
assert!(!supports_option_anchors(&[0x00, 0x10]));
}

// BOLT 3 Appendix B: Funding Transaction Test Vectors
// https://github.com/lightning/bolts/blob/master/03-transactions.md#appendix-b-funding-transaction-test-vectors

#[test]
fn funding_redeemscript_is_key_order_independent() {
let local_funding_pubkey =
pubkey("023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb");
let remote_funding_pubkey =
pubkey("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1");

let funding_redeemscript_1 =
build_funding_redeemscript(&local_funding_pubkey, &remote_funding_pubkey);
let funding_redeemscript_2 =
build_funding_redeemscript(&remote_funding_pubkey, &local_funding_pubkey);

// Argument order must not matter as keys are sorted lexicographically.
assert_eq!(funding_redeemscript_1, funding_redeemscript_2);

assert_eq!(
hex::encode(funding_redeemscript_1.as_bytes()),
"5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae",
);
}

fn bolt3_commitment_params(
feerate_per_kw: u32,
to_opener_msat: u64,
Expand Down
Loading