Skip to content

Commit

Permalink
[Zcash]: Migrate Zcash blockchain to Rust (#4232)
Browse files Browse the repository at this point in the history
* [Zcash]: Add `google::protobuf::Any chain_specific`  parameters to BitcoinV2

* [BitcoinV2]: Add `TransactionBuilder::fee_policy`

* Add `TransactionBuilder::zcash_extra_data`

* [BitcoinV2]: Fix compile errors

* [BitcoinV2]: Add `tw_zcash` blockchain skeleton

* Add `ZcashTransaction`, `ZcashSighash`

* [BitcoinV2]: Add `ZcashSigningRequestBuilder`

* [BitcoinV2]: Make Bitcoin's Signer, Compiler and Planner generic

* Add `UtxoSigningContext`
* Revert `TransactionBuilder.fee_policy`
* Add `Utxo.proto`

* [BitcoinV2]: Add `TAddress`

* [Zcash]: Add `preImageHashes` test

* Fix `ZcashTransaction`preImage and sighashing

* refactor(zcash): Add signing, compiling, planning tests

* refactor(zcash): Add Komodo Rust blockchain

* refactor(komodo): Add integration tests

* refactor(komodo): Add Compiling test

* refactor(komodo): Add Signing test

* refactor(zcash): Add PSBT not-supported test

* refactor(zcash): Add support for BitcoinV2 protocol for Zcash implementation in C++

* refactor(zcash): Add Android test

* refactor(zcash): Add TS test

* refactor(zcash): Add last tests

* Add `tw_zcash` doc comments
* Add ZCash transaction builder test
* Enable Rust doc tests on CI

* [CI] Trigger CI

* refactor(zcash): Resolve review comments

* refactor(zcash): Fix Rust sample
  • Loading branch information
satoshiotomakan authored Jan 27, 2025
1 parent 9efc9bb commit 6adb32d
Show file tree
Hide file tree
Showing 130 changed files with 3,533 additions and 710 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/linux-ci-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ jobs:
run: |
tools/check-coverage rust/coverage.stats rust/coverage.info
- name: Run Doc tests
run: |
tools/rust-test doc
# Run Rust tests in WASM.
test-wasm:
runs-on: ubuntu-24.04
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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.Utxo
import wallet.core.jni.proto.Common.SigningError

class TestBitcoinSigning {
Expand Down Expand Up @@ -172,7 +173,7 @@ class TestBitcoinSigning {
val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data())

val utxo0 = BitcoinV2.Input.newBuilder()
.setOutPoint(BitcoinV2.OutPoint.newBuilder().apply {
.setOutPoint(Utxo.OutPoint.newBuilder().apply {
hash = ByteString.copyFrom(txId)
vout = 1
})
Expand Down Expand Up @@ -238,7 +239,7 @@ class TestBitcoinSigning {
val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data())

val utxo0 = BitcoinV2.Input.newBuilder()
.setOutPoint(BitcoinV2.OutPoint.newBuilder().apply {
.setOutPoint(Utxo.OutPoint.newBuilder().apply {
hash = ByteString.copyFrom(txIdCommit)
vout = 0
})
Expand Down Expand Up @@ -299,7 +300,7 @@ class TestBitcoinSigning {
val bobAddress = "bc1qazgc2zhu2kmy42py0vs8d7yff67l3zgpwfzlpk"

val utxo0 = BitcoinV2.Input.newBuilder()
.setOutPoint(BitcoinV2.OutPoint.newBuilder().apply {
.setOutPoint(Utxo.OutPoint.newBuilder().apply {
hash = ByteString.copyFrom(txIdInscription)
vout = 0
})
Expand All @@ -310,7 +311,7 @@ class TestBitcoinSigning {
})

val utxo1 = BitcoinV2.Input.newBuilder()
.setOutPoint(BitcoinV2.OutPoint.newBuilder().apply {
.setOutPoint(Utxo.OutPoint.newBuilder().apply {
hash = ByteString.copyFrom(txIdForFees)
vout = 1
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.junit.Test
import wallet.core.java.AnySigner
import wallet.core.jni.BitcoinSigHashType
import wallet.core.jni.CoinType
import wallet.core.jni.proto.Utxo
import wallet.core.jni.proto.Bitcoin
import wallet.core.jni.proto.BitcoinV2
import wallet.core.jni.proto.Common.SigningError
Expand All @@ -22,7 +23,7 @@ class TestBitcoinCashSigning {
val privateKey = "7fdafb9db5bc501f2096e7d13d331dc7a75d9594af3d251313ba8b6200f4e384".toHexBytesInByteString()

val utxo1 = BitcoinV2.Input.newBuilder().apply {
outPoint = BitcoinV2.OutPoint.newBuilder().apply {
outPoint = Utxo.OutPoint.newBuilder().apply {
hash = "e28c2b955293159898e34c6840d99bf4d390e2ee1c6f606939f18ee1e2000d05".toHexBytesInByteString()
vout = 2
}.build()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

package com.trustwallet.core.app.blockchains.zcash

import com.google.protobuf.ByteString
import com.trustwallet.core.app.utils.Numeric
import org.junit.Assert.assertEquals
import org.junit.Test

import wallet.core.java.AnySigner
import wallet.core.jni.BitcoinSigHashType
import wallet.core.jni.CoinType.ZCASH
import wallet.core.jni.proto.Common.SigningError
import wallet.core.jni.proto.Bitcoin
import wallet.core.jni.proto.Bitcoin.SigningOutput
import wallet.core.jni.proto.BitcoinV2
import wallet.core.jni.proto.Utxo
import wallet.core.jni.proto.Zcash

class TestZcashSigner {

init {
System.loadLibrary("TrustWalletCore")
}

@Test
fun testSignZcashV2() {
// Successfully broadcasted: https://explorer.zcha.in/transactions/ec9033381c1cc53ada837ef9981c03ead1c7c41700ff3a954389cfaddc949256
val privateKeyData = Numeric.hexStringToByteArray("a9684f5bebd0e1208aae2e02bc9e9163bd1965ad23d8538644e1df8b99b99559")
val dustSatoshis = 546.toLong()
val senderAddress = "t1gWVE2uyrET2CxSmCaBiKzmWxQdHhnvMSz"
val toAddress = "t1QahNjDdibyE4EdYkawUSKBBcVTSqv64CS"
val sapplingBranchId = Numeric.hexStringToByteArray("0xbb09b876")

val txid0 = Numeric.hexStringToByteArray("3a19dd44032dfed61bfca5ba5751aab8a107b30609cbd5d70dc5ef09885b6853").reversedArray()
val utxo0 = BitcoinV2.Input.newBuilder()
.setOutPoint(Utxo.OutPoint.newBuilder().apply {
hash = ByteString.copyFrom(txid0)
vout = 0
})
.setValue(494_000)
.setSighashType(BitcoinSigHashType.ALL.value())
.setReceiverAddress(senderAddress)

val out0 = BitcoinV2.Output.newBuilder()
.setValue(488_000)
.setToAddress(toAddress)

val builder = BitcoinV2.TransactionBuilder.newBuilder()
.setVersion(BitcoinV2.TransactionVersion.UseDefault)
.addInputs(utxo0)
.addOutputs(out0)
.setInputSelector(BitcoinV2.InputSelector.UseAll)
.setFixedDustThreshold(dustSatoshis)
// Set ZCash specific extra parameters.
.setZcashExtraData(Zcash.TransactionBuilderExtraData.newBuilder().apply {
branchId = ByteString.copyFrom(sapplingBranchId)
})
val signingInput = BitcoinV2.SigningInput.newBuilder()
.setBuilder(builder)
.addPrivateKeys(ByteString.copyFrom(privateKeyData))
.setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply {
p2PkhPrefix = 184
p2ShPrefix = 189
})
.build()

val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply {
signingV2 = signingInput
coinType = ZCASH.value()
}

val output = AnySigner.sign(legacySigningInput.build(), ZCASH, SigningOutput.parser())

assertEquals(output.error, SigningError.OK)
assertEquals(output.signingResultV2.errorMessage, "")
assertEquals(output.signingResultV2.error, SigningError.OK)
assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x0400008085202f890153685b8809efc50dd7d5cb0906b307a1b8aa5157baa5fc1bd6fe2d0344dd193a000000006b483045022100ca0be9f37a4975432a52bb65b25e483f6f93d577955290bb7fb0060a93bfc92002203e0627dff004d3c72a957dc9f8e4e0e696e69d125e4d8e275d119001924d3b48012103b243171fae5516d1dc15f9178cfcc5fdc67b0a883055c117b01ba8af29b953f6ffffffff0140720700000000001976a91449964a736f3713d64283fd0018626ba50091c7e988ac00000000000000000000000000000000000000")
assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0xec9033381c1cc53ada837ef9981c03ead1c7c41700ff3a954389cfaddc949256")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@

use tw_any_coin::test_utils::address_utils::{
test_address_derive, test_address_get_data, test_address_invalid, test_address_normalization,
test_address_valid,
test_address_valid, KeyType,
};
use tw_coin_registry::coin_type::CoinType;

#[test]
fn test_{COIN_ID}_address_derive() {
test_address_derive(CoinType::{COIN_TYPE}, "PRIVATE_KEY", "EXPECTED ADDRESS");
test_address_derive(
CoinType::{COIN_TYPE},
KeyType::PrivateKey("PRIVATE_KEY"),
"EXPECTED ADDRESS"
);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions include/TrustWalletCore/TWBlockchain.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ enum TWBlockchain {
TWBlockchainNativeInjective = 54, // Cosmos
TWBlockchainBitcoinCash = 55,
TWBlockchainPactus = 56,
TWBlockchainKomodo = 57,
};

TW_EXTERN_C_END
2 changes: 1 addition & 1 deletion registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,7 @@
"coinId": 141,
"symbol": "KMD",
"decimals": 8,
"blockchain": "Zcash",
"blockchain": "Komodo",
"derivation": [
{
"path": "m/44'/141'/0'/0/0",
Expand Down
29 changes: 29 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"chains/tw_ethereum",
"chains/tw_greenfield",
"chains/tw_internet_computer",
"chains/tw_komodo",
"chains/tw_native_evmos",
"chains/tw_native_injective",
"chains/tw_pactus",
Expand All @@ -18,6 +19,7 @@ members = [
"chains/tw_sui",
"chains/tw_thorchain",
"chains/tw_ton",
"chains/tw_zcash",
"frameworks/tw_substrate",
"frameworks/tw_ton_sdk",
"frameworks/tw_utxo",
Expand Down
25 changes: 25 additions & 0 deletions rust/chains/tw_bitcoin/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,35 @@
//
// Copyright © 2017 Trust Wallet.

use crate::modules::protobuf_builder::standard_protobuf_builder::StandardProtobufBuilder;
use crate::modules::protobuf_builder::ProtobufBuilder;
use crate::modules::psbt_request::standard_psbt_request_builder::StandardPsbtRequestBuilder;
use crate::modules::psbt_request::PsbtRequestBuilder;
use crate::modules::signing_request::standard_signing_request::StandardSigningRequestBuilder;
use crate::modules::signing_request::SigningRequestBuilder;
use tw_coin_entry::error::prelude::SigningResult;
use tw_utxo::address::standard_bitcoin::StandardBitcoinAddress;
use tw_utxo::context::{AddressPrefixes, UtxoContext};
use tw_utxo::fee::fee_estimator::StandardFeeEstimator;
use tw_utxo::script::Script;
use tw_utxo::transaction::standard_transaction::Transaction;

/// A set of associated modules different from a chain to chain.
/// The modules are mainly used to generate a signing request from a [`BitcoinV2::Proto::SigningInput`],
/// and generate a [`Utxo::Proto::Transaction`] output when the transaction is signed.
pub trait BitcoinSigningContext: UtxoContext + Sized {
type SigningRequestBuilder: SigningRequestBuilder<Self>;
type ProtobufBuilder: ProtobufBuilder<Self>;
type PsbtRequestBuilder: PsbtRequestBuilder<Self>;
}

#[derive(Default)]
pub struct StandardBitcoinContext;

impl UtxoContext for StandardBitcoinContext {
type Address = StandardBitcoinAddress;
type Transaction = Transaction;
type FeeEstimator = StandardFeeEstimator<Self::Transaction>;

fn addr_to_script_pubkey(
addr: &Self::Address,
Expand All @@ -26,3 +45,9 @@ impl UtxoContext for StandardBitcoinContext {
}
}
}

impl BitcoinSigningContext for StandardBitcoinContext {
type SigningRequestBuilder = StandardSigningRequestBuilder;
type ProtobufBuilder = StandardProtobufBuilder;
type PsbtRequestBuilder = StandardPsbtRequestBuilder;
}
20 changes: 10 additions & 10 deletions rust/chains/tw_bitcoin/src/modules/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
//
// Copyright © 2017 Trust Wallet.

use crate::context::BitcoinSigningContext;
use crate::modules::protobuf_builder::ProtobufBuilder;
use crate::modules::psbt_request::PsbtRequest;
use crate::modules::psbt_request::{PsbtRequest, PsbtRequestBuilder};
use crate::modules::signing_request::SigningRequestBuilder;
use std::borrow::Cow;
use std::marker::PhantomData;
Expand All @@ -17,6 +18,7 @@ use tw_proto::BitcoinV2::Proto::mod_PreSigningOutput::{
};
use tw_proto::BitcoinV2::Proto::mod_SigningInput::OneOftransaction as TransactionType;
use tw_utxo::context::UtxoContext;
use tw_utxo::encode::Encodable;
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 @@ -29,7 +31,7 @@ pub struct BitcoinCompiler<Context: UtxoContext> {
_phantom: PhantomData<Context>,
}

impl<Context: UtxoContext> BitcoinCompiler<Context> {
impl<Context: BitcoinSigningContext> BitcoinCompiler<Context> {
/// Please note that [`Proto::SigningInput::public_key`] must be set.
/// If the public key should be derived from a private key, please do it before this method is called.
#[inline]
Expand All @@ -47,11 +49,11 @@ impl<Context: UtxoContext> BitcoinCompiler<Context> {
) -> SigningResult<Proto::PreSigningOutput<'static>> {
let unsigned_tx = match input.transaction {
TransactionType::builder(ref tx_builder) => {
let request = SigningRequestBuilder::<Context>::build(coin, &input, tx_builder)?;
let request = Context::SigningRequestBuilder::build(coin, &input, tx_builder)?;
TxPlanner::plan(request)?.unsigned_tx
},
TransactionType::psbt(ref psbt) => {
PsbtRequest::<Context>::build(&input, psbt)?.unsigned_tx
Context::PsbtRequestBuilder::build(&input, psbt)?.unsigned_tx
},
TransactionType::None => {
return SigningError::err(SigningErrorType::Error_invalid_params)
Expand Down Expand Up @@ -110,15 +112,14 @@ impl<Context: UtxoContext> BitcoinCompiler<Context> {
tx_builder_input: &Proto::TransactionBuilder,
signatures: Vec<SignatureBytes>,
) -> SigningResult<Proto::SigningOutput<'static>> {
let request = SigningRequestBuilder::<Context>::build(coin, input, tx_builder_input)?;
let request = Context::SigningRequestBuilder::build(coin, input, tx_builder_input)?;
let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?;

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),
transaction: Context::ProtobufBuilder::tx_to_proto(&signed_tx),
encoded: Cow::from(signed_tx.encode_out()),
txid: Cow::from(signed_tx.txid()),
// `vsize` could have been changed after the transaction being signed.
Expand All @@ -136,15 +137,14 @@ impl<Context: UtxoContext> BitcoinCompiler<Context> {
psbt: &Proto::Psbt,
signatures: Vec<SignatureBytes>,
) -> SigningResult<Proto::SigningOutput<'static>> {
let PsbtRequest { unsigned_tx, .. } = PsbtRequest::<Context>::build(input, psbt)?;
let PsbtRequest { unsigned_tx, .. } = Context::PsbtRequestBuilder::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),
transaction: Context::ProtobufBuilder::tx_to_proto(&signed_tx),
encoded: Cow::from(signed_tx.encode_out()),
txid: Cow::from(signed_tx.txid()),
// `vsize` could have been changed after the transaction being signed.
Expand Down
Loading

0 comments on commit 6adb32d

Please sign in to comment.