Skip to content

Commit

Permalink
Merge pull request #11 from chainwayxyz/taproot-key-path-spending-for…
Browse files Browse the repository at this point in the history
…-database

Key path spending
  • Loading branch information
ceyhunsen authored Jul 10, 2024
2 parents 8b0523d + de48686 commit 4b38a93
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 83 deletions.
17 changes: 15 additions & 2 deletions src/client/rpc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use bitcoincore_rpc::{
},
RpcApi,
};
use secp256k1::rand::{self, RngCore};

impl RpcApi for Client {
/// TL;DR: If this function is called for `cmd`, it's corresponding mock is
Expand Down Expand Up @@ -151,9 +152,21 @@ impl RpcApi for Client {
_confirmation_target: Option<u32>,
_estimate_mode: Option<json::EstimateMode>,
) -> bitcoincore_rpc::Result<bitcoin::Txid> {
let target_txout = self.ledger.create_txout(amount, address.script_pubkey());
// First, create a random input. Why? Because calling this function for
// same amount twice will trigger a database error about same TXID blah,
// blah, blah.
let txout = self.ledger.create_txout(
Amount::from_sat(rand::thread_rng().next_u64()) + amount,
address.script_pubkey(),
);
let tx = self.ledger.create_transaction(vec![], vec![txout]);
let txid = tx.compute_txid();
self.ledger.add_transaction_unconditionally(tx)?;

let tx = self.ledger.create_transaction(vec![], vec![target_txout]);
// Now send amount to address.
let txin = self.ledger.create_txin(txid, 0);
let txout = self.ledger.create_txout(amount, address.script_pubkey());
let tx = self.ledger.create_transaction(vec![txin], vec![txout]);

Ok(self.ledger.add_transaction_unconditionally(tx)?)
}
Expand Down
162 changes: 115 additions & 47 deletions src/ledger/spending_requirements.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
use super::errors::LedgerError;
use bitcoin::ecdsa::Signature;
use bitcoin::key::TweakedPublicKey;
use bitcoin::opcodes::all::OP_PUSHBYTES_20;
use bitcoin::secp256k1::Message;
use bitcoin::sighash::SighashCache;
use bitcoin::taproot::{ControlBlock, LeafVersion};
use bitcoin::{
secp256k1, CompressedPublicKey, Script, ScriptBuf, TapLeafHash, Transaction, TxOut,
WitnessProgram, XOnlyPublicKey,
};
use bitcoin_scriptexec::{Exec, ExecCtx, Options, TxTemplate};

pub struct P2WPKHChecker;

impl P2WPKHChecker {
pub fn check(tx: &Transaction, prevouts: &TxOut, input_idx: usize) -> Result<(), LedgerError> {
if prevouts.script_pubkey.len() != 22 {
//! # Spending Requirements
pub mod p2wpkh_checker {
use crate::ledger::errors::LedgerError;
use bitcoin::{
ecdsa::Signature, opcodes::all::OP_PUSHBYTES_20, sighash::SighashCache,
CompressedPublicKey, ScriptBuf, Transaction, TxOut,
};
use secp256k1::Message;

pub fn check(
tx: &Transaction,
prevouts: &[TxOut],
input_idx: usize,
) -> Result<(), LedgerError> {
if prevouts[input_idx].script_pubkey.len() != 22 {
return Err(LedgerError::SpendingRequirements(
"The ScriptPubKey is not for P2WPKH.".to_owned(),
));
}

let witness_version = prevouts.script_pubkey.as_bytes()[0];
let witness_version = prevouts[input_idx].script_pubkey.as_bytes()[0];
let witness = &tx.input[input_idx].witness;

if witness.len() != 2 {
return Err(LedgerError::SpendingRequirements("The number of witness elements should be exactly two (the signature and the public key).".to_owned()));
}

if witness_version != 0 || prevouts.script_pubkey.as_bytes()[1] != OP_PUSHBYTES_20.to_u8() {
if witness_version != 0
|| prevouts[input_idx].script_pubkey.as_bytes()[1] != OP_PUSHBYTES_20.to_u8()
{
return Err(LedgerError::SpendingRequirements(
"The ScriptPubKey is not for P2WPKH.".to_owned(),
));
Expand All @@ -38,7 +38,7 @@ impl P2WPKHChecker {

let wpkh = pk.wpubkey_hash();

if !prevouts.script_pubkey.as_bytes()[2..22].eq(AsRef::<[u8]>::as_ref(&wpkh)) {
if !prevouts[input_idx].script_pubkey.as_bytes()[2..22].eq(AsRef::<[u8]>::as_ref(&wpkh)) {
return Err(LedgerError::SpendingRequirements(
"The script does not match the script public key.".to_owned(),
));
Expand All @@ -51,7 +51,7 @@ impl P2WPKHChecker {
.p2wpkh_signature_hash(
input_idx,
&ScriptBuf::new_p2wpkh(&wpkh),
prevouts.value,
prevouts[input_idx].value,
sig.sighash_type,
)
.unwrap();
Expand All @@ -64,11 +64,17 @@ impl P2WPKHChecker {
}
}

pub struct P2WSHChecker;
pub mod p2wsh_checker {
use crate::ledger::errors::LedgerError;
use bitcoin::{Script, ScriptBuf, Transaction, TxOut, WitnessProgram};
use bitcoin_scriptexec::{Exec, ExecCtx, Options, TxTemplate};

impl P2WSHChecker {
pub fn check(tx: &Transaction, prevouts: &TxOut, input_idx: usize) -> Result<(), LedgerError> {
let witness_version = prevouts.script_pubkey.as_bytes()[0];
pub fn check(
tx: &Transaction,
prevouts: &[TxOut],
input_idx: usize,
) -> Result<(), LedgerError> {
let witness_version = prevouts[input_idx].script_pubkey.as_bytes()[0];

if witness_version != 0 {
return Err(LedgerError::SpendingRequirements(
Expand All @@ -93,15 +99,15 @@ impl P2WSHChecker {
let witness_program = WitnessProgram::p2wsh(&Script::from_bytes(&script));
let sig_pub_key_expected = ScriptBuf::new_witness_program(&witness_program);

if *prevouts.script_pubkey != sig_pub_key_expected {
if *prevouts[input_idx].script_pubkey != sig_pub_key_expected {
return Err(LedgerError::SpendingRequirements(
"The script does not match the script public key.".to_owned(),
));
}

let tx_template = TxTemplate {
tx: tx.clone(),
prevouts: vec![prevouts.clone()],
prevouts: prevouts.to_vec(),
input_idx,
taproot_annex_scriptleaf: None,
};
Expand Down Expand Up @@ -133,11 +139,25 @@ impl P2WSHChecker {
}
}

pub struct P2TRChecker;
pub mod p2tr_checker {
use crate::ledger::errors::LedgerError;
use bitcoin::{
key::TweakedPublicKey,
sighash::{Prevouts, SighashCache},
taproot::{ControlBlock, LeafVersion},
Script, ScriptBuf, TapLeafHash, Transaction, TxOut, XOnlyPublicKey,
};
use bitcoin_scriptexec::{Exec, ExecCtx, Options, TxTemplate};
use secp256k1::Message;

pub fn check(
tx: &Transaction,
prevouts: &[TxOut],
input_idx: usize,
) -> Result<(), LedgerError> {
let secp = secp256k1::Secp256k1::new();

impl P2TRChecker {
pub fn check(tx: &Transaction, prevouts: &TxOut, input_idx: usize) -> Result<(), LedgerError> {
let sig_pub_key_bytes = prevouts.script_pubkey.as_bytes();
let sig_pub_key_bytes = prevouts[input_idx].script_pubkey.as_bytes();

let witness_version = sig_pub_key_bytes[0];
if witness_version != 0x51 {
Expand All @@ -155,22 +175,35 @@ impl P2TRChecker {
let mut witness = tx.input[input_idx].witness.to_vec();
let mut annex: Option<Vec<u8>> = None;

if witness.len() >= 2 && witness[witness.len() - 1][0] == 0x50 {
annex = Some(witness.pop().unwrap());
}

// Key path spend.
if witness.len() == 1 {
return Err(LedgerError::SpendingRequirements(
"The key path spending of Taproot is not implemented.".to_owned(),
));
let signature = witness.pop().unwrap();
let signature = bitcoin::taproot::Signature::from_slice(&signature).unwrap();

let x_only_public_key = XOnlyPublicKey::from_slice(&sig_pub_key_bytes[2..]).unwrap();
let mut sighashcache = SighashCache::new(tx.clone());
let h = sighashcache
.taproot_key_spend_signature_hash(
input_idx,
&Prevouts::All(&prevouts),
signature.sighash_type,
)
.unwrap();

let msg = Message::from(h);

return match x_only_public_key.verify(&secp, &msg, &signature.signature) {
Ok(()) => Ok(()),
Err(e) => Err(LedgerError::Transaction(e.to_string())),
};
}

if witness.len() < 2 {
if witness.len() >= 2 && witness[witness.len() - 1][0] == 0x50 {
annex = Some(witness.pop().unwrap());
} else if witness.len() < 2 {
return Err(LedgerError::SpendingRequirements("The number of witness elements should be at least two (the script and the control block).".to_owned()));
}

let secp = secp256k1::Secp256k1::new();

let control_block = ControlBlock::decode(&witness.pop().unwrap()).unwrap();
let script_buf = witness.pop().unwrap();
let script = Script::from_bytes(&script_buf);
Expand All @@ -187,7 +220,7 @@ impl P2TRChecker {

let tx_template = TxTemplate {
tx: tx.clone(),
prevouts: vec![prevouts.clone()],
prevouts: prevouts.to_vec(),
input_idx,
taproot_annex_scriptleaf: Some((
TapLeafHash::from_script(script, LeafVersion::TapScript),
Expand Down Expand Up @@ -225,7 +258,7 @@ impl P2TRChecker {
#[cfg(test)]
mod test {
define_pushable!();
use crate::ledger::spending_requirements::{P2TRChecker, P2WPKHChecker, P2WSHChecker};
use crate::ledger::spending_requirements::{p2tr_checker, p2wpkh_checker, p2wsh_checker};
use crate::ledger::Ledger;
use bitcoin::absolute::LockTime;
use bitcoin::ecdsa::Signature;
Expand Down Expand Up @@ -295,7 +328,7 @@ mod test {

tx2.input[0].witness = Witness::p2wpkh(&signature, &credential.public_key);

let res = P2WPKHChecker::check(&tx2, &output, 0);
let res = p2wpkh_checker::check(&tx2, &[output], 0);
assert!(res.is_ok());
}

Expand Down Expand Up @@ -341,7 +374,7 @@ mod test {
output: vec![],
};

let res = P2WSHChecker::check(&tx2, &output, 0);
let res = p2wsh_checker::check(&tx2, &[output], 0);
assert!(res.is_ok());
}

Expand Down Expand Up @@ -405,7 +438,42 @@ mod test {
output: vec![],
};

let res = P2TRChecker::check(&tx2, &output, 0);
let res = p2tr_checker::check(&tx2, &[output], 0);
assert!(res.is_ok());
}

#[test]
fn p2tr_random_witness() {
let credential = Ledger::generate_credential_from_witness();

let output = TxOut {
value: Amount::from_sat(1_000_000_000),
script_pubkey: ScriptBuf::new_witness_program(&credential.witness_program.unwrap()),
};

let tx = bitcoin::Transaction {
version: Version::ONE,
lock_time: LockTime::ZERO,
input: vec![],
output: vec![output.clone()],
};

let tx_id = tx.compute_txid();

let input = TxIn {
previous_output: OutPoint::new(tx_id, 0),
script_sig: ScriptBuf::default(),
sequence: Sequence::MAX,
witness: credential.witness.unwrap(),
};

let tx2 = bitcoin::Transaction {
version: Version::ONE,
lock_time: LockTime::ZERO,
input: vec![input.clone()],
output: vec![],
};

p2tr_checker::check(&tx2, &[output], 0).unwrap();
}
}
61 changes: 33 additions & 28 deletions src/ledger/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use super::{
errors::LedgerError,
spending_requirements::{P2TRChecker, P2WPKHChecker, P2WSHChecker},
spending_requirements::{p2tr_checker, p2wpkh_checker, p2wsh_checker},
Ledger,
};
use bitcoin::{
Expand Down Expand Up @@ -32,14 +32,12 @@ impl Ledger {
Err(e) => return Err(LedgerError::Transaction(e.to_string())),
};

self.database
.lock()
.unwrap()
.execute(
"INSERT INTO \"transactions\" (txid, body) VALUES (?1, ?2)",
params![txid.to_string(), body],
)
.unwrap();
if let Err(e) = self.database.lock().unwrap().execute(
"INSERT INTO \"transactions\" (txid, body) VALUES (?1, ?2)",
params![txid.to_string(), body],
) {
return Err(LedgerError::AnyHow(e.into()));
};

Ok(txid)
}
Expand Down Expand Up @@ -96,24 +94,31 @@ impl Ledger {
input_value, output_value
)));
}

let mut prev_outs = vec![];
for input in transaction.input.iter() {
for input_idx in 0..transaction.input.len() {
let previous_output = self.get_transaction(input.previous_output.txid)?.output;
let previous_output = previous_output
.get(input.previous_output.vout as usize)
.unwrap()
.to_owned();

let script_pubkey = previous_output.clone().script_pubkey;

if script_pubkey.is_p2wpkh() {
P2WPKHChecker::check(&transaction, &previous_output, input_idx)?;
} else if script_pubkey.is_p2wsh() {
P2WSHChecker::check(&transaction, &previous_output, input_idx)?;
} else if script_pubkey.is_p2tr() {
P2TRChecker::check(&transaction, &previous_output, input_idx)?;
}
assert_eq!(
input.script_sig.len(),
0,
"Bitcoin simulator only verifies inputs that support segregated witness."
);

let prev_out = self
.get_transaction(input.previous_output.txid)?
.output
.get(input.previous_output.vout as usize)
.unwrap()
.to_owned();

prev_outs.push(prev_out);
}

for input_idx in 0..transaction.input.len() {
if prev_outs[input_idx].script_pubkey.is_p2wpkh() {
p2wpkh_checker::check(&transaction, prev_outs.as_slice(), input_idx)?;
} else if prev_outs[input_idx].script_pubkey.is_p2wsh() {
p2wsh_checker::check(&transaction, &prev_outs, input_idx)?;
} else if prev_outs[input_idx].script_pubkey.is_p2tr() {
p2tr_checker::check(&transaction, &prev_outs, input_idx)?;
}
}

Expand Down Expand Up @@ -148,7 +153,7 @@ impl Ledger {
}

/// Creates a `TxIn` with some defaults.
pub fn _create_txin(&self, txid: Txid, vout: u32) -> TxIn {
pub fn create_txin(&self, txid: Txid, vout: u32) -> TxIn {
TxIn {
previous_output: OutPoint { txid, vout },
..Default::default()
Expand Down Expand Up @@ -270,7 +275,7 @@ mod tests {
Amount::from_sat(0)
);
// Valid input should be OK.
let txin = ledger._create_txin(txid, 0);
let txin = ledger.create_txin(txid, 0);
let tx = ledger.create_transaction(vec![txin], vec![txout]);
assert_eq!(
ledger.calculate_transaction_input_value(tx).unwrap(),
Expand Down
Loading

0 comments on commit 4b38a93

Please sign in to comment.