Skip to content

Commit

Permalink
[BTC]: Add support for Babylon Staking transactions (#4165)
Browse files Browse the repository at this point in the history
* Kotlin multiplatform leaking memory (#4037)

* Add deinit for KMP iOS and JVM targets

* Add deinit for JS target

* Add deinit for JS target

* Fix JVM native name

* Reuse one thread on JVM

---------

Co-authored-by: satoshiotomakan <[email protected]>

* [KMP] Fix issue: memory leak found in Base58.decode in iOS (#4031)

* Fix kmp issue: memory leak found in Base58.decode in iOS

* Remove unused functions

* Fix failed test cases

* Revert "Fix failed test cases"

This reverts commit 57eee39.

* Revert val -> value argument name refactoring

* Output better indentation

* Revert changes in TWEthereumAbiFunction.h

* Fix inconsistent naming

---------

Co-authored-by: satoshiotomakan <[email protected]>

* [TON]: Add support for TON 24-words mnemonic (#3998)

* feat(ton): Add support for TON 24-words mnemonic in Rust

* feat(ton): Add tw_ton_wallet FFIs

* feat(ton): Add TWTONWallet FFI in C++

* feat(ton): Add tonMnemonic StoredKey type

* feat(ton): Add StoredKey TON tests

* feat(ton): Add TWStoredKey TON tests

* feat(ton): Add TONWallet support in Swift

* TODO add iOS tests

* feat(ton): Add `KeyStore` iOS tests

* feat(ton): Add TONWallet support in JavaScript

* Add `KeyStore` TypeScript tests

* feat(ton): Remove `TonMnemonic` structure, replace with a `validate_mnemonic_words` function

* [CI] Trigger CI

* feat(ton): Fix rustfmt

* feat(ton): Fix C++ build

* feat(ton): Fix C++ build

* feat(ton): Fix C++ build

* feat(ton): Fix C++ address analyzer

* feat(ton): Fix C++ tests

* feat(ton): Add Android tests

* feat(ton): Bump `actions/upload-artifact` to v4

* Bump `dawidd6/action-download-artifact` to v6

* feat(eth): Fix PR comments

* Fix Java JVM leak (#4092)

* [Chore]: Fix Android bindings (#4095)

* [Chore]: Add GenericPhantomReference.java

* [Chore]: Fix unnecessary null assertion in WalletCoreLibLoader.kt

* Fix memory lead found in public key in kmp binding (#4097)

* Update Callisto explorer (#4131)

* [Babylon]: Add Babylon Staking Inputs and Outputs to BitcoinV2.proto

* [Babylon]: Add `covenant_committee_signatures`

* Revert "[TON]: Add support for TON 24-words mnemonic (#3998)" (#4148)

This reverts commit 0b16771.

* [Babylon]: Add timelock, unbonding, slashing condition scripts

* Test taproot merkle root generator

* [BTC]: Add Babylon Staking output

* [BTC]: Add Babylon Staking OP_RETURN

* [BTC]: Minor changes

* [BTC]: Add Babylon Staking UTXO

* [BTC]: Add Babylon Unbonding UTXO

* [BTC]: Refactor by adding `BabylonStaking.proto`

* [BTC]: Add Babylon Staking transaction test

* [BTC]: Add spending of Staking output via Unbonding path

* [BTC]: Withdraw Unbonding transaction via timelock path

* [BTC]: Withdraw Staking transaction via timelock path

* [BTC]: Fix tests, minor improves

* [BTC]: Fix TODOs

* [CI] Trigger CI

* [BTC]: Fix Rust sample

---------

Co-authored-by: Viacheslav Kulish <[email protected]>
Co-authored-by: 10gic <[email protected]>
  • Loading branch information
3 people authored Jan 3, 2025
1 parent 6ac227a commit a6e7ac0
Show file tree
Hide file tree
Showing 49 changed files with 2,914 additions and 88 deletions.
5 changes: 5 additions & 0 deletions rust/Cargo.lock

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

4 changes: 3 additions & 1 deletion rust/chains/tw_bitcoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ version = "0.1.0"
edition = "2021"

[dependencies]
bitcoin = { version = "0.30.0", features = ["rand-std"] }
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"] }
serde_json = "1.0"
Expand Down
153 changes: 153 additions & 0 deletions rust/chains/tw_bitcoin/src/babylon/conditions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

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;

/// 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_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_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();
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
///
/// ```txt
/// <StakerPK> OP_CHECKSIGVERIFY <TimelockBlocks> OP_CHECKSEQUENCEVERIFY
/// ```
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_key, VERIFY);
s.push_int(locktime as i64);
s.push(OP_CHECKSEQUENCEVERIFY);
s
}

/// The unbonding path allows the staker to on-demand unlock their locked Bitcoin before the timelock expires.
/// https://github.com/babylonchain/babylon/blob/dev/docs/staking-script.md#2-unbonding-path
///
/// ```txt
/// <StakerPk> OP_CHECKSIGVERIFY
/// <CovenantPk1> OP_CHECKSIG <CovenantPk2> OP_CHECKSIGADD ... <CovenantPkN> OP_CHECKSIGADD
/// <CovenantThreshold> OP_NUMEQUAL
/// ```
pub fn new_unbonding_script(
staker_key: &schnorr::XOnlyPublicKey,
covenants: &MultiSigOrderedKeys,
) -> Script {
let mut s = Script::with_capacity(64);
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.public_keys_ordered(),
covenants.quorum(),
NO_VERIFY,
);
s
}

/// The slashing path is utilized for punishing finality providers and their delegators in the case of double signing.
/// https://github.com/babylonchain/babylon/blob/dev/docs/staking-script.md#3-slashing-path
///
/// ```txt
/// <StakerPk> OP_CHECKSIGVERIFY
/// <FinalityProviderPk1> OP_CHECKSIG <FinalityProviderPk2> OP_CHECKSIGADD ... <FinalityProviderPkN> OP_CHECKSIGADD
/// 1 OP_NUMEQUAL
/// <CovenantPk1> OP_CHECKSIG <CovenantPk2> OP_CHECKSIGADD ... <CovenantPkN> OP_CHECKSIGADD
/// <CovenantThreshold> OP_NUMEQUAL
/// ```
pub fn new_slashing_script(
staker_key: &schnorr::XOnlyPublicKey,
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.public_keys_ordered(),
finality_providers_keys.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.public_keys_ordered(),
covenants.quorum(),
NO_VERIFY,
);
s
}

fn append_single_sig(dst: &mut Script, key: &schnorr::XOnlyPublicKey, verify: bool) {
dst.push_slice(key.bytes().as_slice());
if verify {
dst.push(OP_CHECKSIGVERIFY);
} else {
dst.push(OP_CHECKSIG);
}
}

/// Creates a multisig script with given keys and signer threshold to
/// 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 and sorted.
fn append_multi_sig(
dst: &mut Script,
pubkeys: &[schnorr::XOnlyPublicKey],
quorum: u32,
verify: bool,
) {
if pubkeys.len() == 1 {
return append_single_sig(dst, &pubkeys[0], verify);
}

for (i, pk_xonly) in pubkeys.iter().enumerate() {
dst.push_slice(pk_xonly.bytes().as_slice());
if i == 0 {
dst.push(OP_CHECKSIG);
} else {
dst.push(OP_CHECKSIGADD);
}
}

dst.push_int(quorum as i64);
if verify {
dst.push(OP_NUMEQUALVERIFY);
} else {
dst.push(OP_NUMEQUAL);
}
}
10 changes: 10 additions & 0 deletions rust/chains/tw_bitcoin/src/babylon/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

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

use itertools::Itertools;
use std::collections::BTreeMap;
use tw_coin_entry::error::prelude::{
OrTWError, ResultContext, SigningError, SigningErrorType, SigningResult,
};
use tw_keypair::schnorr;
use tw_utxo::signature::BitcoinSchnorrSignature;

type OptionalSignature = Option<BitcoinSchnorrSignature>;
type PubkeySigMap = BTreeMap<schnorr::XOnlyPublicKey, OptionalSignature>;

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

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 public keys provided");
}

if pks.len() < quorum as usize {
return SigningError::err(SigningErrorType::Error_invalid_params).context(
"Required number of valid signers is greater than number of provided keys",
);
}

// 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("Public keys must be unique");
}

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

pub fn public_keys_ordered(&self) -> &[schnorr::XOnlyPublicKey] {
&self.pks
}

pub fn quorum(&self) -> u32 {
self.quorum
}

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 = MultiSigOrdered::checked(self.pks, self.quorum);
pk_sig_map.set_partial_signatures(sigs)?;
pk_sig_map.check_quorum()?;
Ok(pk_sig_map)
}
}

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

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();

// Initialize the map with public keys and null signatures first.
for pk in pks {
pk_sig_map.insert(pk, None);
}

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 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 public key")?;

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

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 signatures '{sig_num}' is less than quorum '{}'",
self.quorum
));
}
Ok(())
}

/// Get signatures sorted by corresponding public keys in reverse lexicographical order
/// because the script interpreter will pop the left-most byte-array as the first stack element,
/// the second-left-most byte array as the second stack element, and so on.
///
/// In other words,
/// `[SigN] [SigN-1] ... [Sig0]`
/// where the list `Sig0 ... SigN` are the Schnorr signatures corresponding to the public keys `Pk0 ... PkN`.
///
/// https://gnusha.org/pi/bitcoindev/20220820134850.ofvz7225zwcyffit@artanis (=== Spending K-of-N Multisig outputs ===)
pub fn get_signatures_reverse_order(&self) -> Vec<OptionalSignature> {
self.pk_sig_map
.iter()
.rev()
.map(|(_pk, sig)| sig.clone())
.collect()
}
}
Loading

0 comments on commit a6e7ac0

Please sign in to comment.