Skip to content

Commit 4af0ee3

Browse files
[Bitcoin]: Refactor PSBT protocol (#4038)
* 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
1 parent bbb9913 commit 4af0ee3

File tree

46 files changed

+637
-648
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+637
-648
lines changed

android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinPsbt.kt

+29-22
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.trustwallet.core.app.utils.toHexBytes
77
import com.trustwallet.core.app.utils.toHexBytesInByteString
88
import org.junit.Assert.assertEquals
99
import org.junit.Test
10-
import wallet.core.jni.BitcoinPsbt
10+
import wallet.core.java.AnySigner
1111
import wallet.core.jni.BitcoinScript
1212
import wallet.core.jni.BitcoinSigHashType
1313
import wallet.core.jni.CoinType
@@ -17,7 +17,6 @@ import wallet.core.jni.PrivateKey
1717
import wallet.core.jni.PublicKey
1818
import wallet.core.jni.PublicKeyType
1919
import wallet.core.jni.proto.Bitcoin
20-
import wallet.core.jni.proto.Bitcoin.SigningOutput
2120
import wallet.core.jni.proto.BitcoinV2
2221
import wallet.core.jni.proto.Common.SigningError
2322

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

37-
val input = BitcoinV2.PsbtSigningInput.newBuilder()
38-
.setPsbt(psbt)
36+
val input = BitcoinV2.SigningInput.newBuilder()
37+
.setPsbt(BitcoinV2.Psbt.newBuilder().setPsbt(psbt))
3938
.addPrivateKeys(privateKey)
4039
.build()
4140

42-
val outputData = BitcoinPsbt.sign(input.toByteArray(), BITCOIN)
43-
val output = BitcoinV2.PsbtSigningOutput.parseFrom(outputData)
41+
val legacyInput = Bitcoin.SigningInput.newBuilder()
42+
.setSigningV2(input)
43+
.build()
44+
45+
val legacyOutput = AnySigner.sign(legacyInput, BITCOIN, Bitcoin.SigningOutput.parser())
46+
val output = legacyOutput.signingResultV2
4447

4548
assertEquals(output.error, SigningError.OK)
4649
assertEquals(
47-
output.psbt.toByteArray().toHex(),
50+
output.psbt.psbt.toByteArray().toHex(),
4851
"0x70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d01086c02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000"
4952
)
5053
assertEquals(
@@ -65,37 +68,41 @@ class TestBitcoinPsbt {
6568
val publicKey = privateKey.getPublicKeySecp256k1(true)
6669
val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString()
6770

68-
val input = BitcoinV2.PsbtSigningInput.newBuilder()
69-
.setPsbt(psbt)
71+
val input = BitcoinV2.SigningInput.newBuilder()
72+
.setPsbt(BitcoinV2.Psbt.newBuilder().setPsbt(psbt))
7073
.addPublicKeys(ByteString.copyFrom(publicKey.data()))
7174
.build()
7275

73-
val outputData = BitcoinPsbt.plan(input.toByteArray(), BITCOIN)
74-
val output = BitcoinV2.TransactionPlan.parseFrom(outputData)
76+
val legacyInput = Bitcoin.SigningInput.newBuilder()
77+
.setSigningV2(input)
78+
.build()
7579

76-
assertEquals(output.error, SigningError.OK)
80+
val legacyPlan = AnySigner.plan(legacyInput, BITCOIN, Bitcoin.TransactionPlan.parser())
81+
val plan = legacyPlan.planningResultV2
82+
83+
assertEquals(plan.error, SigningError.OK)
7784

78-
assertEquals(output.getInputs(0).receiverAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
79-
assertEquals(output.getInputs(0).value, 66_406)
85+
assertEquals(plan.getInputs(0).receiverAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
86+
assertEquals(plan.getInputs(0).value, 66_406)
8087

8188
// Vault transfer
82-
assertEquals(output.getOutputs(0).toAddress, "bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0")
83-
assertEquals(output.getOutputs(0).value, 60_000)
89+
assertEquals(plan.getOutputs(0).toAddress, "bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0")
90+
assertEquals(plan.getOutputs(0).value, 60_000)
8491

8592
// OP_RETURN
8693
assertEquals(
87-
output.getOutputs(1).customScriptPubkey.toByteArray().toHex(),
94+
plan.getOutputs(1).customScriptPubkey.toByteArray().toHex(),
8895
"0x6a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a3530"
8996
)
90-
assertEquals(output.getOutputs(1).value, 0)
97+
assertEquals(plan.getOutputs(1).value, 0)
9198

9299
// Change output
93-
assertEquals(output.getOutputs(2).toAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
94-
assertEquals(output.getOutputs(2).value, 4_670)
100+
assertEquals(plan.getOutputs(2).toAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
101+
assertEquals(plan.getOutputs(2).value, 4_670)
95102

96-
assertEquals(output.feeEstimate, 1736)
103+
assertEquals(plan.feeEstimate, 1736)
97104
// Please note that `change` in PSBT planning is always 0.
98105
// That's because we aren't able to determine which output is an actual change from PSBT.
99-
assertEquals(output.change, 0)
106+
assertEquals(plan.change, 0)
100107
}
101108
}

android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt

+15-9
Original file line numberDiff line numberDiff line change
@@ -198,19 +198,21 @@ class TestBitcoinSigning {
198198
p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build()
199199
})
200200

201-
val signingInput = BitcoinV2.SigningInput.newBuilder()
201+
val builder = BitcoinV2.TransactionBuilder.newBuilder()
202202
.setVersion(BitcoinV2.TransactionVersion.V2)
203-
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
204203
.addInputs(utxo0)
205204
.addOutputs(out0)
206205
.addOutputs(changeOutput)
207206
.setInputSelector(BitcoinV2.InputSelector.UseAll)
207+
.setFixedDustThreshold(dustSatoshis)
208+
val signingInput = BitcoinV2.SigningInput.newBuilder()
209+
.setBuilder(builder)
210+
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
208211
.setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply {
209212
p2PkhPrefix = 0
210213
p2ShPrefix = 5
211214
})
212215
.setDangerousUseFixedSchnorrRng(true)
213-
.setFixedDustThreshold(dustSatoshis)
214216
.build()
215217

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

259-
val signingInput = BitcoinV2.SigningInput.newBuilder()
261+
val builder = BitcoinV2.TransactionBuilder.newBuilder()
260262
.setVersion(BitcoinV2.TransactionVersion.V2)
261-
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
262263
.addInputs(utxo0)
263264
.addOutputs(out0)
264265
.setInputSelector(BitcoinV2.InputSelector.UseAll)
266+
.setFixedDustThreshold(dustSatoshis)
267+
val signingInput = BitcoinV2.SigningInput.newBuilder()
268+
.setBuilder(builder)
269+
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
265270
.setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply {
266271
p2PkhPrefix = 0
267272
p2ShPrefix = 5
268273
})
269274
.setDangerousUseFixedSchnorrRng(true)
270-
.setFixedDustThreshold(dustSatoshis)
271275
.build()
272276

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

329-
val signingInput = BitcoinV2.SigningInput.newBuilder()
333+
val builder = BitcoinV2.TransactionBuilder.newBuilder()
330334
.setVersion(BitcoinV2.TransactionVersion.V2)
331-
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
332335
.addInputs(utxo0)
333336
.addInputs(utxo1)
334337
.addOutputs(out0)
335338
.addOutputs(changeOutput)
336339
.setInputSelector(BitcoinV2.InputSelector.UseAll)
340+
.setFixedDustThreshold(dustSatoshis)
341+
val signingInput = BitcoinV2.SigningInput.newBuilder()
342+
.setBuilder(builder)
343+
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
337344
.setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply {
338345
p2PkhPrefix = 0
339346
p2ShPrefix = 5
340347
})
341348
.setDangerousUseFixedSchnorrRng(true)
342-
.setFixedDustThreshold(dustSatoshis)
343349
.build()
344350

345351
val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply {

include/TrustWalletCore/TWBitcoinPsbt.h

-36
This file was deleted.

rust/chains/tw_bitcoin/src/entry.rs

-26
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::modules::compiler::BitcoinCompiler;
22
use crate::modules::planner::BitcoinPlanner;
3-
use crate::modules::psbt_planner::PsbtPlanner;
43
use crate::modules::signer::BitcoinSigner;
54
use crate::modules::transaction_util::BitcoinTransactionUtil;
65
use std::str::FromStr;
@@ -15,7 +14,6 @@ use tw_coin_entry::modules::wallet_connector::NoWalletConnector;
1514
use tw_keypair::tw::PublicKey;
1615
use tw_proto::BitcoinV2::Proto;
1716
use tw_utxo::address::standard_bitcoin::{StandardBitcoinAddress, StandardBitcoinPrefix};
18-
use tw_utxo::utxo_entry::UtxoEntry;
1917

2018
pub struct BitcoinEntry;
2119

@@ -99,27 +97,3 @@ impl CoinEntry for BitcoinEntry {
9997
Some(BitcoinTransactionUtil)
10098
}
10199
}
102-
103-
impl UtxoEntry for BitcoinEntry {
104-
type PsbtSigningInput<'a> = Proto::PsbtSigningInput<'a>;
105-
type PsbtSigningOutput = Proto::PsbtSigningOutput<'static>;
106-
type PsbtTransactionPlan = Proto::TransactionPlan<'static>;
107-
108-
#[inline]
109-
fn sign_psbt(
110-
&self,
111-
coin: &dyn CoinContext,
112-
input: Self::PsbtSigningInput<'_>,
113-
) -> Self::PsbtSigningOutput {
114-
BitcoinSigner::sign_psbt(coin, &input)
115-
}
116-
117-
#[inline]
118-
fn plan_psbt(
119-
&self,
120-
coin: &dyn CoinContext,
121-
input: Self::PsbtSigningInput<'_>,
122-
) -> Self::PsbtTransactionPlan {
123-
PsbtPlanner::plan_psbt(coin, &input)
124-
}
125-
}

rust/chains/tw_bitcoin/src/modules/compiler.rs

+55-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Copyright © 2017 Trust Wallet.
44

55
use crate::modules::protobuf_builder::ProtobufBuilder;
6+
use crate::modules::psbt_request::PsbtRequest;
67
use crate::modules::signing_request::SigningRequestBuilder;
78
use std::borrow::Cow;
89
use tw_coin_entry::coin_context::CoinContext;
@@ -13,6 +14,7 @@ use tw_proto::BitcoinV2::Proto;
1314
use tw_proto::BitcoinV2::Proto::mod_PreSigningOutput::{
1415
SigningMethod as ProtoSigningMethod, TaprootTweak as ProtoTaprootTweak,
1516
};
17+
use tw_proto::BitcoinV2::Proto::mod_SigningInput::OneOftransaction as TransactionType;
1618
use tw_utxo::modules::sighash_computer::{SighashComputer, TaprootTweak, TxPreimage};
1719
use tw_utxo::modules::sighash_verifier::SighashVerifier;
1820
use tw_utxo::modules::tx_compiler::TxCompiler;
@@ -39,8 +41,17 @@ impl BitcoinCompiler {
3941
coin: &dyn CoinContext,
4042
input: Proto::SigningInput<'_>,
4143
) -> SigningResult<Proto::PreSigningOutput<'static>> {
42-
let request = SigningRequestBuilder::build(coin, &input)?;
43-
let SelectResult { unsigned_tx, .. } = TxPlanner::plan(request)?;
44+
let unsigned_tx = match input.transaction {
45+
TransactionType::builder(ref tx_builder) => {
46+
let request = SigningRequestBuilder::build(coin, &input, tx_builder)?;
47+
TxPlanner::plan(request)?.unsigned_tx
48+
},
49+
TransactionType::psbt(ref psbt) => PsbtRequest::build(&input, psbt)?.unsigned_tx,
50+
TransactionType::None => {
51+
return SigningError::err(SigningErrorType::Error_invalid_params)
52+
.context("Either `TransactionBuilder` or `Psbt` should be set")
53+
},
54+
};
4455

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

@@ -77,7 +88,23 @@ impl BitcoinCompiler {
7788
signatures: Vec<SignatureBytes>,
7889
_public_keys: Vec<PublicKeyBytes>,
7990
) -> SigningResult<Proto::SigningOutput<'static>> {
80-
let request = SigningRequestBuilder::build(coin, &input)?;
91+
match input.transaction {
92+
TransactionType::builder(ref tx) => {
93+
Self::compile_with_tx_builder(coin, &input, tx, signatures)
94+
},
95+
TransactionType::psbt(ref psbt) => Self::compile_psbt(coin, &input, psbt, signatures),
96+
TransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params)
97+
.context("No transaction type specified"),
98+
}
99+
}
100+
101+
fn compile_with_tx_builder(
102+
coin: &dyn CoinContext,
103+
input: &Proto::SigningInput,
104+
tx_builder_input: &Proto::TransactionBuilder,
105+
signatures: Vec<SignatureBytes>,
106+
) -> SigningResult<Proto::SigningOutput<'static>> {
107+
let request = SigningRequestBuilder::build(coin, input, tx_builder_input)?;
81108
let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?;
82109

83110
SighashVerifier::verify_signatures(&unsigned_tx, &signatures)?;
@@ -96,6 +123,31 @@ impl BitcoinCompiler {
96123
..Proto::SigningOutput::default()
97124
})
98125
}
126+
127+
fn compile_psbt(
128+
_coin: &dyn CoinContext,
129+
input: &Proto::SigningInput,
130+
psbt: &Proto::Psbt,
131+
signatures: Vec<SignatureBytes>,
132+
) -> SigningResult<Proto::SigningOutput<'static>> {
133+
let PsbtRequest { unsigned_tx, .. } = PsbtRequest::build(input, psbt)?;
134+
let fee = unsigned_tx.fee()?;
135+
136+
SighashVerifier::verify_signatures(&unsigned_tx, &signatures)?;
137+
let signed_tx = TxCompiler::compile(unsigned_tx, &signatures)?;
138+
let tx_proto = ProtobufBuilder::tx_to_proto(&signed_tx);
139+
140+
Ok(Proto::SigningOutput {
141+
transaction: Some(tx_proto),
142+
encoded: Cow::from(signed_tx.encode_out()),
143+
txid: Cow::from(signed_tx.txid()),
144+
// `vsize` could have been changed after the transaction being signed.
145+
vsize: signed_tx.vsize() as u64,
146+
weight: signed_tx.weight() as u64,
147+
fee,
148+
..Proto::SigningOutput::default()
149+
})
150+
}
99151
}
100152

101153
pub fn signing_method(s: SigningMethod) -> ProtoSigningMethod {

rust/chains/tw_bitcoin/src/modules/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ pub mod compiler;
66
pub mod planner;
77
pub mod protobuf_builder;
88
pub mod psbt;
9-
pub mod psbt_planner;
109
pub mod psbt_request;
1110
pub mod signer;
1211
pub mod signing_request;

rust/chains/tw_bitcoin/src/modules/planner.rs rust/chains/tw_bitcoin/src/modules/planner/mod.rs

+22-3
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,39 @@ use tw_proto::BitcoinV2::Proto;
1414
use tw_utxo::modules::tx_planner::TxPlanner;
1515
use tw_utxo::modules::utxo_selector::SelectResult;
1616

17+
pub mod psbt_planner;
18+
1719
pub struct BitcoinPlanner;
1820

1921
impl BitcoinPlanner {
2022
pub fn plan_impl<'a>(
2123
coin: &dyn CoinContext,
2224
input: &Proto::SigningInput<'a>,
2325
) -> SigningResult<Proto::TransactionPlan<'a>> {
24-
let request = SigningRequestBuilder::build(coin, input)?;
26+
use Proto::mod_SigningInput::OneOftransaction as TransactionType;
27+
28+
match input.transaction {
29+
TransactionType::builder(ref tx) => Self::plan_with_tx_builder(coin, input, tx),
30+
TransactionType::psbt(ref psbt) => {
31+
psbt_planner::PsbtPlanner::plan_psbt(coin, input, psbt)
32+
},
33+
TransactionType::None => SigningError::err(SigningErrorType::Error_invalid_params)
34+
.context("Either `TransactionBuilder` or `Psbt` should be set"),
35+
}
36+
}
37+
38+
pub fn plan_with_tx_builder<'a>(
39+
coin: &dyn CoinContext,
40+
input: &Proto::SigningInput<'a>,
41+
tx_builder: &Proto::TransactionBuilder<'a>,
42+
) -> SigningResult<Proto::TransactionPlan<'a>> {
43+
let request = SigningRequestBuilder::build(coin, input, tx_builder)?;
2544
let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?;
2645

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

0 commit comments

Comments
 (0)