Skip to content

Commit

Permalink
[Bitcoin]: Refactor PSBT protocol (#4038)
Browse files Browse the repository at this point in the history
* feat(btc): Refactor BitcoinV2.proto

* Move PSBT input to `BitcoinV2.SigningInput.transaction.psbt`
* Add `BitcoinV2.TransactionBuilder`

* feat(btc): Refactor Signer, Planner and Compiler to the refactored Protobuf

* Remove `tw_bitcoin_psbt_sign` and `tw_bitcoin_psbt_plan`

* feat(btc): Adopt integration tests

* feat(btc): Remove `TWBitcoinPsbt` module

* Adopt C++ integration tests

* feat(btc): Adopt iOS tests

* feat(btc): Adopt Android tests

* feat(btc): Adopt WASM tests
  • Loading branch information
satoshiotomakan authored Sep 25, 2024
1 parent bbb9913 commit 4af0ee3
Show file tree
Hide file tree
Showing 46 changed files with 637 additions and 648 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.trustwallet.core.app.utils.toHexBytes
import com.trustwallet.core.app.utils.toHexBytesInByteString
import org.junit.Assert.assertEquals
import org.junit.Test
import wallet.core.jni.BitcoinPsbt
import wallet.core.java.AnySigner
import wallet.core.jni.BitcoinScript
import wallet.core.jni.BitcoinSigHashType
import wallet.core.jni.CoinType
Expand All @@ -17,7 +17,6 @@ import wallet.core.jni.PrivateKey
import wallet.core.jni.PublicKey
import wallet.core.jni.PublicKeyType
import wallet.core.jni.proto.Bitcoin
import wallet.core.jni.proto.Bitcoin.SigningOutput
import wallet.core.jni.proto.BitcoinV2
import wallet.core.jni.proto.Common.SigningError

Expand All @@ -34,17 +33,21 @@ class TestBitcoinPsbt {
val privateKey = "f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55".toHexBytesInByteString()
val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString()

val input = BitcoinV2.PsbtSigningInput.newBuilder()
.setPsbt(psbt)
val input = BitcoinV2.SigningInput.newBuilder()
.setPsbt(BitcoinV2.Psbt.newBuilder().setPsbt(psbt))
.addPrivateKeys(privateKey)
.build()

val outputData = BitcoinPsbt.sign(input.toByteArray(), BITCOIN)
val output = BitcoinV2.PsbtSigningOutput.parseFrom(outputData)
val legacyInput = Bitcoin.SigningInput.newBuilder()
.setSigningV2(input)
.build()

val legacyOutput = AnySigner.sign(legacyInput, BITCOIN, Bitcoin.SigningOutput.parser())
val output = legacyOutput.signingResultV2

assertEquals(output.error, SigningError.OK)
assertEquals(
output.psbt.toByteArray().toHex(),
output.psbt.psbt.toByteArray().toHex(),
"0x70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d01086c02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000"
)
assertEquals(
Expand All @@ -65,37 +68,41 @@ class TestBitcoinPsbt {
val publicKey = privateKey.getPublicKeySecp256k1(true)
val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString()

val input = BitcoinV2.PsbtSigningInput.newBuilder()
.setPsbt(psbt)
val input = BitcoinV2.SigningInput.newBuilder()
.setPsbt(BitcoinV2.Psbt.newBuilder().setPsbt(psbt))
.addPublicKeys(ByteString.copyFrom(publicKey.data()))
.build()

val outputData = BitcoinPsbt.plan(input.toByteArray(), BITCOIN)
val output = BitcoinV2.TransactionPlan.parseFrom(outputData)
val legacyInput = Bitcoin.SigningInput.newBuilder()
.setSigningV2(input)
.build()

assertEquals(output.error, SigningError.OK)
val legacyPlan = AnySigner.plan(legacyInput, BITCOIN, Bitcoin.TransactionPlan.parser())
val plan = legacyPlan.planningResultV2

assertEquals(plan.error, SigningError.OK)

assertEquals(output.getInputs(0).receiverAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
assertEquals(output.getInputs(0).value, 66_406)
assertEquals(plan.getInputs(0).receiverAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
assertEquals(plan.getInputs(0).value, 66_406)

// Vault transfer
assertEquals(output.getOutputs(0).toAddress, "bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0")
assertEquals(output.getOutputs(0).value, 60_000)
assertEquals(plan.getOutputs(0).toAddress, "bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0")
assertEquals(plan.getOutputs(0).value, 60_000)

// OP_RETURN
assertEquals(
output.getOutputs(1).customScriptPubkey.toByteArray().toHex(),
plan.getOutputs(1).customScriptPubkey.toByteArray().toHex(),
"0x6a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a3530"
)
assertEquals(output.getOutputs(1).value, 0)
assertEquals(plan.getOutputs(1).value, 0)

// Change output
assertEquals(output.getOutputs(2).toAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
assertEquals(output.getOutputs(2).value, 4_670)
assertEquals(plan.getOutputs(2).toAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
assertEquals(plan.getOutputs(2).value, 4_670)

assertEquals(output.feeEstimate, 1736)
assertEquals(plan.feeEstimate, 1736)
// Please note that `change` in PSBT planning is always 0.
// That's because we aren't able to determine which output is an actual change from PSBT.
assertEquals(output.change, 0)
assertEquals(plan.change, 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,19 +198,21 @@ class TestBitcoinSigning {
p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build()
})

val signingInput = BitcoinV2.SigningInput.newBuilder()
val builder = BitcoinV2.TransactionBuilder.newBuilder()
.setVersion(BitcoinV2.TransactionVersion.V2)
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
.addInputs(utxo0)
.addOutputs(out0)
.addOutputs(changeOutput)
.setInputSelector(BitcoinV2.InputSelector.UseAll)
.setFixedDustThreshold(dustSatoshis)
val signingInput = BitcoinV2.SigningInput.newBuilder()
.setBuilder(builder)
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
.setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply {
p2PkhPrefix = 0
p2ShPrefix = 5
})
.setDangerousUseFixedSchnorrRng(true)
.setFixedDustThreshold(dustSatoshis)
.build()

val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply {
Expand Down Expand Up @@ -256,18 +258,20 @@ class TestBitcoinSigning {
p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build()
})

val signingInput = BitcoinV2.SigningInput.newBuilder()
val builder = BitcoinV2.TransactionBuilder.newBuilder()
.setVersion(BitcoinV2.TransactionVersion.V2)
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
.addInputs(utxo0)
.addOutputs(out0)
.setInputSelector(BitcoinV2.InputSelector.UseAll)
.setFixedDustThreshold(dustSatoshis)
val signingInput = BitcoinV2.SigningInput.newBuilder()
.setBuilder(builder)
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
.setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply {
p2PkhPrefix = 0
p2ShPrefix = 5
})
.setDangerousUseFixedSchnorrRng(true)
.setFixedDustThreshold(dustSatoshis)
.build()

val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply {
Expand Down Expand Up @@ -326,20 +330,22 @@ class TestBitcoinSigning {
p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build()
})

val signingInput = BitcoinV2.SigningInput.newBuilder()
val builder = BitcoinV2.TransactionBuilder.newBuilder()
.setVersion(BitcoinV2.TransactionVersion.V2)
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
.addInputs(utxo0)
.addInputs(utxo1)
.addOutputs(out0)
.addOutputs(changeOutput)
.setInputSelector(BitcoinV2.InputSelector.UseAll)
.setFixedDustThreshold(dustSatoshis)
val signingInput = BitcoinV2.SigningInput.newBuilder()
.setBuilder(builder)
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
.setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply {
p2PkhPrefix = 0
p2ShPrefix = 5
})
.setDangerousUseFixedSchnorrRng(true)
.setFixedDustThreshold(dustSatoshis)
.build()

val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply {
Expand Down
36 changes: 0 additions & 36 deletions include/TrustWalletCore/TWBitcoinPsbt.h

This file was deleted.

26 changes: 0 additions & 26 deletions rust/chains/tw_bitcoin/src/entry.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::modules::compiler::BitcoinCompiler;
use crate::modules::planner::BitcoinPlanner;
use crate::modules::psbt_planner::PsbtPlanner;
use crate::modules::signer::BitcoinSigner;
use crate::modules::transaction_util::BitcoinTransactionUtil;
use std::str::FromStr;
Expand All @@ -15,7 +14,6 @@ use tw_coin_entry::modules::wallet_connector::NoWalletConnector;
use tw_keypair::tw::PublicKey;
use tw_proto::BitcoinV2::Proto;
use tw_utxo::address::standard_bitcoin::{StandardBitcoinAddress, StandardBitcoinPrefix};
use tw_utxo::utxo_entry::UtxoEntry;

pub struct BitcoinEntry;

Expand Down Expand Up @@ -99,27 +97,3 @@ impl CoinEntry for BitcoinEntry {
Some(BitcoinTransactionUtil)
}
}

impl UtxoEntry for BitcoinEntry {
type PsbtSigningInput<'a> = Proto::PsbtSigningInput<'a>;
type PsbtSigningOutput = Proto::PsbtSigningOutput<'static>;
type PsbtTransactionPlan = Proto::TransactionPlan<'static>;

#[inline]
fn sign_psbt(
&self,
coin: &dyn CoinContext,
input: Self::PsbtSigningInput<'_>,
) -> Self::PsbtSigningOutput {
BitcoinSigner::sign_psbt(coin, &input)
}

#[inline]
fn plan_psbt(
&self,
coin: &dyn CoinContext,
input: Self::PsbtSigningInput<'_>,
) -> Self::PsbtTransactionPlan {
PsbtPlanner::plan_psbt(coin, &input)
}
}
58 changes: 55 additions & 3 deletions rust/chains/tw_bitcoin/src/modules/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Copyright © 2017 Trust Wallet.

use crate::modules::protobuf_builder::ProtobufBuilder;
use crate::modules::psbt_request::PsbtRequest;
use crate::modules::signing_request::SigningRequestBuilder;
use std::borrow::Cow;
use tw_coin_entry::coin_context::CoinContext;
Expand All @@ -13,6 +14,7 @@ use tw_proto::BitcoinV2::Proto;
use tw_proto::BitcoinV2::Proto::mod_PreSigningOutput::{
SigningMethod as ProtoSigningMethod, TaprootTweak as ProtoTaprootTweak,
};
use tw_proto::BitcoinV2::Proto::mod_SigningInput::OneOftransaction as TransactionType;
use tw_utxo::modules::sighash_computer::{SighashComputer, TaprootTweak, TxPreimage};
use tw_utxo::modules::sighash_verifier::SighashVerifier;
use tw_utxo::modules::tx_compiler::TxCompiler;
Expand All @@ -39,8 +41,17 @@ impl BitcoinCompiler {
coin: &dyn CoinContext,
input: Proto::SigningInput<'_>,
) -> SigningResult<Proto::PreSigningOutput<'static>> {
let request = SigningRequestBuilder::build(coin, &input)?;
let SelectResult { unsigned_tx, .. } = TxPlanner::plan(request)?;
let unsigned_tx = match input.transaction {
TransactionType::builder(ref tx_builder) => {
let request = SigningRequestBuilder::build(coin, &input, tx_builder)?;
TxPlanner::plan(request)?.unsigned_tx
},
TransactionType::psbt(ref psbt) => PsbtRequest::build(&input, psbt)?.unsigned_tx,
TransactionType::None => {
return SigningError::err(SigningErrorType::Error_invalid_params)
.context("Either `TransactionBuilder` or `Psbt` should be set")
},
};

let TxPreimage { sighashes } = SighashComputer::preimage_tx(&unsigned_tx)?;

Expand Down Expand Up @@ -77,7 +88,23 @@ impl BitcoinCompiler {
signatures: Vec<SignatureBytes>,
_public_keys: Vec<PublicKeyBytes>,
) -> SigningResult<Proto::SigningOutput<'static>> {
let request = SigningRequestBuilder::build(coin, &input)?;
match input.transaction {
TransactionType::builder(ref tx) => {
Self::compile_with_tx_builder(coin, &input, tx, signatures)
},
TransactionType::psbt(ref psbt) => Self::compile_psbt(coin, &input, psbt, signatures),
TransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params)
.context("No transaction type specified"),
}
}

fn compile_with_tx_builder(
coin: &dyn CoinContext,
input: &Proto::SigningInput,
tx_builder_input: &Proto::TransactionBuilder,
signatures: Vec<SignatureBytes>,
) -> SigningResult<Proto::SigningOutput<'static>> {
let request = SigningRequestBuilder::build(coin, input, tx_builder_input)?;
let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?;

SighashVerifier::verify_signatures(&unsigned_tx, &signatures)?;
Expand All @@ -96,6 +123,31 @@ impl BitcoinCompiler {
..Proto::SigningOutput::default()
})
}

fn compile_psbt(
_coin: &dyn CoinContext,
input: &Proto::SigningInput,
psbt: &Proto::Psbt,
signatures: Vec<SignatureBytes>,
) -> SigningResult<Proto::SigningOutput<'static>> {
let PsbtRequest { unsigned_tx, .. } = PsbtRequest::build(input, psbt)?;
let fee = unsigned_tx.fee()?;

SighashVerifier::verify_signatures(&unsigned_tx, &signatures)?;
let signed_tx = TxCompiler::compile(unsigned_tx, &signatures)?;
let tx_proto = ProtobufBuilder::tx_to_proto(&signed_tx);

Ok(Proto::SigningOutput {
transaction: Some(tx_proto),
encoded: Cow::from(signed_tx.encode_out()),
txid: Cow::from(signed_tx.txid()),
// `vsize` could have been changed after the transaction being signed.
vsize: signed_tx.vsize() as u64,
weight: signed_tx.weight() as u64,
fee,
..Proto::SigningOutput::default()
})
}
}

pub fn signing_method(s: SigningMethod) -> ProtoSigningMethod {
Expand Down
1 change: 0 additions & 1 deletion rust/chains/tw_bitcoin/src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ pub mod compiler;
pub mod planner;
pub mod protobuf_builder;
pub mod psbt;
pub mod psbt_planner;
pub mod psbt_request;
pub mod signer;
pub mod signing_request;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,39 @@ use tw_proto::BitcoinV2::Proto;
use tw_utxo::modules::tx_planner::TxPlanner;
use tw_utxo::modules::utxo_selector::SelectResult;

pub mod psbt_planner;

pub struct BitcoinPlanner;

impl BitcoinPlanner {
pub fn plan_impl<'a>(
coin: &dyn CoinContext,
input: &Proto::SigningInput<'a>,
) -> SigningResult<Proto::TransactionPlan<'a>> {
let request = SigningRequestBuilder::build(coin, input)?;
use Proto::mod_SigningInput::OneOftransaction as TransactionType;

match input.transaction {
TransactionType::builder(ref tx) => Self::plan_with_tx_builder(coin, input, tx),
TransactionType::psbt(ref psbt) => {
psbt_planner::PsbtPlanner::plan_psbt(coin, input, psbt)
},
TransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params)
.context("Either `TransactionBuilder` or `Psbt` should be set"),
}
}

pub fn plan_with_tx_builder<'a>(
coin: &dyn CoinContext,
input: &Proto::SigningInput<'a>,
tx_builder: &Proto::TransactionBuilder<'a>,
) -> SigningResult<Proto::TransactionPlan<'a>> {
let request = SigningRequestBuilder::build(coin, input, tx_builder)?;
let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?;

// Prepare a map of source Inputs Proto `{ OutPoint -> Input }`.
// It will be used to find a Input Proto by its `OutPoint`.
let mut inputs_map = HashMap::with_capacity(input.inputs.len());
for utxo in input.inputs.iter() {
let mut inputs_map = HashMap::with_capacity(tx_builder.inputs.len());
for utxo in tx_builder.inputs.iter() {
let key = parse_out_point(&utxo.out_point)?;
if inputs_map.insert(key, utxo).is_some() {
// Found a duplicate UTXO. Return an error.
Expand Down
Loading

0 comments on commit 4af0ee3

Please sign in to comment.