Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bitcoin/V2] Pass Signing/Planning 2.0 requests to Rust through FFI #3665

Merged
merged 8 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/linux-ci-sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,48 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- name: Install system dependencies
run: |
tools/install-sys-dependencies-linux
tools/install-rust-dependencies

- name: Cache internal dependencies
id: internal_cache
uses: actions/cache@v3
with:
path: build/local
key: ${{ runner.os }}-internal-${{ hashFiles('tools/install-dependencies') }}

- name: Install internal dependencies
run: |
tools/install-dependencies
env:
CC: /usr/bin/clang
CXX: /usr/bin/clang++
if: steps.internal_cache.outputs.cache-hit != 'true'

- name: Code generation
run: |
tools/generate-files native
env:
CC: /usr/bin/clang
CXX: /usr/bin/clang++

- name: CMake (coverage/clang-tidy/clang-asan)
run: |
cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DTW_CODE_COVERAGE=ON -DTW_ENABLE_CLANG_TIDY=ON -DTW_CLANG_ASAN=ON -GNinja
cat build/compile_commands.json
env:
CC: /usr/bin/clang
CXX: /usr/bin/clang++

- name: SonarCloud Scan
run: |
./tools/sonarcloud-analysis
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linux-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: |
sudo rm -f /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-jammy.list
sudo apt-get update
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
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
- uses: actions/checkout@v3
- name: Install system dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linux-sampleapp-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: |
sudo rm -f /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-jammy.list
sudo apt-get update
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
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
- uses: actions/checkout@v3
- name: Install system dependencies
run: |
Expand Down
5 changes: 4 additions & 1 deletion rust/tw_bitcoin/src/modules/legacy/build_and_sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub fn taproot_build_and_sign_transaction(

let transaction = signed
.transaction
.as_ref()
.expect("transaction not returned from signer");

// Convert the returned transaction data into the (legacy) `Transaction`
Expand Down Expand Up @@ -141,10 +142,12 @@ pub fn taproot_build_and_sign_transaction(
// Put the `Transaction` into the `SigningOutput`, return.
let legacy_output = LegacyProto::SigningOutput {
transaction: Some(legacy_transaction),
encoded: signed.encoded,
encoded: signed.encoded.clone(),
transaction_id: txid_hex.into(),
error: CommonProto::SigningError::OK,
error_message: Default::default(),
// Set the Bitcoin 2.0 result as well.
signing_result_v2: Some(signed),
};

Ok(legacy_output)
Expand Down
2 changes: 2 additions & 0 deletions samples/rust/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ fn main() {
.out_dir(out_dir)
.input(proto_src.to_string() + "/Common.proto")
.input(proto_src.to_string() + "/Bitcoin.proto")
.input(proto_src.to_string() + "/BitcoinV2.proto")
.input(proto_src.to_string() + "/Ethereum.proto")
.input(proto_src.to_string() + "/Utxo.proto")
.include(proto_src)
.run()
.expect("Codegen failed.");
Expand Down
31 changes: 31 additions & 0 deletions src/Bitcoin/Signer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@
namespace TW::Bitcoin {

Proto::TransactionPlan Signer::plan(const Proto::SigningInput& input) noexcept {
if (input.has_planning_v2()) {
Proto::TransactionPlan plan;

// Forward the `Bitcoin.Proto.SigningInput.planning_v2` request to Rust.
auto planningV2Data = data(input.planning_v2().SerializeAsString());
Rust::TWDataWrapper planningV2DataPtr(planningV2Data);
Rust::TWDataWrapper planningOutputV2DataPtr = Rust::tw_any_signer_plan(planningV2DataPtr.get(), input.coin_type());

auto planningOutputV2Data = planningOutputV2DataPtr.toDataOrDefault();
BitcoinV2::Proto::TransactionPlan planningOutputV2;
planningOutputV2.ParseFromArray(planningOutputV2Data.data(), static_cast<int>(planningOutputV2Data.size()));

// Set `Bitcoin.Proto.TransactionPlan.planning_result_v2`. Remain other fields default.
*plan.mutable_planning_result_v2() = planningOutputV2;
return plan;
}

auto plan = TransactionSigner<Transaction, TransactionBuilder>::plan(input);
return plan.proto();
}
Expand All @@ -26,7 +43,21 @@ Proto::SigningOutput Signer::sign(const Proto::SigningInput& input, std::optiona
Rust::CByteArrayWrapper res = Rust::tw_bitcoin_legacy_taproot_build_and_sign_transaction(serializedInput.data(), serializedInput.size());
output.ParseFromArray(res.data.data(), static_cast<int>(res.data.size()));
return output;
} else if (input.has_signing_v2()) {
// Forward the `Bitcoin.Proto.SigningInput.signing_v2` request to Rust.
auto signingV2Data = data(input.signing_v2().SerializeAsString());
Rust::TWDataWrapper signingV2DataPtr(signingV2Data);
Rust::TWDataWrapper signingOutputV2DataPtr = Rust::tw_any_signer_sign(signingV2DataPtr.get(), input.coin_type());

auto signingOutputV2Data = signingOutputV2DataPtr.toDataOrDefault();
BitcoinV2::Proto::SigningOutput signingOutputV2;
signingOutputV2.ParseFromArray(signingOutputV2Data.data(), static_cast<int>(signingOutputV2Data.size()));

// Set `Bitcoin.Proto.SigningOutput.signing_result_v2`. Remain other fields default.
*output.mutable_signing_result_v2() = signingOutputV2;
return output;
}

auto result = TransactionSigner<Transaction, TransactionBuilder>::sign(input, false, optionalExternalSigs);
if (!result) {
output.set_error(result.error());
Expand Down
18 changes: 18 additions & 0 deletions src/proto/Bitcoin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ syntax = "proto3";
package TW.Bitcoin.Proto;
option java_package = "wallet.core.jni.proto";

import "BitcoinV2.proto";
import "Common.proto";

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

// Deprecated. Consider using `Bitcoin.Proto.SigningInput.signing_v2` instead.
bool is_it_brc_operation = 18;

// If set, uses Bitcoin 2.0 Planning protocol.
// As a result, `Bitcoin.Proto.TransactionPlan.planning_result_v2` is set.
BitcoinV2.Proto.ComposePlan planning_v2 = 20;

// If set, uses Bitcoin 2.0 Signing protocol.
// As a result, `Bitcoin.Proto.SigningOutput.signing_result_v2` is set.
BitcoinV2.Proto.SigningInput signing_v2 = 21;
}

// Describes a preliminary transaction plan.
Expand Down Expand Up @@ -187,6 +197,10 @@ message TransactionPlan {

// zen preblockheight
int64 preblockheight = 10;

// Result of a transaction planning using the Bitcoin 2.0 protocol.
// Set if `Bitcoin.Proto.SigningInput.planning_v2` used.
BitcoinV2.Proto.TransactionPlan planning_result_v2 = 12;
};

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

// error description
string error_message = 5;

// Result of a transaction signing using the Bitcoin 2.0 protocol.
// Set if `Bitcoin.Proto.SigningInput.signing_v2` used.
BitcoinV2.Proto.SigningOutput signing_result_v2 = 7;
}

/// Pre-image hash to be used for signing
Expand Down
130 changes: 130 additions & 0 deletions tests/chains/Bitcoin/TWBitcoinSigningTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

namespace TW::Bitcoin {

constexpr uint64_t ONE_BTC = 100'000'000;

// clang-format off
SigningInput buildInputP2PKH(bool omitKey = false) {
auto hash0 = parse_hex("fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f");
Expand Down Expand Up @@ -574,6 +576,134 @@ TEST(BitcoinSigning, SignNftInscriptionReveal) {
ASSERT_EQ(result.substr(292, result.size() - 292), expectedHex.substr(292, result.size() - 292));
}

TEST(BitcoinSigning, PlanAndSignBrc20) {
auto privateKey = parse_hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129");
auto publicKey = parse_hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb");

// Construct a `BitcoinV2.Proto.ComposePlan` message.

auto dustSatoshis = 546;
auto ticker = "oadf";
auto tokensAmount = 20;
auto feePerVb = 25;

auto txId1 = parse_hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911");
std::reverse(begin(txId1), end(txId1));

BitcoinV2::Proto::Input tx1;
tx1.set_txid(txId1.data(), (int)txId1.size());
tx1.set_vout(0);
tx1.set_value(ONE_BTC);
tx1.set_sighash_type(Utxo::Proto::SighashType::All);
tx1.mutable_builder()->set_p2wpkh(publicKey.data(), (int)publicKey.size());

auto txId2 = parse_hex("858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e");
std::reverse(begin(txId2), end(txId2));

BitcoinV2::Proto::Input tx2;
tx2.set_txid(txId2.data(), (int)txId2.size());
tx2.set_vout(0);
tx2.set_value(2 * ONE_BTC);
tx2.set_sighash_type(Utxo::Proto::SighashType::All);
tx2.mutable_builder()->set_p2wpkh(publicKey.data(), (int)publicKey.size());

BitcoinV2::Proto::Output taggedOutput;
taggedOutput.set_value(dustSatoshis);
taggedOutput.mutable_builder()->mutable_p2wpkh()->set_pubkey(publicKey.data(), (int)publicKey.size());

BitcoinV2::Proto::Output changeOutput;
// Will be set by the library.
changeOutput.set_value(0);
changeOutput.mutable_builder()->mutable_p2wpkh()->set_pubkey(publicKey.data(), (int)publicKey.size());

BitcoinV2::Proto::Input_InputBrc20Inscription brc20Inscription;
brc20Inscription.set_one_prevout(false);
brc20Inscription.set_inscribe_to(publicKey.data(), (int)publicKey.size());
brc20Inscription.set_ticker(ticker);
brc20Inscription.set_transfer_amount(tokensAmount);

BitcoinV2::Proto::ComposePlan composePlan;
auto& composeBrc20 = *composePlan.mutable_brc20();
composeBrc20.set_private_key(privateKey.data(), (int)privateKey.size());
*composeBrc20.add_inputs() = tx1;
*composeBrc20.add_inputs() = tx2;
composeBrc20.set_input_selector(Utxo::Proto::InputSelector::SelectAscending);
*composeBrc20.mutable_tagged_output() = taggedOutput;
*composeBrc20.mutable_inscription() = brc20Inscription;
composeBrc20.set_fee_per_vb(feePerVb);
*composeBrc20.mutable_change_output() = changeOutput;
composeBrc20.set_disable_change_output(false);

// Construct a `Bitcoin.Proto.SigningInput` message with `planning_v2` field only.
Proto::SigningInput input;
*input.mutable_planning_v2() = composePlan;

// Plan the transaction using standard `TWAnySignerPlan`.
Proto::TransactionPlan plan;
ANY_PLAN(input, plan, TWCoinTypeBitcoin);

// Check the result Planning V2.
EXPECT_TRUE(plan.has_planning_result_v2());
const auto& planV2 = plan.planning_result_v2();
EXPECT_EQ(planV2.error(), BitcoinV2::Proto::Error::OK);
EXPECT_TRUE(planV2.has_brc20());
const auto& brc20Plan = planV2.brc20();

// Check the result Commit `SigningInput`.
auto commitOutputAmount = 3846;
EXPECT_TRUE(brc20Plan.has_commit());
const auto& brc20Commit = brc20Plan.commit();
EXPECT_EQ(brc20Commit.version(), 2);
EXPECT_EQ(brc20Commit.inputs_size(), 1);
EXPECT_EQ(brc20Commit.outputs_size(), 2);
// Change output generation is disabled, included in `commit.outputs`.
EXPECT_FALSE(brc20Commit.has_change_output());
EXPECT_EQ(brc20Commit.outputs(0).value(), commitOutputAmount);
EXPECT_EQ(brc20Commit.outputs(0).builder().brc20_inscribe().ticker(), ticker);
EXPECT_EQ(brc20Commit.outputs(0).builder().brc20_inscribe().transfer_amount(), tokensAmount);
// Change: tx1 value - out1 value
EXPECT_EQ(brc20Commit.outputs(1).value(), ONE_BTC - commitOutputAmount - 3175);

// Check the result Reveal `SigningInput`.
EXPECT_TRUE(brc20Plan.has_reveal());
const auto& brc20Reveal = brc20Plan.reveal();
EXPECT_EQ(brc20Reveal.version(), 2);
EXPECT_EQ(brc20Reveal.inputs_size(), 1);
EXPECT_EQ(brc20Reveal.outputs_size(), 1);
// Change output generation is disabled, included in `commit.outputs`.
EXPECT_FALSE(brc20Reveal.has_change_output());
EXPECT_EQ(brc20Reveal.inputs(0).value(), commitOutputAmount);
EXPECT_EQ(brc20Reveal.inputs(0).builder().brc20_inscribe().ticker(), ticker);
EXPECT_EQ(brc20Reveal.inputs(0).builder().brc20_inscribe().transfer_amount(), tokensAmount);
EXPECT_EQ(brc20Reveal.outputs(0).value(), dustSatoshis);

// Construct a `Bitcoin.Proto.SigningInput` message with `signing_v2` (Commit) field only.
{
Proto::SigningInput commitInput;
*commitInput.mutable_signing_v2() = brc20Commit;

Proto::SigningOutput output;
ANY_SIGN(commitInput, TWCoinTypeBitcoin);
EXPECT_EQ(output.error(), Common::Proto::SigningError::OK);
EXPECT_TRUE(output.has_signing_result_v2());
EXPECT_EQ(hex(output.signing_result_v2().encoded()), "0200000000010111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c180000000000ffffffff02060f000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc0593c5f50500000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100912004efb9b4e8368ba00d6bfbdfde22a43b037f64ae09d79aac030c77edbc2802206c5702646eadea2274c4aafee99c12b5054cb60da18c21f67f5f3003a318112d0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000");
}

// Construct a `Bitcoin.Proto.SigningInput` message with `signing_v2` (Reveal) field only.
{
Proto::SigningInput revealInput;
*revealInput.mutable_signing_v2() = brc20Reveal;
// `schnorr` is used to sign the Reveal transaction.
revealInput.mutable_signing_v2()->set_dangerous_use_fixed_schnorr_rng(true);

Proto::SigningOutput output;
ANY_SIGN(revealInput, TWCoinTypeBitcoin);
EXPECT_EQ(output.error(), Common::Proto::SigningError::OK);
EXPECT_TRUE(output.has_signing_result_v2());
EXPECT_EQ(hex(output.signing_result_v2().encoded()), "0200000000010173711b50d9adb30fdc51231cd56a95b3b627453add775c56188449f2dccaef250000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d03405cd7fb811a8ebcc55ac791321243a6d1a4089abc548d93288dfe5870f6af7f96b0cb0c7c41c0126791179e8c190d8fecf9bdc4cc740ec3e7d6a43b1b0a345f155b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000");
}
}

TEST(BitcoinSigning, SignP2PKH) {
auto input = buildInputP2PKH();

Expand Down
4 changes: 2 additions & 2 deletions tools/sonarcloud-analysis
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash

TARGET=sonar-scanner-cli-4.7.0.2747-linux.zip
TARGET_DIR=sonar-scanner-4.7.0.2747-linux
TARGET=sonar-scanner-cli-5.0.1.3006-linux.zip
TARGET_DIR=sonar-scanner-5.0.1.3006-linux
curl https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/${TARGET} --output ${TARGET}
unzip ${TARGET}
cp tools/sonar-scanner.properties ${TARGET_DIR}/conf
Expand Down
Loading