diff --git a/rust/chains/tw_bitcoin/src/babylon/claims/mod.rs b/rust/chains/tw_bitcoin/src/babylon/claims/mod.rs index 058666d424c..c5868aba97b 100644 --- a/rust/chains/tw_bitcoin/src/babylon/claims/mod.rs +++ b/rust/chains/tw_bitcoin/src/babylon/claims/mod.rs @@ -84,4 +84,36 @@ impl StakingSpendInfo { .or_tw_err(SigningErrorType::Error_internal) .context("No merkle root of the Babylon Staking transaction spend info") } + + pub fn timelock_script(&self) -> &Script { + &self.timelock_script + } + + pub fn unbonding_script(&self) -> &Script { + &self.unbonding_script + } + + pub fn slashing_script(&self) -> &Script { + &self.slashing_script + } + + pub fn timelock_control_block(&self) -> SigningResult { + self.control_block(&self.timelock_script) + } + + pub fn unbonding_control_block(&self) -> SigningResult { + self.control_block(&self.unbonding_script) + } + + pub fn slashing_control_block(&self) -> SigningResult { + self.control_block(&self.slashing_script) + } + + fn control_block(&self, script: &Script) -> SigningResult { + let script = bitcoin::script::ScriptBuf::from_bytes(script.to_vec()); + self.spend_info + .control_block(&(script, bitcoin::taproot::LeafVersion::TapScript)) + .or_tw_err(SigningErrorType::Error_internal) + .context("'TaprootSpendInfo::control_block' is None") + } } diff --git a/rust/chains/tw_bitcoin/src/babylon/tx_builder/mod.rs b/rust/chains/tw_bitcoin/src/babylon/tx_builder/mod.rs new file mode 100644 index 00000000000..1dc8b33389e --- /dev/null +++ b/rust/chains/tw_bitcoin/src/babylon/tx_builder/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod output; +pub mod utxo; diff --git a/rust/chains/tw_bitcoin/src/babylon/tx_builder.rs b/rust/chains/tw_bitcoin/src/babylon/tx_builder/output.rs similarity index 100% rename from rust/chains/tw_bitcoin/src/babylon/tx_builder.rs rename to rust/chains/tw_bitcoin/src/babylon/tx_builder/output.rs diff --git a/rust/chains/tw_bitcoin/src/babylon/tx_builder/utxo.rs b/rust/chains/tw_bitcoin/src/babylon/tx_builder/utxo.rs new file mode 100644 index 00000000000..c22ac329b9a --- /dev/null +++ b/rust/chains/tw_bitcoin/src/babylon/tx_builder/utxo.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::babylon; +use crate::babylon::claims::UNSPENDABLE_KEY_PATH; +use tw_coin_entry::error::prelude::*; +use tw_keypair::schnorr; +use tw_utxo::transaction::standard_transaction::builder::UtxoBuilder; +use tw_utxo::transaction::standard_transaction::TransactionInput; +use tw_utxo::transaction::UtxoToSign; + +/// An extension of the [`UtxoBuilder`] with Babylon BTC Staking outputs. +pub trait BabylonUtxoBuilder: Sized { + fn babylon_timelock_path( + self, + staker: &schnorr::PublicKey, + staking_locktime: u16, + finality_provider: &schnorr::PublicKey, + covenants: &[schnorr::PublicKey], + covenant_quorum: u32, + ) -> SigningResult<(TransactionInput, UtxoToSign)>; +} + +impl BabylonUtxoBuilder for UtxoBuilder { + fn babylon_timelock_path( + self, + staker: &schnorr::PublicKey, + staking_locktime: u16, + finality_provider: &schnorr::PublicKey, + covenants: &[schnorr::PublicKey], + covenant_quorum: u32, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let spend_info = babylon::claims::StakingSpendInfo::new( + staker, + staking_locktime, + finality_provider, + covenants, + covenant_quorum, + )?; + + let control_block = spend_info.timelock_control_block()?.serialize(); + let merkle_root = spend_info.merkle_root()?; + let timelock_payload = spend_info.timelock_script().clone(); + + self.p2tr_script_path( + // Babylon Staking or Unbonding output was created using an unspendable internal public key, + // that means taproot key spends is disabled. + &UNSPENDABLE_KEY_PATH, + timelock_payload, + control_block, + &merkle_root, + ) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs index 98c05c767aa..d46415c4637 100644 --- a/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs @@ -2,6 +2,9 @@ // // Copyright © 2017 Trust Wallet. +use std::borrow::Cow; +use tw_coin_entry::error::prelude::*; +use tw_keypair::schnorr; use tw_utxo::context::AddressPrefixes; pub mod output_protobuf; @@ -24,3 +27,11 @@ impl BitcoinChainInfo { } } } + +fn parse_schnorr_pk(bytes: &Cow<[u8]>) -> SigningResult { + schnorr::PublicKey::try_from(bytes.as_ref()).into_tw() +} + +fn parse_schnorr_pks(pks: &[Cow<[u8]>]) -> SigningResult> { + pks.iter().map(parse_schnorr_pk).collect() +} diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs index c3289f5eb52..32166bb07d2 100644 --- a/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs @@ -2,9 +2,8 @@ // // Copyright © 2017 Trust Wallet. -use crate::babylon::tx_builder::BabylonOutputBuilder; -use crate::modules::tx_builder::BitcoinChainInfo; -use std::borrow::Cow; +use crate::babylon::tx_builder::output::BabylonOutputBuilder; +use crate::modules::tx_builder::{parse_schnorr_pk, parse_schnorr_pks, BitcoinChainInfo}; use std::marker::PhantomData; use std::str::FromStr; use tw_coin_entry::error::prelude::*; @@ -277,11 +276,3 @@ impl<'a, Context: UtxoContext> OutputProtobuf<'a, Context> { .with_context(|| format!("Expected exactly {N} bytes public key hash")) } } - -fn parse_schnorr_pk(bytes: &Cow<[u8]>) -> SigningResult { - schnorr::PublicKey::try_from(bytes.as_ref()).into_tw() -} - -fn parse_schnorr_pks(pks: &[Cow<[u8]>]) -> SigningResult> { - pks.iter().map(parse_schnorr_pk).collect() -} diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs index 06d16de723c..3760fe9c8d5 100644 --- a/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs @@ -2,9 +2,10 @@ // // Copyright © 2017 Trust Wallet. +use crate::babylon::tx_builder::utxo::BabylonUtxoBuilder; use crate::modules::tx_builder::public_keys::PublicKeys; use crate::modules::tx_builder::script_parser::{StandardScript, StandardScriptParser}; -use crate::modules::tx_builder::BitcoinChainInfo; +use crate::modules::tx_builder::{parse_schnorr_pk, parse_schnorr_pks, BitcoinChainInfo}; use std::marker::PhantomData; use std::str::FromStr; use tw_coin_entry::error::prelude::*; @@ -56,9 +57,12 @@ impl<'a, Context: UtxoContext> UtxoProtobuf<'a, Context> { BuilderType::p2tr_key_path(ref key_path) => self.p2tr_key_path(key_path), // BuilderType::p2tr_script_path(ref script) => self.p2tr_script_path(script), BuilderType::brc20_inscribe(ref inscription) => self.brc20_inscribe(inscription), + BuilderType::babylon_timelock_path(ref timelock) => { + self.babylon_timelock_path(timelock) + }, BuilderType::None => SigningError::err(SigningErrorType::Error_invalid_params) .context("No Input Builder type provided"), - _ => todo!() + _ => todo!(), }, ScriptType::script_data(ref script) => self.custom_script(script.to_vec()), ScriptType::receiver_address(ref address) => self.recipient_address(address), @@ -131,6 +135,31 @@ impl<'a, Context: UtxoContext> UtxoProtobuf<'a, Context> { ) } + pub fn babylon_timelock_path( + &self, + timelock: &Proto::mod_Input::BabylonStakingTimelockPath, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let staker = + parse_schnorr_pk(&timelock.staker_public_key).context("Invalid stakerPublicKey")?; + let staking_locktime: u16 = timelock + .staking_time + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("stakingTime cannot be greater than 65535")?; + let finality_provider = parse_schnorr_pk(&timelock.finality_provider_public_key) + .context("Invalid finalityProviderPublicKeys")?; + let covenant_committees = parse_schnorr_pks(&timelock.covenant_committee_public_keys) + .context("Invalid covenantCommitteePublicKeys")?; + + self.prepare_builder()?.babylon_timelock_path( + &staker, + staking_locktime, + &finality_provider, + &covenant_committees, + timelock.covenant_committee_quorum, + ) + } + pub fn custom_script( &self, script_data: Data, diff --git a/src/proto/BitcoinV2.proto b/src/proto/BitcoinV2.proto index 2e7a9d4a053..5c79bc469a6 100644 --- a/src/proto/BitcoinV2.proto +++ b/src/proto/BitcoinV2.proto @@ -127,9 +127,17 @@ message Input { message BabylonStakingTimelockPath { // User's public key. bytes staker_public_key = 1; + // Finality provider's public key chosen by the user. + bytes finality_provider_public_key = 2; // Staking Output's lock time. // Must be expired to be spent. uint32 staking_time = 3; + // Retrieved from global_parameters.covenant_pks. + // Babylon nodes that can approve Unbonding tx or Slash the staked position when acting bad. + repeated bytes covenant_committee_public_keys = 4; + // Retrieved from global_parameters.covenant_quorum. + // Specifies the quorum required by the covenant committee for unbonding transactions to be confirmed. + uint32 covenant_committee_quorum = 5; } // Unbonding path can be used to on-demand unlock `Staking Output` before the timelock expires.