Skip to content

Commit 105a900

Browse files
[Bitcoin/V2] Pass Signing/Planning 2.0 requests to Rust through FFI (#3665)
* [Bitcoin/V2]: Add BitcoinV2.proto signing, planning request/result to the legacy Bitcoin.proto * [Bitcoin/V2]: Forward `signing_v2` and `plan_v2` requests to Rust * Add a Plan/Sign V2 test * TODO fix the test * [Bitcoin/V2]: Fix the test * [Linux]: Fix CI * [CI]: Fix Rust sample, SonarCloud pipelines * [Bitcoin/V2]: Try using a GitHub Action for SonarCloud Scan * [CI]: Update sonar-scanner-cli to 5.0.1.3006 * [CI]: Enable all CI pipelines
1 parent cd5a274 commit 105a900

File tree

9 files changed

+201
-5
lines changed

9 files changed

+201
-5
lines changed

.github/workflows/linux-ci-sonarcloud.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,48 @@ jobs:
1616
runs-on: ubuntu-latest
1717
steps:
1818
- uses: actions/checkout@v3
19+
20+
- name: Set up JDK 17
21+
uses: actions/setup-java@v3
22+
with:
23+
java-version: '17'
24+
distribution: 'temurin'
25+
1926
- name: Install system dependencies
2027
run: |
2128
tools/install-sys-dependencies-linux
2229
tools/install-rust-dependencies
30+
2331
- name: Cache internal dependencies
2432
id: internal_cache
2533
uses: actions/cache@v3
2634
with:
2735
path: build/local
2836
key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-dependencies') }}
37+
2938
- name: Install internal dependencies
3039
run: |
3140
tools/install-dependencies
3241
env:
3342
CC: /usr/bin/clang
3443
CXX: /usr/bin/clang++
3544
if: steps.internal_cache.outputs.cache-hit != 'true'
45+
3646
- name: Code generation
3747
run: |
3848
tools/generate-files native
3949
env:
4050
CC: /usr/bin/clang
4151
CXX: /usr/bin/clang++
52+
4253
- name: CMake (coverage/clang-tidy/clang-asan)
4354
run: |
4455
cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DTW_CODE_COVERAGE=ON -DTW_ENABLE_CLANG_TIDY=ON -DTW_CLANG_ASAN=ON -GNinja
4556
cat build/compile_commands.json
4657
env:
4758
CC: /usr/bin/clang
4859
CXX: /usr/bin/clang++
60+
4961
- name: SonarCloud Scan
5062
run: |
5163
./tools/sonarcloud-analysis

.github/workflows/linux-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
run: |
2222
sudo rm -f /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-jammy.list
2323
sudo apt-get update
24-
sudo apt-get install -y --allow-downgrades libc6=2.35-0ubuntu3.5 libc6-dev=2.35-0ubuntu3.5 libstdc++6=12.3.0-1ubuntu1~22.04 libgcc-s1=12.3.0-1ubuntu1~22.04
24+
sudo apt-get install -y --allow-downgrades libc6=2.35-0ubuntu3.6 libc6-dev=2.35-0ubuntu3.6 libstdc++6=12.3.0-1ubuntu1~22.04 libgcc-s1=12.3.0-1ubuntu1~22.04
2525
- uses: actions/checkout@v3
2626
- name: Install system dependencies
2727
run: |

.github/workflows/linux-sampleapp-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
run: |
2222
sudo rm -f /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-jammy.list
2323
sudo apt-get update
24-
sudo apt-get install -y --allow-downgrades libc6=2.35-0ubuntu3.5 libc6-dev=2.35-0ubuntu3.5 libstdc++6=12.3.0-1ubuntu1~22.04 libgcc-s1=12.3.0-1ubuntu1~22.04
24+
sudo apt-get install -y --allow-downgrades libc6=2.35-0ubuntu3.6 libc6-dev=2.35-0ubuntu3.6 libstdc++6=12.3.0-1ubuntu1~22.04 libgcc-s1=12.3.0-1ubuntu1~22.04
2525
- uses: actions/checkout@v3
2626
- name: Install system dependencies
2727
run: |

rust/tw_bitcoin/src/modules/legacy/build_and_sign.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ pub fn taproot_build_and_sign_transaction(
102102

103103
let transaction = signed
104104
.transaction
105+
.as_ref()
105106
.expect("transaction not returned from signer");
106107

107108
// Convert the returned transaction data into the (legacy) `Transaction`
@@ -141,10 +142,12 @@ pub fn taproot_build_and_sign_transaction(
141142
// Put the `Transaction` into the `SigningOutput`, return.
142143
let legacy_output = LegacyProto::SigningOutput {
143144
transaction: Some(legacy_transaction),
144-
encoded: signed.encoded,
145+
encoded: signed.encoded.clone(),
145146
transaction_id: txid_hex.into(),
146147
error: CommonProto::SigningError::OK,
147148
error_message: Default::default(),
149+
// Set the Bitcoin 2.0 result as well.
150+
signing_result_v2: Some(signed),
148151
};
149152

150153
Ok(legacy_output)

samples/rust/src/build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ fn main() {
2323
.out_dir(out_dir)
2424
.input(proto_src.to_string() + "/Common.proto")
2525
.input(proto_src.to_string() + "/Bitcoin.proto")
26+
.input(proto_src.to_string() + "/BitcoinV2.proto")
2627
.input(proto_src.to_string() + "/Ethereum.proto")
28+
.input(proto_src.to_string() + "/Utxo.proto")
2729
.include(proto_src)
2830
.run()
2931
.expect("Codegen failed.");

src/Bitcoin/Signer.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@
1515
namespace TW::Bitcoin {
1616

1717
Proto::TransactionPlan Signer::plan(const Proto::SigningInput& input) noexcept {
18+
if (input.has_planning_v2()) {
19+
Proto::TransactionPlan plan;
20+
21+
// Forward the `Bitcoin.Proto.SigningInput.planning_v2` request to Rust.
22+
auto planningV2Data = data(input.planning_v2().SerializeAsString());
23+
Rust::TWDataWrapper planningV2DataPtr(planningV2Data);
24+
Rust::TWDataWrapper planningOutputV2DataPtr = Rust::tw_any_signer_plan(planningV2DataPtr.get(), input.coin_type());
25+
26+
auto planningOutputV2Data = planningOutputV2DataPtr.toDataOrDefault();
27+
BitcoinV2::Proto::TransactionPlan planningOutputV2;
28+
planningOutputV2.ParseFromArray(planningOutputV2Data.data(), static_cast<int>(planningOutputV2Data.size()));
29+
30+
// Set `Bitcoin.Proto.TransactionPlan.planning_result_v2`. Remain other fields default.
31+
*plan.mutable_planning_result_v2() = planningOutputV2;
32+
return plan;
33+
}
34+
1835
auto plan = TransactionSigner<Transaction, TransactionBuilder>::plan(input);
1936
return plan.proto();
2037
}
@@ -26,7 +43,21 @@ Proto::SigningOutput Signer::sign(const Proto::SigningInput& input, std::optiona
2643
Rust::CByteArrayWrapper res = Rust::tw_bitcoin_legacy_taproot_build_and_sign_transaction(serializedInput.data(), serializedInput.size());
2744
output.ParseFromArray(res.data.data(), static_cast<int>(res.data.size()));
2845
return output;
46+
} else if (input.has_signing_v2()) {
47+
// Forward the `Bitcoin.Proto.SigningInput.signing_v2` request to Rust.
48+
auto signingV2Data = data(input.signing_v2().SerializeAsString());
49+
Rust::TWDataWrapper signingV2DataPtr(signingV2Data);
50+
Rust::TWDataWrapper signingOutputV2DataPtr = Rust::tw_any_signer_sign(signingV2DataPtr.get(), input.coin_type());
51+
52+
auto signingOutputV2Data = signingOutputV2DataPtr.toDataOrDefault();
53+
BitcoinV2::Proto::SigningOutput signingOutputV2;
54+
signingOutputV2.ParseFromArray(signingOutputV2Data.data(), static_cast<int>(signingOutputV2Data.size()));
55+
56+
// Set `Bitcoin.Proto.SigningOutput.signing_result_v2`. Remain other fields default.
57+
*output.mutable_signing_result_v2() = signingOutputV2;
58+
return output;
2959
}
60+
3061
auto result = TransactionSigner<Transaction, TransactionBuilder>::sign(input, false, optionalExternalSigs);
3162
if (!result) {
3263
output.set_error(result.error());

src/proto/Bitcoin.proto

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ syntax = "proto3";
33
package TW.Bitcoin.Proto;
44
option java_package = "wallet.core.jni.proto";
55

6+
import "BitcoinV2.proto";
67
import "Common.proto";
78

89
// A transaction, with its inputs and outputs
@@ -153,7 +154,16 @@ message SigningInput {
153154
// transaction creation time that will be used for verge(xvg)
154155
uint32 time = 17;
155156

157+
// Deprecated. Consider using `Bitcoin.Proto.SigningInput.signing_v2` instead.
156158
bool is_it_brc_operation = 18;
159+
160+
// If set, uses Bitcoin 2.0 Planning protocol.
161+
// As a result, `Bitcoin.Proto.TransactionPlan.planning_result_v2` is set.
162+
BitcoinV2.Proto.ComposePlan planning_v2 = 20;
163+
164+
// If set, uses Bitcoin 2.0 Signing protocol.
165+
// As a result, `Bitcoin.Proto.SigningOutput.signing_result_v2` is set.
166+
BitcoinV2.Proto.SigningInput signing_v2 = 21;
157167
}
158168

159169
// Describes a preliminary transaction plan.
@@ -187,6 +197,10 @@ message TransactionPlan {
187197

188198
// zen preblockheight
189199
int64 preblockheight = 10;
200+
201+
// Result of a transaction planning using the Bitcoin 2.0 protocol.
202+
// Set if `Bitcoin.Proto.SigningInput.planning_v2` used.
203+
BitcoinV2.Proto.TransactionPlan planning_result_v2 = 12;
190204
};
191205

192206
// Result containing the signed and encoded transaction.
@@ -206,6 +220,10 @@ message SigningOutput {
206220

207221
// error description
208222
string error_message = 5;
223+
224+
// Result of a transaction signing using the Bitcoin 2.0 protocol.
225+
// Set if `Bitcoin.Proto.SigningInput.signing_v2` used.
226+
BitcoinV2.Proto.SigningOutput signing_result_v2 = 7;
209227
}
210228

211229
/// Pre-image hash to be used for signing

tests/chains/Bitcoin/TWBitcoinSigningTests.cpp

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
namespace TW::Bitcoin {
3434

35+
constexpr uint64_t ONE_BTC = 100'000'000;
36+
3537
// clang-format off
3638
SigningInput buildInputP2PKH(bool omitKey = false) {
3739
auto hash0 = parse_hex("fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f");
@@ -574,6 +576,134 @@ TEST(BitcoinSigning, SignNftInscriptionReveal) {
574576
ASSERT_EQ(result.substr(292, result.size() - 292), expectedHex.substr(292, result.size() - 292));
575577
}
576578

579+
TEST(BitcoinSigning, PlanAndSignBrc20) {
580+
auto privateKey = parse_hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129");
581+
auto publicKey = parse_hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb");
582+
583+
// Construct a `BitcoinV2.Proto.ComposePlan` message.
584+
585+
auto dustSatoshis = 546;
586+
auto ticker = "oadf";
587+
auto tokensAmount = 20;
588+
auto feePerVb = 25;
589+
590+
auto txId1 = parse_hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911");
591+
std::reverse(begin(txId1), end(txId1));
592+
593+
BitcoinV2::Proto::Input tx1;
594+
tx1.set_txid(txId1.data(), (int)txId1.size());
595+
tx1.set_vout(0);
596+
tx1.set_value(ONE_BTC);
597+
tx1.set_sighash_type(Utxo::Proto::SighashType::All);
598+
tx1.mutable_builder()->set_p2wpkh(publicKey.data(), (int)publicKey.size());
599+
600+
auto txId2 = parse_hex("858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e");
601+
std::reverse(begin(txId2), end(txId2));
602+
603+
BitcoinV2::Proto::Input tx2;
604+
tx2.set_txid(txId2.data(), (int)txId2.size());
605+
tx2.set_vout(0);
606+
tx2.set_value(2 * ONE_BTC);
607+
tx2.set_sighash_type(Utxo::Proto::SighashType::All);
608+
tx2.mutable_builder()->set_p2wpkh(publicKey.data(), (int)publicKey.size());
609+
610+
BitcoinV2::Proto::Output taggedOutput;
611+
taggedOutput.set_value(dustSatoshis);
612+
taggedOutput.mutable_builder()->mutable_p2wpkh()->set_pubkey(publicKey.data(), (int)publicKey.size());
613+
614+
BitcoinV2::Proto::Output changeOutput;
615+
// Will be set by the library.
616+
changeOutput.set_value(0);
617+
changeOutput.mutable_builder()->mutable_p2wpkh()->set_pubkey(publicKey.data(), (int)publicKey.size());
618+
619+
BitcoinV2::Proto::Input_InputBrc20Inscription brc20Inscription;
620+
brc20Inscription.set_one_prevout(false);
621+
brc20Inscription.set_inscribe_to(publicKey.data(), (int)publicKey.size());
622+
brc20Inscription.set_ticker(ticker);
623+
brc20Inscription.set_transfer_amount(tokensAmount);
624+
625+
BitcoinV2::Proto::ComposePlan composePlan;
626+
auto& composeBrc20 = *composePlan.mutable_brc20();
627+
composeBrc20.set_private_key(privateKey.data(), (int)privateKey.size());
628+
*composeBrc20.add_inputs() = tx1;
629+
*composeBrc20.add_inputs() = tx2;
630+
composeBrc20.set_input_selector(Utxo::Proto::InputSelector::SelectAscending);
631+
*composeBrc20.mutable_tagged_output() = taggedOutput;
632+
*composeBrc20.mutable_inscription() = brc20Inscription;
633+
composeBrc20.set_fee_per_vb(feePerVb);
634+
*composeBrc20.mutable_change_output() = changeOutput;
635+
composeBrc20.set_disable_change_output(false);
636+
637+
// Construct a `Bitcoin.Proto.SigningInput` message with `planning_v2` field only.
638+
Proto::SigningInput input;
639+
*input.mutable_planning_v2() = composePlan;
640+
641+
// Plan the transaction using standard `TWAnySignerPlan`.
642+
Proto::TransactionPlan plan;
643+
ANY_PLAN(input, plan, TWCoinTypeBitcoin);
644+
645+
// Check the result Planning V2.
646+
EXPECT_TRUE(plan.has_planning_result_v2());
647+
const auto& planV2 = plan.planning_result_v2();
648+
EXPECT_EQ(planV2.error(), BitcoinV2::Proto::Error::OK);
649+
EXPECT_TRUE(planV2.has_brc20());
650+
const auto& brc20Plan = planV2.brc20();
651+
652+
// Check the result Commit `SigningInput`.
653+
auto commitOutputAmount = 3846;
654+
EXPECT_TRUE(brc20Plan.has_commit());
655+
const auto& brc20Commit = brc20Plan.commit();
656+
EXPECT_EQ(brc20Commit.version(), 2);
657+
EXPECT_EQ(brc20Commit.inputs_size(), 1);
658+
EXPECT_EQ(brc20Commit.outputs_size(), 2);
659+
// Change output generation is disabled, included in `commit.outputs`.
660+
EXPECT_FALSE(brc20Commit.has_change_output());
661+
EXPECT_EQ(brc20Commit.outputs(0).value(), commitOutputAmount);
662+
EXPECT_EQ(brc20Commit.outputs(0).builder().brc20_inscribe().ticker(), ticker);
663+
EXPECT_EQ(brc20Commit.outputs(0).builder().brc20_inscribe().transfer_amount(), tokensAmount);
664+
// Change: tx1 value - out1 value
665+
EXPECT_EQ(brc20Commit.outputs(1).value(), ONE_BTC - commitOutputAmount - 3175);
666+
667+
// Check the result Reveal `SigningInput`.
668+
EXPECT_TRUE(brc20Plan.has_reveal());
669+
const auto& brc20Reveal = brc20Plan.reveal();
670+
EXPECT_EQ(brc20Reveal.version(), 2);
671+
EXPECT_EQ(brc20Reveal.inputs_size(), 1);
672+
EXPECT_EQ(brc20Reveal.outputs_size(), 1);
673+
// Change output generation is disabled, included in `commit.outputs`.
674+
EXPECT_FALSE(brc20Reveal.has_change_output());
675+
EXPECT_EQ(brc20Reveal.inputs(0).value(), commitOutputAmount);
676+
EXPECT_EQ(brc20Reveal.inputs(0).builder().brc20_inscribe().ticker(), ticker);
677+
EXPECT_EQ(brc20Reveal.inputs(0).builder().brc20_inscribe().transfer_amount(), tokensAmount);
678+
EXPECT_EQ(brc20Reveal.outputs(0).value(), dustSatoshis);
679+
680+
// Construct a `Bitcoin.Proto.SigningInput` message with `signing_v2` (Commit) field only.
681+
{
682+
Proto::SigningInput commitInput;
683+
*commitInput.mutable_signing_v2() = brc20Commit;
684+
685+
Proto::SigningOutput output;
686+
ANY_SIGN(commitInput, TWCoinTypeBitcoin);
687+
EXPECT_EQ(output.error(), Common::Proto::SigningError::OK);
688+
EXPECT_TRUE(output.has_signing_result_v2());
689+
EXPECT_EQ(hex(output.signing_result_v2().encoded()), "0200000000010111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c180000000000ffffffff02060f000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc0593c5f50500000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100912004efb9b4e8368ba00d6bfbdfde22a43b037f64ae09d79aac030c77edbc2802206c5702646eadea2274c4aafee99c12b5054cb60da18c21f67f5f3003a318112d0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000");
690+
}
691+
692+
// Construct a `Bitcoin.Proto.SigningInput` message with `signing_v2` (Reveal) field only.
693+
{
694+
Proto::SigningInput revealInput;
695+
*revealInput.mutable_signing_v2() = brc20Reveal;
696+
// `schnorr` is used to sign the Reveal transaction.
697+
revealInput.mutable_signing_v2()->set_dangerous_use_fixed_schnorr_rng(true);
698+
699+
Proto::SigningOutput output;
700+
ANY_SIGN(revealInput, TWCoinTypeBitcoin);
701+
EXPECT_EQ(output.error(), Common::Proto::SigningError::OK);
702+
EXPECT_TRUE(output.has_signing_result_v2());
703+
EXPECT_EQ(hex(output.signing_result_v2().encoded()), "0200000000010173711b50d9adb30fdc51231cd56a95b3b627453add775c56188449f2dccaef250000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d03405cd7fb811a8ebcc55ac791321243a6d1a4089abc548d93288dfe5870f6af7f96b0cb0c7c41c0126791179e8c190d8fecf9bdc4cc740ec3e7d6a43b1b0a345f155b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000");
704+
}
705+
}
706+
577707
TEST(BitcoinSigning, SignP2PKH) {
578708
auto input = buildInputP2PKH();
579709

tools/sonarcloud-analysis

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bash
22

3-
TARGET=sonar-scanner-cli-4.7.0.2747-linux.zip
4-
TARGET_DIR=sonar-scanner-4.7.0.2747-linux
3+
TARGET=sonar-scanner-cli-5.0.1.3006-linux.zip
4+
TARGET_DIR=sonar-scanner-5.0.1.3006-linux
55
curl https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/${TARGET} --output ${TARGET}
66
unzip ${TARGET}
77
cp tools/sonar-scanner.properties ${TARGET_DIR}/conf

0 commit comments

Comments
 (0)