Skip to content

Commit

Permalink
[BTC]: Add Babylon Staking OP_RETURN
Browse files Browse the repository at this point in the history
  • Loading branch information
satoshiotomakan committed Dec 12, 2024
1 parent 1ef78f5 commit cfe0b38
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 20 deletions.
7 changes: 2 additions & 5 deletions rust/chains/tw_bitcoin/src/babylon/claims/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,13 @@ impl StakingSpendInfo {
pub fn new(
staker: &schnorr::PublicKey,
staking_locktime: u16,
finality_providers: &[schnorr::PublicKey],
finality_provider: &schnorr::PublicKey,
covenants: &[schnorr::PublicKey],
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<_> = finality_providers
.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).into();
Expand Down
26 changes: 25 additions & 1 deletion rust/chains/tw_bitcoin/src/babylon/conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Copyright © 2017 Trust Wallet.

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

Expand All @@ -12,6 +12,30 @@ 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
/// OP_RETURN OP_DATA_71 <Tag> <Version> <StakerPublicKey> <FinalityProviderPublicKey> <StakingTime>
/// ```
pub fn new_op_return_script(
tag: &H32,
version: u8,
staker_xonly: &H256,
finality_provider_xonly: &H256,
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(&locktime.to_be_bytes());

let mut s = Script::new();
s.push(OP_RETURN);
s.push_slice(&buf);
s
}

/// The timelock path locks the staker's Bitcoin for a pre-determined number of Bitcoin blocks.
/// https://github.com/babylonchain/babylon/blob/dev/docs/staking-script.md#1-timelock-path
///
Expand Down
42 changes: 36 additions & 6 deletions rust/chains/tw_bitcoin/src/babylon/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
//
// Copyright © 2017 Trust Wallet.

use crate::babylon::claims;
use crate::babylon::claims::UNSPENDABLE_KEY_PATH;
use crate::babylon;
use tw_coin_entry::error::prelude::*;
use tw_hash::H32;
use tw_keypair::schnorr;
use tw_utxo::script::standard_script::conditions;
use tw_utxo::transaction::standard_transaction::builder::OutputBuilder;
use tw_utxo::transaction::standard_transaction::TransactionOutput;

pub const VERSION: u8 = 0;

/// An extension of the [`OutputBuilder`] with Babylon BTC Staking outputs.
pub trait BabylonOutputBuilder: Sized {
fn babylon_staking(
Expand All @@ -19,6 +21,14 @@ pub trait BabylonOutputBuilder: Sized {
finality_providers: &[schnorr::PublicKey],
covenants: &[schnorr::PublicKey],
covenant_quorum: u32,
) -> TransactionOutput;

fn babylon_staking_op_return(
self,
tag: &[u8],
staker: &schnorr::PublicKey,
finality_providers: &[schnorr::PublicKey],
staking_locktime: u16,
) -> SigningResult<TransactionOutput>;
}

Expand All @@ -27,14 +37,14 @@ impl BabylonOutputBuilder for OutputBuilder {
self,
staker: &schnorr::PublicKey,
staking_locktime: u16,
finality_providers: &[schnorr::PublicKey],
finality_provider: &schnorr::PublicKey,
covenants: &[schnorr::PublicKey],
covenant_quorum: u32,
) -> SigningResult<TransactionOutput> {
let spend_info = claims::StakingSpendInfo::new(
let spend_info = babylon::claims::StakingSpendInfo::new(
staker,
staking_locktime,
finality_providers,
finality_provider,
covenants,
covenant_quorum,
)?;
Expand All @@ -44,9 +54,29 @@ impl BabylonOutputBuilder for OutputBuilder {
value: self.get_amount(),
script_pubkey: conditions::new_p2tr_script_path(
// Using an unspendable key as a P2TR internal public key effectively disables taproot key spends.
&UNSPENDABLE_KEY_PATH.compressed(),
&babylon::claims::UNSPENDABLE_KEY_PATH.compressed(),
&merkle_root,
),
})
}

fn babylon_staking_op_return(
self,
tag: &H32,
staker: &schnorr::PublicKey,
finality_provider: &schnorr::PublicKey,
staking_locktime: u16,
) -> TransactionOutput {
let op_return = babylon::conditions::new_op_return_script(
tag,
VERSION,
&staker.x_only().bytes(),
&finality_provider.x_only().bytes(),
staking_locktime,
);
TransactionOutput {
value: self.get_amount(),
script_pubkey: op_return,
}
}
}
34 changes: 31 additions & 3 deletions rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::str::FromStr;
use tw_coin_entry::error::prelude::*;
use tw_hash::hasher::sha256_ripemd;
use tw_hash::sha2::sha256;
use tw_hash::{Hash, H256};
use tw_hash::{Hash, H256, H32};
use tw_keypair::{ecdsa, schnorr};
use tw_memory::Data;
use tw_proto::BitcoinV2::Proto;
Expand Down Expand Up @@ -54,6 +54,9 @@ impl<'a, Context: UtxoContext> OutputProtobuf<'a, Context> {
BuilderType::brc20_inscribe(ref inscription) => self.brc20_inscribe(inscription),
BuilderType::op_return(ref data) => self.op_return(data),
BuilderType::babylon_staking(ref staking) => self.babylon_staking(staking),
BuilderType::babylon_staking_op_return(ref op_return) => {
self.babylon_op_return(op_return)
},
BuilderType::None => SigningError::err(SigningErrorType::Error_invalid_params)
.context("No Output Builder type provided"),
_ => todo!(),
Expand Down Expand Up @@ -166,20 +169,45 @@ impl<'a, Context: UtxoContext> OutputProtobuf<'a, Context> {
.try_into()
.tw_err(|_| SigningErrorType::Error_invalid_params)
.context("stakingTime cannot be greater than 65535")?;
let finality_providers = parse_schnorr_pks(&staking.finality_provider_public_keys)
let finality_provider = &parse_schnorr_pk(&staking.finality_provider_public_key)
.context("Invalid finalityProviderPublicKeys")?;
let covenant_committees = parse_schnorr_pks(&staking.covenant_committee_public_keys)
.context("Invalid covenantCommitteePublicKeys")?;

self.prepare_builder()?.babylon_staking(
&staker,
staking_locktime,
&finality_providers,
&finality_provider,
&covenant_committees,
staking.covenant_committee_quorum,
)
}

pub fn babylon_op_return(
&self,
op_return: &Proto::mod_Output::BabylonStakingOpReturn,
) -> SigningResult<TransactionOutput> {
let tag = H32::try_from(&op_return.tag)
.into_tw()
.context("Expected exactly 4 bytes tag")?;
let staker =
parse_schnorr_pk(&op_return.staker_public_key).context("Invalid stakerPublicKey")?;
let staking_locktime: u16 = op_return
.staking_time
.try_into()
.tw_err(|_| SigningErrorType::Error_invalid_params)
.context("stakingTime cannot be greater than 65535")?;
let finality_provider = &parse_schnorr_pk(&op_return.finality_provider_public_key)
.context("Invalid finalityProviderPublicKeys")?;

Ok(self.prepare_builder()?.babylon_staking_op_return(
&tag,
&staker,
&finality_provider,
staking_locktime,
))
}

pub fn custom_script(&self, script_data: Data) -> SigningResult<TransactionOutput> {
let script = Script::from(script_data);
Ok(self.prepare_builder()?.custom_script_pubkey(script))
Expand Down
9 changes: 4 additions & 5 deletions src/proto/BitcoinV2.proto
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,8 @@ message Output {
message BabylonStakingOutput {
// User's public key.
bytes staker_public_key = 1;
// Finality providers' public keys chosen by the user. In most cases, only one finality provider is needed.
// If there is more than one, it means that delegation is re-staked.
repeated bytes finality_provider_public_keys = 2;
// Finality provider's public key chosen by the user.
bytes finality_provider_public_key = 2;
// global_parameters.min_staking_time <= staking_time <= global_parameters.max_staking_time.
uint32 staking_time = 3;
// Retrieved from global_parameters.covenant_pks.
Expand All @@ -239,7 +238,7 @@ message Output {
bytes tag = 1;
// User's public key.
bytes staker_public_key = 2;
// Chosen by the user sending the staking transaction.
// Finality provider's public key chosen by the user.
bytes finality_provider_public_key = 3;
// global_parameters.min_staking_time <= staking_time <= global_parameters.max_staking_time.
uint32 staking_time = 4;
Expand All @@ -249,7 +248,7 @@ message Output {
message BabylonUnbondingOutput {
// User's public key.
bytes staker_public_key = 1;
// Chosen by the user sending the staking transaction.
// Finality provider's public key chosen by the user.
bytes finality_provider_public_key = 2;
// Retrieved from global_parameters.covenant_pks.
// Babylon nodes that can approve Unbonding tx or Slash the staked position when acting bad.
Expand Down

0 comments on commit cfe0b38

Please sign in to comment.