From 8c95bd6e6b7f44dfd81db66848b61834bc60c7e6 Mon Sep 17 00:00:00 2001 From: cypt4 <106654560+cypt4@users.noreply.github.com> Date: Wed, 29 Nov 2023 22:01:30 +0700 Subject: [PATCH] Implement ZCash transparent tx signing (#20895) Resolves https://github.com/brave/brave-browser/issues/33661 --- browser/about_flags.cc | 8 + components/brave_wallet/browser/BUILD.gn | 3 + .../browser/bitcoin/bitcoin_serializer.cc | 87 +---- .../browser/bitcoin/bitcoin_serializer.h | 21 -- .../bitcoin/bitcoin_serializer_unittest.cc | 147 --------- .../browser/brave_wallet_utils.cc | 3 +- .../brave_wallet/browser/keyring_service.cc | 31 ++ .../brave_wallet/browser/keyring_service.h | 7 + components/brave_wallet/browser/test/BUILD.gn | 2 + components/brave_wallet/browser/tx_service.cc | 4 + components/brave_wallet/browser/zcash/DEPS | 6 + .../zcash/protos/zcash_grpc_data.proto | 5 + .../browser/zcash/zcash_block_tracker.cc | 2 +- .../browser/zcash/zcash_block_tracker.h | 6 +- .../browser/zcash/zcash_keyring.cc | 26 +- .../brave_wallet/browser/zcash/zcash_rpc.cc | 87 ++++- .../brave_wallet/browser/zcash/zcash_rpc.h | 44 ++- .../browser/zcash/zcash_serializer.cc | 296 ++++++++++++++++++ .../browser/zcash/zcash_serializer.h | 44 +++ .../zcash/zcash_serializer_unittest.cc | 188 +++++++++++ .../browser/zcash/zcash_transaction.cc | 50 +-- .../browser/zcash/zcash_transaction.h | 13 +- .../browser/zcash/zcash_tx_manager.h | 2 +- .../browser/zcash/zcash_wallet_service.cc | 167 ++++++++-- .../browser/zcash/zcash_wallet_service.h | 27 +- .../zcash/zcash_wallet_service_unittest.cc | 213 +++++++++++++ components/brave_wallet/common/BUILD.gn | 3 + components/brave_wallet/common/DEPS | 3 + .../common/btc_like_serializer_stream.cc | 88 ++++++ .../common/btc_like_serializer_stream.h | 40 +++ .../btc_like_serializer_stream_unittest.cc | 164 ++++++++++ components/brave_wallet/common/zcash_utils.cc | 71 +++++ components/brave_wallet/common/zcash_utils.h | 22 ++ .../utils/block-explorer-utils.ts | 7 + third_party/argon2/BUILD.gn | 1 + 35 files changed, 1564 insertions(+), 324 deletions(-) create mode 100644 components/brave_wallet/browser/zcash/DEPS create mode 100644 components/brave_wallet/browser/zcash/zcash_serializer.cc create mode 100644 components/brave_wallet/browser/zcash/zcash_serializer.h create mode 100644 components/brave_wallet/browser/zcash/zcash_serializer_unittest.cc create mode 100644 components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc create mode 100644 components/brave_wallet/common/btc_like_serializer_stream.cc create mode 100644 components/brave_wallet/common/btc_like_serializer_stream.h create mode 100644 components/brave_wallet/common/btc_like_serializer_stream_unittest.cc diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 72f451247662..d8cdde989fae 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -223,6 +223,14 @@ FEATURE_VALUE_TYPE( \ brave_wallet::features::kNativeBraveWalletFeature), \ }, \ + { \ + "brave-wallet-zcash", \ + "Enable BraveWallet ZCash support", \ + "ZCash support for native Brave Wallet", \ + kOsDesktop | kOsAndroid, \ + FEATURE_VALUE_TYPE( \ + brave_wallet::features::kBraveWalletZCashFeature), \ + }, \ { \ "brave-wallet-bitcoin", \ "Enable Brave Wallet Bitcoin support", \ diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index b2afe6181d15..6275279a9f61 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -212,6 +212,8 @@ static_library("browser") { "zcash/zcash_block_tracker.h", "zcash/zcash_rpc.cc", "zcash/zcash_rpc.h", + "zcash/zcash_serializer.cc", + "zcash/zcash_serializer.h", "zcash/zcash_transaction.cc", "zcash/zcash_transaction.h", "zcash/zcash_tx_manager.cc", @@ -259,6 +261,7 @@ static_library("browser") { "//brave/components/json/rs:rust_lib", "//brave/components/p3a_utils", "//brave/components/resources:strings_grit", + "//brave/third_party/argon2", "//components/component_updater", "//components/content_settings/core/browser", "//components/keyed_service/core", diff --git a/components/brave_wallet/browser/bitcoin/bitcoin_serializer.cc b/components/brave_wallet/browser/bitcoin/bitcoin_serializer.cc index 6eaca598ed3f..83d16b4c0d1b 100644 --- a/components/brave_wallet/browser/bitcoin/bitcoin_serializer.cc +++ b/components/brave_wallet/browser/bitcoin/bitcoin_serializer.cc @@ -9,6 +9,7 @@ #include "base/sys_byteorder.h" #include "brave/components/brave_wallet/common/bitcoin_utils.h" +#include "brave/components/brave_wallet/common/btc_like_serializer_stream.h" #include "brave/components/brave_wallet/common/hash_utils.h" namespace brave_wallet { @@ -64,13 +65,13 @@ const std::vector& DummyWitness() { } void PushOutpoint(const BitcoinTransaction::Outpoint& outpoint, - BitcoinSerializerStream& stream) { + BtcLikeSerializerStream& stream) { stream.PushBytesReversed(outpoint.txid); stream.Push32AsLE(outpoint.index); } void PushScriptCodeForSigninig(const DecodedBitcoinAddress& decoded_address, - BitcoinSerializerStream& stream) { + BtcLikeSerializerStream& stream) { // TODO(apaymyshev): support more. DCHECK_EQ(decoded_address.address_type, BitcoinAddressType::kWitnessV0PubkeyHash); @@ -87,7 +88,7 @@ SHA256HashArray HashPrevouts(const BitcoinTransaction& tx) { DCHECK_EQ(tx.sighash_type(), kBitcoinSigHashAll); std::vector data; - BitcoinSerializerStream stream(&data); + BtcLikeSerializerStream stream(&data); for (const auto& input : tx.inputs()) { PushOutpoint(input.utxo_outpoint, stream); } @@ -99,7 +100,7 @@ SHA256HashArray HashSequence(const BitcoinTransaction& tx) { DCHECK_EQ(tx.sighash_type(), kBitcoinSigHashAll); std::vector data; - BitcoinSerializerStream stream(&data); + BtcLikeSerializerStream stream(&data); for (const auto& input : tx.inputs()) { stream.Push32AsLE(input.n_sequence()); } @@ -108,7 +109,7 @@ SHA256HashArray HashSequence(const BitcoinTransaction& tx) { } void PushOutput(const BitcoinTransaction::TxOutput& output, - BitcoinSerializerStream& stream) { + BtcLikeSerializerStream& stream) { stream.Push64AsLE(output.amount); CHECK(output.script_pubkey.size()); stream.PushSizeAndBytes(output.script_pubkey); @@ -118,7 +119,7 @@ SHA256HashArray HashOutputs(const BitcoinTransaction& tx) { DCHECK_EQ(tx.sighash_type(), kBitcoinSigHashAll); std::vector data; - BitcoinSerializerStream stream(&data); + BtcLikeSerializerStream stream(&data); for (const auto& output : tx.outputs()) { PushOutput(output, stream); } @@ -127,7 +128,7 @@ SHA256HashArray HashOutputs(const BitcoinTransaction& tx) { } void SerializeInputs(const BitcoinTransaction& tx, - BitcoinSerializerStream& stream) { + BtcLikeSerializerStream& stream) { stream.PushVarInt(tx.inputs().size()); for (const auto& input : tx.inputs()) { PushOutpoint(input.utxo_outpoint, stream); @@ -197,7 +198,7 @@ uint32_t GetOutputsVBytes(const BitcoinTransaction& tx) { } void SerializeOutputs(const BitcoinTransaction& tx, - BitcoinSerializerStream& stream) { + BtcLikeSerializerStream& stream) { stream.PushVarInt(tx.outputs().size()); for (const auto& output : tx.outputs()) { PushOutput(output, stream); @@ -205,7 +206,7 @@ void SerializeOutputs(const BitcoinTransaction& tx, } void SerializeWitnesses(const BitcoinTransaction& tx, - BitcoinSerializerStream& stream) { + BtcLikeSerializerStream& stream) { for (const auto& input : tx.inputs()) { DCHECK(!input.witness.empty()); stream.PushBytes(input.witness); @@ -233,66 +234,6 @@ uint32_t GetWitnessesWeightUnits(const BitcoinTransaction& tx, } // namespace -void BitcoinSerializerStream::Push8AsLE(uint8_t i) { - base::span data_to_insert(reinterpret_cast(&i), sizeof(i)); - PushBytes(data_to_insert); -} - -void BitcoinSerializerStream::Push16AsLE(uint16_t i) { - i = base::ByteSwapToLE16(i); - base::span data_to_insert(reinterpret_cast(&i), sizeof(i)); - PushBytes(data_to_insert); -} - -void BitcoinSerializerStream::Push32AsLE(uint32_t i) { - i = base::ByteSwapToLE32(i); - base::span data_to_insert(reinterpret_cast(&i), sizeof(i)); - PushBytes(data_to_insert); -} - -void BitcoinSerializerStream::Push64AsLE(uint64_t i) { - i = base::ByteSwapToLE64(i); - base::span data_to_insert(reinterpret_cast(&i), sizeof(i)); - PushBytes(data_to_insert); -} - -// https://developer.bitcoin.org/reference/transactions.html#compactsize-unsigned-integers -void BitcoinSerializerStream::PushVarInt(uint64_t i) { - if (i < 0xfd) { - Push8AsLE(i); - } else if (i <= 0xffff) { - Push8AsLE(0xfd); - Push16AsLE(i); - } else if (i <= 0xffffffff) { - Push8AsLE(0xfe); - Push32AsLE(i); - } else { - Push8AsLE(0xff); - Push64AsLE(i); - } -} - -void BitcoinSerializerStream::PushSizeAndBytes( - base::span bytes) { - PushVarInt(bytes.size()); - PushBytes(bytes); -} - -void BitcoinSerializerStream::PushBytes(base::span bytes) { - if (to()) { - to()->insert(to()->end(), bytes.begin(), bytes.end()); - } - serialized_bytes_ += bytes.size(); -} - -void BitcoinSerializerStream::PushBytesReversed( - base::span bytes) { - if (to()) { - to()->insert(to()->end(), bytes.rbegin(), bytes.rend()); - } - serialized_bytes_ += bytes.size(); -} - std::vector BitcoinSerializer::AddressToScriptPubkey( const std::string& address, bool testnet) { @@ -306,7 +247,7 @@ std::vector BitcoinSerializer::AddressToScriptPubkey( } std::vector data; - BitcoinSerializerStream stream(&data); + BtcLikeSerializerStream stream(&data); // https://github.com/bitcoin/bitcoin/blob/v25.0/src/script/standard.cpp#L302-L325 @@ -387,7 +328,7 @@ absl::optional BitcoinSerializer::SerializeInputForSign( } std::vector data; - BitcoinSerializerStream stream(&data); + BtcLikeSerializerStream stream(&data); // https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#specification stream.Push32AsLE(2); // 1. stream.PushBytes(HashPrevouts(tx)); // 2. @@ -410,7 +351,7 @@ std::vector BitcoinSerializer::SerializeWitness( const std::vector& signature, const std::vector& pubkey) { std::vector result; - BitcoinSerializerStream witness_stream(&result); + BtcLikeSerializerStream witness_stream(&result); // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-id // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh witness_stream.PushVarInt(2); @@ -425,7 +366,7 @@ std::vector BitcoinSerializer::SerializeSignedTransaction( DCHECK(tx.IsSigned()); std::vector data; - BitcoinSerializerStream stream(&data); + BtcLikeSerializerStream stream(&data); // https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki#specification stream.Push32AsLE(kTransactionsVersion); // version diff --git a/components/brave_wallet/browser/bitcoin/bitcoin_serializer.h b/components/brave_wallet/browser/bitcoin/bitcoin_serializer.h index af581918c652..18dfac222b7f 100644 --- a/components/brave_wallet/browser/bitcoin/bitcoin_serializer.h +++ b/components/brave_wallet/browser/bitcoin/bitcoin_serializer.h @@ -17,27 +17,6 @@ namespace brave_wallet { -class BitcoinSerializerStream { - public: - explicit BitcoinSerializerStream(std::vector* to) : to_(to) {} - - void Push8AsLE(uint8_t i); - void Push16AsLE(uint16_t i); - void Push32AsLE(uint32_t i); - void Push64AsLE(uint64_t i); - void PushVarInt(uint64_t i); - void PushSizeAndBytes(base::span bytes); - void PushBytes(base::span bytes); - void PushBytesReversed(base::span bytes); - - uint32_t serialized_bytes() const { return serialized_bytes_; } - - private: - uint32_t serialized_bytes_ = 0; - std::vector* to() { return to_.get(); } - raw_ptr> to_; -}; - // TODO(apaymyshev): test with reference test vectors. class BitcoinSerializer { public: diff --git a/components/brave_wallet/browser/bitcoin/bitcoin_serializer_unittest.cc b/components/brave_wallet/browser/bitcoin/bitcoin_serializer_unittest.cc index 096537729061..d002f5bf1271 100644 --- a/components/brave_wallet/browser/bitcoin/bitcoin_serializer_unittest.cc +++ b/components/brave_wallet/browser/bitcoin/bitcoin_serializer_unittest.cc @@ -29,153 +29,6 @@ const char kAddress2[] = "tb1qva8clyftt2fstawn5dy0nvrfmygpzulf3lwulm"; } // namespace -TEST(BitcoinSerializerStream, Push8AsLE) { - std::vector data; - BitcoinSerializerStream stream(&data); - stream.Push8AsLE(0xab); - EXPECT_EQ(base::HexEncode(data), "AB"); - stream.Push8AsLE(0x12); - EXPECT_EQ(base::HexEncode(data), "AB12"); - - EXPECT_EQ(stream.serialized_bytes(), 2u); -} - -TEST(BitcoinSerializerStream, Push16AsLE) { - std::vector data; - BitcoinSerializerStream stream(&data); - stream.Push16AsLE(0xab); - EXPECT_EQ(base::HexEncode(data), "AB00"); - stream.Push16AsLE(0x1234); - EXPECT_EQ(base::HexEncode(data), "AB003412"); - - EXPECT_EQ(stream.serialized_bytes(), 4u); -} - -TEST(BitcoinSerializerStream, Push32AsLE) { - std::vector data; - BitcoinSerializerStream stream(&data); - stream.Push32AsLE(0xabcd); - EXPECT_EQ(base::HexEncode(data), "CDAB0000"); - stream.Push32AsLE(0x12345678); - EXPECT_EQ(base::HexEncode(data), "CDAB000078563412"); - - EXPECT_EQ(stream.serialized_bytes(), 8u); -} - -TEST(BitcoinSerializerStream, Push64AsLE) { - std::vector data; - BitcoinSerializerStream stream(&data); - stream.Push64AsLE(0xabcd); - EXPECT_EQ(base::HexEncode(data), "CDAB000000000000"); - stream.Push64AsLE(0x1234567890abcdef); - EXPECT_EQ(base::HexEncode(data), "CDAB000000000000EFCDAB9078563412"); - - EXPECT_EQ(stream.serialized_bytes(), 16u); -} - -TEST(BitcoinSerializerStream, PushVarInt) { - std::vector data; - BitcoinSerializerStream stream(&data); - stream.PushVarInt(0xab); - EXPECT_EQ(base::HexEncode(data), "AB"); - stream.PushVarInt(0xabcd); - EXPECT_EQ(base::HexEncode(data), "ABFDCDAB"); - stream.PushVarInt(0xabcdef01); - EXPECT_EQ(base::HexEncode(data), "ABFDCDABFE01EFCDAB"); - stream.PushVarInt(0xabcdef0123456789); - EXPECT_EQ(base::HexEncode(data), "ABFDCDABFE01EFCDABFF8967452301EFCDAB"); - - EXPECT_EQ(stream.serialized_bytes(), 18u); -} - -TEST(BitcoinSerializerStream, PushSizeAndBytes) { - { - std::vector bytes(10, 0xab); - std::vector data; - BitcoinSerializerStream stream(&data); - stream.PushSizeAndBytes(bytes); - EXPECT_EQ(data.size(), 1u + 10u); - EXPECT_EQ(base::HexEncode(base::make_span(data).first(1)), "0A"); - EXPECT_TRUE(base::ranges::all_of(base::make_span(data).last(10), - [](auto c) { return c == 0xab; })); - EXPECT_EQ(stream.serialized_bytes(), 11u); - } - - { - std::vector bytes(300, 0xcd); - std::vector data; - BitcoinSerializerStream stream(&data); - stream.PushSizeAndBytes(bytes); - EXPECT_EQ(data.size(), 3u + 300u); - EXPECT_EQ(base::HexEncode(base::make_span(data).first(3)), "FD2C01"); - EXPECT_TRUE(base::ranges::all_of(base::make_span(data).last(300), - [](auto c) { return c == 0xcd; })); - EXPECT_EQ(stream.serialized_bytes(), 303u); - } - - { - std::vector bytes(0x10000, 0xef); - std::vector data; - BitcoinSerializerStream stream(&data); - stream.PushSizeAndBytes(bytes); - EXPECT_EQ(data.size(), 5u + 0x10000); - EXPECT_EQ(base::HexEncode(base::make_span(data).first(5)), "FE00000100"); - EXPECT_TRUE(base::ranges::all_of(base::make_span(data).last(0x10000), - [](auto c) { return c == 0xef; })); - EXPECT_EQ(stream.serialized_bytes(), 65541u); - } -} - -TEST(BitcoinSerializerStream, PushBytes) { - std::vector bytes({0x01, 0x02, 0xab, 0xcd, 0xef}); - std::vector data; - BitcoinSerializerStream stream(&data); - stream.PushBytes(bytes); - EXPECT_EQ(base::HexEncode(data), "0102ABCDEF"); - - EXPECT_EQ(stream.serialized_bytes(), 5u); -} - -TEST(BitcoinSerializerStream, PushBytesReversed) { - std::vector bytes({0x01, 0x02, 0xab, 0xcd, 0xef}); - std::vector data; - BitcoinSerializerStream stream(&data); - stream.PushBytesReversed(bytes); - EXPECT_EQ(base::HexEncode(data), "EFCDAB0201"); - - EXPECT_EQ(stream.serialized_bytes(), 5u); -} - -TEST(BitcoinSerializerStream, NoVectorInCtor) { - std::vector bytes({0x01, 0x02, 0xab, 0xcd, 0xef}); - - BitcoinSerializerStream stream(nullptr); - - stream.Push8AsLE(0xab); - EXPECT_EQ(stream.serialized_bytes(), 1u); - - stream.Push16AsLE(0xab); - EXPECT_EQ(stream.serialized_bytes(), 3u); - - stream.Push32AsLE(0x12345678); - EXPECT_EQ(stream.serialized_bytes(), 7u); - - stream.Push64AsLE(0xabcd); - EXPECT_EQ(stream.serialized_bytes(), 15u); - - stream.PushBytes(bytes); - EXPECT_EQ(stream.serialized_bytes(), 20u); - - stream.PushBytesReversed(bytes); - EXPECT_EQ(stream.serialized_bytes(), 25u); - - stream.PushSizeAndBytes(bytes); - EXPECT_EQ(stream.serialized_bytes(), 31u); - - stream.PushVarInt(0xabcdef01); - EXPECT_EQ(stream.serialized_bytes(), 36u); -} - TEST(BitcoinSerializer, SerializeInputForSign) { BitcoinTransaction tx; diff --git a/components/brave_wallet/browser/brave_wallet_utils.cc b/components/brave_wallet/browser/brave_wallet_utils.cc index facd79e4b9a6..34b6b94ffce9 100644 --- a/components/brave_wallet/browser/brave_wallet_utils.cc +++ b/components/brave_wallet/browser/brave_wallet_utils.cc @@ -598,7 +598,8 @@ const mojom::NetworkInfo* GetZCashMainnet() { static base::NoDestructor network_info( {chain_id, "ZCash Mainnet", - {""}, // TODO(cypt4): explorer url + {"https://zcashblockexplorer.com/transactions"}, // TODO(cypt4): + // explorer url {}, 0, {ZCashMainnetRpcUrl()}, diff --git a/components/brave_wallet/browser/keyring_service.cc b/components/brave_wallet/browser/keyring_service.cc index 0518c082ad9f..1e338d25b6b2 100644 --- a/components/brave_wallet/browser/keyring_service.cc +++ b/components/brave_wallet/browser/keyring_service.cc @@ -2552,6 +2552,21 @@ absl::optional KeyringService::GetZCashAddress( return zcash_keyring->GetAddress(key_id); } +absl::optional> KeyringService::GetZCashPubKey( + const mojom::AccountIdPtr& account_id, + const mojom::ZCashKeyIdPtr& key_id) { + CHECK(account_id); + CHECK(key_id); + CHECK(IsZCashAccount(*account_id)); + + auto* zcash_keyring = GetZCashKeyringById(account_id->keyring_id); + if (!zcash_keyring) { + return absl::nullopt; + } + + return zcash_keyring->GetPubkey(*key_id); +} + void KeyringService::UpdateNextUnusedAddressForBitcoinAccount( const mojom::AccountIdPtr& account_id, absl::optional next_receive_index, @@ -2744,6 +2759,22 @@ KeyringService::SignMessageByBitcoinKeyring( *key_id, message); } +absl::optional> KeyringService::SignMessageByZCashKeyring( + const mojom::AccountIdPtr& account_id, + const mojom::ZCashKeyIdPtr& key_id, + const base::span message) { + CHECK(account_id); + CHECK(key_id); + CHECK(IsZCashAccount(*account_id)); + + auto* zcash_keyring = GetZCashKeyringById(account_id->keyring_id); + if (!zcash_keyring) { + return absl::nullopt; + } + + return zcash_keyring->SignMessage(*key_id, message); +} + void KeyringService::ResetAllAccountInfosCache() { account_info_cache_.reset(); } diff --git a/components/brave_wallet/browser/keyring_service.h b/components/brave_wallet/browser/keyring_service.h index c8fa9ce8f948..6627217fb881 100644 --- a/components/brave_wallet/browser/keyring_service.h +++ b/components/brave_wallet/browser/keyring_service.h @@ -238,12 +238,19 @@ class KeyringService : public KeyedService, public mojom::KeyringService { const mojom::AccountIdPtr& account_id, const mojom::BitcoinKeyIdPtr& key_id, base::span message); + absl::optional> SignMessageByZCashKeyring( + const mojom::AccountIdPtr& account_id, + const mojom::ZCashKeyIdPtr& key_id, + const base::span message); absl::optional>> GetZCashAddresses(const mojom::AccountId& account_id); absl::optional GetZCashAddress( const mojom::AccountId& account_id, const mojom::ZCashKeyId& key_id); + absl::optional> GetZCashPubKey( + const mojom::AccountIdPtr& account_id, + const mojom::ZCashKeyIdPtr& key_id); const std::vector& GetAllAccountInfos(); mojom::AccountInfoPtr GetSelectedWalletAccount(); diff --git a/components/brave_wallet/browser/test/BUILD.gn b/components/brave_wallet/browser/test/BUILD.gn index 7f653af251a0..bfe603eec964 100644 --- a/components/brave_wallet/browser/test/BUILD.gn +++ b/components/brave_wallet/browser/test/BUILD.gn @@ -95,6 +95,8 @@ source_set("brave_wallet_unit_tests") { "//brave/components/brave_wallet/browser/unstoppable_domains_multichain_calls_unittest.cc", "//brave/components/brave_wallet/browser/wallet_data_files_installer_unittest.cc", "//brave/components/brave_wallet/browser/zcash/zcash_keyring_unittest.cc", + "//brave/components/brave_wallet/browser/zcash/zcash_serializer_unittest.cc", + "//brave/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc", ] if (enable_ipfs_local_node) { diff --git a/components/brave_wallet/browser/tx_service.cc b/components/brave_wallet/browser/tx_service.cc index c127f83d47e3..86785373d5d4 100644 --- a/components/brave_wallet/browser/tx_service.cc +++ b/components/brave_wallet/browser/tx_service.cc @@ -78,6 +78,10 @@ std::string GetToAddressFromTxDataUnion( return tx_data_union.get_btc_tx_data()->to; } + if (tx_data_union.is_zec_tx_data()) { + return tx_data_union.get_zec_tx_data()->to; + } + NOTREACHED_NORETURN(); } diff --git a/components/brave_wallet/browser/zcash/DEPS b/components/brave_wallet/browser/zcash/DEPS new file mode 100644 index 000000000000..b5d296ad319c --- /dev/null +++ b/components/brave_wallet/browser/zcash/DEPS @@ -0,0 +1,6 @@ +specific_include_rules = { + "zcash_serializer.cc": [ + "+brave/third_party/argon2", + ], +} + diff --git a/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.proto b/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.proto index bcb76765d66f..a5f1c738580b 100644 --- a/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.proto +++ b/components/brave_wallet/browser/zcash/protos/zcash_grpc_data.proto @@ -40,3 +40,8 @@ message RawTransaction { bytes data = 1; uint64 height = 2; } + +message SendResponse { + int32 errorCode = 1; + string errorMessage = 2; +} diff --git a/components/brave_wallet/browser/zcash/zcash_block_tracker.cc b/components/brave_wallet/browser/zcash/zcash_block_tracker.cc index 763afa6fbb7f..76fb57e146db 100644 --- a/components/brave_wallet/browser/zcash/zcash_block_tracker.cc +++ b/components/brave_wallet/browser/zcash/zcash_block_tracker.cc @@ -14,7 +14,7 @@ namespace brave_wallet { -ZCashBlockTracker::ZCashBlockTracker(zcash_rpc::ZCashRpc* zcash_rpc) +ZCashBlockTracker::ZCashBlockTracker(ZCashRpc* zcash_rpc) : zcash_rpc_(zcash_rpc) {} ZCashBlockTracker::~ZCashBlockTracker() = default; diff --git a/components/brave_wallet/browser/zcash/zcash_block_tracker.h b/components/brave_wallet/browser/zcash/zcash_block_tracker.h index ee4282d11d4d..cac0bf14f31f 100644 --- a/components/brave_wallet/browser/zcash/zcash_block_tracker.h +++ b/components/brave_wallet/browser/zcash/zcash_block_tracker.h @@ -19,13 +19,11 @@ namespace brave_wallet { -namespace zcash_rpc { class ZCashRpc; -} class ZCashBlockTracker : public BlockTracker { public: - explicit ZCashBlockTracker(zcash_rpc::ZCashRpc* zcash_rpc); + explicit ZCashBlockTracker(ZCashRpc* zcash_rpc); ~ZCashBlockTracker() override; ZCashBlockTracker(const ZCashBlockTracker&) = delete; ZCashBlockTracker operator=(const ZCashBlockTracker&) = delete; @@ -52,7 +50,7 @@ class ZCashBlockTracker : public BlockTracker { std::map latest_height_map_; base::ObserverList observers_; - raw_ptr zcash_rpc_ = nullptr; + raw_ptr zcash_rpc_ = nullptr; base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/components/brave_wallet/browser/zcash/zcash_keyring.cc b/components/brave_wallet/browser/zcash/zcash_keyring.cc index 6a1766b77745..becead04bf3f 100644 --- a/components/brave_wallet/browser/zcash/zcash_keyring.cc +++ b/components/brave_wallet/browser/zcash/zcash_keyring.cc @@ -36,19 +36,6 @@ absl::optional> ZCashKeyring::GetPubkey( return hd_key_base->GetPublicKeyBytes(); } -absl::optional> ZCashKeyring::SignMessage( - const mojom::ZCashKeyId& key_id, - base::span message) { - auto hd_key_base = DeriveKey(key_id); - if (!hd_key_base) { - return absl::nullopt; - } - - auto* hd_key = static_cast(hd_key_base.get()); - - return hd_key->SignDer(message); -} - std::string ZCashKeyring::GetAddressInternal(HDKeyBase* hd_key_base) const { if (!hd_key_base) { return std::string(); @@ -82,4 +69,17 @@ std::unique_ptr ZCashKeyring::DeriveKey( return key->DeriveNormalChild(key_id.index); } +absl::optional> ZCashKeyring::SignMessage( + const mojom::ZCashKeyId& key_id, + base::span message) { + auto hd_key_base = DeriveKey(key_id); + if (!hd_key_base) { + return absl::nullopt; + } + + auto* hd_key = static_cast(hd_key_base.get()); + + return hd_key->SignDer(message); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_rpc.cc b/components/brave_wallet/browser/zcash/zcash_rpc.cc index b50f9f5b2d47..f0b198ae914b 100644 --- a/components/brave_wallet/browser/zcash/zcash_rpc.cc +++ b/components/brave_wallet/browser/zcash/zcash_rpc.cc @@ -15,7 +15,7 @@ #include "services/network/public/cpp/resource_request.h" #include "services/network/public/mojom/url_loader.mojom.h" -namespace brave_wallet::zcash_rpc { +namespace brave_wallet { namespace { @@ -64,6 +64,23 @@ const GURL MakeGetAddressUtxosURL(const GURL& base_url) { return base_url.ReplaceComponents(replacements); } +const GURL MakeSendTransactionURL(const GURL& base_url) { + if (!base_url.is_valid()) { + return GURL(); + } + if (!UrlPathEndsWithSlash(base_url)) { + return GURL(); + } + + GURL::Replacements replacements; + std::string path = + base::StrCat({base_url.path(), + "cash.z.wallet.sdk.rpc.CompactTxStreamer/SendTransaction"}); + replacements.SetPathStr(path); + + return base_url.ReplaceComponents(replacements); +} + const GURL MakeGetLatestBlockHeightURL(const GURL& base_url) { if (!base_url.is_valid()) { return GURL(); @@ -115,7 +132,6 @@ std::string GetPrefixedProtobuf(const std::string& serialized_proto) { std::string MakeGetAddressUtxosURLParams(const std::string& address) { zcash::GetAddressUtxosRequest request; request.add_addresses(address); - request.set_maxentries(1); request.set_startheight(0); return GetPrefixedProtobuf(request.SerializeAsString()); } @@ -127,7 +143,16 @@ std::string MakeGetLatestBlockHeightParams() { std::string MakeGetTransactionParams(const std::string& tx_hash) { zcash::TxFilter request; - request.set_hash(tx_hash); + std::string as_bytes; + base::HexStringToString(tx_hash, &as_bytes); + std::reverse(as_bytes.begin(), as_bytes.end()); + request.set_hash(as_bytes); + return GetPrefixedProtobuf(request.SerializeAsString()); +} + +std::string MakeSendTransactionParams(const std::string& data) { + zcash::RawTransaction request; + request.set_data(data); return GetPrefixedProtobuf(request.SerializeAsString()); } @@ -350,4 +375,58 @@ void ZCashRpc::OnGetTransactionResponse( std::move(callback).Run(response); } -} // namespace brave_wallet::zcash_rpc +void ZCashRpc::SendTransaction(const std::string& chain_id, + const std::string& data, + SendTransactionCallback callback) { + GURL request_url = MakeSendTransactionURL( + GetNetworkURL(prefs_, chain_id, mojom::CoinType::ZEC)); + + if (!request_url.is_valid()) { + std::move(callback).Run(base::unexpected("Request URL is invalid.")); + return; + } + + auto url_loader = + MakeGRPCLoader(request_url, MakeSendTransactionParams(data)); + + UrlLoadersList::iterator it = url_loaders_list_.insert( + url_loaders_list_.begin(), std::move(url_loader)); + + (*it)->DownloadToString( + url_loader_factory_.get(), + base::BindOnce(&ZCashRpc::OnSendTransactionResponse, + weak_ptr_factory_.GetWeakPtr(), std::move(callback), it), + 5000); +} + +void ZCashRpc::OnSendTransactionResponse( + ZCashRpc::SendTransactionCallback callback, + UrlLoadersList::iterator it, + const std::unique_ptr response_body) { + auto current_loader = std::move(*it); + url_loaders_list_.erase(it); + zcash::SendResponse response; + if (current_loader->NetError()) { + std::move(callback).Run(base::unexpected("Network error")); + return; + } + + if (!response_body) { + std::move(callback).Run(base::unexpected("Response body is empty")); + return; + } + + auto message = ResolveSerializedMessage(*response_body); + if (!message) { + std::move(callback).Run(base::unexpected("Wrong response format")); + return; + } + + if (!response.ParseFromString(message.value())) { + std::move(callback).Run(base::unexpected("Can't parse response")); + return; + } + + std::move(callback).Run(response); +} +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_rpc.h b/components/brave_wallet/browser/zcash/zcash_rpc.h index 33db3027f4ed..037523b5e3e4 100644 --- a/components/brave_wallet/browser/zcash/zcash_rpc.h +++ b/components/brave_wallet/browser/zcash/zcash_rpc.h @@ -21,7 +21,7 @@ #include "services/network/public/cpp/simple_url_loader.h" #include "third_party/abseil-cpp/absl/types/optional.h" -namespace brave_wallet::zcash_rpc { +namespace brave_wallet { // lightwalletd interface class ZCashRpc { @@ -32,24 +32,31 @@ class ZCashRpc { base::OnceCallback)>; using GetTransactionCallback = base::OnceCallback)>; + using SendTransactionCallback = base::OnceCallback)>; - explicit ZCashRpc( - PrefService* prefs, - scoped_refptr url_loader_factory); - ~ZCashRpc(); + ZCashRpc(PrefService* prefs, + scoped_refptr url_loader_factory); + virtual ~ZCashRpc(); - void GetUtxoList(const std::string& chain_id, - const std::string& address, - GetUtxoListCallback callback); + virtual void GetUtxoList(const std::string& chain_id, + const std::string& address, + GetUtxoListCallback callback); - void GetLatestBlock(const std::string& chain_id, - GetLatestBlockCallback callback); + virtual void GetLatestBlock(const std::string& chain_id, + GetLatestBlockCallback callback); - void GetTransaction(const std::string& chain_id, - const std::string& tx_hash, - GetTransactionCallback callback); + virtual void GetTransaction(const std::string& chain_id, + const std::string& tx_hash, + GetTransactionCallback callback); + + virtual void SendTransaction(const std::string& chain_id, + const std::string& data, + SendTransactionCallback callback); private: + friend class base::RefCountedThreadSafe; + using UrlLoadersList = std::list>; void OnGetUtxosResponse(ZCashRpc::GetUtxoListCallback callback, @@ -66,12 +73,17 @@ class ZCashRpc { UrlLoadersList::iterator it, const std::unique_ptr response_body); + void OnSendTransactionResponse( + ZCashRpc::SendTransactionCallback callback, + UrlLoadersList::iterator it, + const std::unique_ptr response_body); + UrlLoadersList url_loaders_list_; - raw_ptr prefs_; - scoped_refptr url_loader_factory_; + raw_ptr prefs_ = nullptr; + scoped_refptr url_loader_factory_ = nullptr; base::WeakPtrFactory weak_ptr_factory_{this}; }; -} // namespace brave_wallet::zcash_rpc +} // namespace brave_wallet #endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_RPC_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_serializer.cc b/components/brave_wallet/browser/zcash/zcash_serializer.cc new file mode 100644 index 000000000000..696c20cd51c6 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_serializer.cc @@ -0,0 +1,296 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/zcash/zcash_serializer.h" + +#include +#include + +#include "base/big_endian.h" +#include "base/sys_byteorder.h" +#include "brave/components/brave_wallet/common/btc_like_serializer_stream.h" +#include "brave/third_party/argon2/src/src/blake2/blake2.h" + +namespace brave_wallet { + +namespace { + +// https://zips.z.cash/zip-0244 +constexpr char kTransparentHashPersonalizer[] = "ZTxIdTranspaHash"; +constexpr char kSaplingHashPersonalizer[] = "ZTxIdSaplingHash"; +constexpr char kOrchardHashPersonalizer[] = "ZTxIdOrchardHash"; + +constexpr uint32_t kV5TxVersion = 5 | 1 << 31 /* overwintered bit */; +// https://zips.z.cash/protocol/protocol.pdf#txnconsensus +constexpr uint32_t kV5VersionGroupId = 0x26A7270A; +constexpr uint32_t kConsensusBranchId = 0xC2D6D0B4; + +// https://zips.z.cash/zip-0244#txid-digest-1 +std::string GetTxHashPersonalizer() { + uint32_t consensus = base::ByteSwapToLE32(kConsensusBranchId); + std::string personalizer("ZcashTxHash_"); + personalizer.append(reinterpret_cast(&consensus), + sizeof(kConsensusBranchId)); + return personalizer; +} + +std::array blake2b256(const std::vector& payload, + std::string_view personalizer) { + blake2b_state blake_state = {}; + blake2b_param params = {}; + + if (personalizer.length() != sizeof(params.personal)) { + NOTREACHED(); + return {}; + } + + params.digest_length = 32; + params.fanout = 1; + params.depth = 1; + memcpy(params.personal, personalizer.data(), sizeof(params.personal)); + if (blake2b_init_param(&blake_state, ¶ms) != 0) { + NOTREACHED(); + return {}; + } + if (blake2b_update(&blake_state, payload.data(), payload.size()) != 0) { + NOTREACHED(); + return {}; + } + std::array result; + if (blake2b_final(&blake_state, result.data(), 32) != 0) { + NOTREACHED(); + return {}; + } + + return result; +} + +void PushHeader(const ZCashTransaction& tx, BtcLikeSerializerStream& stream) { + stream.Push32AsLE(kV5TxVersion); + stream.Push32AsLE(kV5VersionGroupId); + stream.Push32AsLE(kConsensusBranchId); + stream.Push32AsLE(tx.locktime()); + stream.Push32AsLE(tx.expiry_height()); +} + +void PushOutpoint(const ZCashTransaction::Outpoint& outpoint, + BtcLikeSerializerStream& stream) { + stream.PushBytes(outpoint.txid); + stream.Push32AsLE(outpoint.index); +} + +void PushOutput(const ZCashTransaction::TxOutput& output, + BtcLikeSerializerStream& stream) { + stream.Push64AsLE(output.amount); + stream.PushSizeAndBytes(output.script_pubkey); +} + +// https://zips.z.cash/zip-0244#s-2c-amounts-sig-digest +std::array HashAmounts(const ZCashTransaction& tx) { + std::vector data; + BtcLikeSerializerStream stream(&data); + for (const auto& input : tx.inputs()) { + stream.Push64AsLE(input.utxo_value); + } + return blake2b256(data, "ZTxTrAmountsHash"); +} + +// https://zips.z.cash/zip-0244#s-2d-scriptpubkeys-sig-digest +std::array HashScriptPubKeys(const ZCashTransaction& tx) { + std::vector data; + BtcLikeSerializerStream stream(&data); + for (const auto& input : tx.inputs()) { + stream.PushSizeAndBytes(input.script_pub_key); + } + return blake2b256(data, "ZTxTrScriptsHash"); +} + +} // namespace + +// static +// https://zips.z.cash/zip-0244#s-2g-txin-sig-digest +std::array ZCashSerializer::HashTxIn( + const ZCashTransaction::TxInput& tx_in) { + std::vector data; + BtcLikeSerializerStream stream(&data); + + PushOutpoint(tx_in.utxo_outpoint, stream); + stream.Push64AsLE(tx_in.utxo_value); + + stream.PushSizeAndBytes(tx_in.script_pub_key); + + stream.Push32AsLE(tx_in.n_sequence); + return blake2b256(data, "Zcash___TxInHash"); +} + +// static +// https://zips.z.cash/zip-0244#t-2a-prevouts-digest +std::array ZCashSerializer::HashPrevouts( + const ZCashTransaction& tx) { + std::vector data; + BtcLikeSerializerStream stream(&data); + for (const auto& input : tx.inputs()) { + PushOutpoint(input.utxo_outpoint, stream); + } + return blake2b256(data, "ZTxIdPrevoutHash"); +} + +// static +// https://zips.z.cash/zip-0244#t-2b-sequence-digest +std::array ZCashSerializer::HashSequences( + const ZCashTransaction& tx) { + std::vector data; + BtcLikeSerializerStream stream(&data); + for (const auto& input : tx.inputs()) { + stream.Push32AsLE(input.n_sequence); + } + return blake2b256(data, "ZTxIdSequencHash"); +} + +// static +// https://zips.z.cash/zip-0244#t-2a-prevouts-digest +std::array ZCashSerializer::HashOutputs( + const ZCashTransaction& tx) { + std::vector data; + BtcLikeSerializerStream stream(&data); + for (const auto& output : tx.outputs()) { + PushOutput(output, stream); + } + return blake2b256(data, "ZTxIdOutputsHash"); +} + +// static +// https://zips.z.cash/zip-0244#t-1-header-digest +std::array ZCashSerializer::HashHeader( + const ZCashTransaction& tx) { + std::vector data; + BtcLikeSerializerStream stream(&data); + PushHeader(tx, stream); + return blake2b256(data, "ZTxIdHeadersHash"); +} + +// static +// https://zips.z.cash/zip-0244#txid-digest +std::array ZCashSerializer::CalculateTxIdDigest( + const ZCashTransaction& zcash_transaction) { + std::array header_hash = HashHeader(zcash_transaction); + + std::array transaprent_hash; + { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushBytes(ZCashSerializer::HashPrevouts(zcash_transaction)); + stream.PushBytes(ZCashSerializer::HashSequences(zcash_transaction)); + stream.PushBytes(ZCashSerializer::HashOutputs(zcash_transaction)); + transaprent_hash = blake2b256(data, kTransparentHashPersonalizer); + } + + std::array sapling_hash; + { sapling_hash = blake2b256({}, kSaplingHashPersonalizer); } + + std::array orchard_hash; + { orchard_hash = blake2b256({}, kOrchardHashPersonalizer); } + + std::array digest_hash; + { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushBytes(header_hash); + stream.PushBytes(transaprent_hash); + stream.PushBytes(sapling_hash); + stream.PushBytes(orchard_hash); + + digest_hash = blake2b256(data, GetTxHashPersonalizer()); + } + + std::reverse(digest_hash.begin(), digest_hash.end()); + + return digest_hash; +} + +// static +// https://zips.z.cash/zip-0244#signature-digest +std::array ZCashSerializer::CalculateSignatureDigest( + const ZCashTransaction& zcash_transaction, + const ZCashTransaction::TxInput& input) { + std::array header_hash = HashHeader(zcash_transaction); + + std::array transaprent_hash; + { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.Push8AsLE(zcash_transaction.sighash_type()); + stream.PushBytes(HashPrevouts(zcash_transaction)); + stream.PushBytes(HashAmounts(zcash_transaction)); + stream.PushBytes(HashScriptPubKeys(zcash_transaction)); + stream.PushBytes(HashSequences(zcash_transaction)); + stream.PushBytes(HashOutputs(zcash_transaction)); + stream.PushBytes(HashTxIn(input)); + + transaprent_hash = blake2b256(data, kTransparentHashPersonalizer); + } + + std::array sapling_hash; + { sapling_hash = blake2b256({}, kSaplingHashPersonalizer); } + + std::array orchard_hash; + { orchard_hash = blake2b256({}, kOrchardHashPersonalizer); } + + std::array digest_hash; + { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushBytes(header_hash); + stream.PushBytes(transaprent_hash); + stream.PushBytes(sapling_hash); + stream.PushBytes(orchard_hash); + + digest_hash = blake2b256(data, GetTxHashPersonalizer()); + } + + return digest_hash; +} + +// static +// https://zips.z.cash/zip-0225 +std::vector ZCashSerializer::SerializeRawTransaction( + const ZCashTransaction& zcash_transaction) { + std::vector data; + BtcLikeSerializerStream stream(&data); + + PushHeader(zcash_transaction, stream); + + // Tx In + { + // Inputs size + stream.PushVarInt(zcash_transaction.inputs().size()); + for (const auto& input : zcash_transaction.inputs()) { + // Outpoint + PushOutpoint(input.utxo_outpoint, stream); + stream.PushSizeAndBytes(input.script_sig); + // Sequence + stream.Push32AsLE(input.n_sequence); + } + } + + // Tx Out + + // Outputs size + stream.PushVarInt(zcash_transaction.outputs().size()); + for (const auto& output : zcash_transaction.outputs()) { + PushOutput(output, stream); + } + + // Sapling + stream.PushVarInt(0); + stream.PushVarInt(0); + + // Orchard + stream.PushVarInt(0); + + return data; +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_serializer.h b/components/brave_wallet/browser/zcash/zcash_serializer.h new file mode 100644 index 000000000000..a48205a9082b --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_serializer.h @@ -0,0 +1,44 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_SERIALIZER_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_SERIALIZER_H_ + +#include +#include + +#include "brave/components/brave_wallet/browser/zcash/zcash_transaction.h" + +namespace brave_wallet { + +// https://zips.z.cash/zip-0244 +class ZCashSerializer { + public: + static std::array CalculateTxIdDigest( + const ZCashTransaction& zcash_transaction); + static std::array CalculateSignatureDigest( + const ZCashTransaction& zcash_transaction, + const ZCashTransaction::TxInput& input); + static std::vector SerializeRawTransaction( + const ZCashTransaction& zcash_transaction); + + private: + FRIEND_TEST_ALL_PREFIXES(ZCashSerializerTest, HashHeader); + FRIEND_TEST_ALL_PREFIXES(ZCashSerializerTest, HashOutputs); + FRIEND_TEST_ALL_PREFIXES(ZCashSerializerTest, HashPrevouts); + FRIEND_TEST_ALL_PREFIXES(ZCashSerializerTest, HashSequences); + FRIEND_TEST_ALL_PREFIXES(ZCashSerializerTest, HashTxIn); + + static std::array HashHeader(const ZCashTransaction& tx); + static std::array HashPrevouts(const ZCashTransaction& tx); + static std::array HashSequences(const ZCashTransaction& tx); + static std::array HashOutputs(const ZCashTransaction& tx); + static std::array HashTxIn( + const ZCashTransaction::TxInput& tx_in); +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_SERIALIZER_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_serializer_unittest.cc b/components/brave_wallet/browser/zcash/zcash_serializer_unittest.cc new file mode 100644 index 000000000000..d4d101996429 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_serializer_unittest.cc @@ -0,0 +1,188 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/zcash/zcash_serializer.h" + +#include +#include +#include + +#include "brave/components/brave_wallet/browser/zcash/zcash_transaction.h" +#include "brave/components/brave_wallet/common/hex_utils.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace brave_wallet { + +TEST(ZCashSerializerTest, HashPrevouts) { + ZCashTransaction zcash_transaciton; + + { + ZCashTransaction::TxInput tx_input; + tx_input.utxo_outpoint.txid = {20, 107, 157, 73, 221, 140, 120, 53, + 244, 58, 55, 220, 160, 120, 126, 62, + 201, 246, 96, 82, 35, 213, 186, 122, + 224, 171, 144, 37, 183, 59, 192, 63}; + tx_input.utxo_outpoint.index = 3224808575; + zcash_transaciton.inputs().push_back(std::move(tx_input)); + } + + { + ZCashTransaction::TxInput tx_input; + tx_input.utxo_outpoint.txid = {193, 161, 45, 18, 123, 87, 200, 19, + 137, 118, 231, 145, 1, 59, 1, 95, + 6, 166, 36, 245, 33, 182, 238, 4, + 236, 152, 8, 147, 199, 229, 224, 26}; + tx_input.utxo_outpoint.index = 1493393971; + zcash_transaciton.inputs().push_back(std::move(tx_input)); + } + + { + ZCashTransaction::TxInput tx_input; + tx_input.utxo_outpoint.txid = {208, 145, 48, 246, 53, 17, 218, 84, + 131, 45, 233, 19, 107, 57, 244, 89, + 159, 90, 165, 223, 187, 69, 218, 96, + 205, 206, 171, 126, 239, 222, 137, 190}; + tx_input.utxo_outpoint.index = 3237475171; + zcash_transaciton.inputs().push_back(std::move(tx_input)); + } + + ASSERT_EQ( + "0x7db761d908021c98a19c43f75c6486275eaca3c11f9dc6cbaf66d3050c23b515", + ToHex(ZCashSerializer::HashPrevouts(zcash_transaciton))); +} + +TEST(ZCashSerializerTest, HashOutputs) { + ZCashTransaction zcash_transaciton; + + { + ZCashTransaction::TxOutput tx_output; + base::HexStringToBytes("630063ac", &tx_output.script_pubkey); + tx_output.amount = 1264123119664452; + zcash_transaciton.outputs().push_back(std::move(tx_output)); + } + + { + ZCashTransaction::TxOutput tx_output; + base::HexStringToBytes("636a5351520065ac65", &tx_output.script_pubkey); + tx_output.amount = 810835337737746; + zcash_transaciton.outputs().push_back(std::move(tx_output)); + } + + ASSERT_EQ( + "0x0dc9291fc891c10bdecedde449fa319cfa3f45cf7779423c2272c013d7fe0080", + ToHex(ZCashSerializer::HashOutputs(zcash_transaciton))); +} + +TEST(ZCashSerializerTest, HashSequences) { + ZCashTransaction zcash_transaciton; + + { + ZCashTransaction::TxInput tx_input; + tx_input.n_sequence = 1290119100; + zcash_transaciton.inputs().push_back(std::move(tx_input)); + } + + { + ZCashTransaction::TxInput tx_input; + tx_input.n_sequence = 3797894359; + zcash_transaciton.inputs().push_back(std::move(tx_input)); + } + + { + ZCashTransaction::TxInput tx_input; + tx_input.n_sequence = 4015866081; + zcash_transaciton.inputs().push_back(std::move(tx_input)); + } + + ASSERT_EQ( + "0x17cae6cde4962f6eb86b350eb5a80d5576a958b4bd3438689e94ee387eb80f8e", + ToHex(ZCashSerializer::HashSequences(zcash_transaciton))); +} + +TEST(ZCashSerializerTest, HashHeader) { + ZCashTransaction zcash_transaciton; + zcash_transaciton.set_expiry_height(10000); + zcash_transaciton.set_locktime(1); + EXPECT_EQ( + "0xc632e4b84e69afe329c646d3eaa71935a8922f8f2236ba3603c439bdb939db83", + ToHex(ZCashSerializer::HashHeader(zcash_transaciton))); +} + +TEST(ZCashSerializerTest, HashTxIn) { + { + ZCashTransaction::TxInput tx_input; + + tx_input.utxo_outpoint.txid = {20, 107, 157, 73, 221, 140, 120, 53, + 244, 58, 55, 220, 160, 120, 126, 62, + 201, 246, 96, 82, 35, 213, 186, 122, + 224, 171, 144, 37, 183, 59, 192, 63}; + tx_input.utxo_outpoint.index = 3224808575; + + tx_input.utxo_value = 1848924248978091; + tx_input.n_sequence = 1290119100; + base::HexStringToBytes("ac0000", &tx_input.script_pub_key); + + ASSERT_EQ( + "0xb39969f0fba708491e480d80d4d675a1f1552cc7d479d7942f75fa31ad9c6ad6", + ToHex(ZCashSerializer::HashTxIn(tx_input))); + } +} + +// https://zcashblockexplorer.com/transactions/360d056309669faf0d7937f41581418be5e46b04e2cea0a7b14261d7bff1d825/raw +TEST(ZCashSerializerTest, TxId_TransparentOnly) { + ZCashTransaction tx; + + tx.set_expiry_height(2283846); + tx.set_locktime(2283826); + + { + ZCashTransaction::TxInput tx_input; + + std::vector vec; + base::HexStringToBytes( + "be9ef0f2091d0ef49f7f32c57ec826877175e9a703bef5989261e42bdfd69171", + &vec); + std::reverse(vec.begin(), vec.end()); + std::copy_n(vec.begin(), 32, tx_input.utxo_outpoint.txid.begin()); + tx_input.utxo_outpoint.index = 1; + + tx_input.n_sequence = 4294967295; + tx_input.utxo_value = 751000; + tx_input.utxo_address = "t1cRrYHciuivZZ32jceb7btTpakYBaPW7yi"; + tx_input.script_pub_key = ZCashAddressToScriptPubkey( + "t1cRrYHciuivZZ32jceb7btTpakYBaPW7yi", false); + + tx.inputs().push_back(std::move(tx_input)); + } + + { + ZCashTransaction::TxOutput tx_output; + tx_output.address = "t1KrG29yWzoi7Bs2pvsgXozZYPvGG4D3sGi"; + tx_output.amount = 100000; + tx_output.script_pubkey = ZCashAddressToScriptPubkey( + "t1KrG29yWzoi7Bs2pvsgXozZYPvGG4D3sGi", false); + + tx.outputs().push_back(std::move(tx_output)); + } + + { + ZCashTransaction::TxOutput tx_output; + tx_output.address = "t1cRrYHciuivZZ32jceb7btTpakYBaPW7yi"; + tx_output.amount = 649000; + tx_output.script_pubkey = ZCashAddressToScriptPubkey( + "t1cRrYHciuivZZ32jceb7btTpakYBaPW7yi", false); + + tx.outputs().push_back(std::move(tx_output)); + } + + auto tx_id = ZCashSerializer::CalculateTxIdDigest(tx); + + ASSERT_EQ( + ToHex(tx_id), + "0x360d056309669faf0d7937f41581418be5e46b04e2cea0a7b14261d7bff1d825"); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_transaction.cc b/components/brave_wallet/browser/zcash/zcash_transaction.cc index b22f8aaca7a5..42bf42abbf36 100644 --- a/components/brave_wallet/browser/zcash/zcash_transaction.cc +++ b/components/brave_wallet/browser/zcash/zcash_transaction.cc @@ -5,16 +5,22 @@ #include "brave/components/brave_wallet/browser/zcash/zcash_transaction.h" +#include #include #include #include "base/ranges/algorithm.h" #include "base/strings/string_number_conversions.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_serializer.h" +#include "brave/components/brave_wallet/common/hex_utils.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" namespace brave_wallet { namespace { +constexpr uint8_t kZCashSigHashAll = 0x01; + bool ReadStringTo(const base::Value::Dict& dict, std::string_view key, std::string& to) { @@ -147,9 +153,9 @@ ZCashTransaction::TxInput& ZCashTransaction::TxInput::operator=( bool ZCashTransaction::TxInput::operator==( const ZCashTransaction::TxInput& other) const { return std::tie(this->utxo_address, this->utxo_outpoint, this->utxo_value, - this->script_sig, this->witness) == + this->script_sig, this->script_pub_key) == std::tie(other.utxo_address, other.utxo_outpoint, other.utxo_value, - other.script_sig, other.witness); + other.script_sig, other.script_pub_key); } bool ZCashTransaction::TxInput::operator!=( const ZCashTransaction::TxInput& other) const { @@ -160,10 +166,10 @@ ZCashTransaction::TxInput ZCashTransaction::TxInput::Clone() const { ZCashTransaction::TxInput result; result.utxo_address = utxo_address; + result.script_pub_key = script_pub_key; result.utxo_outpoint = utxo_outpoint; result.utxo_value = utxo_value; result.script_sig = script_sig; - result.witness = witness; return result; } @@ -174,9 +180,8 @@ base::Value::Dict ZCashTransaction::TxInput::ToValue() const { dict.Set("utxo_address", utxo_address); dict.Set("utxo_outpoint", utxo_outpoint.ToValue()); dict.Set("utxo_value", base::NumberToString(utxo_value)); - + dict.Set("script_pub_key", base::HexEncode(script_pub_key)); dict.Set("script_sig", base::HexEncode(script_sig)); - dict.Set("witness", base::HexEncode(witness)); return dict; } @@ -198,11 +203,11 @@ absl::optional ZCashTransaction::TxInput::FromValue( return absl::nullopt; } - if (!ReadHexByteArrayTo(value, "script_sig", result.script_sig)) { + if (!ReadHexByteArrayTo(value, "script_pub_key", result.script_pub_key)) { return absl::nullopt; } - if (!ReadHexByteArrayTo(value, "witness", result.witness)) { + if (!ReadHexByteArrayTo(value, "script_sig", result.script_sig)) { return absl::nullopt; } @@ -213,20 +218,24 @@ absl::optional ZCashTransaction::TxInput::FromValue( absl::optional ZCashTransaction::TxInput::FromRpcUtxo(const std::string& address, const zcash::ZCashUtxo& utxo) { + if (address != utxo.address()) { + return absl::nullopt; + } ZCashTransaction::TxInput result; - result.utxo_address = address; - // result.utxo_outpoint.txid = utxo.txid(); + result.utxo_address = utxo.address(); + result.script_pub_key.insert(result.script_pub_key.begin(), + utxo.script().begin(), utxo.script().end()); + if (utxo.txid().size() != 32) { + return absl::nullopt; + } + std::copy_n(utxo.txid().begin(), 32, result.utxo_outpoint.txid.begin()); result.utxo_outpoint.index = utxo.index(); result.utxo_value = utxo.valuezat(); return result; } -uint32_t ZCashTransaction::TxInput::n_sequence() const { - return 0xfffffffd; -} - bool ZCashTransaction::TxInput::IsSigned() const { - return !script_sig.empty() || !witness.empty(); + return !script_sig.empty(); } ZCashTransaction::TxOutput::TxOutput() = default; @@ -237,8 +246,8 @@ ZCashTransaction::TxOutput& ZCashTransaction::TxOutput::operator=( ZCashTransaction::TxOutput&& other) = default; bool ZCashTransaction::TxOutput::operator==( const ZCashTransaction::TxOutput& other) const { - return std::tie(this->address, this->amount) == - std::tie(other.address, other.amount); + return std::tie(this->address, this->amount, this->script_pubkey) == + std::tie(other.address, other.amount, other.script_pubkey); } bool ZCashTransaction::TxOutput::operator!=( const ZCashTransaction::TxOutput& other) const { @@ -250,6 +259,7 @@ ZCashTransaction::TxOutput ZCashTransaction::TxOutput::Clone() const { result.address = address; result.amount = amount; + result.script_pubkey = script_pubkey; return result; } @@ -259,6 +269,7 @@ base::Value::Dict ZCashTransaction::TxOutput::ToValue() const { dict.Set("address", address); dict.Set("amount", base::NumberToString(amount)); + dict.Set("script_pub_key", base::HexEncode(script_pubkey)); return dict; } @@ -275,6 +286,10 @@ ZCashTransaction::TxOutput::FromValue(const base::Value::Dict& value) { return absl::nullopt; } + if (!ReadHexByteArrayTo(value, "script_pub_key", result.script_pubkey)) { + return absl::nullopt; + } + return result; } @@ -389,8 +404,7 @@ uint64_t ZCashTransaction::TotalInputsAmount() const { uint8_t ZCashTransaction::sighash_type() const { // We always sign all inputs. - // return kZCashSigHashAll; - return 0; + return kZCashSigHashAll; } } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_transaction.h b/components/brave_wallet/browser/zcash/zcash_transaction.h index c015f167d758..c77ea86c8cb8 100644 --- a/components/brave_wallet/browser/zcash/zcash_transaction.h +++ b/components/brave_wallet/browser/zcash/zcash_transaction.h @@ -32,7 +32,7 @@ class ZCashTransaction { base::Value::Dict ToValue() const; static absl::optional FromValue(const base::Value::Dict& value); - SHA256HashArray txid; + std::array txid; uint32_t index = 0; }; @@ -56,10 +56,10 @@ class ZCashTransaction { std::string utxo_address; Outpoint utxo_outpoint; uint64_t utxo_value = 0; + uint32_t n_sequence = 0xffffffff; + std::vector script_pub_key; std::vector script_sig; // scriptSig aka unlock script. - std::vector witness; // serialized witness stack. - uint32_t n_sequence() const; bool IsSigned() const; }; @@ -79,6 +79,7 @@ class ZCashTransaction { static absl::optional FromValue(const base::Value::Dict& value); std::string address; + std::vector script_pubkey; uint64_t amount = 0; }; @@ -118,10 +119,16 @@ class ZCashTransaction { uint32_t locktime() const { return locktime_; } void set_locktime(uint32_t locktime) { locktime_ = locktime; } + uint32_t expiry_height() const { return expiry_height_; } + void set_expiry_height(uint32_t expiry_height) { + expiry_height_ = expiry_height; + } + private: std::vector inputs_; std::vector outputs_; uint32_t locktime_ = 0; + uint32_t expiry_height_ = 0; std::string to_; uint64_t amount_ = 0; uint64_t fee_ = 0; diff --git a/components/brave_wallet/browser/zcash/zcash_tx_manager.h b/components/brave_wallet/browser/zcash/zcash_tx_manager.h index d406066f6ffc..c8de61b438bd 100644 --- a/components/brave_wallet/browser/zcash/zcash_tx_manager.h +++ b/components/brave_wallet/browser/zcash/zcash_tx_manager.h @@ -91,7 +91,7 @@ class ZCashTxManager : public TxManager, public ZCashBlockTracker::Observer { base::expected confirm_status); raw_ptr zcash_wallet_service_ = nullptr; - raw_ptr zcash_rpc_ = nullptr; + raw_ptr zcash_rpc_ = nullptr; base::ScopedObservation block_tracker_observation_{this}; base::WeakPtrFactory weak_factory_{this}; diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service.cc b/components/brave_wallet/browser/zcash/zcash_wallet_service.cc index f27168f8ebab..a49b841b301f 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service.cc +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service.cc @@ -8,11 +8,32 @@ #include #include +#include "base/containers/span.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_serializer.h" +#include "brave/components/brave_wallet/common/btc_like_serializer_stream.h" #include "brave/components/brave_wallet/common/common_utils.h" -#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "brave/components/brave_wallet/common/hex_utils.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" namespace brave_wallet { +namespace { +const uint32_t kDefaultBlockHeightDelta = 20; + +bool OutputAddressSupported(const std::string& address, bool is_testnet) { + auto decoded_address = DecodeZCashAddress(address); + if (!decoded_address) { + return false; + } + if (decoded_address->testnet != is_testnet) { + return false; + } + + return true; +} + +} // namespace + class GetTransparentUtxosContext : public base::RefCountedThreadSafe { public: @@ -193,6 +214,7 @@ bool CreateTransparentTransactionTask::PickInputs() { for (auto& input : all_inputs) { transaction_.inputs().push_back(std::move(input)); + if (transaction_.TotalInputsAmount() >= transaction_.amount() + transaction_.fee()) { done = true; @@ -210,10 +232,13 @@ bool CreateTransparentTransactionTask::PickInputs() { bool CreateTransparentTransactionTask::PrepareOutputs() { auto& target_output = transaction_.outputs().emplace_back(); target_output.address = transaction_.to(); + if (!OutputAddressSupported(target_output.address, IsTestnet())) { + return false; + } + target_output.amount = transaction_.amount(); - // if (!OutputAddressSupported(target_output.address, IsTestnet())) { - // return false; - // } + target_output.script_pubkey = + ZCashAddressToScriptPubkey(target_output.address, IsTestnet()); CHECK_GE(transaction_.TotalInputsAmount(), transaction_.amount() + transaction_.fee()); @@ -230,23 +255,15 @@ bool CreateTransparentTransactionTask::PrepareOutputs() { return false; } - // CHECK(OutputAddressSupported(*change_address, IsTestnet())); + CHECK(OutputAddressSupported(*change_address, IsTestnet())); auto& change_output = transaction_.outputs().emplace_back(); change_output.address = *change_address; change_output.amount = change_amount; + change_output.script_pubkey = + ZCashAddressToScriptPubkey(change_output.address, IsTestnet()); return true; } -ZCashWalletService::ZCashWalletService( - KeyringService* keyring_service, - PrefService* prefs, - scoped_refptr url_loader_factory) - : keyring_service_(keyring_service), - zcash_rpc_( - std::make_unique(prefs, url_loader_factory)) { - zcash_rpc_ = std::make_unique(prefs, url_loader_factory); -} - mojo::PendingRemote ZCashWalletService::MakeRemote() { mojo::PendingRemote remote; @@ -259,6 +276,20 @@ void ZCashWalletService::Bind( receivers_.Add(this, std::move(receiver)); } +ZCashWalletService::ZCashWalletService( + KeyringService* keyring_service, + PrefService* prefs, + scoped_refptr url_loader_factory) + : keyring_service_(keyring_service) { + zcash_rpc_ = std::make_unique(prefs, url_loader_factory); +} + +ZCashWalletService::ZCashWalletService(KeyringService* keyring_service, + std::unique_ptr zcash_rpc) + : keyring_service_(keyring_service) { + zcash_rpc_ = std::move(zcash_rpc); +} + ZCashWalletService::~ZCashWalletService() = default; absl::optional ZCashWalletService::GetUnusedChangeAddress( @@ -267,7 +298,7 @@ absl::optional ZCashWalletService::GetUnusedChangeAddress( // TODO(cypt4): this always returns first change address. Should return // first unused change address. return keyring_service_->GetZCashAddress( - account_id, mojom::ZCashKeyId(account_id.bitcoin_account_index, 1, 0)); + account_id, mojom::ZCashKeyId(account_id.bitcoin_account_index, 0, 0)); } void ZCashWalletService::GetBalance(const std::string& chain_id, @@ -321,11 +352,109 @@ void ZCashWalletService::GetUtxos(const std::string& chain_id, } } +bool ZCashWalletService::SignTransactionInternal( + ZCashTransaction& tx, + const mojom::AccountIdPtr& account_id) { + auto addresses = keyring_service_->GetZCashAddresses(*account_id); + if (!addresses || addresses->empty()) { + return false; + } + + std::map address_map; + for (auto& addr : *addresses) { + address_map.emplace(std::move(addr.first), std::move(addr.second)); + } + + for (size_t input_index = 0; input_index < tx.inputs().size(); + ++input_index) { + auto& input = tx.inputs()[input_index]; + + if (!address_map.contains(input.utxo_address)) { + return false; + } + auto& key_id = address_map.at(input.utxo_address); + + auto pubkey = keyring_service_->GetZCashPubKey(account_id, key_id); + if (!pubkey) { + return false; + } + + auto signature_digest = + ZCashSerializer::CalculateSignatureDigest(tx, input); + + auto signature = keyring_service_->SignMessageByZCashKeyring( + account_id, key_id, + base::make_span<32>(signature_digest.begin(), signature_digest.end())); + if (!signature) { + return false; + } + + BtcLikeSerializerStream stream(&input.script_sig); + stream.PushVarInt(signature.value().size() + 1); + stream.PushBytes(signature.value()); + stream.Push8AsLE(tx.sighash_type()); + stream.PushSizeAndBytes(pubkey.value()); + } + + return true; +} + void ZCashWalletService::SignAndPostTransaction( const std::string& chain_id, const mojom::AccountIdPtr& account_id, ZCashTransaction zcash_transaction, - SignAndPostTransactionCallback callback) {} + SignAndPostTransactionCallback callback) { + zcash_rpc_->GetLatestBlock( + chain_id, + base::BindOnce( + &ZCashWalletService::OnResolveLastBlockHeightForSendTransaction, + weak_ptr_factory_.GetWeakPtr(), chain_id, account_id.Clone(), + std::move(zcash_transaction), std::move(callback))); +} + +void ZCashWalletService::OnResolveLastBlockHeightForSendTransaction( + const std::string& chain_id, + const mojom::AccountIdPtr& account_id, + ZCashTransaction zcash_transaction, + SignAndPostTransactionCallback callback, + base::expected result) { + if (!result.has_value()) { + std::move(callback).Run("", std::move(zcash_transaction), + "Couldn't obtain last block"); + return; + } + + zcash_transaction.set_expiry_height(result->height() + + kDefaultBlockHeightDelta); + + if (!SignTransactionInternal(zcash_transaction, account_id)) { + std::move(callback).Run("", std::move(zcash_transaction), + "Couldn't sign transaciton"); + return; + } + + auto tx = ZCashSerializer::SerializeRawTransaction(zcash_transaction); + std::string as_string(reinterpret_cast(tx.data()), tx.size()); + zcash_rpc_->SendTransaction( + chain_id, as_string, + base::BindOnce(&ZCashWalletService::OnSendTransactionResult, + weak_ptr_factory_.GetWeakPtr(), std::move(callback), + std::move(zcash_transaction))); +} + +void ZCashWalletService::OnSendTransactionResult( + SignAndPostTransactionCallback callback, + ZCashTransaction tx, + base::expected result) { + if (result.has_value() && result->errorcode() == 0) { + auto tx_id = ZCashSerializer::CalculateTxIdDigest(tx); + auto tx_id_hex = ToHex(tx_id); + CHECK(tx_id_hex.starts_with("0x")); + std::move(callback).Run(tx_id_hex.substr(2), std::move(tx), ""); + } else { + std::move(callback).Run("", std::move(tx), "Failed to send transaction"); + } +} void ZCashWalletService::OnGetUtxos( scoped_refptr context, @@ -411,7 +540,7 @@ void ZCashWalletService::OnTransactionResolvedForStatus( return; } - std::move(callback).Run(result.value().height() + 1 != 0); + std::move(callback).Run(result.value().height() > 0); } void ZCashWalletService::CreateTransactionTaskDone( @@ -420,7 +549,7 @@ void ZCashWalletService::CreateTransactionTaskDone( [task](auto& item) { return item.get() == task; })); } -zcash_rpc::ZCashRpc* ZCashWalletService::zcash_rpc() { +ZCashRpc* ZCashWalletService::zcash_rpc() { return zcash_rpc_.get(); } diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service.h b/components/brave_wallet/browser/zcash/zcash_wallet_service.h index ba9c9de16a93..c36bc68ba246 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service.h +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service.h @@ -12,6 +12,7 @@ #include #include +#include "base/memory/scoped_refptr.h" #include "base/types/expected.h" #include "brave/components/brave_wallet/browser/keyring_service.h" #include "brave/components/brave_wallet/browser/keyring_service_observer_base.h" @@ -44,6 +45,10 @@ class ZCashWalletService : public KeyedService, KeyringService* keyring_service, PrefService* prefs, scoped_refptr url_loader_factory); + + ZCashWalletService(KeyringService* keyring_service, + std::unique_ptr zcash_rpc); + ~ZCashWalletService() override; mojo::PendingRemote MakeRemote(); @@ -79,12 +84,16 @@ class ZCashWalletService : public KeyedService, SignAndPostTransactionCallback callback); private: + friend class ZCashWalletServiceUnitTest; friend class CreateTransparentTransactionTask; friend class ZCashTxManager; absl::optional GetUnusedChangeAddress( const mojom::AccountId& account_id); + bool SignTransactionInternal(ZCashTransaction& tx, + const mojom::AccountIdPtr& account_id); + void OnGetUtxos( scoped_refptr context, const std::string& current_address, @@ -97,16 +106,28 @@ class ZCashWalletService : public KeyedService, GetTransactionStatusCallback callback, base::expected result); + void OnResolveLastBlockHeightForSendTransaction( + const std::string& chain_id, + const mojom::AccountIdPtr& account_id, + ZCashTransaction zcash_transaction, + SignAndPostTransactionCallback callback, + base::expected result); + + void OnSendTransactionResult( + SignAndPostTransactionCallback callback, + ZCashTransaction zcash_transaction, + base::expected result); + void CreateTransactionTaskDone(CreateTransparentTransactionTask* task); - zcash_rpc::ZCashRpc* zcash_rpc(); + ZCashRpc* zcash_rpc(); raw_ptr keyring_service_; - scoped_refptr url_loader_factory_; + std::unique_ptr zcash_rpc_; + std::list> create_transaction_tasks_; mojo::ReceiverSet receivers_; - std::unique_ptr zcash_rpc_; base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc b/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc new file mode 100644 index 000000000000..948731ee63e3 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc @@ -0,0 +1,213 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/zcash/zcash_wallet_service.h" + +#include +#include +#include + +#include "base/memory/scoped_refptr.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/test/mock_callback.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/task_environment.h" +#include "brave/components/brave_wallet/browser/brave_wallet_prefs.h" +#include "brave/components/brave_wallet/browser/test_utils.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_rpc.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "brave/components/brave_wallet/common/features.h" +#include "brave/components/brave_wallet/common/hex_utils.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Eq; +using testing::SaveArg; +using testing::Truly; +using testing::WithArg; + +namespace brave_wallet { + +namespace { + +std::array GetTxId(const std::string& hex_string) { + std::vector vec; + std::array sized_vec; + + base::HexStringToBytes(hex_string, &vec); + std::reverse(vec.begin(), vec.end()); + std::copy_n(vec.begin(), 32, sized_vec.begin()); + return sized_vec; +} + +class MockZCashRPC : public ZCashRpc { + public: + MockZCashRPC() : ZCashRpc(nullptr, nullptr) {} + ~MockZCashRPC() override {} + MOCK_METHOD3(GetUtxoList, + void(const std::string& chain_id, + const std::string& address, + GetUtxoListCallback callback)); + + MOCK_METHOD2(GetLatestBlock, + void(const std::string& chain_id, + GetLatestBlockCallback callback)); + + MOCK_METHOD3(GetTransaction, + void(const std::string& chain_id, + const std::string& tx_hash, + GetTransactionCallback callback)); + + MOCK_METHOD3(SendTransaction, + void(const std::string& chain_id, + const std::string& data, + SendTransactionCallback callback)); +}; + +} // namespace + +class ZCashWalletServiceUnitTest : public testing::Test { + public: + ZCashWalletServiceUnitTest() + : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} + + ~ZCashWalletServiceUnitTest() override = default; + + void SetUp() override { + brave_wallet::RegisterProfilePrefs(prefs_.registry()); + brave_wallet::RegisterLocalStatePrefs(local_state_.registry()); + keyring_service_ = + std::make_unique(nullptr, &prefs_, &local_state_); + auto zcash_rpc = std::make_unique>(); + zcash_wallet_service_ = std::make_unique( + keyring_service_.get(), std::move(zcash_rpc)); + keyring_service_->CreateWallet(kMnemonicDivideCruise, kTestWalletPassword, + base::DoNothing()); + zcash_account_ = + GetAccountUtils().EnsureAccount(mojom::KeyringId::kZCashMainnet, 0); + ASSERT_TRUE(zcash_account_); + } + + AccountUtils GetAccountUtils() { + return AccountUtils(keyring_service_.get()); + } + + mojom::AccountIdPtr account_id() const { + return zcash_account_->account_id.Clone(); + } + + testing::NiceMock* zcash_rpc() { + return static_cast*>( + zcash_wallet_service_->zcash_rpc()); + } + + protected: + base::test::ScopedFeatureList feature_list_{ + features::kBraveWalletZCashFeature}; + + mojom::AccountInfoPtr zcash_account_; + + sync_preferences::TestingPrefServiceSyncable prefs_; + sync_preferences::TestingPrefServiceSyncable local_state_; + + std::unique_ptr keyring_service_; + std::unique_ptr zcash_wallet_service_; + + base::test::TaskEnvironment task_environment_; +}; + +// https://zcashblockexplorer.com/transactions/3bc513afc84befb9774f667eb4e63266a7229ab1fdb43476dd7c3a33d16b3101/raw +TEST_F(ZCashWalletServiceUnitTest, SignAndPostTransaction) { + ZCashTransaction zcash_transaction; + zcash_transaction.set_locktime(2286687); + { + ZCashTransaction::TxInput input; + input.utxo_outpoint.txid = GetTxId( + "70f1aa91889eee3e5ba60231a2e625e60480dc2e43ddc9439dc4fe8f09a1a278"); + input.utxo_outpoint.index = 0; + + input.utxo_address = "t1c61yifRMgyhMsBYsFDBa5aEQkgU65CGau"; + input.utxo_value = 537000; + input.script_pub_key = + ZCashAddressToScriptPubkey(input.utxo_address, false); + + zcash_transaction.inputs().push_back(std::move(input)); + } + + { + ZCashTransaction::TxOutput output; + output.address = "t1KrG29yWzoi7Bs2pvsgXozZYPvGG4D3sGi"; + output.amount = 500000; + output.script_pubkey = ZCashAddressToScriptPubkey(output.address, false); + + zcash_transaction.outputs().push_back(std::move(output)); + } + + { + ZCashTransaction::TxOutput output; + output.address = "t1c61yifRMgyhMsBYsFDBa5aEQkgU65CGau"; + output.script_pubkey = ZCashAddressToScriptPubkey(output.address, false); + output.amount = 35000; + + zcash_transaction.outputs().push_back(std::move(output)); + } + + ON_CALL(*zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + zcash::BlockID response; + response.set_height(2286687); + std::move(callback).Run(std::move(response)); + })); + + base::MockCallback + sign_callback; + + ZCashTransaction signed_tx; + EXPECT_CALL( + sign_callback, + Run("3bc513afc84befb9774f667eb4e63266a7229ab1fdb43476dd7c3a33d16b3101", _, + "")) + .WillOnce(WithArg<1>( + [&](const ZCashTransaction& tx) { signed_tx = tx.Clone(); })); + + std::string captured_data; + EXPECT_CALL(*zcash_rpc(), SendTransaction(_, _, _)) + .WillOnce([&](const std::string& chain_id, const std::string& data, + ZCashRpc::SendTransactionCallback callback) { + captured_data = data; + zcash::SendResponse response; + response.set_errorcode(0); + std::move(callback).Run(std::move(response)); + }); + zcash_wallet_service_->SignAndPostTransaction( + mojom::kZCashMainnet, account_id(), std::move(zcash_transaction), + sign_callback.Get()); + base::RunLoop().RunUntilIdle(); + testing::Mock::VerifyAndClearExpectations(&sign_callback); + + EXPECT_EQ(ToHex(signed_tx.inputs()[0].script_sig), + "0x47304402202fc68ead746e8e93bb661ac79e71e1d3d84fd0f2aac76a8cb" + "4fa831a847787ff022028efe32152f282d7167c40d62b07aedad73a66c7" + "a3548413f289e2aef3da96b30121028754aaa5d9198198ecf5fd1849cbf" + "38a92ed707e2f181bd354c73a4a87854c67"); + + EXPECT_EQ(ToHex(captured_data), + "0x050000800a27a726b4d0d6c25fe4220073e422000178a2a1098ffec49d43" + "c9dd432edc8004e625e6a23102a65b3eee9e8891aaf170000000006a473044" + "02202fc68ead746e8e93bb661ac79e71e1d3d84fd0f2aac76a8cb4fa831a84" + "7787ff022028efe32152f282d7167c40d62b07aedad73a66c7a3548413f289" + "e2aef3da96b30121028754aaa5d9198198ecf5fd1849cbf38a92ed707e2f18" + "1bd354c73a4a87854c67ffffffff0220a10700000000001976a91415af26f9" + "b71022a01eade958cd05145f7ba5afe688acb8880000000000001976a914c7" + "cb443e547988b992adc1b47427ce6c40f3ca9e88ac000000"); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/common/BUILD.gn b/components/brave_wallet/common/BUILD.gn index fbcd3dbf11e1..90b5b3e1016c 100644 --- a/components/brave_wallet/common/BUILD.gn +++ b/components/brave_wallet/common/BUILD.gn @@ -36,6 +36,8 @@ static_library("common") { "brave_wallet_response_helpers.h", "brave_wallet_types.cc", "brave_wallet_types.h", + "btc_like_serializer_stream.cc", + "btc_like_serializer_stream.h", "common_utils.cc", "common_utils.h", "encoding_utils.cc", @@ -141,6 +143,7 @@ source_set("unit_tests") { sources = [ "bitcoin_utils_unittest.cc", "brave_wallet_types_unittest.cc", + "btc_like_serializer_stream_unittest.cc", "common_utils_unittest.cc", "encoding_utils_unittest.cc", "eth_abi_utils_unittest.cc", diff --git a/components/brave_wallet/common/DEPS b/components/brave_wallet/common/DEPS index b5cdbf8d0fc8..a578186a462c 100644 --- a/components/brave_wallet/common/DEPS +++ b/components/brave_wallet/common/DEPS @@ -17,4 +17,7 @@ specific_include_rules = { "brave_wallet_types\.h": [ "+boost/multiprecision/cpp_int.hpp", ], + "zcash_utils\.cc": [ + "+brave/third_party/bitcoin-core/src/src/base58.h", + ], } diff --git a/components/brave_wallet/common/btc_like_serializer_stream.cc b/components/brave_wallet/common/btc_like_serializer_stream.cc new file mode 100644 index 000000000000..e8d8d217df4b --- /dev/null +++ b/components/brave_wallet/common/btc_like_serializer_stream.cc @@ -0,0 +1,88 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/common/btc_like_serializer_stream.h" + +#include "base/sys_byteorder.h" + +namespace brave_wallet { + +void BtcLikeSerializerStream::Push8AsLE(uint8_t i) { + base::span data_to_insert(reinterpret_cast(&i), sizeof(i)); + PushBytes(data_to_insert); +} + +void BtcLikeSerializerStream::Push16AsLE(uint16_t i) { + i = base::ByteSwapToLE16(i); + base::span data_to_insert(reinterpret_cast(&i), sizeof(i)); + PushBytes(data_to_insert); +} + +void BtcLikeSerializerStream::Push32AsLE(uint32_t i) { + i = base::ByteSwapToLE32(i); + base::span data_to_insert(reinterpret_cast(&i), sizeof(i)); + PushBytes(data_to_insert); +} + +void BtcLikeSerializerStream::Push64AsLE(uint64_t i) { + i = base::ByteSwapToLE64(i); + base::span data_to_insert(reinterpret_cast(&i), sizeof(i)); + PushBytes(data_to_insert); +} + +// https://developer.bitcoin.org/reference/transactions.html#compactsize-unsigned-integers +void BtcLikeSerializerStream::PushVarInt(uint64_t i) { + if (i < 0xfd) { + Push8AsLE(i); + } else if (i <= 0xffff) { + Push8AsLE(0xfd); + Push16AsLE(i); + } else if (i <= 0xffffffff) { + Push8AsLE(0xfe); + Push32AsLE(i); + } else { + Push8AsLE(0xff); + Push64AsLE(i); + } +} + +void BtcLikeSerializerStream::PushSizeAndBytes( + base::span bytes) { + PushVarInt(bytes.size()); + PushBytes(bytes); +} + +void BtcLikeSerializerStream::PushBytes(base::span bytes) { + if (to()) { + to()->insert(to()->end(), bytes.begin(), bytes.end()); + } + serialized_bytes_ += bytes.size(); +} + +void BtcLikeSerializerStream::PushBytesReversed( + base::span bytes) { + if (to()) { + to()->insert(to()->end(), bytes.rbegin(), bytes.rend()); + } + serialized_bytes_ += bytes.size(); +} + +void BtcLikeSerializerStream::PushCompactSize(uint64_t i) { + if (i < 253) { + Push8AsLE(static_cast(i)); + } else if (i < 0xFFFF) { + Push8AsLE(253); + Push16AsLE(static_cast(i)); + + } else if (i < 0xFFFFFFFF) { + Push8AsLE(254); + Push32AsLE(static_cast(i)); + } else { + Push8AsLE(255); + Push32AsLE(static_cast(i)); + } +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/common/btc_like_serializer_stream.h b/components/brave_wallet/common/btc_like_serializer_stream.h new file mode 100644 index 000000000000..04a1afcb08d5 --- /dev/null +++ b/components/brave_wallet/common/btc_like_serializer_stream.h @@ -0,0 +1,40 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_COMMON_BTC_LIKE_SERIALIZER_STREAM_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_COMMON_BTC_LIKE_SERIALIZER_STREAM_H_ + +#include + +#include "base/containers/span.h" +#include "base/memory/raw_ref.h" + +namespace brave_wallet { + +class BtcLikeSerializerStream { + public: + explicit BtcLikeSerializerStream(std::vector* to) : to_(to) {} + + void Push8AsLE(uint8_t i); + void Push16AsLE(uint16_t i); + void Push32AsLE(uint32_t i); + void Push64AsLE(uint64_t i); + void PushVarInt(uint64_t i); + void PushSizeAndBytes(base::span bytes); + void PushBytes(base::span bytes); + void PushBytesReversed(base::span bytes); + void PushCompactSize(uint64_t i); + + uint32_t serialized_bytes() const { return serialized_bytes_; } + + private: + uint32_t serialized_bytes_ = 0; + std::vector* to() { return to_.get(); } + raw_ptr> to_; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_COMMON_BTC_LIKE_SERIALIZER_STREAM_H_ diff --git a/components/brave_wallet/common/btc_like_serializer_stream_unittest.cc b/components/brave_wallet/common/btc_like_serializer_stream_unittest.cc new file mode 100644 index 000000000000..b3e5cfbac8b9 --- /dev/null +++ b/components/brave_wallet/common/btc_like_serializer_stream_unittest.cc @@ -0,0 +1,164 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/common/btc_like_serializer_stream.h" + +#include +#include + +#include "base/ranges/algorithm.h" +#include "base/strings/string_number_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace brave_wallet { + +TEST(BtcLikeSerializerStreamTest, Push8AsLE) { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.Push8AsLE(0xab); + EXPECT_EQ(base::HexEncode(data), "AB"); + stream.Push8AsLE(0x12); + EXPECT_EQ(base::HexEncode(data), "AB12"); + + EXPECT_EQ(stream.serialized_bytes(), 2u); +} + +TEST(BtcLikeSerializerStreamTest, Push16AsLE) { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.Push16AsLE(0xab); + EXPECT_EQ(base::HexEncode(data), "AB00"); + stream.Push16AsLE(0x1234); + EXPECT_EQ(base::HexEncode(data), "AB003412"); + + EXPECT_EQ(stream.serialized_bytes(), 4u); +} + +TEST(BtcLikeSerializerStreamTest, Push32AsLE) { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.Push32AsLE(0xabcd); + EXPECT_EQ(base::HexEncode(data), "CDAB0000"); + stream.Push32AsLE(0x12345678); + EXPECT_EQ(base::HexEncode(data), "CDAB000078563412"); + + EXPECT_EQ(stream.serialized_bytes(), 8u); +} + +TEST(BtcLikeSerializerStreamTest, Push64AsLE) { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.Push64AsLE(0xabcd); + EXPECT_EQ(base::HexEncode(data), "CDAB000000000000"); + stream.Push64AsLE(0x1234567890abcdef); + EXPECT_EQ(base::HexEncode(data), "CDAB000000000000EFCDAB9078563412"); + + EXPECT_EQ(stream.serialized_bytes(), 16u); +} + +TEST(BtcLikeSerializerStreamTest, PushVarInt) { + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushVarInt(0xab); + EXPECT_EQ(base::HexEncode(data), "AB"); + stream.PushVarInt(0xabcd); + EXPECT_EQ(base::HexEncode(data), "ABFDCDAB"); + stream.PushVarInt(0xabcdef01); + EXPECT_EQ(base::HexEncode(data), "ABFDCDABFE01EFCDAB"); + stream.PushVarInt(0xabcdef0123456789); + EXPECT_EQ(base::HexEncode(data), "ABFDCDABFE01EFCDABFF8967452301EFCDAB"); + + EXPECT_EQ(stream.serialized_bytes(), 18u); +} + +TEST(BtcLikeSerializerStreamTest, PushSizeAndBytes) { + { + std::vector bytes(10, 0xab); + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushSizeAndBytes(bytes); + EXPECT_EQ(data.size(), 1u + 10u); + EXPECT_EQ(base::HexEncode(base::make_span(data).first(1)), "0A"); + EXPECT_TRUE(base::ranges::all_of(base::make_span(data).last(10), + [](auto c) { return c == 0xab; })); + EXPECT_EQ(stream.serialized_bytes(), 11u); + } + + { + std::vector bytes(300, 0xcd); + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushSizeAndBytes(bytes); + EXPECT_EQ(data.size(), 3u + 300u); + EXPECT_EQ(base::HexEncode(base::make_span(data).first(3)), "FD2C01"); + EXPECT_TRUE(base::ranges::all_of(base::make_span(data).last(300), + [](auto c) { return c == 0xcd; })); + EXPECT_EQ(stream.serialized_bytes(), 303u); + } + + { + std::vector bytes(0x10000, 0xef); + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushSizeAndBytes(bytes); + EXPECT_EQ(data.size(), 5u + 0x10000); + EXPECT_EQ(base::HexEncode(base::make_span(data).first(5)), "FE00000100"); + EXPECT_TRUE(base::ranges::all_of(base::make_span(data).last(0x10000), + [](auto c) { return c == 0xef; })); + EXPECT_EQ(stream.serialized_bytes(), 65541u); + } +} + +TEST(BtcLikeSerializerStreamTest, PushBytes) { + std::vector bytes({0x01, 0x02, 0xab, 0xcd, 0xef}); + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushBytes(bytes); + EXPECT_EQ(base::HexEncode(data), "0102ABCDEF"); + + EXPECT_EQ(stream.serialized_bytes(), 5u); +} + +TEST(BtcLikeSerializerStreamTest, PushBytesReversed) { + std::vector bytes({0x01, 0x02, 0xab, 0xcd, 0xef}); + std::vector data; + BtcLikeSerializerStream stream(&data); + stream.PushBytesReversed(bytes); + EXPECT_EQ(base::HexEncode(data), "EFCDAB0201"); + + EXPECT_EQ(stream.serialized_bytes(), 5u); +} + +TEST(BtcLikeSerializerStreamTest, NoVectorInCtor) { + std::vector bytes({0x01, 0x02, 0xab, 0xcd, 0xef}); + + BtcLikeSerializerStream stream(nullptr); + + stream.Push8AsLE(0xab); + EXPECT_EQ(stream.serialized_bytes(), 1u); + + stream.Push16AsLE(0xab); + EXPECT_EQ(stream.serialized_bytes(), 3u); + + stream.Push32AsLE(0x12345678); + EXPECT_EQ(stream.serialized_bytes(), 7u); + + stream.Push64AsLE(0xabcd); + EXPECT_EQ(stream.serialized_bytes(), 15u); + + stream.PushBytes(bytes); + EXPECT_EQ(stream.serialized_bytes(), 20u); + + stream.PushBytesReversed(bytes); + EXPECT_EQ(stream.serialized_bytes(), 25u); + + stream.PushSizeAndBytes(bytes); + EXPECT_EQ(stream.serialized_bytes(), 31u); + + stream.PushVarInt(0xabcdef01); + EXPECT_EQ(stream.serialized_bytes(), 36u); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/common/zcash_utils.cc b/components/brave_wallet/common/zcash_utils.cc index b8dbc5e908fb..77b1eb6f6c29 100644 --- a/components/brave_wallet/common/zcash_utils.cc +++ b/components/brave_wallet/common/zcash_utils.cc @@ -5,11 +5,32 @@ #include "brave/components/brave_wallet/common/zcash_utils.h" +#include "brave/components/brave_wallet/common/btc_like_serializer_stream.h" #include "brave/components/brave_wallet/common/encoding_utils.h" #include "brave/components/brave_wallet/common/hash_utils.h" +#include "brave/third_party/bitcoin-core/src/src/base58.h" namespace brave_wallet { +namespace { +constexpr size_t kPubKeyHashSize = 20; +constexpr size_t kPrefixSize = 2; +} // namespace + +DecodedZCashAddress::DecodedZCashAddress() = default; +DecodedZCashAddress::~DecodedZCashAddress() = default; +DecodedZCashAddress::DecodedZCashAddress(const DecodedZCashAddress& other) = + default; +DecodedZCashAddress& DecodedZCashAddress::operator=( + const DecodedZCashAddress& other) = default; +DecodedZCashAddress::DecodedZCashAddress(DecodedZCashAddress&& other) = default; +DecodedZCashAddress& DecodedZCashAddress::operator=( + DecodedZCashAddress&& other) = default; + +bool IsValidZCashAddress(const std::string& address) { + return true; +} + std::string PubkeyToTransparentAddress(const std::vector& pubkey, bool testnet) { std::vector result = testnet ? std::vector({0x1d, 0x25}) @@ -20,4 +41,54 @@ std::string PubkeyToTransparentAddress(const std::vector& pubkey, return Base58EncodeWithCheck(result); } +absl::optional DecodeZCashAddress( + const std::string& address) { + std::vector decode_result; + if (!DecodeBase58Check(address, decode_result, + kPubKeyHashSize + kPrefixSize)) { + return absl::nullopt; + } + + bool is_testnet = decode_result[0] == 0x1d && decode_result[1] == 0x25; + bool is_mainnet = decode_result[0] == 0x1c && decode_result[1] == 0xb8; + + if (!is_testnet && !is_mainnet) { + return absl::nullopt; + } + + std::vector body(decode_result.begin() + kPrefixSize, + decode_result.end()); + + DecodedZCashAddress result; + result.pubkey_hash = body; + result.testnet = is_testnet; + + return result; +} + +std::vector ZCashAddressToScriptPubkey(const std::string& address, + bool testnet) { + auto decoded_address = DecodeZCashAddress(address); + if (!decoded_address) { + return {}; + } + + if (testnet != decoded_address->testnet) { + return {}; + } + + std::vector data; + BtcLikeSerializerStream stream(&data); + CHECK_EQ(decoded_address->pubkey_hash.size(), 20u); + + stream.Push8AsLE(0x76); // OP_DUP + stream.Push8AsLE(0xa9); // OP_HASH + stream.Push8AsLE(0x14); // hash size + stream.PushBytes(decoded_address->pubkey_hash); // hash + stream.Push8AsLE(0x88); // OP_EQUALVERIFY + stream.Push8AsLE(0xac); // OP_CHECKSIG + + return data; +} + } // namespace brave_wallet diff --git a/components/brave_wallet/common/zcash_utils.h b/components/brave_wallet/common/zcash_utils.h index b0daf0b1d3fa..43ecd4babd37 100644 --- a/components/brave_wallet/common/zcash_utils.h +++ b/components/brave_wallet/common/zcash_utils.h @@ -9,11 +9,33 @@ #include #include +#include "third_party/abseil-cpp/absl/types/optional.h" + namespace brave_wallet { +struct DecodedZCashAddress { + DecodedZCashAddress(); + ~DecodedZCashAddress(); + DecodedZCashAddress(const DecodedZCashAddress& other); + DecodedZCashAddress& operator=(const DecodedZCashAddress& other); + DecodedZCashAddress(DecodedZCashAddress&& other); + DecodedZCashAddress& operator=(DecodedZCashAddress&& other); + + std::vector pubkey_hash; + bool testnet = false; +}; + +bool IsValidZCashAddress(const std::string& address); + std::string PubkeyToTransparentAddress(const std::vector& pubkey, bool testnet); +absl::optional DecodeZCashAddress( + const std::string& address); + +std::vector ZCashAddressToScriptPubkey(const std::string& address, + bool testnet); + } // namespace brave_wallet #endif // BRAVE_COMPONENTS_BRAVE_WALLET_COMMON_ZCASH_UTILS_H_ diff --git a/components/brave_wallet_ui/utils/block-explorer-utils.ts b/components/brave_wallet_ui/utils/block-explorer-utils.ts index b68df6a65a60..49cc26987a0d 100644 --- a/components/brave_wallet_ui/utils/block-explorer-utils.ts +++ b/components/brave_wallet_ui/utils/block-explorer-utils.ts @@ -44,6 +44,9 @@ export const buildExplorerUrl = ( network.chainId === BraveWallet.SOLANA_TESTNET || network.chainId === BraveWallet.SOLANA_DEVNET + const isZecNet = network.chainId === BraveWallet.Z_CASH_MAINNET || + network.chainId === BraveWallet.Z_CASH_TESTNET + if (isFileCoinNet) { return `${explorerURL}?cid=${value}` } @@ -52,6 +55,10 @@ export const buildExplorerUrl = ( return `${explorerURL}/${value}` } + if (isZecNet) { + return `${explorerURL}/${value}` + } + if (isSolanaMainNet && type === 'token') { return `${explorerURL}/address/${value}` } diff --git a/third_party/argon2/BUILD.gn b/third_party/argon2/BUILD.gn index 5677e6366325..f07ba672b53e 100644 --- a/third_party/argon2/BUILD.gn +++ b/third_party/argon2/BUILD.gn @@ -19,6 +19,7 @@ static_library("argon2") { visibility = [ "//brave/browser/brave_wallet:external_wallets_importer", + "//brave/components/brave_wallet/browser", "//brave/components/brave_wallet/common", ]