1430 lines
44 KiB
1430 lines
44 KiB
use anyhow::bail;
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use bdk::bitcoin::hashes::*;
|
|
use bdk::bitcoin::Script;
|
|
|
|
use bdk::database::BatchDatabase;
|
|
use bdk::wallet::AddressIndex;
|
|
use bdk::{
|
|
bitcoin::{
|
|
self, hashes::hex::ToHex, util::bip143::SigHashCache, util::psbt::Global,
|
|
util::psbt::PartiallySignedTransaction, Address, Amount, Network, OutPoint, PublicKey,
|
|
SigHash, SigHashType, Transaction, TxIn, TxOut,
|
|
},
|
|
descriptor::Descriptor,
|
|
miniscript::{descriptor::Wsh, DescriptorTrait},
|
|
};
|
|
use bitcoin::PrivateKey;
|
|
use itertools::Itertools;
|
|
use secp256k1_zkp::EcdsaAdaptorSignature;
|
|
use secp256k1_zkp::SecretKey;
|
|
use secp256k1_zkp::SECP256K1;
|
|
use secp256k1_zkp::{self, schnorrsig, Signature};
|
|
use std::collections::HashMap;
|
|
|
|
/// In satoshi per vbyte.
|
|
const MIN_RELAY_FEE: u64 = 1;
|
|
|
|
/// In satoshi.
|
|
const P2PKH_DUST_LIMIT: u64 = 546;
|
|
|
|
pub trait WalletExt {
|
|
fn build_party_params(&self, amount: Amount, identity_pk: PublicKey) -> Result<PartyParams>;
|
|
}
|
|
|
|
impl<B, D> WalletExt for bdk::Wallet<B, D>
|
|
where
|
|
D: BatchDatabase,
|
|
{
|
|
fn build_party_params(&self, amount: Amount, identity_pk: PublicKey) -> Result<PartyParams> {
|
|
let mut builder = self.build_tx();
|
|
builder
|
|
.ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic)
|
|
.add_recipient(Script::new(), amount.as_sat());
|
|
let (lock_psbt, _) = builder.finish().unwrap();
|
|
let address = self.get_address(AddressIndex::New)?.address;
|
|
Ok(PartyParams {
|
|
lock_psbt,
|
|
identity_pk,
|
|
lock_amount: amount,
|
|
address,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn build_cfd_transactions(
|
|
(maker, maker_punish_params): (PartyParams, PunishParams),
|
|
(taker, taker_punish_params): (PartyParams, PunishParams),
|
|
oracle_params: OracleParams,
|
|
refund_timelock: u32,
|
|
payouts: Vec<Payout>,
|
|
identity_sk: SecretKey,
|
|
) -> Result<CfdTransactions> {
|
|
/// Relative timelock used for every CET.
|
|
///
|
|
/// This is used to allow parties to punish the publication of revoked commitment transactions.
|
|
///
|
|
/// TODO: Should this be an argument to this function?
|
|
const CET_TIMELOCK: u32 = 12;
|
|
|
|
let lock_tx = LockTransaction::new(
|
|
maker.lock_psbt.clone(),
|
|
taker.lock_psbt.clone(),
|
|
maker.identity_pk,
|
|
taker.identity_pk,
|
|
maker.lock_amount + taker.lock_amount,
|
|
)
|
|
.context("cannot build lock tx")?;
|
|
|
|
let commit_tx = CommitTransaction::new(
|
|
&lock_tx,
|
|
(
|
|
maker.identity_pk,
|
|
maker_punish_params.revocation_pk,
|
|
maker_punish_params.publish_pk,
|
|
),
|
|
(
|
|
taker.identity_pk,
|
|
taker_punish_params.revocation_pk,
|
|
taker_punish_params.publish_pk,
|
|
),
|
|
)
|
|
.context("cannot build commit tx")?;
|
|
|
|
let identity_pk = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &identity_sk);
|
|
let commit_encsig = if identity_pk == maker.identity_pk.key {
|
|
commit_tx.encsign(identity_sk, &taker_punish_params.publish_pk)?
|
|
} else if identity_pk == taker.identity_pk.key {
|
|
commit_tx.encsign(identity_sk, &maker_punish_params.publish_pk)?
|
|
} else {
|
|
bail!("identity sk does not belong to taker or maker")
|
|
};
|
|
|
|
let refund = {
|
|
let tx = RefundTransaction::new(
|
|
&commit_tx,
|
|
refund_timelock,
|
|
&maker.address,
|
|
&taker.address,
|
|
maker.lock_amount,
|
|
taker.lock_amount,
|
|
);
|
|
|
|
let sighash =
|
|
secp256k1_zkp::Message::from_slice(&tx.sighash()).expect("sighash is valid message");
|
|
let sig = SECP256K1.sign(&sighash, &identity_sk);
|
|
|
|
(tx.inner, sig)
|
|
};
|
|
|
|
let cets = payouts
|
|
.iter()
|
|
.map(|payout| {
|
|
let cet = ContractExecutionTransaction::new(
|
|
&commit_tx,
|
|
payout,
|
|
&maker.address,
|
|
&taker.address,
|
|
CET_TIMELOCK,
|
|
)?;
|
|
|
|
let encsig = cet.encsign(identity_sk, &oracle_params.pk, &oracle_params.nonce_pk)?;
|
|
|
|
Ok((cet.inner, encsig, payout.message))
|
|
})
|
|
.collect::<Result<Vec<_>>>()
|
|
.context("cannot build and sign all cets")?;
|
|
|
|
Ok(CfdTransactions {
|
|
lock: lock_tx.inner,
|
|
commit: (commit_tx.inner, commit_encsig),
|
|
cets,
|
|
refund,
|
|
})
|
|
}
|
|
|
|
fn lock_descriptor(maker_pk: PublicKey, taker_pk: PublicKey) -> Descriptor<PublicKey> {
|
|
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))";
|
|
|
|
let maker_pk = ToHex::to_hex(&maker_pk.key);
|
|
let taker_pk = ToHex::to_hex(&taker_pk.key);
|
|
|
|
let miniscript = MINISCRIPT_TEMPLATE
|
|
.replace("A", &maker_pk)
|
|
.replace("B", &taker_pk);
|
|
|
|
let miniscript = miniscript.parse().expect("a valid miniscript");
|
|
|
|
Descriptor::Wsh(Wsh::new(miniscript).expect("a valid descriptor"))
|
|
}
|
|
|
|
pub fn commit_descriptor(
|
|
(maker_own_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey),
|
|
(taker_own_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey),
|
|
) -> Descriptor<PublicKey> {
|
|
let maker_own_pk_hash = maker_own_pk.pubkey_hash().as_hash();
|
|
let maker_own_pk = (&maker_own_pk.key.serialize().to_vec()).to_hex();
|
|
let maker_publish_pk_hash = maker_publish_pk.pubkey_hash().as_hash();
|
|
let maker_rev_pk_hash = maker_rev_pk.pubkey_hash().as_hash();
|
|
|
|
let taker_own_pk_hash = taker_own_pk.pubkey_hash().as_hash();
|
|
let taker_own_pk = (&taker_own_pk.key.serialize().to_vec()).to_hex();
|
|
let taker_publish_pk_hash = taker_publish_pk.pubkey_hash().as_hash();
|
|
let taker_rev_pk_hash = taker_rev_pk.pubkey_hash().as_hash();
|
|
|
|
// raw script: or(and(pk(maker_own_pk),pk(taker_own_pk)),or(and(pk(maker_own_pk),and(pk(taker_publish_pk),pk(taker_rev_pk))),and(pk(taker_own_pk),and(pk(maker_publish_pk),pk(maker_rev_pk)))))
|
|
let full_script = format!("wsh(c:andor(pk({maker_own_pk}),pk_k({taker_own_pk}),or_i(and_v(v:pkh({maker_own_pk_hash}),and_v(v:pkh({taker_publish_pk_hash}),pk_h({taker_rev_pk_hash}))),and_v(v:pkh({taker_own_pk_hash}),and_v(v:pkh({maker_publish_pk_hash}),pk_h({maker_rev_pk_hash}))))))",
|
|
maker_own_pk = maker_own_pk,
|
|
taker_own_pk = taker_own_pk,
|
|
maker_own_pk_hash = maker_own_pk_hash,
|
|
taker_own_pk_hash = taker_own_pk_hash,
|
|
taker_publish_pk_hash = taker_publish_pk_hash,
|
|
taker_rev_pk_hash = taker_rev_pk_hash,
|
|
maker_publish_pk_hash = maker_publish_pk_hash,
|
|
maker_rev_pk_hash = maker_rev_pk_hash
|
|
);
|
|
|
|
full_script.parse().expect("a valid miniscript")
|
|
}
|
|
|
|
pub fn spending_tx_sighash(
|
|
spending_tx: &Transaction,
|
|
spent_descriptor: &Descriptor<PublicKey>,
|
|
spent_amount: Amount,
|
|
) -> secp256k1_zkp::Message {
|
|
let sighash = SigHashCache::new(spending_tx).signature_hash(
|
|
0,
|
|
&spent_descriptor.script_code(),
|
|
spent_amount.as_sat(),
|
|
SigHashType::All,
|
|
);
|
|
secp256k1_zkp::Message::from_slice(&sighash).expect("sighash is valid message")
|
|
}
|
|
|
|
pub fn finalize_spend_transaction(
|
|
mut tx: Transaction,
|
|
spent_descriptor: &Descriptor<PublicKey>,
|
|
(maker_pk, maker_sig): (PublicKey, Signature),
|
|
(taker_pk, taker_sig): (PublicKey, Signature),
|
|
) -> Result<Transaction> {
|
|
let satisfier = {
|
|
let mut satisfier = HashMap::with_capacity(2);
|
|
|
|
satisfier.insert(maker_pk, (maker_sig, SigHashType::All));
|
|
satisfier.insert(taker_pk, (taker_sig, SigHashType::All));
|
|
|
|
satisfier
|
|
};
|
|
|
|
let input = tx
|
|
.input
|
|
.iter_mut()
|
|
.exactly_one()
|
|
.expect("all spend transactions to have one input");
|
|
spent_descriptor.satisfy(input, satisfier)?;
|
|
|
|
Ok(tx)
|
|
}
|
|
|
|
pub fn punish_transaction(
|
|
commit_descriptor: &Descriptor<PublicKey>,
|
|
address: &Address,
|
|
encsig: EcdsaAdaptorSignature,
|
|
sk: SecretKey,
|
|
revocation_them_sk: SecretKey,
|
|
publish_them_pk: PublicKey,
|
|
revoked_commit_tx: &Transaction,
|
|
) -> Result<Transaction> {
|
|
// CommitTransaction has only one input
|
|
let input = revoked_commit_tx.input.clone().into_iter().exactly_one()?;
|
|
|
|
// Extract all signatures from witness stack
|
|
let mut sigs = Vec::new();
|
|
for witness in input.witness.iter() {
|
|
let witness = witness.as_slice();
|
|
|
|
let res = bitcoin::secp256k1::Signature::from_der(&witness[..witness.len() - 1]);
|
|
match res {
|
|
Ok(sig) => sigs.push(sig),
|
|
Err(_) => {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if sigs.is_empty() {
|
|
// TODO: No signature found, this should fail
|
|
unimplemented!()
|
|
}
|
|
|
|
// Attempt to extract y_other from every signature
|
|
let publish_them_sk = sigs
|
|
.into_iter()
|
|
.find_map(|sig| encsig.recover(SECP256K1, &sig, &publish_them_pk.key).ok())
|
|
.context("Could not recover secret key from revoked transaction")?;
|
|
|
|
let commit_vout = revoked_commit_tx
|
|
.output
|
|
.iter()
|
|
.position(|out| out.script_pubkey == commit_descriptor.script_pubkey())
|
|
.expect("to find commit output in commit tx");
|
|
let commit_amount = revoked_commit_tx.output[commit_vout].value;
|
|
|
|
// Fixme: need to subtract tx fee otherwise we won't be able to publish this transaction.
|
|
let mut punish_tx = {
|
|
let txid = revoked_commit_tx.txid();
|
|
|
|
let previous_output = OutPoint {
|
|
txid,
|
|
vout: commit_vout as u32,
|
|
};
|
|
|
|
let output = TxOut {
|
|
value: commit_amount,
|
|
script_pubkey: address.script_pubkey(),
|
|
};
|
|
Transaction {
|
|
version: 2,
|
|
lock_time: 0,
|
|
input: vec![TxIn {
|
|
previous_output,
|
|
..Default::default()
|
|
}],
|
|
output: vec![output],
|
|
}
|
|
};
|
|
|
|
let digest = SigHashCache::new(&punish_tx).signature_hash(
|
|
0,
|
|
&commit_descriptor.script_code(),
|
|
commit_amount,
|
|
SigHashType::All,
|
|
);
|
|
|
|
let satisfier = {
|
|
let mut satisfier = HashMap::with_capacity(3);
|
|
|
|
let pk = bitcoin::secp256k1::PublicKey::from_secret_key(SECP256K1, &sk);
|
|
let pk = bitcoin::PublicKey {
|
|
compressed: true,
|
|
key: pk,
|
|
};
|
|
let pk_hash = pk.pubkey_hash();
|
|
let sig_sk = SECP256K1.sign(&secp256k1_zkp::Message::from_slice(&digest)?, &sk);
|
|
|
|
let publish_them_pk_hash = publish_them_pk.pubkey_hash();
|
|
let sig_publish_other = SECP256K1.sign(
|
|
&secp256k1_zkp::Message::from_slice(&digest)?,
|
|
&publish_them_sk,
|
|
);
|
|
|
|
let revocation_them_pk = PublicKey::from_private_key(
|
|
SECP256K1,
|
|
&PrivateKey {
|
|
compressed: true,
|
|
network: Network::Regtest,
|
|
key: revocation_them_sk,
|
|
},
|
|
);
|
|
let revocation_them_pk_hash = revocation_them_pk.pubkey_hash();
|
|
let sig_revocation_other = SECP256K1.sign(
|
|
&secp256k1_zkp::Message::from_slice(&digest)?,
|
|
&revocation_them_sk,
|
|
);
|
|
|
|
satisfier.insert(pk_hash.as_hash(), (pk, (sig_sk, SigHashType::All)));
|
|
|
|
satisfier.insert(
|
|
publish_them_pk_hash.as_hash(),
|
|
(publish_them_pk, (sig_publish_other, SigHashType::All)),
|
|
);
|
|
satisfier.insert(
|
|
revocation_them_pk_hash.as_hash(),
|
|
(revocation_them_pk, (sig_revocation_other, SigHashType::All)),
|
|
);
|
|
|
|
satisfier
|
|
};
|
|
|
|
commit_descriptor.satisfy(&mut punish_tx.input[0], satisfier)?;
|
|
|
|
Ok(punish_tx)
|
|
}
|
|
|
|
// NOTE: We have decided to not offer any verification utility because
|
|
// the APIs would be incredibly thin
|
|
|
|
#[derive(Clone)]
|
|
pub struct PartyParams {
|
|
pub lock_psbt: PartiallySignedTransaction,
|
|
pub identity_pk: PublicKey,
|
|
pub lock_amount: Amount,
|
|
pub address: Address,
|
|
}
|
|
|
|
pub struct PunishParams {
|
|
pub revocation_pk: PublicKey,
|
|
pub publish_pk: PublicKey,
|
|
}
|
|
|
|
pub struct OracleParams {
|
|
pub pk: schnorrsig::PublicKey,
|
|
pub nonce_pk: schnorrsig::PublicKey,
|
|
}
|
|
|
|
pub struct CfdTransactions {
|
|
pub lock: PartiallySignedTransaction,
|
|
pub commit: (Transaction, EcdsaAdaptorSignature),
|
|
pub cets: Vec<(Transaction, EcdsaAdaptorSignature, Message)>,
|
|
pub refund: (Transaction, Signature),
|
|
}
|
|
|
|
// NOTE: This is a simplification. Our use-case will not work with
|
|
// a simple enumeration of possible messages
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct Payout {
|
|
message: Message,
|
|
maker_amount: Amount,
|
|
taker_amount: Amount,
|
|
}
|
|
|
|
impl Payout {
|
|
fn as_txouts(&self, maker_address: &Address, taker_address: &Address) -> Vec<TxOut> {
|
|
let txouts = [
|
|
(self.maker_amount, maker_address),
|
|
(self.taker_amount, taker_address),
|
|
]
|
|
.iter()
|
|
.filter_map(|(amount, address)| {
|
|
(amount >= &Amount::from_sat(P2PKH_DUST_LIMIT)).then(|| TxOut {
|
|
value: amount.as_sat(),
|
|
script_pubkey: address.script_pubkey(),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
txouts
|
|
}
|
|
|
|
/// Subtracts fee fairly from both outputs
|
|
///
|
|
/// We need to consider a few cases:
|
|
/// - If both amounts are >= DUST, they share the fee equally
|
|
/// - If one amount is < DUST, it set to 0 and the other output needs to cover for the fee.
|
|
fn with_updated_fee(self, fee: Amount) -> Result<Self> {
|
|
let mut updated = self;
|
|
let dust_limit = Amount::from_sat(P2PKH_DUST_LIMIT);
|
|
|
|
match (
|
|
self.maker_amount
|
|
.checked_sub(fee / 2)
|
|
.map(|a| a > dust_limit)
|
|
.unwrap_or(false),
|
|
self.taker_amount
|
|
.checked_sub(fee / 2)
|
|
.map(|a| a > dust_limit)
|
|
.unwrap_or(false),
|
|
) {
|
|
(true, true) => {
|
|
updated.maker_amount -= fee / 2;
|
|
updated.taker_amount -= fee / 2;
|
|
}
|
|
(false, true) => {
|
|
updated.maker_amount = Amount::ZERO;
|
|
updated.taker_amount = self.taker_amount - (fee + self.maker_amount);
|
|
}
|
|
(true, false) => {
|
|
updated.maker_amount = self.maker_amount - (fee + self.taker_amount);
|
|
updated.taker_amount = Amount::ZERO;
|
|
}
|
|
(false, false) => bail!("Amounts are too small, could not subtract fee."),
|
|
}
|
|
Ok(updated)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum Message {
|
|
Win,
|
|
Lose,
|
|
}
|
|
|
|
impl From<Message> for secp256k1_zkp::Message {
|
|
fn from(msg: Message) -> Self {
|
|
// TODO: Tag hash with prefix and other public data
|
|
secp256k1_zkp::Message::from_hashed_data::<secp256k1_zkp::bitcoin_hashes::sha256::Hash>(
|
|
msg.to_string().as_bytes(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl ToString for Message {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
Message::Win => "win".to_string(),
|
|
Message::Lose => "lose".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
const BIP340_MIDSTATE: [u8; 32] = [
|
|
0x9c, 0xec, 0xba, 0x11, 0x23, 0x92, 0x53, 0x81, 0x11, 0x67, 0x91, 0x12, 0xd1, 0x62, 0x7e, 0x0f,
|
|
0x97, 0xc8, 0x75, 0x50, 0x00, 0x3c, 0xc7, 0x65, 0x90, 0xf6, 0x11, 0x64, 0x33, 0xe9, 0xb6, 0x6a,
|
|
];
|
|
|
|
sha256t_hash_newtype!(
|
|
BIP340Hash,
|
|
BIP340HashTag,
|
|
BIP340_MIDSTATE,
|
|
64,
|
|
doc = "bip340 hash",
|
|
true
|
|
);
|
|
|
|
/// Compute a signature point for the given oracle public key, announcement nonce public key and message.
|
|
fn compute_signature_point(
|
|
oracle_pk: &schnorrsig::PublicKey,
|
|
nonce_pk: &schnorrsig::PublicKey,
|
|
message: Message,
|
|
) -> Result<secp256k1_zkp::PublicKey> {
|
|
fn schnorr_pubkey_to_pubkey(pk: &schnorrsig::PublicKey) -> Result<secp256k1_zkp::PublicKey> {
|
|
let mut buf = Vec::<u8>::with_capacity(33);
|
|
buf.push(0x02);
|
|
buf.extend(&pk.serialize());
|
|
Ok(secp256k1_zkp::PublicKey::from_slice(&buf)?)
|
|
}
|
|
|
|
let hash = {
|
|
let mut buf = Vec::<u8>::new();
|
|
buf.extend(&nonce_pk.serialize());
|
|
buf.extend(&oracle_pk.serialize());
|
|
buf.extend(secp256k1_zkp::Message::from(message).as_ref().to_vec());
|
|
BIP340Hash::hash(&buf).into_inner().to_vec()
|
|
};
|
|
let mut oracle_pk = schnorr_pubkey_to_pubkey(oracle_pk)?;
|
|
oracle_pk.mul_assign(SECP256K1, &hash)?;
|
|
let nonce_pk = schnorr_pubkey_to_pubkey(nonce_pk)?;
|
|
Ok(nonce_pk.combine(&oracle_pk)?)
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct ContractExecutionTransaction {
|
|
inner: Transaction,
|
|
message: Message,
|
|
sighash: SigHash,
|
|
commit_descriptor: Descriptor<PublicKey>,
|
|
}
|
|
|
|
impl ContractExecutionTransaction {
|
|
fn new(
|
|
commit_tx: &CommitTransaction,
|
|
payout: &Payout,
|
|
maker_address: &Address,
|
|
taker_address: &Address,
|
|
relative_timelock_in_blocks: u32,
|
|
) -> Result<Self> {
|
|
let commit_input = TxIn {
|
|
previous_output: commit_tx.outpoint(),
|
|
sequence: relative_timelock_in_blocks,
|
|
..Default::default()
|
|
};
|
|
|
|
let mut tx = Transaction {
|
|
version: 2,
|
|
lock_time: 0,
|
|
input: vec![commit_input],
|
|
output: payout.as_txouts(maker_address, taker_address),
|
|
};
|
|
|
|
let fee = tx.get_size() * MIN_RELAY_FEE as usize;
|
|
let payout = payout.with_updated_fee(Amount::from_sat(fee as u64))?;
|
|
tx.output = payout.as_txouts(maker_address, taker_address);
|
|
|
|
let sighash = SigHashCache::new(&tx).signature_hash(
|
|
0,
|
|
&commit_tx.descriptor.script_code(),
|
|
commit_tx.amount.as_sat(),
|
|
SigHashType::All,
|
|
);
|
|
|
|
Ok(Self {
|
|
inner: tx,
|
|
message: payout.message,
|
|
sighash,
|
|
commit_descriptor: commit_tx.descriptor(),
|
|
})
|
|
}
|
|
|
|
fn encsign(
|
|
&self,
|
|
sk: SecretKey,
|
|
oracle_pk: &schnorrsig::PublicKey,
|
|
nonce_pk: &schnorrsig::PublicKey,
|
|
) -> Result<EcdsaAdaptorSignature> {
|
|
let signature_point = compute_signature_point(oracle_pk, nonce_pk, self.message)?;
|
|
|
|
Ok(EcdsaAdaptorSignature::encrypt(
|
|
SECP256K1,
|
|
&secp256k1_zkp::Message::from_slice(&self.sighash).expect("sighash is valid message"),
|
|
&sk,
|
|
&signature_point,
|
|
))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct RefundTransaction {
|
|
inner: Transaction,
|
|
sighash: SigHash,
|
|
commit_output_descriptor: Descriptor<PublicKey>,
|
|
}
|
|
|
|
impl RefundTransaction {
|
|
/// Refund transaction fee. It is paid evenly by the maker and the
|
|
/// taker.
|
|
///
|
|
/// Ideally we don't commit to a transaction fee ahead of time and
|
|
/// instead resort to fee-bumping. But that would be unfair for the
|
|
/// party that executes the fee-bumping.
|
|
///
|
|
/// TODO: Calculate reasonable fee given the fact that the
|
|
/// transaction consists of 1 input and 2 outputs.
|
|
const REFUND_TX_FEE: u64 = 10_000;
|
|
|
|
fn new(
|
|
commit_tx: &CommitTransaction,
|
|
relative_locktime_in_blocks: u32,
|
|
maker_address: &Address,
|
|
taker_address: &Address,
|
|
maker_amount: Amount,
|
|
taker_amount: Amount,
|
|
) -> Self {
|
|
let commit_input = TxIn {
|
|
previous_output: commit_tx.outpoint(),
|
|
sequence: relative_locktime_in_blocks,
|
|
..Default::default()
|
|
};
|
|
|
|
let per_party_fee = Self::REFUND_TX_FEE / 2;
|
|
|
|
let maker_output = TxOut {
|
|
value: maker_amount.as_sat() - per_party_fee,
|
|
script_pubkey: maker_address.script_pubkey(),
|
|
};
|
|
|
|
let taker_output = TxOut {
|
|
value: taker_amount.as_sat() - per_party_fee,
|
|
script_pubkey: taker_address.script_pubkey(),
|
|
};
|
|
|
|
let tx = Transaction {
|
|
version: 2,
|
|
lock_time: 0,
|
|
input: vec![commit_input],
|
|
output: vec![maker_output, taker_output],
|
|
};
|
|
|
|
let commit_output_descriptor = commit_tx.descriptor();
|
|
|
|
let sighash = SigHashCache::new(&tx).signature_hash(
|
|
0,
|
|
&commit_tx.descriptor().script_code(),
|
|
commit_tx.amount().as_sat(),
|
|
SigHashType::All,
|
|
);
|
|
|
|
Self {
|
|
inner: tx,
|
|
sighash,
|
|
commit_output_descriptor,
|
|
}
|
|
}
|
|
|
|
fn sighash(&self) -> SigHash {
|
|
self.sighash
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct CommitTransaction {
|
|
inner: Transaction,
|
|
descriptor: Descriptor<PublicKey>,
|
|
amount: Amount,
|
|
sighash: SigHash,
|
|
lock_descriptor: Descriptor<PublicKey>,
|
|
}
|
|
|
|
impl CommitTransaction {
|
|
fn new(
|
|
lock_tx: &LockTransaction,
|
|
(maker_own_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey),
|
|
(taker_own_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey),
|
|
) -> Result<Self> {
|
|
// FIXME: Fee to be paid by leftover lock output
|
|
let amount = lock_tx.amount();
|
|
|
|
let lock_input = TxIn {
|
|
previous_output: lock_tx.lock_outpoint(),
|
|
..Default::default()
|
|
};
|
|
|
|
let descriptor = commit_descriptor(
|
|
(maker_own_pk, maker_rev_pk, maker_publish_pk),
|
|
(taker_own_pk, taker_rev_pk, taker_publish_pk),
|
|
);
|
|
|
|
let output = TxOut {
|
|
value: lock_tx.amount().as_sat(),
|
|
script_pubkey: descriptor
|
|
.address(Network::Regtest)
|
|
.expect("can derive address from descriptor")
|
|
.script_pubkey(),
|
|
};
|
|
|
|
let inner = Transaction {
|
|
version: 2,
|
|
lock_time: 0,
|
|
input: vec![lock_input],
|
|
output: vec![output],
|
|
};
|
|
|
|
let sighash = SigHashCache::new(&inner).signature_hash(
|
|
0,
|
|
&lock_tx.descriptor().script_code(),
|
|
lock_tx.amount().as_sat(),
|
|
SigHashType::All,
|
|
);
|
|
|
|
Ok(Self {
|
|
inner,
|
|
descriptor,
|
|
lock_descriptor: lock_tx.descriptor(),
|
|
amount,
|
|
sighash,
|
|
})
|
|
}
|
|
|
|
fn encsign(&self, sk: SecretKey, publish_them_pk: &PublicKey) -> Result<EcdsaAdaptorSignature> {
|
|
Ok(EcdsaAdaptorSignature::encrypt(
|
|
SECP256K1,
|
|
&secp256k1_zkp::Message::from_slice(&self.sighash).expect("sighash is valid message"),
|
|
&sk,
|
|
&publish_them_pk.key,
|
|
))
|
|
}
|
|
|
|
fn outpoint(&self) -> OutPoint {
|
|
let txid = self.inner.txid();
|
|
let vout = self
|
|
.inner
|
|
.output
|
|
.iter()
|
|
.position(|out| out.script_pubkey == self.descriptor.script_pubkey())
|
|
.expect("to find commit output in commit tx");
|
|
|
|
OutPoint {
|
|
txid,
|
|
vout: vout as u32,
|
|
}
|
|
}
|
|
|
|
fn amount(&self) -> Amount {
|
|
self.amount
|
|
}
|
|
|
|
fn descriptor(&self) -> Descriptor<PublicKey> {
|
|
self.descriptor.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct LockTransaction {
|
|
inner: PartiallySignedTransaction,
|
|
lock_descriptor: Descriptor<PublicKey>,
|
|
amount: Amount,
|
|
}
|
|
|
|
impl LockTransaction {
|
|
fn new(
|
|
maker_psbt: PartiallySignedTransaction,
|
|
taker_psbt: PartiallySignedTransaction,
|
|
maker_pk: PublicKey,
|
|
taker_pk: PublicKey,
|
|
amount: Amount,
|
|
) -> Result<Self> {
|
|
let lock_descriptor = lock_descriptor(maker_pk, taker_pk);
|
|
|
|
let maker_change = maker_psbt
|
|
.global
|
|
.unsigned_tx
|
|
.output
|
|
.into_iter()
|
|
.filter(|out| !out.script_pubkey.is_empty())
|
|
.collect::<Vec<_>>();
|
|
|
|
let taker_change = taker_psbt
|
|
.global
|
|
.unsigned_tx
|
|
.output
|
|
.into_iter()
|
|
.filter(|out| !out.script_pubkey.is_empty())
|
|
.collect();
|
|
|
|
let lock_output = TxOut {
|
|
value: amount.as_sat(),
|
|
script_pubkey: lock_descriptor
|
|
.address(Network::Regtest)
|
|
.expect("can derive address from descriptor")
|
|
.script_pubkey(),
|
|
};
|
|
|
|
let lock_tx = Transaction {
|
|
version: 2,
|
|
lock_time: 0,
|
|
input: vec![
|
|
maker_psbt.global.unsigned_tx.input,
|
|
taker_psbt.global.unsigned_tx.input,
|
|
]
|
|
.concat(),
|
|
output: vec![vec![lock_output], maker_change, taker_change].concat(),
|
|
};
|
|
|
|
let inner = PartiallySignedTransaction {
|
|
global: Global::from_unsigned_tx(lock_tx)?,
|
|
inputs: vec![maker_psbt.inputs, taker_psbt.inputs].concat(),
|
|
outputs: vec![maker_psbt.outputs, taker_psbt.outputs].concat(),
|
|
};
|
|
|
|
Ok(Self {
|
|
inner,
|
|
lock_descriptor,
|
|
amount,
|
|
})
|
|
}
|
|
|
|
fn lock_outpoint(&self) -> OutPoint {
|
|
let txid = self.inner.global.unsigned_tx.txid();
|
|
let vout = self
|
|
.inner
|
|
.global
|
|
.unsigned_tx
|
|
.output
|
|
.iter()
|
|
.position(|out| out.script_pubkey == self.lock_descriptor.script_pubkey())
|
|
.expect("to find lock output in lock tx");
|
|
|
|
OutPoint {
|
|
txid,
|
|
vout: vout as u32,
|
|
}
|
|
}
|
|
|
|
fn descriptor(&self) -> Descriptor<PublicKey> {
|
|
self.lock_descriptor.clone()
|
|
}
|
|
|
|
fn amount(&self) -> Amount {
|
|
self.amount
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use bdk::bitcoin::{util::bip32::ExtendedPrivKey, PrivateKey};
|
|
use bdk::wallet::AddressIndex;
|
|
use bdk::SignOptions;
|
|
use rand::SeedableRng;
|
|
use rand::{CryptoRng, RngCore};
|
|
use rand_chacha::ChaChaRng;
|
|
|
|
#[test]
|
|
fn run_cfd_protocol() {
|
|
let mut rng = ChaChaRng::seed_from_u64(0);
|
|
|
|
let maker_lock_amount = Amount::ONE_BTC;
|
|
let taker_lock_amount = Amount::ONE_BTC;
|
|
|
|
let oracle = Oracle::new(&mut rng);
|
|
let (event, announcement) = announce(&mut rng);
|
|
|
|
let (maker_sk, maker_pk) = make_keypair(&mut rng);
|
|
let (taker_sk, taker_pk) = make_keypair(&mut rng);
|
|
|
|
let payouts = vec![
|
|
Payout {
|
|
message: Message::Win,
|
|
maker_amount: Amount::from_btc(2.0).unwrap(),
|
|
taker_amount: Amount::ZERO,
|
|
},
|
|
Payout {
|
|
message: Message::Lose,
|
|
maker_amount: Amount::ZERO,
|
|
taker_amount: Amount::from_btc(2.0).unwrap(),
|
|
},
|
|
];
|
|
|
|
let refund_timelock = 0;
|
|
|
|
let maker_wallet = build_wallet(&mut rng, Amount::from_btc(0.4).unwrap(), 5).unwrap();
|
|
let taker_wallet = build_wallet(&mut rng, Amount::from_btc(0.4).unwrap(), 5).unwrap();
|
|
|
|
let maker_address = maker_wallet.get_address(AddressIndex::New).unwrap();
|
|
let taker_address = taker_wallet.get_address(AddressIndex::New).unwrap();
|
|
|
|
let lock_amount = maker_lock_amount + taker_lock_amount;
|
|
let (maker_revocation_sk, maker_revocation_pk) = make_keypair(&mut rng);
|
|
let (maker_publish_sk, maker_publish_pk) = make_keypair(&mut rng);
|
|
|
|
let (taker_revocation_sk, taker_revocation_pk) = make_keypair(&mut rng);
|
|
let (taker_publish_sk, taker_publish_pk) = make_keypair(&mut rng);
|
|
|
|
let maker_params = maker_wallet
|
|
.build_party_params(maker_lock_amount, maker_pk)
|
|
.unwrap();
|
|
let taker_params = taker_wallet
|
|
.build_party_params(taker_lock_amount, taker_pk)
|
|
.unwrap();
|
|
|
|
let maker_cfd_txs = build_cfd_transactions(
|
|
(
|
|
maker_params.clone(),
|
|
PunishParams {
|
|
revocation_pk: maker_revocation_pk,
|
|
publish_pk: maker_publish_pk,
|
|
},
|
|
),
|
|
(
|
|
taker_params.clone(),
|
|
PunishParams {
|
|
revocation_pk: taker_revocation_pk,
|
|
publish_pk: taker_publish_pk,
|
|
},
|
|
),
|
|
OracleParams {
|
|
pk: oracle.public_key(),
|
|
nonce_pk: event.nonce_pk,
|
|
},
|
|
refund_timelock,
|
|
payouts.clone(),
|
|
maker_sk,
|
|
)
|
|
.unwrap();
|
|
|
|
let taker_cfd_txs = build_cfd_transactions(
|
|
(
|
|
maker_params,
|
|
PunishParams {
|
|
revocation_pk: maker_revocation_pk,
|
|
publish_pk: maker_publish_pk,
|
|
},
|
|
),
|
|
(
|
|
taker_params,
|
|
PunishParams {
|
|
revocation_pk: taker_revocation_pk,
|
|
publish_pk: taker_publish_pk,
|
|
},
|
|
),
|
|
OracleParams {
|
|
pk: oracle.public_key(),
|
|
nonce_pk: event.nonce_pk,
|
|
},
|
|
refund_timelock,
|
|
payouts,
|
|
taker_sk,
|
|
)
|
|
.unwrap();
|
|
|
|
let commit_descriptor = commit_descriptor(
|
|
(maker_pk, maker_revocation_pk, maker_publish_pk),
|
|
(taker_pk, taker_revocation_pk, taker_publish_pk),
|
|
);
|
|
|
|
let commit_amount = Amount::from_sat(maker_cfd_txs.commit.0.output[0].value);
|
|
assert_eq!(
|
|
commit_amount.as_sat(),
|
|
taker_cfd_txs.commit.0.output[0].value
|
|
);
|
|
|
|
{
|
|
let refund_sighash =
|
|
spending_tx_sighash(&taker_cfd_txs.refund.0, &commit_descriptor, commit_amount);
|
|
SECP256K1
|
|
.verify(&refund_sighash, &maker_cfd_txs.refund.1, &maker_pk.key)
|
|
.expect("valid maker refund sig")
|
|
};
|
|
|
|
{
|
|
let refund_sighash =
|
|
spending_tx_sighash(&maker_cfd_txs.refund.0, &commit_descriptor, commit_amount);
|
|
SECP256K1
|
|
.verify(&refund_sighash, &taker_cfd_txs.refund.1, &taker_pk.key)
|
|
.expect("valid taker refund sig")
|
|
};
|
|
|
|
// TODO: We should not rely on order
|
|
for (maker_cet, taker_cet) in maker_cfd_txs.cets.iter().zip(taker_cfd_txs.cets.iter()) {
|
|
let cet_sighash = {
|
|
let maker_sighash =
|
|
spending_tx_sighash(&maker_cet.0, &commit_descriptor, commit_amount);
|
|
let taker_sighash =
|
|
spending_tx_sighash(&taker_cet.0, &commit_descriptor, commit_amount);
|
|
|
|
assert_eq!(maker_sighash, taker_sighash);
|
|
maker_sighash
|
|
};
|
|
|
|
let encryption_point = {
|
|
let maker_encryption_point = compute_signature_point(
|
|
&oracle.public_key(),
|
|
&announcement.nonce_pk(),
|
|
maker_cet.2,
|
|
)
|
|
.unwrap();
|
|
let taker_encryption_point = compute_signature_point(
|
|
&oracle.public_key(),
|
|
&announcement.nonce_pk(),
|
|
taker_cet.2,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(maker_encryption_point, taker_encryption_point);
|
|
maker_encryption_point
|
|
};
|
|
|
|
let maker_encsig = maker_cet.1;
|
|
maker_encsig
|
|
.verify(SECP256K1, &cet_sighash, &maker_pk.key, &encryption_point)
|
|
.expect("valid maker cet encsig");
|
|
|
|
let taker_encsig = taker_cet.1;
|
|
taker_encsig
|
|
.verify(SECP256K1, &cet_sighash, &taker_pk.key, &encryption_point)
|
|
.expect("valid taker cet encsig");
|
|
}
|
|
|
|
let lock_descriptor = lock_descriptor(maker_pk, taker_pk);
|
|
|
|
{
|
|
let commit_sighash =
|
|
spending_tx_sighash(&maker_cfd_txs.commit.0, &lock_descriptor, lock_amount);
|
|
let commit_encsig = maker_cfd_txs.commit.1;
|
|
commit_encsig
|
|
.verify(
|
|
SECP256K1,
|
|
&commit_sighash,
|
|
&maker_pk.key,
|
|
&taker_publish_pk.key,
|
|
)
|
|
.expect("valid maker commit encsig");
|
|
};
|
|
|
|
{
|
|
let commit_sighash =
|
|
spending_tx_sighash(&taker_cfd_txs.commit.0, &lock_descriptor, lock_amount);
|
|
let commit_encsig = taker_cfd_txs.commit.1;
|
|
commit_encsig
|
|
.verify(
|
|
SECP256K1,
|
|
&commit_sighash,
|
|
&taker_pk.key,
|
|
&maker_publish_pk.key,
|
|
)
|
|
.expect("valid taker commit encsig");
|
|
};
|
|
|
|
// sign lock transaction
|
|
|
|
let mut signed_lock_tx = maker_cfd_txs.lock;
|
|
maker_wallet
|
|
.sign(
|
|
&mut signed_lock_tx,
|
|
SignOptions {
|
|
trust_witness_utxo: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
taker_wallet
|
|
.sign(
|
|
&mut signed_lock_tx,
|
|
SignOptions {
|
|
trust_witness_utxo: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
// verify commit transaction
|
|
let commit_tx = maker_cfd_txs.commit.0;
|
|
let maker_sig = maker_cfd_txs.commit.1.decrypt(&taker_publish_sk).unwrap();
|
|
let taker_sig = taker_cfd_txs.commit.1.decrypt(&maker_publish_sk).unwrap();
|
|
let signed_commit_tx = finalize_spend_transaction(
|
|
commit_tx,
|
|
&lock_descriptor,
|
|
(maker_pk, maker_sig),
|
|
(taker_pk, taker_sig),
|
|
)
|
|
.expect("To be signed");
|
|
|
|
lock_descriptor
|
|
.address(Network::Regtest)
|
|
.expect("can derive address from descriptor")
|
|
.script_pubkey()
|
|
.verify(
|
|
0,
|
|
lock_amount.as_sat(),
|
|
bitcoin::consensus::serialize(&signed_commit_tx).as_slice(),
|
|
)
|
|
.expect("valid signed commit transaction");
|
|
|
|
// verify commit transaction
|
|
|
|
let maker_sig = maker_cfd_txs.refund.1;
|
|
let taker_sig = taker_cfd_txs.refund.1;
|
|
let signed_refund_tx = finalize_spend_transaction(
|
|
maker_cfd_txs.refund.0,
|
|
&commit_descriptor,
|
|
(maker_pk, maker_sig),
|
|
(taker_pk, taker_sig),
|
|
)
|
|
.expect("To be signed");
|
|
|
|
commit_descriptor
|
|
.address(Network::Regtest)
|
|
.expect("can derive address from descriptor")
|
|
.script_pubkey()
|
|
.verify(
|
|
0,
|
|
commit_amount.as_sat(),
|
|
bitcoin::consensus::serialize(&signed_refund_tx).as_slice(),
|
|
)
|
|
.expect("valid signed refund transaction");
|
|
|
|
// verify cets
|
|
|
|
let attestations = [Message::Win, Message::Lose]
|
|
.iter()
|
|
.map(|msg| (*msg, oracle.attest(&event, *msg)))
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
maker_cfd_txs
|
|
.cets
|
|
.into_iter()
|
|
.zip(taker_cfd_txs.cets)
|
|
.try_for_each(|((cet, maker_encsig, msg), (_, taker_encsig, _))| {
|
|
let oracle_sig = attestations
|
|
.get(&msg)
|
|
.expect("oracle to sign all messages in test");
|
|
let (_nonce_pk, signature_scalar) = schnorrsig_decompose(oracle_sig);
|
|
|
|
let maker_sig = maker_encsig
|
|
.decrypt(&signature_scalar)
|
|
.context("could not decrypt maker encsig on cet")?;
|
|
let taker_sig = taker_encsig
|
|
.decrypt(&signature_scalar)
|
|
.context("could not decrypt taker encsig on cet")?;
|
|
|
|
let signed_cet = finalize_spend_transaction(
|
|
cet,
|
|
&commit_descriptor,
|
|
(maker_pk, maker_sig),
|
|
(taker_pk, taker_sig),
|
|
)?;
|
|
|
|
commit_descriptor
|
|
.address(Network::Regtest)
|
|
.expect("can derive address from descriptor")
|
|
.script_pubkey()
|
|
.verify(
|
|
0,
|
|
commit_amount.as_sat(),
|
|
bitcoin::consensus::serialize(&signed_cet).as_slice(),
|
|
)
|
|
.context("failed to verify cet")
|
|
})
|
|
.expect("all cets to be properly signed");
|
|
|
|
// verify punishment transactions
|
|
|
|
let punish_tx = punish_transaction(
|
|
&commit_descriptor,
|
|
&maker_address,
|
|
maker_cfd_txs.commit.1,
|
|
maker_sk,
|
|
taker_revocation_sk,
|
|
taker_publish_pk,
|
|
&signed_commit_tx,
|
|
)
|
|
.unwrap();
|
|
|
|
commit_descriptor
|
|
.address(Network::Regtest)
|
|
.expect("can derive address from descriptor")
|
|
.script_pubkey()
|
|
.verify(
|
|
0,
|
|
commit_amount.as_sat(),
|
|
bitcoin::consensus::serialize(&punish_tx).as_slice(),
|
|
)
|
|
.expect("valid punish transaction signed by maker");
|
|
|
|
let punish_tx = punish_transaction(
|
|
&commit_descriptor,
|
|
&taker_address,
|
|
taker_cfd_txs.commit.1,
|
|
taker_sk,
|
|
maker_revocation_sk,
|
|
maker_publish_pk,
|
|
&signed_commit_tx,
|
|
)
|
|
.unwrap();
|
|
|
|
commit_descriptor
|
|
.address(Network::Regtest)
|
|
.expect("can derive address from descriptor")
|
|
.script_pubkey()
|
|
.verify(
|
|
0,
|
|
commit_amount.as_sat(),
|
|
bitcoin::consensus::serialize(&punish_tx).as_slice(),
|
|
)
|
|
.expect("valid punish transaction signed by taker");
|
|
}
|
|
|
|
#[test]
|
|
fn test_fee_subtraction_bigger_than_dust() {
|
|
let orig_maker_amount = 1000;
|
|
let orig_taker_amount = 1000;
|
|
let payout = Payout {
|
|
message: Message::Win,
|
|
maker_amount: Amount::from_sat(orig_maker_amount),
|
|
taker_amount: Amount::from_sat(orig_taker_amount),
|
|
};
|
|
let fee = 100;
|
|
let updated_payout = payout.with_updated_fee(Amount::from_sat(fee)).unwrap();
|
|
|
|
assert_eq!(
|
|
updated_payout.maker_amount,
|
|
Amount::from_sat(orig_maker_amount - fee / 2)
|
|
);
|
|
assert_eq!(
|
|
updated_payout.taker_amount,
|
|
Amount::from_sat(orig_taker_amount - fee / 2)
|
|
);
|
|
}
|
|
|
|
// TODO add proptest for this
|
|
#[test]
|
|
fn test_fee_subtraction_smaller_than_dust() {
|
|
let orig_maker_amount = P2PKH_DUST_LIMIT - 1;
|
|
let orig_taker_amount = 1000;
|
|
let payout = Payout {
|
|
message: Message::Win,
|
|
maker_amount: Amount::from_sat(orig_maker_amount),
|
|
taker_amount: Amount::from_sat(orig_taker_amount),
|
|
};
|
|
let fee = 100;
|
|
let updated_payout = payout.with_updated_fee(Amount::from_sat(fee)).unwrap();
|
|
|
|
assert_eq!(updated_payout.maker_amount, Amount::from_sat(0));
|
|
assert_eq!(
|
|
updated_payout.taker_amount,
|
|
Amount::from_sat(orig_taker_amount - (fee + orig_maker_amount))
|
|
);
|
|
}
|
|
|
|
fn build_wallet<R>(
|
|
rng: &mut R,
|
|
utxo_amount: Amount,
|
|
num_utxos: u8,
|
|
) -> Result<bdk::Wallet<(), bdk::database::MemoryDatabase>>
|
|
where
|
|
R: RngCore + CryptoRng,
|
|
{
|
|
use bdk::populate_test_db;
|
|
use bdk::testutils;
|
|
|
|
let mut seed = [0u8; 32];
|
|
rng.fill_bytes(&mut seed);
|
|
|
|
let key = ExtendedPrivKey::new_master(Network::Regtest, &seed)?;
|
|
let descriptors = testutils!(@descriptors (&format!("wpkh({}/*)", key)));
|
|
|
|
let mut database = bdk::database::MemoryDatabase::new();
|
|
|
|
for index in 0..num_utxos {
|
|
populate_test_db!(
|
|
&mut database,
|
|
testutils! {
|
|
@tx ( (@external descriptors, index as u32) => utxo_amount.as_sat() ) (@confirmations 1)
|
|
},
|
|
Some(100)
|
|
);
|
|
}
|
|
|
|
let wallet = bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database)?;
|
|
|
|
Ok(wallet)
|
|
}
|
|
|
|
struct Oracle {
|
|
key_pair: schnorrsig::KeyPair,
|
|
}
|
|
|
|
impl Oracle {
|
|
fn new<R>(rng: &mut R) -> Self
|
|
where
|
|
R: RngCore + CryptoRng,
|
|
{
|
|
let key_pair = schnorrsig::KeyPair::new(SECP256K1, rng);
|
|
|
|
Self { key_pair }
|
|
}
|
|
|
|
fn public_key(&self) -> schnorrsig::PublicKey {
|
|
schnorrsig::PublicKey::from_keypair(SECP256K1, &self.key_pair)
|
|
}
|
|
|
|
fn attest(&self, event: &Event, msg: Message) -> schnorrsig::Signature {
|
|
secp_utils::schnorr_sign_with_nonce(&msg.into(), &self.key_pair, &event.nonce)
|
|
}
|
|
}
|
|
|
|
fn announce<R>(rng: &mut R) -> (Event, Announcement)
|
|
where
|
|
R: RngCore + CryptoRng,
|
|
{
|
|
let event = Event::new(rng);
|
|
let announcement = event.announcement();
|
|
|
|
(event, announcement)
|
|
}
|
|
|
|
/// Represents the oracle's commitment to a nonce that will be used to
|
|
/// sign a specific event in the future.
|
|
struct Event {
|
|
/// Nonce.
|
|
///
|
|
/// Must remain secret.
|
|
nonce: SecretKey,
|
|
nonce_pk: schnorrsig::PublicKey,
|
|
}
|
|
|
|
impl Event {
|
|
fn new<R>(rng: &mut R) -> Self
|
|
where
|
|
R: RngCore + CryptoRng,
|
|
{
|
|
let nonce = SecretKey::new(rng);
|
|
|
|
let key_pair = schnorrsig::KeyPair::from_secret_key(SECP256K1, nonce);
|
|
let nonce_pk = schnorrsig::PublicKey::from_keypair(SECP256K1, &key_pair);
|
|
|
|
Self { nonce, nonce_pk }
|
|
}
|
|
|
|
fn announcement(&self) -> Announcement {
|
|
Announcement {
|
|
nonce_pk: self.nonce_pk,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Public message which can be used by anyone to perform a DLC
|
|
/// protocol based on a specific event.
|
|
///
|
|
/// These would normally include more information to identify the
|
|
/// specific event, but we omit this for simplicity. See:
|
|
/// https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md#oracle-events
|
|
#[derive(Clone, Copy)]
|
|
struct Announcement {
|
|
nonce_pk: schnorrsig::PublicKey,
|
|
}
|
|
|
|
impl Announcement {
|
|
fn nonce_pk(&self) -> schnorrsig::PublicKey {
|
|
self.nonce_pk
|
|
}
|
|
}
|
|
|
|
fn make_keypair<R>(rng: &mut R) -> (SecretKey, PublicKey)
|
|
where
|
|
R: RngCore + CryptoRng,
|
|
{
|
|
let sk = SecretKey::new(rng);
|
|
let pk = PublicKey::from_private_key(
|
|
SECP256K1,
|
|
&PrivateKey {
|
|
compressed: true,
|
|
network: Network::Regtest,
|
|
key: sk,
|
|
},
|
|
);
|
|
|
|
(sk, pk)
|
|
}
|
|
|
|
/// Decompose a BIP340 signature into R and s.
|
|
pub fn schnorrsig_decompose(
|
|
signature: &schnorrsig::Signature,
|
|
) -> (schnorrsig::PublicKey, SecretKey) {
|
|
let bytes = signature.as_ref();
|
|
|
|
let nonce_pk = schnorrsig::PublicKey::from_slice(&bytes[0..32]).expect("R value in sig");
|
|
let s = SecretKey::from_slice(&bytes[32..64]).expect("s value in sig");
|
|
|
|
(nonce_pk, s)
|
|
}
|
|
|
|
mod secp_utils {
|
|
use super::*;
|
|
|
|
use secp256k1_zkp::secp256k1_zkp_sys::types::c_void;
|
|
use secp256k1_zkp::secp256k1_zkp_sys::CPtr;
|
|
use std::os::raw::c_int;
|
|
use std::os::raw::c_uchar;
|
|
use std::ptr;
|
|
|
|
/// Create a Schnorr signature using the provided nonce instead of generating one.
|
|
pub fn schnorr_sign_with_nonce(
|
|
msg: &secp256k1_zkp::Message,
|
|
keypair: &schnorrsig::KeyPair,
|
|
nonce: &SecretKey,
|
|
) -> schnorrsig::Signature {
|
|
unsafe {
|
|
let mut sig = [0u8; secp256k1_zkp::constants::SCHNORRSIG_SIGNATURE_SIZE];
|
|
assert_eq!(
|
|
1,
|
|
secp256k1_zkp::ffi::secp256k1_schnorrsig_sign(
|
|
*SECP256K1.ctx(),
|
|
sig.as_mut_c_ptr(),
|
|
msg.as_c_ptr(),
|
|
keypair.as_ptr(),
|
|
Some(constant_nonce_fn),
|
|
nonce.as_c_ptr() as *const c_void
|
|
)
|
|
);
|
|
|
|
schnorrsig::Signature::from_slice(&sig).unwrap()
|
|
}
|
|
}
|
|
|
|
extern "C" fn constant_nonce_fn(
|
|
nonce32: *mut c_uchar,
|
|
_msg32: *const c_uchar,
|
|
_key32: *const c_uchar,
|
|
_xonly_pk32: *const c_uchar,
|
|
_algo16: *const c_uchar,
|
|
data: *mut c_void,
|
|
) -> c_int {
|
|
unsafe {
|
|
ptr::copy_nonoverlapping(data as *const c_uchar, nonce32, 32);
|
|
}
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|