Skip to content

Commit

Permalink
Break up the utxo_package test file into the tests and a general regt…
Browse files Browse the repository at this point in the history
…est file
  • Loading branch information
djordon committed May 12, 2024
1 parent 51e8e1d commit 956e99f
Show file tree
Hide file tree
Showing 2 changed files with 445 additions and 399 deletions.
362 changes: 362 additions & 0 deletions signer/tests/regtest/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
use bitcoin::absolute::LockTime;
use bitcoin::key::TapTweak;
use bitcoin::sighash::Prevouts;
use bitcoin::sighash::SighashCache;
use bitcoin::transaction::Version;
use bitcoin::Address;
use bitcoin::AddressType;
use bitcoin::Amount;
use bitcoin::CompressedPublicKey;
use bitcoin::EcdsaSighashType;
use bitcoin::Network;
use bitcoin::OutPoint;
use bitcoin::PublicKey;
use bitcoin::ScriptBuf;
use bitcoin::Sequence;
use bitcoin::TapLeafHash;
use bitcoin::TapNodeHash;
use bitcoin::TapSighashType;
use bitcoin::Transaction;
use bitcoin::TxIn;
use bitcoin::TxOut;
use bitcoin::Witness;
use bitcoincore_rpc::json::ImportDescriptors;
use bitcoincore_rpc::json::ListUnspentQueryOptions;
use bitcoincore_rpc::json::ListUnspentResultEntry;
use bitcoincore_rpc::json::Timestamp;
use bitcoincore_rpc::jsonrpc::error::Error as JsonRpcError;
use bitcoincore_rpc::jsonrpc::error::RpcError;
use bitcoincore_rpc::Auth;
use bitcoincore_rpc::Client;
use bitcoincore_rpc::Error as BtcRpcError;
use bitcoincore_rpc::RpcApi;
use secp256k1::Keypair;
use secp256k1::Message;
use secp256k1::SECP256K1;
use std::sync::OnceLock;

/// These must match the username and password in bitcoin.conf
const BITCOIN_CORE_RPC_USERNAME: &str = "alice";
const BITCOIN_CORE_RPC_PASSWORD: &str = "pw";

pub const BITCOIN_CORE_FALLBACK_FEE: Amount = Amount::from_sat(1000);

/// The name of our wallet on bitcoin-core
const BITCOIN_CORE_WALLET_NAME: &str = "integration-tests-wallet";

/// The faucet has a fixed secret key so that any mined amounts are
/// preseved between test runs.
const FAUCET_SECRET_KEY: &str = "0000000000000000000000000000000000000000000000000000000000000001";

const FAUCET_LABEL: Option<&str> = Some("faucet");

static BTC_CLIENT: OnceLock<(Client, Recipient)> = OnceLock::new();

/// Initializes a blockchain and wallet on bitcoin-core. It can be called
/// multiple times (even concurrently) but only generates the client and
/// recipient once.
///
/// This function does the following:
/// * Creates an RPC client to bitcoin-core
/// * Creates or loads a watch-only wallet on bitcoin-core
/// * Loads a "faucet" private-public key pair with a P2WPKH address.
/// * Has the bitcoin-core wallet watch the generated address.
/// * Ensures that the faucet has at least 1 bitcoin spent to its address.
pub fn initialize_blockchain() -> &'static (Client, Recipient) {
BTC_CLIENT.get_or_init(|| {
let username = BITCOIN_CORE_RPC_USERNAME.to_string();
let password = BITCOIN_CORE_RPC_PASSWORD.to_string();
let auth = Auth::UserPass(username, password);
let rpc = Client::new("http://localhost:18443", auth).unwrap();

get_or_create_wallet(&rpc, BITCOIN_CORE_WALLET_NAME);
let faucet = Recipient::from_key(FAUCET_SECRET_KEY, AddressType::P2wpkh);
faucet.track_address(&rpc, FAUCET_LABEL);

let amount = rpc
.get_received_by_address(&faucet.address, Some(1))
.unwrap();

if amount < Amount::from_int_btc(1) {
faucet.generate_blocks(&rpc, 101);
}

(rpc, faucet)
})
}

fn get_or_create_wallet(rpc: &Client, wallet: &str) {
match rpc.load_wallet(wallet) {
// Success
Ok(_) => (),
// This happens if the wallet has already been loaded.
Err(BtcRpcError::JsonRpc(JsonRpcError::Rpc(RpcError { code: -35, .. }))) => (),
// The wallet probably hasn't been created yet, so lets do that
Err(_) => {
// We want a wallet that is watch only, since we manage keys
let disable_private_keys = Some(true);
rpc.create_wallet(wallet, disable_private_keys, None, None, None)
.unwrap();
}
};
}

/// Helper struct for representing an address on bitcoin.
pub struct Recipient {
pub keypair: Keypair,
pub address: Address,
}

fn descriptor_base(public_key: &PublicKey, kind: AddressType) -> String {
match kind {
AddressType::P2wpkh => format!("wpkh({public_key})"),
AddressType::P2tr => format!("tr({public_key})"),
AddressType::P2pkh => format!("pkh({public_key})"),
// We're missing pay-to-script-hash (P2SH) and
// pay-to-witness-script-hash (P2WSH)
_ => unimplemented!(""),
}
}

impl Recipient {
/// Generate a new public-private key pair and address of the given
/// kind.
pub fn new(kind: AddressType) -> Self {
let keypair = Keypair::new_global(&mut rand::rngs::OsRng);
let pk = keypair.public_key();
let address = match kind {
AddressType::P2wpkh => Address::p2wpkh(&CompressedPublicKey(pk), Network::Regtest),
AddressType::P2pkh => Address::p2pkh(PublicKey::new(pk), Network::Regtest),
AddressType::P2tr => {
let (internal_key, _) = pk.x_only_public_key();
Address::p2tr(SECP256K1, internal_key, None, Network::Regtest)
}
_ => unimplemented!(),
};

Recipient { keypair, address }
}

fn from_key(secret_key: &str, kind: AddressType) -> Self {
let keypair = Keypair::from_seckey_str_global(secret_key).unwrap();
let pk = keypair.public_key();
let address = match kind {
AddressType::P2wpkh => Address::p2wpkh(&CompressedPublicKey(pk), Network::Regtest),
AddressType::P2pkh => Address::p2pkh(PublicKey::new(pk), Network::Regtest),
AddressType::P2tr => {
let (internal_key, _) = pk.x_only_public_key();
Address::p2tr(SECP256K1, internal_key, None, Network::Regtest)
}
_ => unimplemented!(),
};

Recipient { keypair, address }
}

/// Tell bitcoin core to track transactions associated with this address,
///
/// Note: this is needed in order for get_utxos and get_balance to work
/// as expected.
pub fn track_address(&self, rpc: &Client, label: Option<&str>) {
let public_key = PublicKey::new(self.keypair.public_key());
let kind = self.address.address_type().unwrap();

let desc = descriptor_base(&public_key, kind);
let descriptor_info = rpc.get_descriptor_info(&desc).unwrap();

let req = ImportDescriptors {
descriptor: descriptor_info.descriptor,
label: label.map(ToString::to_string),
internal: Some(false),
timestamp: Timestamp::Time(0),
active: None,
next_index: None,
range: None,
};
let response = rpc.import_descriptors(req).unwrap();
response.into_iter().for_each(|item| assert!(item.success));
}

/// Generate num_blocks blocks with coinbase rewards being sent to this
/// recipient.
pub fn generate_blocks(&self, rpc: &Client, num_blocks: u64) {
rpc.generate_to_address(num_blocks, &self.address).unwrap();
}

/// Return all UTXOs for this recipient where the amount is greater
/// than or equal to the given amount. The address must be tracked by
/// the bitcoin-core wallet.
pub fn get_utxos(&self, rpc: &Client, amount: Option<u64>) -> Vec<ListUnspentResultEntry> {
let query_options = amount.map(|sats| ListUnspentQueryOptions {
minimum_amount: Some(Amount::from_sat(sats)),
..Default::default()
});
rpc.list_unspent(None, None, Some(&[&self.address]), None, query_options)
.unwrap()
}

/// Get the total amount of UTXOs controlled by the recipient.
pub fn get_balance(&self, rpc: &Client) -> Amount {
self.get_utxos(rpc, None)
.into_iter()
.map(|x| x.amount)
.sum()
}

/// Send the specified amount to the specific address.
///
/// Note: only P2TR and P2WPKH addresses are supported.
pub fn send_to(&self, rpc: &Client, amount: u64, address: &Address) {
let fee = BITCOIN_CORE_FALLBACK_FEE.to_sat();
let utxo = self.get_utxos(&rpc, Some(amount + fee)).pop().unwrap();

let tx = Transaction {
version: Version::ONE,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::new(utxo.txid, utxo.vout),
sequence: Sequence::ZERO,
script_sig: ScriptBuf::new(),
witness: Witness::new(),
}],
output: vec![
TxOut {
value: Amount::from_sat(amount),
script_pubkey: address.script_pubkey(),
},
TxOut {
value: utxo.amount.unchecked_sub(Amount::from_sat(amount + fee)),
script_pubkey: self.address.script_pubkey(),
},
],
};

let input_index = 0;
let tx = match self.address.address_type().unwrap() {
AddressType::P2wpkh => p2wpkh_sign_transaction(tx, input_index, &utxo, &self.keypair),
AddressType::P2tr => {
let leaf = Either::Left(None);
let (mut tx, signature) =
p2tr_signature(tx, input_index, &[utxo], self.keypair, leaf);
tx.input[input_index].witness = Witness::p2tr_key_spend(&signature);
tx
}
_ => unimplemented!(),
};
rpc.send_raw_transaction(&tx).unwrap();
}
}

pub trait Utxo {
fn amount(&self) -> Amount;
fn script_pubkey(&self) -> &ScriptBuf;
fn to_tx_out(&self) -> TxOut {
TxOut {
value: self.amount(),
script_pubkey: self.script_pubkey().clone(),
}
}
}

impl Utxo for ListUnspentResultEntry {
fn amount(&self) -> Amount {
self.amount
}

fn script_pubkey(&self) -> &ScriptBuf {
&self.script_pub_key
}
}

impl Utxo for TxOut {
fn amount(&self) -> Amount {
self.value
}

fn script_pubkey(&self) -> &ScriptBuf {
&self.script_pubkey
}
}

pub fn p2wpkh_sign_transaction<U>(
tx: Transaction,
input_index: usize,
utxo: &U,
keys: &Keypair,
) -> Transaction
where
U: Utxo,
{
let sighash_type = EcdsaSighashType::All;
let mut sighasher = SighashCache::new(tx);
let sighash = sighasher
.p2wpkh_signature_hash(
input_index,
utxo.script_pubkey().as_script(),
utxo.amount(),
sighash_type,
)
.expect("failed to create sighash");

let msg = Message::from(sighash);
let signature = SECP256K1.sign_ecdsa(&msg, &keys.secret_key());

let signature = bitcoin::ecdsa::Signature { signature, sighash_type };
*sighasher.witness_mut(input_index).unwrap() = Witness::p2wpkh(&signature, &keys.public_key());

sighasher.into_transaction()
}

/// The enum `Either` with variants `Left` and `Right` is a general purpose
/// sum type with two cases.
pub enum Either<L, R> {
/// A value of type `L`.
Left(L),
/// A value of type `R`.
Right(R),
}

pub fn p2tr_signature<U>(
tx: Transaction,
input_index: usize,
utxos: &[U],
keypair: Keypair,
leaf_hash: Either<Option<TapNodeHash>, TapLeafHash>,
) -> (Transaction, bitcoin::taproot::Signature)
where
U: Utxo,
{
let tx_outs: Vec<TxOut> = utxos.iter().map(Utxo::to_tx_out).collect();
let prevouts = Prevouts::All(tx_outs.as_slice());
let sighash_type = TapSighashType::Default;
let mut sighasher = SighashCache::new(tx);

let (sighash, keypair) = match leaf_hash {
Either::Left(merkle_root) => {
let sighash = sighasher
.taproot_key_spend_signature_hash(input_index, &prevouts, sighash_type)
.expect("failed to create taproot key-spend sighash");
let tweaked = keypair.tap_tweak(SECP256K1, merkle_root);

(sighash, tweaked.to_inner())
}
Either::Right(leaf_hash) => {
let sighash = sighasher
.taproot_script_spend_signature_hash(
input_index,
&prevouts,
leaf_hash,
sighash_type,
)
.expect("failed to create taproot key-spend sighash");

(sighash, keypair)
}
};

let msg = Message::from(sighash);
let signature = SECP256K1.sign_schnorr(&msg, &keypair);
let signature = bitcoin::taproot::Signature { signature, sighash_type };

(sighasher.into_transaction(), signature)
}
Loading

0 comments on commit 956e99f

Please sign in to comment.