-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[BTC]: Add support for Babylon Staking transactions (#4165)
* 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
1 parent
6ac227a
commit a6e7ac0
Showing
49 changed files
with
2,914 additions
and
88 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
134
rust/chains/tw_bitcoin/src/babylon/multi_sig_ordered.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
Oops, something went wrong.