Skip to content

Commit

Permalink
[BTC]: Fix tests, minor improves
Browse files Browse the repository at this point in the history
  • Loading branch information
satoshiotomakan committed Dec 19, 2024
1 parent ac9faa2 commit 16fd383
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 120 deletions.
22 changes: 10 additions & 12 deletions rust/chains/tw_bitcoin/src/babylon/conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
//
// Copyright © 2017 Trust Wallet.

use crate::babylon::covenant_committee::CovenantCommittee;
use crate::babylon::multi_sig_ordered::MultiSigOrderedKeys;
use tw_hash::H32;
use tw_keypair::schnorr;
use tw_utxo::script::standard_script::opcodes::*;
use tw_utxo::script::Script;

const VERIFY: bool = true;
const NO_VERIFY: bool = false;
/// We always require only one finality provider to sign.
const FINALITY_PROVIDERS_QUORUM: u32 = 1;

/// https://github.com/babylonchain/babylon/blob/dev/docs/transaction-impl-spec.md#op_return-output-description
/// ```txt
Expand All @@ -27,8 +25,8 @@ pub fn new_op_return_script(
let mut buf = Vec::with_capacity(71);
buf.extend_from_slice(tag.as_slice());
buf.push(version);
buf.extend_from_slice(staker_key.as_slice());
buf.extend_from_slice(finality_provider_key.as_slice());
buf.extend_from_slice(staker_key.bytes().as_slice());
buf.extend_from_slice(finality_provider_key.bytes().as_slice());
buf.extend_from_slice(&locktime.to_be_bytes());

let mut s = Script::new();
Expand Down Expand Up @@ -61,7 +59,7 @@ pub fn new_timelock_script(staker_key: &schnorr::XOnlyPublicKey, locktime: u16)
/// ```
pub fn new_unbonding_script(
staker_key: &schnorr::XOnlyPublicKey,
covenants: &CovenantCommittee,
covenants: &MultiSigOrderedKeys,
) -> Script {
let mut s = Script::with_capacity(64);
append_single_sig(&mut s, staker_key, VERIFY);
Expand Down Expand Up @@ -89,16 +87,16 @@ pub fn new_unbonding_script(
/// ```
pub fn new_slashing_script(
staker_key: &schnorr::XOnlyPublicKey,
finality_providers_keys: &[schnorr::XOnlyPublicKey],
covenants: &CovenantCommittee,
finality_providers_keys: &MultiSigOrderedKeys,
covenants: &MultiSigOrderedKeys,
) -> Script {
let mut s = Script::with_capacity(64);
append_single_sig(&mut s, staker_key, VERIFY);
// We need to run verify to clear the stack, as finality provider multisig is in the middle of the script.
append_multi_sig(
&mut s,
finality_providers_keys,
FINALITY_PROVIDERS_QUORUM,
finality_providers_keys.public_keys_ordered(),
finality_providers_keys.quorum(),
VERIFY,
);
// Covenant multisig is always last in script so we do not run verify and leave
Expand All @@ -114,7 +112,7 @@ pub fn new_slashing_script(
}

fn append_single_sig(dst: &mut Script, key: &schnorr::XOnlyPublicKey, verify: bool) {
dst.push_slice(key.as_slice());
dst.push_slice(key.bytes().as_slice());
if verify {
dst.push(OP_CHECKSIGVERIFY);
} else {
Expand All @@ -138,7 +136,7 @@ fn append_multi_sig(
}

for (i, pk_xonly) in pubkeys.iter().enumerate() {
dst.push_slice(pk_xonly.as_slice());
dst.push_slice(pk_xonly.bytes().as_slice());
if i == 0 {
dst.push(OP_CHECKSIG);
} else {
Expand Down
2 changes: 1 addition & 1 deletion rust/chains/tw_bitcoin/src/babylon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Copyright © 2017 Trust Wallet.

pub mod conditions;
pub mod covenant_committee;
pub mod multi_sig_ordered;
pub mod proto_builder;
pub mod spending_data;
pub mod spending_info;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ use tw_utxo::signature::BitcoinSchnorrSignature;
type OptionalSignature = Option<BitcoinSchnorrSignature>;
type PubkeySigMap = BTreeMap<schnorr::XOnlyPublicKey, OptionalSignature>;

pub struct CovenantCommittee {
pub struct MultiSigOrderedKeys {
pks: Vec<schnorr::XOnlyPublicKey>,
quorum: u32,
}

impl CovenantCommittee {
impl MultiSigOrderedKeys {
/// Sort the keys in lexicographical order.
pub fn new(mut pks: Vec<schnorr::XOnlyPublicKey>, quorum: u32) -> SigningResult<Self> {
if pks.is_empty() {
return SigningError::err(SigningErrorType::Error_invalid_params)
.context("No covenant public keys provided");
.context("No public keys provided");
}

if pks.len() < quorum as usize {
Expand All @@ -35,11 +35,11 @@ impl CovenantCommittee {
// TODO it's not optimal to use a `HashSet` because the keys are sorted already.
if !pks.iter().all_unique() {
return SigningError::err(SigningErrorType::Error_invalid_params)
.context("Covenant committee public keys must be unique");
.context("Public keys must be unique");
}

pks.sort();
Ok(CovenantCommittee { pks, quorum })
Ok(MultiSigOrderedKeys { pks, quorum })
}

pub fn public_keys_ordered(&self) -> &[schnorr::XOnlyPublicKey] {
Expand All @@ -50,28 +50,25 @@ impl CovenantCommittee {
self.quorum
}

pub fn with_partial_signatures<'a, I>(
self,
sigs: I,
) -> SigningResult<CovenantCommitteeSignatures>
pub fn with_partial_signatures<'a, I>(self, sigs: I) -> SigningResult<MultiSigOrdered>
where
I: IntoIterator<Item = &'a (schnorr::XOnlyPublicKey, BitcoinSchnorrSignature)>,
{
let mut pk_sig_map = CovenantCommitteeSignatures::checked(self.pks, self.quorum);
let mut pk_sig_map = MultiSigOrdered::checked(self.pks, self.quorum);
pk_sig_map.set_partial_signatures(sigs)?;
pk_sig_map.check_covenant_quorum()?;
pk_sig_map.check_quorum()?;
Ok(pk_sig_map)
}
}

#[derive(Clone, Debug)]
pub struct CovenantCommitteeSignatures {
pub struct MultiSigOrdered {
pk_sig_map: PubkeySigMap,
quorum: u32,
}

impl CovenantCommitteeSignatures {
/// `pk_sig_map` and `quorum` must be checked at [`CovenantCommittee::new`] already.
impl MultiSigOrdered {
/// `pk_sig_map` and `quorum` must be checked at [`MultiSigOrderedKeys::new`] already.
fn checked(pks: Vec<schnorr::XOnlyPublicKey>, quorum: u32) -> Self {
let mut pk_sig_map = PubkeySigMap::new();

Expand All @@ -80,38 +77,38 @@ impl CovenantCommitteeSignatures {
pk_sig_map.insert(pk, None);
}

CovenantCommitteeSignatures { pk_sig_map, quorum }
MultiSigOrdered { pk_sig_map, quorum }
}

fn set_partial_signatures<'a, I>(&mut self, sigs: I) -> SigningResult<()>
where
I: IntoIterator<Item = &'a (schnorr::XOnlyPublicKey, BitcoinSchnorrSignature)>,
{
// Set the signatures for the specific public keys.
// There can be less signatures than covenant public keys, but not less than `covenant_quorum`.
// There can be less signatures than public keys, but not less than `quorum`.
for (pk, sig) in sigs {
// Find the signature of the corresponding public key.
let pk_sig = self
.pk_sig_map
.get_mut(pk)
.or_tw_err(SigningErrorType::Error_invalid_params)
.context("Signature provided for an unknown covenant committee")?;
.context("Signature provided for an unknown public key")?;

// Only one signature per public key allowed.
if pk_sig.is_some() {
return SigningError::err(SigningErrorType::Error_invalid_params)
.context("Duplicate covenant committee public key");
.context("Duplicate public key");
}
*pk_sig = Some(sig.clone());
}
Ok(())
}

fn check_covenant_quorum(&self) -> SigningResult<()> {
fn check_quorum(&self) -> SigningResult<()> {
let sig_num = self.pk_sig_map.values().filter(|sig| sig.is_some()).count();
if sig_num < self.quorum as usize {
return SigningError::err(SigningErrorType::Error_invalid_params).context(format!(
"Number of covenant committee signatures '{sig_num}' is less than quorum '{}'",
"Number of signatures '{sig_num}' is less than quorum '{}'",
self.quorum
));
}
Expand Down
26 changes: 19 additions & 7 deletions rust/chains/tw_bitcoin/src/babylon/proto_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// Copyright © 2017 Trust Wallet.

use crate::babylon::covenant_committee::CovenantCommittee;
use crate::babylon::multi_sig_ordered::MultiSigOrderedKeys;
use crate::babylon::tx_builder::BabylonStakingParams;
use std::borrow::Cow;
use tw_coin_entry::error::prelude::*;
Expand All @@ -14,6 +14,10 @@ use tw_utxo::signature::BitcoinSchnorrSignature;
pub mod output_protobuf;
pub mod utxo_protobuf;

/// We always require only one finality provider to sign,
/// even if there are multiple finality providers in the script.
const FINALITY_PROVIDERS_QUORUM: u32 = 1;

pub fn staking_params_from_proto(
params: &Option<Proto::StakingInfo>,
) -> SigningResult<BabylonStakingParams> {
Expand All @@ -30,22 +34,30 @@ pub fn staking_params_from_proto(
.try_into()
.tw_err(|_| SigningErrorType::Error_invalid_params)
.context("stakingTime cannot be greater than 65535")?;
let finality_provider = parse_schnorr_pk(&params.finality_provider_public_key)
.context("Invalid finalityProviderPublicKeys")?;

let covenants_pks = parse_schnorr_pks(&params.covenant_committee_public_keys)
.context("Invalid covenantCommitteePublicKeys")?;
let covenants = CovenantCommittee::new(covenants_pks, params.covenant_quorum)?;
let covenants = MultiSigOrderedKeys::new(covenants_pks, params.covenant_quorum)
.context("Invalid covenantCommitteePublicKeys")?;

let finality_provider = parse_schnorr_pk(&params.finality_provider_public_key)
.context("Invalid finalityProviderPublicKey")?;
let finality_providers =
MultiSigOrderedKeys::new(vec![finality_provider], FINALITY_PROVIDERS_QUORUM)
.context("Invalid finalityProviderPublicKey")?;

Ok(BabylonStakingParams {
staker,
staking_locktime,
finality_provider,
finality_providers,
covenants,
})
}

pub fn parse_schnorr_pk(bytes: &Cow<[u8]>) -> SigningResult<schnorr::XOnlyPublicKey> {
pub fn parse_schnorr_pk<T>(bytes: T) -> SigningResult<schnorr::XOnlyPublicKey>
where
T: AsRef<[u8]>,
{
schnorr::XOnlyPublicKey::try_from(bytes.as_ref()).into_tw()
}

Expand All @@ -57,7 +69,7 @@ pub fn parse_schnorr_pubkey_sig(
pubkey_sig: &Proto::PublicKeySignature,
sighash_ty: SighashType,
) -> SigningResult<(schnorr::XOnlyPublicKey, BitcoinSchnorrSignature)> {
let pk = parse_schnorr_pk(&pubkey_sig.public_key)?;
let pk = parse_schnorr_pk(pubkey_sig.public_key.as_ref())?;
let sig = schnorr::Signature::try_from(pubkey_sig.signature.as_ref())
.tw_err(|_| SigningErrorType::Error_invalid_params)
.context("Invalid signature")?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl<'a, Context: UtxoContext> BabylonOutputProtobuf for OutputProtobuf<'a, Cont
Ok(self.prepare_builder()?.babylon_staking_op_return(
&tag,
&staker,
&finality_provider,
finality_provider,
staking_locktime,
))
}
Expand Down
6 changes: 3 additions & 3 deletions rust/chains/tw_bitcoin/src/babylon/spending_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// Copyright © 2017 Trust Wallet.

use crate::babylon::covenant_committee::CovenantCommitteeSignatures;
use crate::babylon::multi_sig_ordered::MultiSigOrdered;
use tw_memory::Data;
use tw_utxo::script::standard_script::claims;
use tw_utxo::script::Script;
Expand All @@ -15,14 +15,14 @@ pub struct BabylonUnbondingPath {
control_block: Data,
/// Signatures signed by covenant committees.
/// Sorted by covenant committees public keys in reverse order.
covenant_committee_signatures: CovenantCommitteeSignatures,
covenant_committee_signatures: MultiSigOrdered,
}

impl BabylonUnbondingPath {
pub fn new(
unbonding_script: Script,
control_block: Data,
covenant_committee_signatures: CovenantCommitteeSignatures,
covenant_committee_signatures: MultiSigOrdered,
) -> Self {
BabylonUnbondingPath {
unbonding_script,
Expand Down
22 changes: 13 additions & 9 deletions rust/chains/tw_bitcoin/src/babylon/spending_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ lazy_static! {
schnorr::PublicKey::try_from(UNSPENDABLE_KEY_PATH_BYTES.as_slice())
.expect("Expected a valid unspendable key path");
pub static ref UNSPENDABLE_KEY_PATH_XONLY: bitcoin::key::UntweakedPublicKey =
bitcoin::key::UntweakedPublicKey::from_slice(UNSPENDABLE_KEY_PATH.x_only().as_slice())
bitcoin::key::UntweakedPublicKey::from_slice(UNSPENDABLE_KEY_PATH.x_only().bytes().as_slice())
.expect("Expected a valid unspendable key path");
}

Expand All @@ -33,14 +33,16 @@ pub struct StakingSpendInfo {

impl StakingSpendInfo {
pub fn new(params: &BabylonStakingParams) -> SigningResult<StakingSpendInfo> {
let fp_xonly = [params.finality_provider.clone()];
let staker_xonly = params.staker.x_only();

let timelock_script =
conditions::new_timelock_script(&staker_xonly, params.staking_locktime);
let unbonding_script = conditions::new_unbonding_script(&staker_xonly, &params.covenants);
let slashing_script =
conditions::new_slashing_script(&staker_xonly, &fp_xonly, &params.covenants);
let slashing_script = conditions::new_slashing_script(
&staker_xonly,
&params.finality_providers,
&params.covenants,
);

// IMPORTANT - order and leaf depths are important!
let spend_info = bitcoin::taproot::TaprootBuilder::new()
Expand All @@ -50,7 +52,7 @@ impl StakingSpendInfo {
.expect("Leaf added at a valid depth")
.add_leaf(1, slashing_script.clone().into())
.expect("Leaf added at a valid depth")
.finalize(&secp256k1::SECP256K1, UNSPENDABLE_KEY_PATH_XONLY.clone())
.finalize(secp256k1::SECP256K1, *UNSPENDABLE_KEY_PATH_XONLY)
.expect("Expected a valid Taproot tree");

Ok(StakingSpendInfo {
Expand Down Expand Up @@ -98,21 +100,23 @@ pub struct UnbondingSpendInfo {

impl UnbondingSpendInfo {
pub fn new(params: &BabylonStakingParams) -> SigningResult<UnbondingSpendInfo> {
let fp_xonly = [params.finality_provider.clone()];
let staker_xonly = params.staker.x_only();

let timelock_script =
conditions::new_timelock_script(&staker_xonly, params.staking_locktime);
let slashing_script =
conditions::new_slashing_script(&staker_xonly, &fp_xonly, &params.covenants);
let slashing_script = conditions::new_slashing_script(
&staker_xonly,
&params.finality_providers,
&params.covenants,
);

// IMPORTANT - order and leaf depths are important!
let spend_info = bitcoin::taproot::TaprootBuilder::new()
.add_leaf(1, slashing_script.clone().into())
.expect("Leaf added at a valid depth")
.add_leaf(1, timelock_script.clone().into())
.expect("Leaf added at a valid depth")
.finalize(&secp256k1::SECP256K1, UNSPENDABLE_KEY_PATH_XONLY.clone())
.finalize(secp256k1::SECP256K1, *UNSPENDABLE_KEY_PATH_XONLY)
.expect("Expected a valid Taproot tree");

Ok(UnbondingSpendInfo {
Expand Down
6 changes: 3 additions & 3 deletions rust/chains/tw_bitcoin/src/babylon/tx_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// Copyright © 2017 Trust Wallet.

use crate::babylon::covenant_committee::CovenantCommittee;
use crate::babylon::multi_sig_ordered::MultiSigOrderedKeys;
use tw_keypair::schnorr;

pub mod output;
Expand All @@ -14,6 +14,6 @@ pub type BabylonUnbondingParams = BabylonStakingParams;
pub struct BabylonStakingParams {
pub staker: schnorr::PublicKey,
pub staking_locktime: u16,
pub finality_provider: schnorr::XOnlyPublicKey,
pub covenants: CovenantCommittee,
pub finality_providers: MultiSigOrderedKeys,
pub covenants: MultiSigOrderedKeys,
}
Loading

0 comments on commit 16fd383

Please sign in to comment.