#[cfg(test)]
extern crate link_cplusplus;
mod util;
use std::io::{self, Cursor, Seek};
use bitcoin::{Amount, FeeRate, Weight};
use bitcoin::secp256k1::{self, PublicKey, Secp256k1};
use elements::script::Builder;
use elements::opcodes::all::*;
use crate::util::{BitcoinEncodableExt, ElementsEncodableExt};
#[derive(Debug, Clone)]
pub struct BitcoinUtxo {
pub outpoint: bitcoin::OutPoint,
pub output: bitcoin::TxOut,
}
#[derive(Debug, Clone)]
pub struct ElementsUtxo {
pub outpoint: elements::OutPoint,
pub output: elements::TxOut,
}
trait BuilderExt: Into<Builder> + From<Builder> {
fn check_stack_item_size(self, size: i64) -> Self {
self.into()
.push_opcode(OP_SIZE)
.push_int(size)
.push_opcode(OP_EQUALVERIFY)
.into()
}
fn check_input_sighash(self, pubkey: &PublicKey) -> Self {
self.into()
.check_stack_item_size(68)
.push_opcode(OP_OVER)
.check_stack_item_size(36)
.push_opcode(OP_TOALTSTACK)
.push_opcode(OP_SWAP)
.push_opcode(OP_CAT)
.push_opcode(OP_SWAP)
.push_opcode(OP_CAT)
.push_opcode(OP_OVER)
.check_stack_item_size(32)
.push_opcode(OP_TOALTSTACK)
.push_opcode(OP_SWAP)
.push_opcode(OP_CAT)
.push_opcode(OP_SWAP)
.check_stack_item_size(8)
.push_opcode(OP_CAT)
.push_opcode(OP_SHA256)
.push_slice(&pubkey.serialize())
.push_opcode(OP_CHECKSIGFROMSTACKVERIFY)
.into()
}
fn burn_covenant(self, burn_amount: Amount) -> Self {
let burn_txout = elements::TxOut {
asset: elements::confidential::Asset::Explicit(elements::AssetId::LIQUID_BTC),
value: elements::confidential::Value::Explicit(burn_amount.to_sat()),
nonce: elements::confidential::Nonce::Null,
script_pubkey: Builder::new().push_opcode(OP_RETURN).into_script(),
witness: elements::TxOutWitness::default(),
};
self.into()
.push_slice(&elements::encode::serialize(&burn_txout))
.push_opcode(OP_SWAP)
.push_opcode(OP_CAT)
.push_opcode(OP_SHA256)
.push_opcode(OP_CAT)
.push_opcode(OP_SWAP)
.push_opcode(OP_CAT)
.push_opcode(OP_SHA256)
.push_opcode(OP_TOALTSTACK)
.push_opcode(OP_2DUP)
.push_opcode(OP_FROMALTSTACK)
.push_opcode(OP_SWAP)
.push_opcode(OP_CHECKSIGFROMSTACKVERIFY)
.push_opcode(OP_CHECKSIGVERIFY)
.into()
}
}
impl BuilderExt for Builder {}
trait ReadExt: io::Read {
fn take_bytes(&mut self, n: usize) -> Result<Vec<u8>, io::Error> {
let mut buf = vec![0; n];
self.read_exact(&mut buf)?;
Ok(buf)
}
}
impl<T: AsRef<[u8]>> ReadExt for Cursor<T> {}
pub struct SegwitV0BondSpec {
pub pubkey: PublicKey,
pub bond_value: Amount,
pub lock_time: elements::LockTime,
pub reclaim_pubkey: PublicKey,
}
pub fn create_segwit_v0_bond_script(
spec: &SegwitV0BondSpec,
) -> elements::Script {
Builder::new()
.push_opcode(OP_DUP) .push_opcode(OP_NOTIF)
.push_opcode(OP_DROP)
.push_int(spec.lock_time.to_consensus_u32() as i64)
.push_opcode(OP_CLTV)
.push_opcode(OP_DROP)
.push_slice(&spec.reclaim_pubkey.serialize())
.push_opcode(OP_CHECKSIGVERIFY)
.push_opcode(OP_ENDIF)
.check_input_sighash(&spec.pubkey)
.check_input_sighash(&spec.pubkey)
.push_opcode(OP_FROMALTSTACK)
.push_opcode(OP_FROMALTSTACK)
.push_opcode(OP_FROMALTSTACK)
.push_opcode(OP_FROMALTSTACK)
.push_int(2)
.push_opcode(OP_ROLL)
.push_opcode(OP_EQUALVERIFY)
.push_opcode(OP_EQUAL)
.push_int(0)
.push_opcode(OP_EQUALVERIFY)
.burn_covenant(spec.bond_value)
.into_script()
}
pub fn create_segwit_v0_bond_address(
spec: &SegwitV0BondSpec,
network: &'static elements::AddressParams,
) -> elements::Address {
let script = create_segwit_v0_bond_script(spec);
elements::Address::p2wsh(&script, None, network)
}
pub struct SpendDataV0<'a> {
tx: &'a bitcoin::Transaction,
input_idx: usize,
input_value: u64,
script_code: bitcoin::ScriptBuf,
signature: bitcoin::ecdsa::Signature,
}
fn determine_scriptcode_v0(
spk: &bitcoin::Script,
witness: &bitcoin::Witness,
) -> Result<bitcoin::ScriptBuf, &'static str> {
if let Some(sc) = bitcoin::ScriptBuf::from(spk).p2wpkh_script_code() {
Ok(sc)
} else if spk.is_v0_p2wsh() {
let bytes = witness.last().ok_or("p2wsh scriptPubkey but invalid witness")?;
let script = bitcoin::Script::from_bytes(bytes);
if script.instructions().all(|r| r.is_ok()) {
Ok(script.into())
} else {
Err("invalid script in witness push")
}
} else {
Err("scriptPubkey is not p2wpkh or p2wsh")
}
}
impl<'a> SpendDataV0<'a> {
fn determine(
secp: &Secp256k1<impl secp256k1::Verification>,
pubkey: &PublicKey,
tx: &'a bitcoin::Transaction,
utxo: &BitcoinUtxo,
) -> Result<SpendDataV0<'a>, &'static str> {
let input_idx = tx.input.iter()
.position(|i| i.previous_output == utxo.outpoint)
.ok_or("tx doesn't spend utxo")?;
let script_code = determine_scriptcode_v0(
&utxo.output.script_pubkey, &tx.input[input_idx].witness,
)?;
let sig = tx.input[input_idx].witness.iter()
.filter_map(|b| bitcoin::ecdsa::Signature::from_slice(b).ok())
.find_map(|sig| {
let sighash_data = Self::create_sighash_data(
tx, input_idx, utxo.output.value, &script_code, sig.hash_ty,
);
let sighash = Self::sighash(&sighash_data);
if secp.verify_ecdsa(&sighash, &sig.sig, pubkey).is_ok() {
Some(sig)
} else {
None
}
})
.ok_or("no signature found in witness")?;
Ok(SpendDataV0 {
tx: tx,
input_idx: input_idx,
input_value: utxo.output.value,
script_code: script_code,
signature: sig,
})
}
fn create_sighash_data(
tx: &bitcoin::Transaction,
input_idx: usize,
input_value: u64,
script_code: &bitcoin::Script,
sighash_type: bitcoin::sighash::EcdsaSighashType
) -> Vec<u8> {
let scriptcode_len = script_code.encoded_len();
let supposed_len = 156 + scriptcode_len;
let mut buf = Vec::with_capacity(supposed_len);
let mut shc = bitcoin::sighash::SighashCache::new(tx);
shc.segwit_encode_signing_data_to(
&mut buf,
input_idx,
script_code,
input_value,
sighash_type,
).expect("error doing sighash");
debug_assert_eq!(buf.len(), supposed_len);
buf
}
fn sighash(sighash_data: &[u8]) -> secp256k1::Message {
secp256k1::Message::from_hashed_data::<bitcoin::sighash::SegwitV0Sighash>(&sighash_data)
}
fn sighash_data(&self) -> Vec<u8> {
Self::create_sighash_data(
self.tx, self.input_idx, self.input_value, &self.script_code, self.signature.hash_ty,
)
}
fn push_segwit_v0_sighash_items(&self, witness: &mut Vec<Vec<u8>>) {
let sighash_data = self.sighash_data();
let scriptcode_len = self.script_code.as_script().encoded_len();
assert_eq!(sighash_data.len(), 156 + scriptcode_len);
let mut cur = Cursor::new(&sighash_data);
witness.push(cur.take_bytes(68).unwrap());
witness.push(cur.take_bytes(36).unwrap());
witness.push(cur.take_bytes(12 + scriptcode_len).unwrap());
witness.push(cur.take_bytes(32).unwrap());
witness.push(cur.take_bytes(8).unwrap());
witness.push(self.signature.to_vec());
}
}
fn push_v0_burn_covenant_items(
secp: &Secp256k1<impl secp256k1::Signing>,
witness: &mut Vec<Vec<u8>>,
other_output: &elements::TxOut,
spending_tx: &elements::Transaction,
covenant_script: &elements::Script,
total_amount: Amount,
) {
let mut shc = elements::sighash::SighashCache::new(spending_tx);
let covenant_script_len = covenant_script.encoded_len();
let mut buf = Vec::with_capacity(160 + 1 + covenant_script_len);
shc.encode_segwitv0_signing_data_to(
&mut buf,
0,
covenant_script,
elements::confidential::Value::Explicit(total_amount.to_sat()),
elements::EcdsaSighashType::All,
).expect("error doing sighash");
assert_eq!(buf.len(), 189 + covenant_script.encoded_len(),
"covenant len {}", covenant_script.encoded_len(),
);
let sign_msg = secp256k1::Message::from_hashed_data::<elements::Sighash>(&buf);
let (signing_pk, signature) = loop {
let (sk, pk) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
let sig = secp.sign_ecdsa(&sign_msg, &sk);
if sig.serialize_der().len() == 70 {
break (pk, sig);
}
};
let mut cur = Cursor::new(&buf);
let first_part = cur.take_bytes(4 + 32 + 32 + 36 + covenant_script_len + 8 + 4).unwrap();
cur.seek(io::SeekFrom::Current(32)).unwrap();
let last_part = cur.take_bytes(4 + 4).unwrap();
witness.push(elements::encode::serialize(other_output));
witness.push(first_part);
witness.push(last_part);
witness.push(signing_pk.serialize().to_vec());
witness.push(signature.serialize_der().to_vec());
}
pub fn create_burn_segwit_v0_bond_tx(
secp: &Secp256k1<impl secp256k1::Signing + secp256k1::Verification>,
bond_utxo: &ElementsUtxo,
spec: &SegwitV0BondSpec,
double_spend_utxo: &BitcoinUtxo,
tx1: &bitcoin::Transaction,
tx2: &bitcoin::Transaction,
fee_rate: FeeRate,
reward_address: elements::Address,
) -> Result<elements::Transaction, &'static str> {
let spend1 = SpendDataV0::determine(secp, &spec.pubkey, tx1, double_spend_utxo)?;
let spend2 = SpendDataV0::determine(secp, &spec.pubkey, tx2, double_spend_utxo)?;
let mut ret = elements::Transaction {
version: 2,
lock_time: elements::LockTime::ZERO,
input: vec![elements::TxIn {
previous_output: bond_utxo.outpoint,
is_pegin: false,
script_sig: elements::Script::new(), sequence: elements::Sequence::MAX,
asset_issuance: elements::AssetIssuance::default(),
witness: elements::TxInWitness {
amount_rangeproof: None,
inflation_keys_rangeproof: None,
pegin_witness: Vec::new(),
script_witness: Vec::new(),
},
}],
output: vec![
elements::TxOut {
asset: elements::confidential::Asset::Explicit(elements::AssetId::LIQUID_BTC),
value: elements::confidential::Value::Explicit(spec.bond_value.to_sat()),
nonce: elements::confidential::Nonce::Null,
script_pubkey: Builder::new()
.push_opcode(OP_RETURN)
.into_script(),
witness: elements::TxOutWitness::default(),
},
elements::TxOut {
asset: elements::confidential::Asset::Explicit(elements::AssetId::LIQUID_BTC),
value: elements::confidential::Value::Explicit(0),
nonce: elements::confidential::Nonce::Null,
script_pubkey: reward_address.script_pubkey(),
witness: elements::TxOutWitness::default(),
},
elements::TxOut::new_fee(0, elements::AssetId::LIQUID_BTC),
],
};
let sc1_len = spend1.script_code.as_script().encoded_len();
let sc2_len = spend2.script_code.as_script().encoded_len();
let reward_len = reward_address.script_pubkey().encoded_len();
let total_tx_weight = 1679 + sc1_len + sc2_len + reward_len
+ BitcoinEncodableExt::encoded_len(&spend1.signature.to_vec())
+ BitcoinEncodableExt::encoded_len(&spend2.signature.to_vec());
let fee = fee_rate * Weight::from_wu(total_tx_weight as u64);
let change = bond_utxo.output.value.explicit().unwrap() - fee.to_sat();
ret.output[2].value = elements::confidential::Value::Explicit(fee.to_sat());
ret.output[1].value = elements::confidential::Value::Explicit(change);
let mut witness = Vec::with_capacity(6 + 6 + 5);
spend1.push_segwit_v0_sighash_items(&mut witness);
spend2.push_segwit_v0_sighash_items(&mut witness);
let covenant_script = create_segwit_v0_bond_script(spec);
push_v0_burn_covenant_items(
secp,
&mut witness,
&ret.output[1],
&ret,
&covenant_script,
Amount::from_sat(bond_utxo.output.value.explicit().unwrap()),
);
witness.reverse();
ret.input[0].witness.script_witness = witness;
assert_eq!(ret.weight(), total_tx_weight,
"sc1: {}; sc2: {}, reward: {}, sig1: {}, sig2: {}", sc1_len, sc2_len, reward_len,
spend1.signature.to_vec().len(), spend2.signature.to_vec().len(),
);
Ok(ret)
}
#[cfg(test)]
mod test {
use super::*;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::SecretKey;
use bitcoin::secp256k1::rand::{self, Rng, SeedableRng};
static TEST_NET: &'static elements::AddressParams = &elements::AddressParams::ELEMENTS;
static TEST_ASSET: elements::confidential::Asset =
elements::confidential::Asset::Explicit(elements::AssetId::LIQUID_BTC);
fn btc(amount: Amount) -> elements::confidential::Value {
elements::confidential::Value::Explicit(amount.to_sat())
}
fn create_spend_with_key(
secp: &Secp256k1<impl secp256k1::Signing>,
rand: &mut impl Rng,
utxo: &BitcoinUtxo,
key: &SecretKey,
) -> bitcoin::Transaction {
let mut ret = bitcoin::Transaction {
version: 3,
lock_time: bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![
bitcoin::TxIn {
previous_output: utxo.outpoint,
sequence: bitcoin::Sequence::MAX,
script_sig: Default::default(),
witness: Default::default(),
}
],
output: vec![
bitcoin::TxOut {
value: 350,
script_pubkey: bitcoin::ScriptBuf::new_v0_p2wpkh(
&bitcoin::WPubkeyHash::from_byte_array(rand.gen()),
),
}
],
};
let mut shc = bitcoin::sighash::SighashCache::new(&ret);
let sighash = shc.segwit_signature_hash(
0,
&utxo.output.script_pubkey.p2wpkh_script_code().unwrap(),
utxo.output.value,
bitcoin::sighash::EcdsaSighashType::All,
).expect("error doing sighash");
let msg = secp256k1::Message::from_slice(&sighash[..]).unwrap();
let signature = secp.sign_ecdsa(&msg, &key);
ret.input[0].witness.push_bitcoin_signature(
&signature.serialize_der(), bitcoin::sighash::EcdsaSighashType::All,
);
ret.input[0].witness.push(key.public_key(secp).serialize());
ret
}
fn test_v0_with_random(secp: &Secp256k1<secp256k1::All>, rand: &mut impl Rng) {
let (bond_sk, bond_pk) = secp.generate_keypair(rand);
let expiry = SystemTime::now() + Duration::from_secs(1 * 365 * 24 * 60 * 60);
let locktime = elements::LockTime::from_time(
expiry.duration_since(UNIX_EPOCH).unwrap().as_secs().try_into().unwrap(),
).unwrap();
let (_reclaim_sk, reclaim_pk) = secp.generate_keypair(rand);
let bond_spec = SegwitV0BondSpec {
pubkey: bond_pk.clone(),
bond_value: Amount::from_btc(5.0).unwrap(),
lock_time: locktime,
reclaim_pubkey: reclaim_pk,
};
let bond_script = create_segwit_v0_bond_script(&bond_spec);
let bond_addr = create_segwit_v0_bond_address(&bond_spec, TEST_NET);
let bond_utxo = ElementsUtxo {
outpoint: elements::OutPoint {
txid: elements::Txid::from_byte_array(rand.gen()),
vout: rand.gen(),
},
output: elements::TxOut {
value: btc(Amount::from_btc(6.0).unwrap()),
asset: TEST_ASSET,
nonce: elements::confidential::Nonce::Null,
script_pubkey: bond_addr.script_pubkey(),
witness: elements::TxOutWitness::default(),
},
};
let double_spend_utxo = BitcoinUtxo {
outpoint: bitcoin::OutPoint {
txid: bitcoin::Txid::from_byte_array(rand.gen()),
vout: rand::random(),
},
output: bitcoin::TxOut {
value: Amount::from_btc(1.0).unwrap().to_sat(),
script_pubkey: bitcoin::ScriptBuf::new_v0_p2wpkh(
&bitcoin::PublicKey::new(bond_pk).wpubkey_hash().unwrap(),
),
},
};
let spend1 = create_spend_with_key(&secp, rand, &double_spend_utxo, &bond_sk);
let spend2 = create_spend_with_key(&secp, rand, &double_spend_utxo, &bond_sk);
let (_, reward_pk) = secp.generate_keypair(rand);
let burn_tx = create_burn_segwit_v0_bond_tx(
&secp,
&bond_utxo,
&bond_spec,
&double_spend_utxo,
&spend1,
&spend2,
FeeRate::from_sat_per_vb(1).unwrap(),
elements::Address::p2wpkh(&bitcoin::PublicKey::new(reward_pk), None, TEST_NET),
).unwrap();
verify_tx(&bond_script, &bond_utxo.output.value, 0, &burn_tx).expect("invalid tx");
}
#[test]
fn test_v0_fo_real() {
let secp = Secp256k1::new();
let mut rand = rand::rngs::StdRng::from_seed([
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
]);
test_v0_with_random(&secp, &mut rand);
let mut rand = rand::thread_rng();
for _ in 0..1000 {
test_v0_with_random(&secp, &mut rand);
}
}
fn verify_tx(
script: &elements::Script,
value: &elements::confidential::Value,
index: usize,
transaction: &elements::Transaction,
) -> Result<(), elements_consensus::ConsensusViolation> {
use elements_consensus::elements::encode::deserialize;
use elements::encode::serialize;
let value = value.explicit().expect("only support explicit values");
println!("tx: {}", elements::encode::serialize_hex(transaction));
elements_consensus::verify(
deserialize(&serialize(script)).unwrap(),
&elements_consensus::elements::confidential::Value::Explicit(value),
index,
&deserialize(&serialize(transaction)).unwrap(),
).expect("index error")
}
}