Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Starting taproot implementation #223

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ jobs:
stable,
nightly
]
test: [
transactions,
taproot
]

runs-on: ubuntu-latest
container:
Expand Down Expand Up @@ -74,7 +78,7 @@ jobs:
profile: minimal

- name: Run regtest Bitcoin transactions
run: cargo test --verbose --test transactions --features rpc -- --test-threads=1
run: cargo test --verbose --test ${{ matrix.test }} --features taproot,rpc -- --test-threads=1
env:
RPC_HOST: bitcoind
RPC_PORT: 18443
8 changes: 7 additions & 1 deletion contrib/run-rpc-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ do
done

export CI=false RPC_USER=test RPC_PASS=cEl2o3tHHgzYeuu3CiiZ2FjdgSiw9wNeMFzoNbFmx9k=
cargo test --test transactions --features rpc -- --test-threads=1

case "$1" in
"--taproot")
cargo test --test taproot --features taproot,rpc -- --test-threads=1;;
*)
cargo test --test transactions --features rpc -- --test-threads=1;;
esac

docker kill bitcoind

Expand Down
96 changes: 96 additions & 0 deletions src/bitcoin/taproot/funding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use bitcoin::blockdata::transaction::{OutPoint, Transaction};
use bitcoin::secp256k1::Secp256k1;
use bitcoin::{Address, XOnlyPublicKey};

use crate::blockchain::Network;
use crate::transaction::{Error, Fundable, Linkable};

use crate::bitcoin::taproot::Taproot;
use crate::bitcoin::transaction::MetadataOutput;
use crate::bitcoin::Bitcoin;
use bitcoin::util::taproot::TaprootSpendInfo;

#[derive(Debug, Clone)]
pub struct Funding {
untweaked_public_key: Option<XOnlyPublicKey>,
network: Option<bitcoin::Network>,
seen_tx: Option<Transaction>,
}

impl Linkable<MetadataOutput> for Funding {
fn get_consumable_output(&self) -> Result<MetadataOutput, Error> {
let secp = Secp256k1::new();

let address = Address::p2tr(
&secp,
self.untweaked_public_key.ok_or(Error::MissingPublicKey)?,
None,
self.network.ok_or(Error::MissingNetwork)?,
);
let script_pubkey = address.script_pubkey();

match &self.seen_tx {
Some(t) => t
.output
.iter()
.enumerate()
.find(|(_, tx_out)| tx_out.script_pubkey == script_pubkey)
.map(|(ix, tx_out)| MetadataOutput {
out_point: OutPoint::new(t.txid(), ix as u32),
tx_out: tx_out.clone(),
script_pubkey: Some(script_pubkey),
})
.ok_or(Error::MissingUTXO),
// The transaction has not been see yet, cannot infer the UTXO
None => Err(Error::MissingOnchainTransaction),
}
}
}

impl Fundable<Bitcoin<Taproot>, MetadataOutput> for Funding {
fn initialize(pubkey: XOnlyPublicKey, network: Network) -> Result<Self, Error> {
let network = match network {
Network::Mainnet => bitcoin::Network::Bitcoin,
Network::Testnet => bitcoin::Network::Testnet,
Network::Local => bitcoin::Network::Regtest,
};
Ok(Funding {
untweaked_public_key: Some(pubkey),
network: Some(network),
seen_tx: None,
})
}

fn get_address(&self) -> Result<Address, Error> {
let secp = Secp256k1::new();
let taproot_info = TaprootSpendInfo::new_key_spend(
&secp,
self.untweaked_public_key.ok_or(Error::MissingPublicKey)?,
None,
);

Ok(Address::p2tr(
&secp,
taproot_info.internal_key(),
taproot_info.merkle_root(),
self.network.ok_or(Error::MissingNetwork)?,
))
}

fn update(&mut self, tx: Transaction) -> Result<(), Error> {
self.seen_tx = Some(tx);
Ok(())
}

fn raw(tx: Transaction) -> Result<Self, Error> {
Ok(Self {
untweaked_public_key: None,
network: None,
seen_tx: Some(tx),
})
}

fn was_seen(&self) -> bool {
self.seen_tx.is_some()
}
}
125 changes: 125 additions & 0 deletions src/bitcoin/taproot/lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use std::marker::PhantomData;

use bitcoin::blockdata::opcodes::all::{OP_CHECKSIG, OP_CHECKSIGADD, OP_EQUAL};
use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::{TxIn, TxOut};
use bitcoin::blockdata::witness::Witness;
use bitcoin::secp256k1::rand::thread_rng;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::util::psbt::PartiallySignedTransaction;
use bitcoin::Script;
use bitcoin::{Amount, KeyPair, XOnlyPublicKey};

use crate::script;
use crate::transaction::{Error as FError, Fundable, Lockable};

use crate::bitcoin::taproot::Taproot;
use crate::bitcoin::timelock::CSVTimelock;
use crate::bitcoin::transaction::{Error, MetadataOutput, SubTransaction, Tx};
use crate::bitcoin::Bitcoin;
use bitcoin::util::taproot::TaprootBuilder;

#[derive(Debug)]
pub struct Lock;

impl SubTransaction for Lock {
fn finalize(psbt: &mut PartiallySignedTransaction) -> Result<(), FError> {
//let (pubkey, full_sig) = psbt.inputs[0]
// .partial_sigs
// .iter()
// .next()
// .ok_or(FError::MissingSignature)?;
//psbt.inputs[0].final_script_witness = Some(Witness::from_vec(vec![
// full_sig.to_vec(),
// pubkey.serialize().to_vec(),
//]));
let sig = psbt.inputs[0].tap_key_sig.ok_or(FError::MissingSignature)?;
psbt.inputs[0].final_script_witness = Some(Witness::from_vec(vec![sig.to_vec()]));
Ok(())
}
}

impl Lockable<Bitcoin<Taproot>, MetadataOutput> for Tx<Lock> {
fn initialize(
prev: &impl Fundable<Bitcoin<Taproot>, MetadataOutput>,
lock: script::DataLock<Bitcoin<Taproot>>,
target_amount: Amount,
) -> Result<Self, FError> {
let secp = Secp256k1::new();
// FIXME: for a no key spend taproot tx choose a non-spendable internal key and not a
// random one as follow
let untweaked_public_key =
XOnlyPublicKey::from_keypair(&KeyPair::new(&secp, &mut thread_rng()));
let spend_info = TaprootBuilder::new()
// Buy script
.add_leaf(
1,
Builder::new()
.push_slice(lock.success.alice.serialize().as_ref())
.push_opcode(OP_CHECKSIG)
.push_slice(lock.success.bob.serialize().as_ref())
.push_opcode(OP_CHECKSIGADD)
.push_int(2)
.push_opcode(OP_EQUAL)
.into_script(),
)
// FIXME
.unwrap()
// Cancel script
.add_leaf(
1,
Builder::new()
.push_slice(lock.failure.alice.serialize().as_ref())
.push_opcode(OP_CHECKSIG)
.push_slice(lock.failure.bob.serialize().as_ref())
.push_opcode(OP_CHECKSIGADD)
.push_int(1) // FIXME this is just for making different script (same keys for now between success and failure)
.push_opcode(OP_EQUAL)
.into_script(),
)
// FIXME
.unwrap()
.finalize(&secp, untweaked_public_key)
.expect("Valid taproot FIXME");
println!("{:#?}", spend_info);
let tweaked_pubkey = spend_info.output_key();
let output_metadata = prev.get_consumable_output()?;

if output_metadata.tx_out.value < target_amount.as_sat() {
return Err(FError::NotEnoughAssets);
}

let unsigned_tx = bitcoin::Transaction {
version: 2,
lock_time: 0,
input: vec![TxIn {
previous_output: output_metadata.out_point,
script_sig: Script::default(),
sequence: CSVTimelock::disable(),
witness: Witness::new(),
}],
output: vec![TxOut {
value: target_amount.as_sat(),
script_pubkey: Script::new_v1_p2tr_tweaked(tweaked_pubkey),
}],
};

let mut psbt =
PartiallySignedTransaction::from_unsigned_tx(unsigned_tx).map_err(Error::from)?;

// Set the input witness data and sighash type
psbt.inputs[0].witness_utxo = Some(output_metadata.tx_out);
psbt.inputs[0].witness_script = output_metadata.script_pubkey;

// FIXME: add tap scripts in PSBT

Ok(Tx {
psbt,
_t: PhantomData,
})
}

fn verify_template(&self, _lock: script::DataLock<Bitcoin<Taproot>>) -> Result<(), FError> {
todo!()
}
}
30 changes: 27 additions & 3 deletions src/bitcoin/taproot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,27 @@ use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::str::FromStr;

use crate::bitcoin::taproot::{funding::Funding, lock::Lock};

use crate::bitcoin::transaction::Tx;
use crate::bitcoin::{Bitcoin, BitcoinTaproot, Btc, Strategy};
use crate::consensus::{self, CanonicalBytes};
use crate::crypto::{Keys, SharedKeyId, SharedSecretKeys, Signatures};
use bitcoin::util::taproot::TapSighashHash;
//use crate::role::Arbitrating;

use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::secp256k1::{constants::SECRET_KEY_SIZE, schnorr::Signature, KeyPair, XOnlyPublicKey};
use bitcoin::secp256k1::{
constants::SECRET_KEY_SIZE, schnorr::Signature, KeyPair, Message, Secp256k1, XOnlyPublicKey,
};

pub mod funding;
pub mod lock;

/// Funding the swap creating a Taproot (SegWit v1) output.
pub type FundingTx = Funding;

/// Locking the funding UTXO in a lock and allow buy or cancel transaction.
pub type LockTx = Tx<Lock>;

/// Inner type for the Taproot strategy with on-chain scripts.
#[derive(Clone, Debug, Copy, Eq, PartialEq)]
Expand Down Expand Up @@ -119,7 +133,7 @@ impl SharedSecretKeys for Bitcoin<Taproot> {
}

impl Signatures for Bitcoin<Taproot> {
type Message = Sha256dHash;
type Message = TapSighashHash;
type Signature = Signature;
type EncryptedSignature = Signature;
}
Expand All @@ -136,3 +150,13 @@ impl CanonicalBytes for Signature {
Signature::from_slice(bytes).map_err(consensus::Error::new)
}
}

/// Create a Schnorr signature for the given Taproot sighash
pub fn sign_hash(
sighash: TapSighashHash,
keypair: &bitcoin::secp256k1::KeyPair,
) -> Result<Signature, bitcoin::secp256k1::Error> {
let context = Secp256k1::new();
let msg = Message::from_slice(&sighash[..])?;
Ok(context.sign_schnorr(&msg, keypair))
}
Loading