Skip to content

Commit

Permalink
[BTC]: Add Babylon Unbonding UTXO
Browse files Browse the repository at this point in the history
  • Loading branch information
satoshiotomakan committed Dec 16, 2024
1 parent dc9fec7 commit 57447ea
Show file tree
Hide file tree
Showing 19 changed files with 351 additions and 136 deletions.
1 change: 1 addition & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/chains/tw_bitcoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
bitcoin = { version = "0.30.0", features = ["rand-std", "serde"] }
itertools = "0.10.5"
lazy_static = "1.4.0"
secp256k1 = { version = "0.27.0", features = ["global-context", "rand-std"] }
serde = { version = "1.0", features = ["derive"] }
Expand Down
47 changes: 23 additions & 24 deletions rust/chains/tw_bitcoin/src/babylon/conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// Copyright © 2017 Trust Wallet.

use tw_coin_entry::error::prelude::*;
use tw_hash::{H256, H32};
use tw_hash::H32;
use tw_keypair::schnorr;
use tw_utxo::script::standard_script::opcodes::*;
use tw_utxo::script::Script;

Expand All @@ -19,15 +20,15 @@ const FINALITY_PROVIDERS_QUORUM: u32 = 1;
pub fn new_op_return_script(
tag: &H32,
version: u8,
staker_xonly: &H256,
finality_provider_xonly: &H256,
staker_key: &schnorr::XOnlyPublicKey,
finality_provider_key: &schnorr::XOnlyPublicKey,
locktime: u16,
) -> Script {
let mut buf = Vec::with_capacity(71);
buf.extend_from_slice(tag.as_slice());
buf.push(version);
buf.extend_from_slice(staker_xonly.as_slice());
buf.extend_from_slice(finality_provider_xonly.as_slice());
buf.extend_from_slice(staker_key.as_slice());
buf.extend_from_slice(finality_provider_key.as_slice());
buf.extend_from_slice(&locktime.to_be_bytes());

let mut s = Script::new();
Expand All @@ -42,9 +43,9 @@ pub fn new_op_return_script(
/// ```txt
/// <StakerPK> OP_CHECKSIGVERIFY <TimelockBlocks> OP_CHECKSEQUENCEVERIFY
/// ```
pub fn new_timelock_script(staker_xonly: &H256, locktime: u16) -> Script {
pub fn new_timelock_script(staker_key: &schnorr::XOnlyPublicKey, locktime: u16) -> Script {
let mut s = Script::with_capacity(64);
append_single_sig(&mut s, staker_xonly, VERIFY);
append_single_sig(&mut s, staker_key, VERIFY);
s.push_int(locktime as i64);
s.push(OP_CHECKSEQUENCEVERIFY);
s
Expand All @@ -59,16 +60,16 @@ pub fn new_timelock_script(staker_xonly: &H256, locktime: u16) -> Script {
/// <CovenantThreshold> OP_NUMEQUAL
/// ```
pub fn new_unbonding_script(
staker_xonly: &H256,
covenants_xonly: Vec<H256>,
staker_key: &schnorr::XOnlyPublicKey,
covenants_keys: &[schnorr::XOnlyPublicKey],
covenant_quorum: u32,
) -> SigningResult<Script> {
let mut s = Script::with_capacity(64);
append_single_sig(&mut s, staker_xonly, VERIFY);
append_single_sig(&mut s, staker_key, VERIFY);
// Covenant multisig is always last in script so we do not run verify and leave
// last value on the stack. If we do not leave at least one element on the stack
// script will always error.
append_multi_sig(&mut s, covenants_xonly, covenant_quorum, NO_VERIFY)?;
append_multi_sig(&mut s, covenants_keys, covenant_quorum, NO_VERIFY)?;
Ok(s)
}

Expand All @@ -83,29 +84,29 @@ pub fn new_unbonding_script(
/// <CovenantThreshold> OP_NUMEQUAL
/// ```
pub fn new_slashing_script(
staker_xonly: &H256,
finality_providers_xonly: Vec<H256>,
covenants_xonly: Vec<H256>,
staker_key: &schnorr::XOnlyPublicKey,
finality_providers_keys: &[schnorr::XOnlyPublicKey],
covenants_keys: &[schnorr::XOnlyPublicKey],
covenant_quorum: u32,
) -> SigningResult<Script> {
let mut s = Script::with_capacity(64);
append_single_sig(&mut s, staker_xonly, VERIFY);
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_xonly,
finality_providers_keys,
FINALITY_PROVIDERS_QUORUM,
VERIFY,
)?;
// Covenant multisig is always last in script so we do not run verify and leave
// last value on the stack. If we do not leave at least one element on the stack
// script will always error.
append_multi_sig(&mut s, covenants_xonly, covenant_quorum, NO_VERIFY)?;
append_multi_sig(&mut s, covenants_keys, covenant_quorum, NO_VERIFY)?;
Ok(s)
}

fn append_single_sig(dst: &mut Script, xonly: &H256, verify: bool) {
dst.push_slice(xonly.as_slice());
fn append_single_sig(dst: &mut Script, key: &schnorr::XOnlyPublicKey, verify: bool) {
dst.push_slice(key.as_slice());
if verify {
dst.push(OP_CHECKSIGVERIFY);
} else {
Expand All @@ -117,10 +118,10 @@ fn append_single_sig(dst: &mut Script, xonly: &H256, verify: bool) {
/// successfully execute script.
/// It validates whether threshold is not greater than number of keys.
/// If there is only one key provided it will return single key sig script.
/// Note: It is up to the caller to ensure that the keys are unique.
/// Note: It is up to the caller to ensure that the keys are unique and sorted.
fn append_multi_sig(
dst: &mut Script,
mut covenants_xonly: Vec<H256>,
covenants_xonly: &[schnorr::XOnlyPublicKey],
covenant_quorum: u32,
verify: bool,
) -> SigningResult<()> {
Expand All @@ -137,9 +138,7 @@ fn append_multi_sig(
return Ok(());
}

// Sort the keys in lexicographical order bytes.
covenants_xonly.sort();
for (i, covenant_xonly) in covenants_xonly.into_iter().enumerate() {
for (i, covenant_xonly) in covenants_xonly.iter().enumerate() {
dst.push_slice(covenant_xonly.as_slice());
if i == 0 {
dst.push(OP_CHECKSIG);
Expand Down
33 changes: 33 additions & 0 deletions rust/chains/tw_bitcoin/src/babylon/covenant_committee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use itertools::Itertools;
use std::cmp;
use tw_keypair::schnorr;
use tw_utxo::signature::BitcoinSchnorrSignature;

pub struct CovenantCommittee;

impl CovenantCommittee {
/// Sort the keys in lexicographical order bytes.
pub fn sort_public_keys(pks: &mut [schnorr::XOnlyPublicKey]) {
pks.sort_by(Self::cmp_pk);
}

/// Sort the signatures by corresponding public keys in lexicographical order bytes.
pub fn sort_signatures<'a, I>(sigs: I) -> Vec<BitcoinSchnorrSignature>
where
I: IntoIterator<Item = &'a (schnorr::XOnlyPublicKey, BitcoinSchnorrSignature)>,
{
sigs.into_iter()
// Sort the elements by public keys.
.sorted_by(|x, y| Self::cmp_pk(&x.0, &y.0))
.map(|(_pk, sig)| sig.clone())
.collect()
}

fn cmp_pk(pk1: &schnorr::XOnlyPublicKey, pk2: &schnorr::XOnlyPublicKey) -> cmp::Ordering {
pk1.as_slice().cmp(pk2.as_slice())
}
}
4 changes: 3 additions & 1 deletion rust/chains/tw_bitcoin/src/babylon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// Copyright © 2017 Trust Wallet.

pub mod claims;
pub mod conditions;
pub mod covenant_committee;
pub mod spending_data;
pub mod spending_info;
pub mod tx_builder;
55 changes: 55 additions & 0 deletions rust/chains/tw_bitcoin/src/babylon/spending_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use crate::babylon::covenant_committee::CovenantCommittee;
use tw_keypair::schnorr;
use tw_memory::Data;
use tw_utxo::script::standard_script::claims;
use tw_utxo::script::Script;
use tw_utxo::signature::BitcoinSchnorrSignature;
use tw_utxo::spending_data::{SchnorrSpendingDataConstructor, SpendingData};

#[derive(Clone, Debug)]
pub struct BabylonUnbondingPath {
unbonding_script: Script,
control_block: Data,
/// Signatures signed by covenant committees. Sorted by covenant committees public keys.
covenant_committee_signatures: Vec<BitcoinSchnorrSignature>,
}

impl BabylonUnbondingPath {
pub fn new(
unbonding_script: Script,
control_block: Data,
covenant_committee_signatures: &[(schnorr::XOnlyPublicKey, BitcoinSchnorrSignature)],
) -> Self {
let covenant_committee_signatures =
CovenantCommittee::sort_signatures(covenant_committee_signatures);
BabylonUnbondingPath {
unbonding_script,
control_block,
covenant_committee_signatures,
}
}
}

impl SchnorrSpendingDataConstructor for BabylonUnbondingPath {
fn get_spending_data(&self, sig: &BitcoinSchnorrSignature) -> SpendingData {
// User's signature is always first.
// Then, covenant committee signatures sorted by their public keys.
// For more info, see [`babylon::conditions::new_unbonding_script`].
let unbonding_sigs: Vec<_> = std::iter::once(sig.clone())
.chain(self.covenant_committee_signatures.iter().cloned())
.collect();

SpendingData {
script_sig: Script::default(),
witness: claims::new_p2tr_script_path(
&unbonding_sigs,
self.unbonding_script.clone(),
self.control_block.clone(),
),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Copyright © 2017 Trust Wallet.

use crate::babylon::conditions;
use crate::babylon::covenant_committee::CovenantCommittee;
use bitcoin::hashes::Hash;
use lazy_static::lazy_static;
use tw_coin_entry::error::prelude::*;
Expand All @@ -29,36 +30,28 @@ pub struct StakingSpendInfo {

impl StakingSpendInfo {
pub fn new(
staker: &schnorr::PublicKey,
staker: &schnorr::XOnlyPublicKey,
staking_locktime: u16,
finality_provider: &schnorr::PublicKey,
covenants: &[schnorr::PublicKey],
finality_provider: schnorr::XOnlyPublicKey,
mut covenants: Vec<schnorr::XOnlyPublicKey>,
covenant_quorum: u32,
) -> SigningResult<StakingSpendInfo> {
let staker_xonly = staker.x_only().bytes();
let covenants_xonly: Vec<_> = covenants.iter().map(|pk| pk.x_only().bytes()).collect();
let fp_xonly: Vec<_> = vec![finality_provider.x_only().bytes()];

let timelock_script = conditions::new_timelock_script(&staker_xonly, staking_locktime);
let unbonding_script = conditions::new_unbonding_script(
&staker_xonly,
covenants_xonly.clone(),
covenant_quorum,
)
.context("Invalid number of covenants")?;
let slashing_script = conditions::new_slashing_script(
&staker_xonly,
fp_xonly,
covenants_xonly,
covenant_quorum,
)
.context("Invalid number of finality providers")?;
let fp_xonly = [finality_provider];

CovenantCommittee::sort_public_keys(&mut covenants);

let timelock_script = conditions::new_timelock_script(&staker, staking_locktime);
let unbonding_script =
conditions::new_unbonding_script(&staker, &covenants, covenant_quorum)
.context("Invalid number of covenants")?;
let slashing_script =
conditions::new_slashing_script(&staker, &fp_xonly, &covenants, covenant_quorum)
.context("Invalid number of finality providers")?;

// IMPORTANT - order and leaf depths are important!
let internal_pubkey =
bitcoin::key::UntweakedPublicKey::from_slice(&staker_xonly.as_slice())
.tw_err(|_| SigningErrorType::Error_invalid_params)
.context("Invalid stakerPublicKey")?;
let internal_pubkey = bitcoin::key::UntweakedPublicKey::from_slice(staker.as_slice())
.tw_err(|_| SigningErrorType::Error_invalid_params)
.context("Invalid stakerPublicKey")?;
let spend_info = bitcoin::taproot::TaprootBuilder::new()
.add_leaf(2, timelock_script.clone().into())
.expect("Leaf added at a valid depth")
Expand Down
10 changes: 10 additions & 0 deletions rust/chains/tw_bitcoin/src/babylon/tx_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,15 @@
//
// Copyright © 2017 Trust Wallet.

use tw_keypair::schnorr;

pub mod output;
pub mod utxo;

pub struct BabylonStakingParams {
pub staker: schnorr::XOnlyPublicKey,
pub staking_locktime: u16,
pub finality_provider: schnorr::XOnlyPublicKey,
pub covenants: Vec<schnorr::XOnlyPublicKey>,
pub covenant_quorum: u32,
}
Loading

0 comments on commit 57447ea

Please sign in to comment.