From 72d10782b93d9d93d4dcfe8265f3df31e99ef07e Mon Sep 17 00:00:00 2001 From: oisupov Date: Thu, 1 Aug 2024 10:38:21 +0300 Subject: [PATCH] Shielded inputs support WIP --- build/commands/lib/util.js | 2 +- components/brave_wallet/browser/BUILD.gn | 10 + .../brave_wallet/browser/internal/BUILD.gn | 2 + .../browser/internal/orchard_block_scanner.cc | 64 +- .../browser/internal/orchard_block_scanner.h | 16 +- .../internal/orchard_shard_tree_manager.cc | 35 + .../internal/orchard_shard_tree_manager.h | 33 + .../brave_wallet/browser/zcash/rust/BUILD.gn | 20 + .../browser/zcash/rust/cxx/src/shard_store.h | 74 ++ .../brave_wallet/browser/zcash/rust/lib.rs | 782 ++++++++++++++- .../browser/zcash/rust/librustzcash/BUILD.gn | 6 +- .../zcash/rust/orchard_block_decoder.h | 7 +- .../zcash/rust/orchard_block_decoder_impl.cc | 83 +- .../zcash/rust/orchard_block_decoder_impl.h | 7 +- .../zcash/rust/orchard_decoded_blocks_bunde.h | 22 + .../rust/orchard_decoded_blocks_bunde_impl.cc | 35 + .../rust/orchard_decoded_blocks_bunde_impl.h | 30 + .../browser/zcash/rust/orchard_shard_tree.h | 24 + .../zcash/rust/orchard_shard_tree_impl.cc | 325 +++++++ .../zcash/rust/orchard_shard_tree_impl.h | 26 + .../zcash/zcash_blocks_batch_scan_task.cc | 200 ++++ .../zcash/zcash_blocks_batch_scan_task.h | 73 ++ .../browser/zcash/zcash_orchard_storage.cc | 915 +++++++++++++++++- .../browser/zcash/zcash_orchard_storage.h | 73 +- .../zcash/zcash_orchard_storage_unittest.cc | 520 +++++++++- .../browser/zcash/zcash_orchard_sync_state.cc | 307 ++++++ .../browser/zcash/zcash_orchard_sync_state.h | 62 ++ .../brave_wallet/browser/zcash/zcash_rpc.cc | 87 ++ .../brave_wallet/browser/zcash/zcash_rpc.h | 17 + .../browser/zcash/zcash_scan_blocks_task.cc | 130 +++ .../browser/zcash/zcash_scan_blocks_task.h | 59 ++ .../zcash/zcash_shield_sync_service.cc | 285 ++---- .../browser/zcash/zcash_shield_sync_service.h | 84 +- .../zcash/zcash_update_subtree_roots_task.cc | 86 ++ .../zcash/zcash_update_subtree_roots_task.h | 42 + .../zcash/zcash_verify_chain_state_task.cc | 172 ++++ .../zcash/zcash_verify_chain_state_task.h | 62 ++ .../brave_wallet/common/brave_wallet.mojom | 6 +- components/brave_wallet/common/zcash_utils.cc | 48 + components/brave_wallet/common/zcash_utils.h | 138 +++ .../page/screens/dev-zcash/dev-zcash.tsx | 4 +- .../public/mojom/zcash_decoder.mojom | 7 + .../public/proto/zcash_grpc_data.proto | 16 + .../brave_wallet/zcash/zcash_decoder.cc | 15 + .../brave_wallet/zcash/zcash_decoder.h | 2 + 45 files changed, 4664 insertions(+), 349 deletions(-) create mode 100644 components/brave_wallet/browser/internal/orchard_shard_tree_manager.cc create mode 100644 components/brave_wallet/browser/internal/orchard_shard_tree_manager.h create mode 100644 components/brave_wallet/browser/zcash/rust/cxx/src/shard_store.h create mode 100644 components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h create mode 100644 components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.cc create mode 100644 components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.h create mode 100644 components/brave_wallet/browser/zcash/rust/orchard_shard_tree.h create mode 100644 components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.cc create mode 100644 components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.h create mode 100644 components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.cc create mode 100644 components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.h create mode 100644 components/brave_wallet/browser/zcash/zcash_orchard_sync_state.cc create mode 100644 components/brave_wallet/browser/zcash/zcash_orchard_sync_state.h create mode 100644 components/brave_wallet/browser/zcash/zcash_scan_blocks_task.cc create mode 100644 components/brave_wallet/browser/zcash/zcash_scan_blocks_task.h create mode 100644 components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.cc create mode 100644 components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.h create mode 100644 components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.cc create mode 100644 components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.h diff --git a/build/commands/lib/util.js b/build/commands/lib/util.js index 73d39ffe53e0..e50dc1d4924e 100644 --- a/build/commands/lib/util.js +++ b/build/commands/lib/util.js @@ -626,7 +626,7 @@ const util = { wasInterrupted if (shouldRunGnGen) { - util.run('gn', ['gen', outputDir, ...extraGnGenOpts, ...internalOpts], options) + util.run('gn', ['gen', '--ide=qtcreator', outputDir, ...extraGnGenOpts, ...internalOpts], options) } }) }, diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index ada523a6a076..83ec3f511282 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -304,12 +304,22 @@ static_library("browser") { if (enable_orchard) { sources += [ + "zcash/zcash_blocks_batch_scan_task.cc", + "zcash/zcash_blocks_batch_scan_task.h", "zcash/zcash_create_shield_all_transaction_task.cc", "zcash/zcash_create_shield_all_transaction_task.h", "zcash/zcash_orchard_storage.cc", "zcash/zcash_orchard_storage.h", + "zcash/zcash_orchard_sync_state.cc", + "zcash/zcash_orchard_sync_state.h", + "zcash/zcash_scan_blocks_task.cc", + "zcash/zcash_scan_blocks_task.h", "zcash/zcash_shield_sync_service.cc", "zcash/zcash_shield_sync_service.h", + "zcash/zcash_update_subtree_roots_task.cc", + "zcash/zcash_update_subtree_roots_task.h", + "zcash/zcash_verify_chain_state_task.cc", + "zcash/zcash_verify_chain_state_task.h", ] deps += [ diff --git a/components/brave_wallet/browser/internal/BUILD.gn b/components/brave_wallet/browser/internal/BUILD.gn index c957e9fbd3b8..453ff3ec4748 100644 --- a/components/brave_wallet/browser/internal/BUILD.gn +++ b/components/brave_wallet/browser/internal/BUILD.gn @@ -45,6 +45,8 @@ if (enable_orchard) { "orchard_block_scanner.h", "orchard_bundle_manager.cc", "orchard_bundle_manager.h", + "orchard_shard_tree_manager.cc", + "orchard_shard_tree_manager.h", ] deps = [ "//brave/components/brave_wallet/browser/zcash/rust" ] } diff --git a/components/brave_wallet/browser/internal/orchard_block_scanner.cc b/components/brave_wallet/browser/internal/orchard_block_scanner.cc index 186d740b0c34..84b9693687c3 100644 --- a/components/brave_wallet/browser/internal/orchard_block_scanner.cc +++ b/components/brave_wallet/browser/internal/orchard_block_scanner.cc @@ -10,14 +10,19 @@ namespace brave_wallet { OrchardBlockScanner::Result::Result() = default; OrchardBlockScanner::Result::Result(std::vector discovered_notes, - std::vector spent_notes) + std::vector spent_notes, + std::unique_ptr scanned_blocks) : discovered_notes(std::move(discovered_notes)), - spent_notes(std::move(spent_notes)) {} + found_nullifiers(std::move(spent_notes)), + scanned_blocks(std::move(scanned_blocks)) {} -OrchardBlockScanner::Result::Result(const Result&) = default; +OrchardBlockScanner::Result::Result(OrchardBlockScanner::Result&&) = default; +OrchardBlockScanner::Result& OrchardBlockScanner::Result::operator=(OrchardBlockScanner::Result&&) = default; -OrchardBlockScanner::Result& OrchardBlockScanner::Result::operator=( - const Result&) = default; +// OrchardBlockScanner::Result::Result(const Result&) = default; + +// OrchardBlockScanner::Result& OrchardBlockScanner::Result::operator=( +// const Result&) = default; OrchardBlockScanner::Result::~Result() = default; @@ -29,24 +34,22 @@ OrchardBlockScanner::~OrchardBlockScanner() = default; base::expected OrchardBlockScanner::ScanBlocks( - std::vector known_notes, + FrontierChainState chain_state, std::vector blocks) { + std::unique_ptr result = + decoder_->ScanBlocks(chain_state, blocks); + if (!result) { + return base::unexpected(ErrorCode::kInputError); + } + + if (!result->GetDiscoveredNotes()) { + return base::unexpected(ErrorCode::kInputError); + } + std::vector found_nullifiers; - std::vector found_notes; + std::vector found_notes = result->GetDiscoveredNotes().value(); for (const auto& block : blocks) { - // Scan block using the decoder initialized with the provided fvk - // to find new spendable notes. - auto scan_result = decoder_->ScanBlock(block); - if (!scan_result) { - return base::unexpected(ErrorCode::kDecoderError); - } - found_notes.insert(found_notes.end(), scan_result->begin(), - scan_result->end()); - // Place found notes to the known notes list so we can also check for - // nullifiers - known_notes.insert(known_notes.end(), scan_result->begin(), - scan_result->end()); for (const auto& tx : block->vtx) { // We only scan orchard actions here for (const auto& orchard_action : tx->orchard_actions) { @@ -54,25 +57,20 @@ OrchardBlockScanner::ScanBlocks( return base::unexpected(ErrorCode::kInputError); } - std::array action_nullifier; - base::ranges::copy(orchard_action->nullifier, action_nullifier.begin()); - + OrchardNullifier nullifier; // Nullifier is a public information about some note being spent. - // Here we are trying to find a known spendable notes which nullifier + // -- Here we are trying to find a known spendable notes which nullifier // matches nullifier from the processed transaction. - if (std::find_if(known_notes.begin(), known_notes.end(), - [&action_nullifier](const auto& v) { - return v.nullifier == action_nullifier; - }) != known_notes.end()) { - OrchardNullifier nullifier; - nullifier.block_id = block->height; - nullifier.nullifier = action_nullifier; - found_nullifiers.push_back(std::move(nullifier)); - } + base::ranges::copy(orchard_action->nullifier, + nullifier.nullifier.begin()); + nullifier.block_id = block->height; + found_nullifiers.push_back(std::move(nullifier)); } } } - return Result({std::move(found_notes), std::move(found_nullifiers)}); + + return Result({std::move(found_notes), std::move(found_nullifiers), + std::move(result)}); } } // namespace brave_wallet diff --git a/components/brave_wallet/browser/internal/orchard_block_scanner.h b/components/brave_wallet/browser/internal/orchard_block_scanner.h index e5380b02edd4..ce1d27f177ca 100644 --- a/components/brave_wallet/browser/internal/orchard_block_scanner.h +++ b/components/brave_wallet/browser/internal/orchard_block_scanner.h @@ -12,7 +12,9 @@ #include #include "base/types/expected.h" +#include "brave/components/brave_wallet/browser/internal/orchard_block_scanner.h" #include "brave/components/brave_wallet/browser/zcash/rust/orchard_block_decoder.h" +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h" #include "brave/components/brave_wallet/common/zcash_utils.h" namespace brave_wallet { @@ -26,15 +28,19 @@ class OrchardBlockScanner { struct Result { Result(); Result(std::vector discovered_notes, - std::vector spent_notes); - Result(const Result&); - Result& operator=(const Result&); + std::vector spent_notes, + std::unique_ptr scanned_blocks); + Result(const Result&) = delete; + Result& operator=(const Result&) = delete; + Result(Result&&); + Result& operator=(Result&&); ~Result(); // New notes have been discovered std::vector discovered_notes; // Nullifiers for the previously discovered notes - std::vector spent_notes; + std::vector found_nullifiers; + std::unique_ptr scanned_blocks; }; explicit OrchardBlockScanner(const OrchardFullViewKey& full_view_key); @@ -43,7 +49,7 @@ class OrchardBlockScanner { // Scans blocks to find incoming notes related to fvk // Also checks whether existing notes were spent. virtual base::expected ScanBlocks( - std::vector known_notes, + FrontierChainState chain_state, std::vector blocks); private: diff --git a/components/brave_wallet/browser/internal/orchard_shard_tree_manager.cc b/components/brave_wallet/browser/internal/orchard_shard_tree_manager.cc new file mode 100644 index 000000000000..799651314bce --- /dev/null +++ b/components/brave_wallet/browser/internal/orchard_shard_tree_manager.cc @@ -0,0 +1,35 @@ +/* Copyright (c) 2024 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/internal/orchard_shard_tree_manager.h" + +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet { + +// static +std::unique_ptr OrchardShardTreeManager::Create( + std::unique_ptr delegate) { + auto shard_tree = orchard::OrchardShardTree::Create(std::move(delegate)); + if (!shard_tree) { + return nullptr; + } + return std::make_unique(std::move(shard_tree)); +} + +OrchardShardTreeManager::OrchardShardTreeManager( + std::unique_ptr<::brave_wallet::orchard::OrchardShardTree> shard_tree) { + orchard_shard_tree_ = std::move(shard_tree); +} + +OrchardShardTreeManager::~OrchardShardTreeManager() {} + +bool OrchardShardTreeManager::InsertCommitments( + OrchardBlockScanner::Result result) { + return orchard_shard_tree_->ApplyScanResults( + std::move(result.scanned_blocks)); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/internal/orchard_shard_tree_manager.h b/components/brave_wallet/browser/internal/orchard_shard_tree_manager.h new file mode 100644 index 000000000000..77305d5ac7f9 --- /dev/null +++ b/components/brave_wallet/browser/internal/orchard_shard_tree_manager.h @@ -0,0 +1,33 @@ +/* Copyright (c) 2024 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_INTERNAL_ORCHARD_SHARD_TREE_MANAGER_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_INTERNAL_ORCHARD_SHARD_TREE_MANAGER_H_ + +#include "base/types/expected.h" +#include "brave/components/brave_wallet/browser/internal/orchard_block_scanner.h" +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_shard_tree.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet { + +class OrchardShardTreeManager { + public: + OrchardShardTreeManager( + std::unique_ptr<::brave_wallet::orchard::OrchardShardTree> shard_tree); + ~OrchardShardTreeManager(); + bool InsertCommitments(OrchardBlockScanner::Result commitments); + + static std::unique_ptr Create( + std::unique_ptr delegate); + + private: + std::unique_ptr<::brave_wallet::orchard::OrchardShardTree> + orchard_shard_tree_; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_INTERNAL_ORCHARD_SHARD_TREE_MANAGER_H_ diff --git a/components/brave_wallet/browser/zcash/rust/BUILD.gn b/components/brave_wallet/browser/zcash/rust/BUILD.gn index 5b4044bdb3e1..893de3dbfb71 100644 --- a/components/brave_wallet/browser/zcash/rust/BUILD.gn +++ b/components/brave_wallet/browser/zcash/rust/BUILD.gn @@ -24,7 +24,9 @@ source_set("orchard_headers") { "authorized_orchard_bundle.h", "extended_spending_key.h", "orchard_block_decoder.h", + "orchard_shard_tree.h", "unauthorized_orchard_bundle.h", + "orchard_decoded_blocks_bunde.h", ] public_deps = [ @@ -44,6 +46,10 @@ source_set("orchard_impl") { "extended_spending_key_impl.h", "orchard_block_decoder_impl.cc", "orchard_block_decoder_impl.h", + "orchard_decoded_blocks_bunde_impl.cc", + "orchard_decoded_blocks_bunde_impl.h", + "orchard_shard_tree_impl.cc", + "orchard_shard_tree_impl.h", "unauthorized_orchard_bundle_impl.cc", "unauthorized_orchard_bundle_impl.h", ] @@ -57,6 +63,16 @@ source_set("orchard_impl") { ] } +source_set("shard_store") { + visibility = [ ":*" ] + sources = [ "cxx/src/shard_store.h" ] + + public_deps = [ + "//base", + "//build/rust:cxx_cppdeps", + ] +} + rust_static_library("rust_lib") { visibility = [ ":orchard_impl" ] @@ -69,6 +85,7 @@ rust_static_library("rust_lib") { deps = [ "librustzcash:zcash_client_backend", "librustzcash:zcash_primitives", + "librustzcash:zcash_protocol", "//brave/components/brave_wallet/rust:rust_lib", "//brave/third_party/rust/byteorder/v1:lib", "//brave/third_party/rust/incrementalmerkletree/v0_5:lib", @@ -76,6 +93,9 @@ rust_static_library("rust_lib") { "//brave/third_party/rust/nonempty/v0_7:lib", "//brave/third_party/rust/orchard/v0_8:lib", "//brave/third_party/rust/rand/v0_8:lib", + "//brave/third_party/rust/shardtree/v0_3:lib", "//brave/third_party/rust/zcash_note_encryption/v0_4:lib", ] + + public_deps = [ ":shard_store" ] } diff --git a/components/brave_wallet/browser/zcash/rust/cxx/src/shard_store.h b/components/brave_wallet/browser/zcash/rust/cxx/src/shard_store.h new file mode 100644 index 000000000000..2515244da223 --- /dev/null +++ b/components/brave_wallet/browser/zcash/rust/cxx/src/shard_store.h @@ -0,0 +1,74 @@ +// Copyright (c) 2024 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 http://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_RUST_CXX_SRC_SHARD_STORE_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_RUST_CXX_SRC_SHARD_STORE_H_ + +#include "brave/components/brave_wallet/common/zcash_utils.h" +#include "third_party/rust/cxx/v1/cxx.h" + +namespace brave_wallet::orchard { + +enum class ShardStoreStatusCode : uint32_t; +struct FfiShardTree; +struct FfiShardAddress; +struct FfiCheckpoint; +struct FfiCap; + +using ShardStoreContext = ::brave_wallet::OrchardShardTreeDelegate; + +ShardStoreStatusCode orchard_last_shard(const ShardStoreContext& ctx, + FfiShardTree& into); +ShardStoreStatusCode orchard_put_shard(ShardStoreContext& ctx, + const FfiShardTree& tree); +ShardStoreStatusCode orchard_get_shard(const ShardStoreContext& ctx, + const FfiShardAddress& addr, + FfiShardTree& tree); +ShardStoreStatusCode orchard_get_shard_roots( + const ShardStoreContext& ctx, + ::rust::Vec& into); +ShardStoreStatusCode orchard_truncate(ShardStoreContext& ctx, + const FfiShardAddress& address); +ShardStoreStatusCode orchard_get_cap(const ShardStoreContext& ctx, + FfiCap& into); +ShardStoreStatusCode orchard_put_cap(ShardStoreContext& ctx, + const FfiCap& tree); +ShardStoreStatusCode orchard_min_checkpoint_id(const ShardStoreContext& ctx, + uint32_t& into); +ShardStoreStatusCode orchard_max_checkpoint_id(const ShardStoreContext& ctx, + uint32_t& into); +ShardStoreStatusCode orchard_add_checkpoint(ShardStoreContext& ctx, + uint32_t checkpoint_id, + const FfiCheckpoint& checkpoint); +ShardStoreStatusCode orchard_checkpoint_count(const ShardStoreContext& ctx, + size_t& into); +ShardStoreStatusCode orchard_get_checkpoint_at_depth( + const ShardStoreContext& ctx, + size_t depth, + uint32_t& into_checkpoint_id, + FfiCheckpoint& into_checpoint); +ShardStoreStatusCode orchard_get_checkpoint(const ShardStoreContext& ctx, + uint32_t checkpoint_id, + FfiCheckpoint& into); +ShardStoreStatusCode orchard_update_checkpoint(ShardStoreContext& ctx, + uint32_t checkpoint_id, + const FfiCheckpoint& checkpoint); +ShardStoreStatusCode orchard_remove_checkpoint(ShardStoreContext& ctx, + uint32_t checkpoint_id); +ShardStoreStatusCode orchard_truncate_checkpoint(ShardStoreContext& ctx, + uint32_t checkpoint_id); +ShardStoreStatusCode orchard_with_checkpoints( + const ShardStoreContext& ctx, + size_t limit, + rust::cxxbridge1::Fn + fn); +ShardStoreStatusCode orchard_get_checkpoints(const ShardStoreContext& ctx, + size_t limit, + ::rust::Vec& into); + +} // namespace brave_wallet::orchard + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_RUST_CXX_SRC_SHARD_STORE_H_ diff --git a/components/brave_wallet/browser/zcash/rust/lib.rs b/components/brave_wallet/browser/zcash/rust/lib.rs index f35291090c55..1690b113d1d1 100644 --- a/components/brave_wallet/browser/zcash/rust/lib.rs +++ b/components/brave_wallet/browser/zcash/rust/lib.rs @@ -3,7 +3,7 @@ // 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/. -use std::fmt; +use std::{fmt, vec}; use orchard::{ builder:: { @@ -32,23 +32,80 @@ use orchard::{ use zcash_note_encryption::EphemeralKeyBytes; use zcash_primitives::transaction::components::amount::Amount; +use zcash_protocol::consensus::BlockHeight; use ffi::OrchardOutput; +use ffi::ShardStoreContext as ShardStoreContext; +use incrementalmerkletree::frontier::{self, Frontier}; use rand::rngs::OsRng; use rand::{RngCore, Error as OtherError}; use rand::CryptoRng; -use brave_wallet::{ - impl_error -}; - +use brave_wallet::impl_error; +use std::sync::Arc; use zcash_note_encryption::{ batch, Domain, ShieldedOutput, COMPACT_NOTE_SIZE, }; use crate::ffi::OrchardCompactAction; +use shardtree::{ + error::ShardTreeError, + store::{Checkpoint, ShardStore, TreeState}, + LocatedPrunableTree, LocatedTree, PrunableTree, RetentionFlags, + ShardTree, +}; + +use zcash_client_backend::serialization::shardtree::{read_shard, write_shard}; +use cxx::UniquePtr; + +use std::rc::Rc; +use std::cell::RefCell; +use std::marker::PhantomData; +use std::error; +use incrementalmerkletree::{Address, Retention}; +use zcash_primitives::merkle_tree::HashSer; + +use crate::ffi::FfiShardTree; +use crate::ffi::FfiShardAddress; +use crate::ffi::FfiCheckpoint; +use crate::ffi::FfiCap; +use crate::ffi::OrchardFrontierChainState; + +use std::cmp::{Ord, Ordering}; +use std::convert::TryFrom; +use std::ops::{Add, Bound, RangeBounds, Sub}; + +use crate::ffi::orchard_get_checkpoint; +use crate::ffi::orchard_remove_checkpoint; +use crate::ffi::orchard_min_checkpoint_id; +use crate::ffi::orchard_max_checkpoint_id; +use crate::ffi::orchard_checkpoint_count; +use crate::ffi::orchard_get_checkpoint_at_depth; +use crate::ffi::orchard_truncate_checkpoint; +use crate::ffi::orchard_get_cap; +use crate::ffi::orchard_last_shard; +use crate::ffi::orchard_get_shard; +use crate::ffi::orchard_get_shard_roots; +use crate::ffi::orchard_truncate; +use crate::ffi::orchard_put_cap; +use crate::ffi::orchard_add_checkpoint; +use crate::ffi::orchard_put_shard; +use crate::ffi::orchard_with_checkpoints; +use crate::ffi::orchard_update_checkpoint; +use crate::ffi::orchard_get_checkpoints; +use shardtree::error::QueryError; + +use crate::ffi::ShardStoreStatusCode; +use incrementalmerkletree::Position; +use std::io::Cursor; +use std::collections::BTreeSet; +// pub use brave_wallet::orchard; + +use orchard::Anchor; +use zcash_primitives::merkle_tree::read_commitment_tree; + // The rest of the wallet code should be updated to use this version of unwrap // and then this code can be removed #[macro_export] @@ -90,6 +147,11 @@ macro_rules! impl_result { }; } +pub(crate) const PRUNING_DEPTH: u8 = 100; +pub(crate) const SHARD_HEIGHT: u8 = 16; +pub(crate) const TREE_HEIGHT: u8 = 32; +pub(crate) const CHUNK_SIZE: usize = 1024; + #[derive(Clone)] pub(crate) struct MockRng(u64); @@ -132,7 +194,7 @@ impl RngCore for MockRng { } } - +#[allow(unused)] #[allow(unsafe_op_in_unsafe_fn)] #[cxx::bridge(namespace = brave_wallet::orchard)] mod ffi { @@ -149,7 +211,54 @@ mod ffi { nullifier: [u8; 32], // kOrchardNullifierSize ephemeral_key: [u8; 32], // kOrchardEphemeralKeySize cmx: [u8; 32], // kOrchardCmxSize - enc_cipher_text : [u8; 52] // kOrchardCipherTextSize + enc_cipher_text : [u8; 52], // kOrchardCipherTextSize + block_id: u32, + is_block_last_action: bool + } + + #[derive(Clone)] + struct OrchardFrontierChainState { + frontier_tree_state: Vec, + frontier_block_height: u32, + frontier_orchard_commitment_tree_size: u32 + } + + #[repr(u32)] + enum ShardStoreStatusCode { + Ok = 0, + None = 1, + Error = 2 + } + + #[derive(Default)] + struct FfiShardAddress { + level: u8, + index: u32 + } + + #[derive(Default)] + struct FfiCap { + data: Vec, + } + + #[derive(Default)] + struct FfiShardTree { + address: FfiShardAddress, + hash: [u8; 32], + data: Vec, + contains_marked: bool + } + + #[derive(Default)] + struct FfiCheckpoint { + empty: bool, + position: u32, + mark_removed: Vec + } + + struct FfiCheckpointBundle { + checkpoint_id: u32, + checkpoint: FfiCheckpoint } extern "Rust" { @@ -159,12 +268,16 @@ mod ffi { type BatchOrchardDecodeBundle; + type OrchardShardTreeBundle; + type OrchardExtendedSpendingKeyResult; type OrchardUnauthorizedBundleResult; type OrchardAuthorizedBundleResult; type BatchOrchardDecodeBundleResult; + type OrchardShardTreeBundleResult; + // OsRng is used fn create_orchard_bundle( tree_state: &[u8], @@ -189,6 +302,7 @@ mod ffi { fn batch_decode( fvk_bytes: &[u8; 96], // Array size should match kOrchardFullViewKeySize + frontier_chain_state: OrchardFrontierChainState, actions: Vec ) -> Box; @@ -228,7 +342,12 @@ mod ffi { fn note_value(self :&BatchOrchardDecodeBundle, index: usize) -> u32; // Result array size should match kOrchardNullifierSize // fvk array size should match kOrchardFullViewKeySize - fn note_nullifier(self :&BatchOrchardDecodeBundle, fvk: &[u8; 96], index: usize) -> [u8; 32]; + fn note_nullifier(self :&BatchOrchardDecodeBundle, index: usize) -> [u8; 32]; + fn note_block_height(self :&BatchOrchardDecodeBundle, index: usize) -> u32; + + fn is_ok(self: &OrchardShardTreeBundleResult) -> bool; + fn error_message(self: &OrchardShardTreeBundleResult) -> String; + fn unwrap(self: &OrchardShardTreeBundleResult) -> Box; // Orchard digest is desribed here https://zips.z.cash/zip-0244#t-4-orchard-digest // Used in constructing signature digest and tx id @@ -240,7 +359,75 @@ mod ffi { // Orchard part of v5 transaction as described in // https://zips.z.cash/zip-0225 fn raw_tx(self: &OrchardAuthorizedBundle) -> Vec; + + fn create_shard_tree(ctx: UniquePtr) -> Box; + fn insert_commitments(self: &mut OrchardShardTreeBundle, scan_result: &mut BatchOrchardDecodeBundle) -> bool; } + + unsafe extern "C++" { + include!("brave/components/brave_wallet/browser/zcash/rust/cxx/src/shard_store.h"); + + type ShardStoreContext; + // type ShardStoreStatusCode; + + fn orchard_last_shard( + ctx: &ShardStoreContext, into: &mut FfiShardTree) -> ShardStoreStatusCode; + fn orchard_get_shard( + ctx: &ShardStoreContext, + addr: &FfiShardAddress, + tree: &mut FfiShardTree) -> ShardStoreStatusCode; + fn orchard_put_shard( + ctx: Pin<&mut ShardStoreContext>, + tree: &FfiShardTree) -> ShardStoreStatusCode; + fn orchard_get_shard_roots( + ctx: &ShardStoreContext, into: &mut Vec) -> ShardStoreStatusCode; + fn orchard_truncate( + ctx: Pin<&mut ShardStoreContext>, + address: &FfiShardAddress) -> ShardStoreStatusCode; + fn orchard_get_cap( + ctx: &ShardStoreContext, + into: &mut FfiCap) -> ShardStoreStatusCode; + fn orchard_put_cap( + ctx: Pin<&mut ShardStoreContext>, + tree: &FfiCap) -> ShardStoreStatusCode; + fn orchard_min_checkpoint_id( + ctx: &ShardStoreContext, into: &mut u32) -> ShardStoreStatusCode; + fn orchard_max_checkpoint_id( + ctx: &ShardStoreContext, into: &mut u32) -> ShardStoreStatusCode; + fn orchard_add_checkpoint( + ctx: Pin<&mut ShardStoreContext>, + checkpoint_id: u32, + checkpoint: &FfiCheckpoint) -> ShardStoreStatusCode; + fn orchard_update_checkpoint( + ctx: Pin<&mut ShardStoreContext>, + checkpoint_id: u32, + checkpoint: &FfiCheckpoint) -> ShardStoreStatusCode; + fn orchard_checkpoint_count(ctx: &ShardStoreContext, into: &mut usize) -> ShardStoreStatusCode; + fn orchard_get_checkpoint_at_depth( + ctx: &ShardStoreContext, + depth: usize, + into_checkpoint_id: &mut u32, + into_checkpoint: &mut FfiCheckpoint) -> ShardStoreStatusCode; + fn orchard_get_checkpoint( + ctx: &ShardStoreContext, + checkpoint_id: u32, + into: &mut FfiCheckpoint) -> ShardStoreStatusCode; + fn orchard_remove_checkpoint( + ctx: Pin<&mut ShardStoreContext>, + checkpoint_id: u32) -> ShardStoreStatusCode; + fn orchard_truncate_checkpoint( + ctx: Pin<&mut ShardStoreContext>, + checkpoint_id: u32) -> ShardStoreStatusCode; + fn orchard_with_checkpoints( + ctx: &ShardStoreContext, + limit: usize, + callback: fn(u32, &FfiCheckpoint) -> ShardStoreStatusCode) ->ShardStoreStatusCode; + fn orchard_get_checkpoints( + ctx: &ShardStoreContext, + limit: usize, + into: &mut Vec) -> ShardStoreStatusCode; + } + } #[derive(Debug)] @@ -251,6 +438,7 @@ pub enum Error { BuildError, FvkError, OrchardActionFormatError, + ShardTreeError, } impl_error!(Zip32Error, Zip32); @@ -264,11 +452,25 @@ impl fmt::Display for Error { Error::WrongOutputError => write!(f, "Error: Can't parse output"), Error::BuildError => write!(f, "Error, build error"), Error::OrchardActionFormatError => write!(f, "Error, orchard action format error"), - Error::FvkError => write!(f, "Error, fvk format error") + Error::FvkError => write!(f, "Error, fvk format error"), + Error::ShardTreeError => write!(f, "Failed to create shard tree") } } } +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + // match &self { + // Error::Serialization(e) => Some(e), + // Error::Query(e) => Some(e), + // Error::CheckpointConflict { .. } => None, + // Error::SubtreeDiscontinuity { .. } => None, + // } + } +} + + // Different random sources are used for testing and for release // Since Orchard uses randomness we need to mock it to get // deterministic resuluts in tests. @@ -295,11 +497,26 @@ pub struct OrchardAuthorizedBundleValue { #[derive(Clone)] pub struct DecryptedOrchardOutput { - note: ::Note + note: ::Note, + block_height: u32 } #[derive(Clone)] pub struct BatchOrchardDecodeBundleValue { - outputs: Vec + fvk: [u8; 96], + outputs: Vec, + commitments: Vec)>>, + frontier_chain_state: OrchardFrontierChainState +} + +#[allow(dead_code)] +pub struct OrchardShardTreeBundleValue { + tree: ShardTree +} + +impl Clone for OrchardShardTreeBundleValue { + fn clone(&self) -> Self { + OrchardShardTreeBundleValue { tree : ShardTree::new(self.tree.store().clone(), PRUNING_DEPTH.try_into().unwrap()) } + } } #[derive(Clone)] @@ -310,17 +527,22 @@ struct OrchardAuthorizedBundle(OrchardAuthorizedBundleValue); struct OrchardUnauthorizedBundle(OrchardUnauthorizedBundleValue); #[derive(Clone)] struct BatchOrchardDecodeBundle(BatchOrchardDecodeBundleValue); +#[allow(dead_code)] +#[derive(Clone)] +struct OrchardShardTreeBundle(OrchardShardTreeBundleValue); struct OrchardExtendedSpendingKeyResult(Result); struct OrchardAuthorizedBundleResult(Result); struct OrchardUnauthorizedBundleResult(Result); struct BatchOrchardDecodeBundleResult(Result); +struct OrchardShardTreeBundleResult(Result); impl_result!(OrchardExtendedSpendingKey, OrchardExtendedSpendingKeyResult, ExtendedSpendingKey); impl_result!(OrchardAuthorizedBundle, OrchardAuthorizedBundleResult, OrchardAuthorizedBundleValue); impl_result!(OrchardUnauthorizedBundle, OrchardUnauthorizedBundleResult, OrchardUnauthorizedBundleValue); impl_result!(BatchOrchardDecodeBundle, BatchOrchardDecodeBundleResult, BatchOrchardDecodeBundleValue); +impl_result!(OrchardShardTreeBundle, OrchardShardTreeBundleResult, OrchardShardTreeBundleValue); fn generate_orchard_extended_spending_key_from_seed( bytes: &[u8] @@ -377,8 +599,7 @@ fn create_orchard_builder_internal( outputs: Vec, random_source: OrchardRandomSource ) -> Box { - use orchard::Anchor; - use zcash_primitives::merkle_tree::read_commitment_tree; + // To construct transaction orchard tree state of some block should be provided // But in tests we can use empty anchor. @@ -497,8 +718,11 @@ impl ShieldedOutput for OrchardCompactAction { fn batch_decode( fvk_bytes: &[u8; 96], + frontier_chain_state: OrchardFrontierChainState, actions: Vec ) -> Box { + let block_height : u32 = 0; + let fvk = match OrchardFVK::from_bytes(fvk_bytes) { Some(fvk) => fvk, None => return Box::new(BatchOrchardDecodeBundleResult::from(Err(Error::FvkError))) @@ -541,17 +765,81 @@ fn batch_decode( Err(e) => return Box::new(BatchOrchardDecodeBundleResult::from(Err(e.into()))) }; - let decrypted_outputs = batch::try_compact_note_decryption(&ivks, &input_actions.as_slice()) - .into_iter() - .map(|res| { - res.map(|((note, _recipient), _ivk_idx)| DecryptedOrchardOutput { - note: note - }) - }) - .filter_map(|x| x) - .collect::>(); + let mut decrypted_len = 0; + let (decrypted_opts, _decrypted_len) = ( + batch::try_compact_note_decryption(&ivks, &input_actions) + .into_iter() + .map(|v| { + v.map(|((note, _), ivk_idx)| { + decrypted_len += 1; + (ivks[ivk_idx].clone(), note) + }) + }) + .collect::>(), + decrypted_len, + ); + + let mut found_notes: Vec = vec![]; + let mut note_commitments: Vec)>> = vec![]; + + for (_output_idx, ((_, output), decrypted_note)) in + input_actions.iter().zip(decrypted_opts).enumerate() { + + // let node = extract_note_commitment(output); + // If the commitment is the last in the block, ensure that is is retained as a checkpoint + let is_checkpoint = &output.is_block_last_action/* && last_commitments_in_block */; + let retention : Retention = match (decrypted_note.is_some(), is_checkpoint) { + (is_marked, true) => Retention::Checkpoint { + id: block_height.into(), + is_marked, + }, + (true, false) => Retention::Marked, + (false, false) => Retention::Ephemeral, + }; + note_commitments.push(Some((MerkleHashOrchard::from_bytes(&output.cmx).unwrap(), retention))); + + + if let Some((_key_id, note)) = decrypted_note { + // let key = keys + // .get(&key_id) + // .expect("Key is available for decrypted output"); + + // A note is marked as "change" if the account that received it + // also spent notes in the same transaction. This will catch, + // for instance: + // - Change created by spending fractions of notes. + // - Notes created by consolidation transactions. + // - Notes sent from one account to itself. + // let is_change = spent_from_accounts.contains(key.account_id()); + // let note_commitment_tree_position = Position::from(u64::from( + // commitment_tree_size + u32::try_from(output_idx).unwrap(), + // )); + // let nf = key.nf(¬e, note_commitment_tree_position); + + found_notes.push(DecryptedOrchardOutput{ + note: note, + block_height: output.block_id + }); + + // shielded_outputs.push(WalletOutput::from_parts( + // output_idx, + // output.ephemeral_key(), + // note, + // is_change, + // note_commitment_tree_position, + // nf, + // *key.account_id(), + // key.key_scope(), + // )); + } + } - Box::new(BatchOrchardDecodeBundleResult::from(Ok(BatchOrchardDecodeBundleValue { outputs: decrypted_outputs }))) + Box::new(BatchOrchardDecodeBundleResult::from(Ok(BatchOrchardDecodeBundleValue { + fvk: *fvk_bytes, + outputs: found_notes, + commitments: note_commitments, + frontier_chain_state: frontier_chain_state + }))) } impl BatchOrchardDecodeBundle { @@ -564,8 +852,452 @@ impl BatchOrchardDecodeBundle { "Outputs are always created from a u32, so conversion back will always succeed") } - fn note_nullifier(self :&BatchOrchardDecodeBundle, fvk: &[u8; 96], index: usize) -> [u8; 32] { - self.0.outputs[index].note.nullifier(&OrchardFVK::from_bytes(fvk).unwrap()).to_bytes() + fn note_nullifier(self :&BatchOrchardDecodeBundle, index: usize) -> [u8; 32] { + self.0.outputs[index].note.nullifier(&OrchardFVK::from_bytes(&self.0.fvk).unwrap()).to_bytes() + } + + fn note_block_height(self :&BatchOrchardDecodeBundle, index: usize) -> u32 { + self.0.outputs[index].block_height + } +} + + +impl OrchardShardTreeBundle { + fn insert_commitments(self: &mut OrchardShardTreeBundle, scan_result: &mut BatchOrchardDecodeBundle) -> bool { + let frontier_commitment_tree = read_commitment_tree::( + &scan_result.0.frontier_chain_state.frontier_tree_state[..]); + + if frontier_commitment_tree.is_err() { + return false; + } + + let frontier_result = self.0.tree.insert_frontier( + frontier_commitment_tree.unwrap().to_frontier(), + Retention::Checkpoint { + id: BlockHeight::from_u32(scan_result.0.frontier_chain_state.frontier_block_height), + is_marked: false, + }, + ); + + if frontier_result.is_err() { + return false; + } + + // TODO(cypt4) : Check proper borders + let start_positions: u64 = (scan_result.0.frontier_chain_state.frontier_orchard_commitment_tree_size + 1).into(); + let orchard_subtrees = scan_result.0.commitments + .rchunks_mut(CHUNK_SIZE) + .enumerate() + .filter_map(|(i, chunk)| { + let start = + start_positions + (i * CHUNK_SIZE) as u64; + let end = start + chunk.len() as u64; + + shardtree::LocatedTree::from_iter( + start.into()..end.into(), + SHARD_HEIGHT.into(), + chunk.iter_mut().map(|n| n.take().expect("always Some")), + ) + }) + .map(|res| (res.subtree, res.checkpoints)) + .collect::>(); + + for (tree, checkpoints) in orchard_subtrees { + let _ = self.0.tree.insert_tree(tree, checkpoints); + } + + true + } +} +#[allow(dead_code)] +#[derive(Clone)] +pub struct CxxShardStoreImpl { + native_context: Rc>>, + _hash_type: PhantomData, +} + +impl From<&FfiCheckpoint> for Checkpoint { + fn from(item: &FfiCheckpoint) -> Self { + let tree_state : TreeState = if item.empty { TreeState::Empty } else { TreeState::AtPosition((item.position as u64).into()) }; + let marks_removed : BTreeSet = item.mark_removed.iter().map(|x| Position::from(*x as u64)).collect(); + Checkpoint::from_parts(tree_state, marks_removed) + } +} + +impl TryFrom<&Checkpoint> for FfiCheckpoint { + type Error = Error; + fn try_from(item: &Checkpoint) -> Result { + let position: u32 = match item.tree_state() { + TreeState::Empty => 0, + TreeState::AtPosition(pos) => (u64::from(pos)).try_into().map_err(|_| Error::ShardTreeError)? + }; + let marks_removed : Result, Error> = item.marks_removed().into_iter().map( + |x| u32::try_from(u64::from(*x)).map_err(|_| Error::ShardTreeError)).collect(); + Ok(FfiCheckpoint { + empty: item.is_tree_empty(), + position: position, + mark_removed: marks_removed? + }) + } +} + +impl TryFrom<&Address> for FfiShardAddress { + type Error = Error; + + fn try_from(item: &Address) -> Result { + let index : u32 = item.index().try_into().map_err(|_| Error::ShardTreeError)?; + Ok(FfiShardAddress{ + level: item.level().into(), + index: index }) + } +} + +impl From<&FfiShardAddress> for Address { + fn from(item: &FfiShardAddress) -> Self { + Address::from_parts(item.level.into(), item.index.into()) + } +} + +impl TryFrom<&FfiShardTree> for LocatedPrunableTree { + type Error = Error; + + fn try_from(item: &FfiShardTree) -> Result { + let shard_tree = read_shard(&mut Cursor::new(&item.data)).map_err(|_| Error::ShardTreeError)?; + let located_tree: LocatedTree<_, (_, RetentionFlags)> = LocatedPrunableTree::from_parts(Address::from(&item.address), shard_tree); + if !item.hash.is_empty() { + let root_hash = H::read(Cursor::new(item.hash)).map_err(|_| Error::ShardTreeError)?; + Ok(located_tree.reannotate_root(Some(Arc::new(root_hash)))) + } else { + Ok(located_tree) + } + } +} + +impl TryFrom<&FfiCap> for PrunableTree { + type Error = Error; + + fn try_from(item: &FfiCap) -> Result { + read_shard(&mut Cursor::new(&item.data)).map_err(|_| Error::ShardTreeError) + } +} + +impl TryFrom<&PrunableTree> for FfiCap { + type Error = Error; + + fn try_from(item: &PrunableTree) -> Result { + let mut data = vec![]; + write_shard(&mut data, item).map_err(|_| Error::ShardTreeError)?; + Ok(FfiCap { + data: data + }) + } +} + +impl TryFrom<&LocatedPrunableTree> for FfiShardTree { + type Error = Error; + + fn try_from(item: &LocatedPrunableTree) -> Result { + let subtree_root_hash : Option> = item + .root() + .annotation() + .and_then(|ann| { + ann.as_ref().map(|rc| { + let mut root_hash = vec![]; + rc.write(&mut root_hash)?; + Ok(root_hash) + }) + }) + .transpose() + .map_err(|_err : std::io::Error| Error::ShardTreeError)?; + + + let mut result = FfiShardTree { + address: FfiShardAddress::try_from(&item.root_addr()).map_err(|_| Error::ShardTreeError)?, + hash: subtree_root_hash.unwrap_or_else(|| vec![0; 32]).try_into().map_err(|_| Error::ShardTreeError)?, + data: vec![], + contains_marked: false, + }; + + write_shard(&mut result.data, &item.root()).map_err(|_| Error::ShardTreeError)?; + Ok(result) } } +type OrchardCxxShardStoreImpl = CxxShardStoreImpl; + +impl ShardStore + for CxxShardStoreImpl +{ + type H = H; + type CheckpointId = BlockHeight; + type Error = Error; + + fn get_shard( + &self, + addr: Address, + ) -> Result>, Self::Error> { + let ctx = self.native_context.clone(); + let mut into = FfiShardTree::default(); + let result = orchard_get_shard(&*ctx.try_borrow().unwrap(), + &FfiShardAddress::try_from(&addr).map_err(|_| Error::ShardTreeError)?, + &mut into); + if result == ShardStoreStatusCode::Ok { + let tree = LocatedPrunableTree::::try_from(&into)?; + return Ok(Some(tree)); + } else if result == ShardStoreStatusCode::None { + return Ok(Option::None); + } else { + return Err(Error::ShardTreeError); + } + } + + fn last_shard(&self) -> Result>, Self::Error> { + let ctx = self.native_context.clone(); + let mut into = FfiShardTree::default(); + let result = orchard_last_shard(&*ctx.try_borrow().unwrap(), &mut into); + if result == ShardStoreStatusCode::Ok { + let tree = LocatedPrunableTree::::try_from(&into)?; + return Ok(Some(tree)); + } else if result == ShardStoreStatusCode::None { + return Ok(Option::None); + } else { + return Err(Error::ShardTreeError); + } + } + + fn put_shard(&mut self, subtree: LocatedPrunableTree) -> Result<(), Self::Error> { + let ctx = self.native_context.clone(); + let ffi_tree = FfiShardTree::try_from(&subtree).map_err(|_| Error::ShardTreeError)?; + let result = orchard_put_shard(ctx.try_borrow_mut().unwrap().pin_mut(), &ffi_tree); + if result == ShardStoreStatusCode::Ok { + return Ok(()); + } + return Err(Error::ShardTreeError); + } + + fn get_shard_roots(&self) -> Result, Self::Error> { + let ctx = self.native_context.clone(); + let mut input : Vec = vec![]; + let result = orchard_get_shard_roots(&*ctx.try_borrow().unwrap(), &mut input); + if result == ShardStoreStatusCode::Ok { + return Ok(input.into_iter().map(|res| { + Address::from_parts(res.level.into(), res.index.into()) + }).collect()) + } else if result == ShardStoreStatusCode::None { + return Ok(vec![]) + } else { + return Err(Error::ShardTreeError) + } + } + + fn truncate(&mut self, from: Address) -> Result<(), Self::Error> { + let ctx = self.native_context.clone(); + let result = orchard_truncate(ctx.try_borrow_mut().unwrap().pin_mut(), + &FfiShardAddress::try_from(&from).map_err(|_| Error::ShardTreeError)?); + if result == ShardStoreStatusCode::Ok || result == ShardStoreStatusCode::None { + return Ok(()); + } else { + return Err(Error::ShardTreeError) + } + } + + fn get_cap(&self) -> Result, Self::Error> { + let ctx = self.native_context.clone(); + let mut input = FfiCap::default(); + let result = orchard_get_cap(&*ctx.try_borrow().unwrap(), &mut input); + + if result == ShardStoreStatusCode::Ok { + let tree = PrunableTree::::try_from(&input)?; + return Ok(tree) + } else + if result == ShardStoreStatusCode::None { + return Ok(PrunableTree::empty()); + } else { + return Err(Error::ShardTreeError); + } + } + + fn put_cap(&mut self, cap: PrunableTree) -> Result<(), Self::Error> { + let ctx = self.native_context.clone(); + let mut ffi_cap = FfiCap::default(); + write_shard(&mut ffi_cap.data, &cap).map_err(|_| Error::ShardTreeError)?; + + let result = orchard_put_cap(ctx.try_borrow_mut().unwrap().pin_mut(), &ffi_cap); + if result == ShardStoreStatusCode::Ok { + return Ok(()); + } + return Err(Error::ShardTreeError); + } + + fn min_checkpoint_id(&self) -> Result, Self::Error> { + let ctx = self.native_context.clone(); + let mut input : u32 = 0; + let result = orchard_min_checkpoint_id(&*ctx.try_borrow().unwrap(), &mut input); + if result == ShardStoreStatusCode::Ok { + return Ok(Some(input.into())); + } else if result == ShardStoreStatusCode::None { + return Ok(Option::None); + } + return Err(Error::ShardTreeError); + } + + fn max_checkpoint_id(&self) -> Result, Self::Error> { + let ctx = self.native_context.clone(); + let mut input : u32 = 0; + let result = orchard_max_checkpoint_id(&*ctx.try_borrow().unwrap(), &mut input); + if result == ShardStoreStatusCode::Ok { + return Ok(Some(input.into())); + } else if result == ShardStoreStatusCode::None { + return Ok(Option::None); + } + return Err(Error::ShardTreeError); + } + + fn add_checkpoint( + &mut self, + checkpoint_id: Self::CheckpointId, + checkpoint: Checkpoint, + ) -> Result<(), Self::Error> { + let ctx = self.native_context.clone(); + let ffi_checkpoint_id : u32 = checkpoint_id.try_into().map_err(|_| Error::ShardTreeError)?; + let result = orchard_add_checkpoint(ctx.try_borrow_mut().unwrap().pin_mut(), ffi_checkpoint_id, &FfiCheckpoint::try_from(&checkpoint)?); + if result == ShardStoreStatusCode::Ok { + return Ok(()); + } + return Err(Error::ShardTreeError); + } + + fn checkpoint_count(&self) -> Result { + let ctx = self.native_context.clone(); + let mut input : usize = 0; + let result = orchard_checkpoint_count(&*ctx.try_borrow().unwrap(), &mut input); + if result == ShardStoreStatusCode::Ok { + return Ok(input.into()); + } else if result == ShardStoreStatusCode::None { + return Ok(0); + } + return Ok(0); + } + + fn get_checkpoint_at_depth( + &self, + checkpoint_depth: usize, + ) -> Result, Self::Error> { + let ctx = self.native_context.clone(); + let mut input_checkpoint_id : u32 = 0; + let mut input_checkpoint : FfiCheckpoint = FfiCheckpoint::default(); + + let result = orchard_get_checkpoint_at_depth(&*ctx.try_borrow().unwrap(), + checkpoint_depth, + &mut input_checkpoint_id, + &mut input_checkpoint); + + if result == ShardStoreStatusCode::Ok { + return Ok(Some((BlockHeight::from(input_checkpoint_id), Checkpoint::from(&input_checkpoint)))); + } else if result == ShardStoreStatusCode::None { + return Ok(Option::None); + } + return Ok(Option::None); + } + + fn get_checkpoint( + &self, + checkpoint_id: &Self::CheckpointId, + ) -> Result, Self::Error> { + let ctx = self.native_context.clone(); + let mut input_checkpoint : FfiCheckpoint = FfiCheckpoint::default(); + + let result = orchard_get_checkpoint(&*ctx.try_borrow().unwrap(), + (*checkpoint_id).into(), + &mut input_checkpoint); + + if result == ShardStoreStatusCode::Ok { + return Ok(Some(Checkpoint::from(&input_checkpoint))); + } else if result == ShardStoreStatusCode::None { + return Ok(Option::None); + } + return Ok(Option::None); + } + + fn with_checkpoints(&mut self, limit: usize, mut callback: F) -> Result<(), Self::Error> + where + F: FnMut(&Self::CheckpointId, &Checkpoint) -> Result<(), Self::Error>, + { + let ctx = self.native_context.clone(); + let mut into : Vec = vec![]; + let result = orchard_get_checkpoints(&*ctx.try_borrow().unwrap(), limit, &mut into); + if result == ShardStoreStatusCode::Ok { + for item in into { + let checkpoint = Checkpoint::from(&item); + callback(&BlockHeight::from(item.position), &checkpoint).map_err(|_| Error::ShardTreeError)?; + } + return Ok(()) + } else if result == ShardStoreStatusCode::None { + return Ok(()) + } + Err(Error::ShardTreeError) + } + + fn update_checkpoint_with( + &mut self, + checkpoint_id: &Self::CheckpointId, + update: F, + ) -> Result + where + F: Fn(&mut Checkpoint) -> Result<(), Self::Error>, + { + let ctx = self.native_context.clone(); + let mut input_checkpoint = FfiCheckpoint::default(); + let result_get_checkpoint = orchard_get_checkpoint(&*ctx.try_borrow().unwrap(), (*checkpoint_id).into(), &mut input_checkpoint); + if result_get_checkpoint == ShardStoreStatusCode::Ok { + return Ok(true); + } else if result_get_checkpoint == ShardStoreStatusCode::None { + return Ok(false); + } + + let mut checkpoint = Checkpoint::from(&input_checkpoint); + + update(&mut checkpoint).map_err(|_| Error::ShardTreeError)?; + let result_update_checkpoint = orchard_update_checkpoint(ctx.try_borrow_mut().unwrap().pin_mut(), (*checkpoint_id).into(), &FfiCheckpoint::try_from(&checkpoint)?); + if result_update_checkpoint == ShardStoreStatusCode::Ok { + return Ok(true); + } else if result_update_checkpoint == ShardStoreStatusCode::None { + return Ok(false); + } + return Err(Error::ShardTreeError); + } + + fn remove_checkpoint(&mut self, checkpoint_id: &Self::CheckpointId) -> Result<(), Self::Error> { + let ctx = self.native_context.clone(); + let result = orchard_remove_checkpoint(ctx.try_borrow_mut().unwrap().pin_mut(), (*checkpoint_id).into()); + if result == ShardStoreStatusCode::Ok { + return Ok(()); + } else if result == ShardStoreStatusCode::None { + return Ok(()); + } + return Err(Error::ShardTreeError); + } + + fn truncate_checkpoints( + &mut self, + checkpoint_id: &Self::CheckpointId, + ) -> Result<(), Self::Error> { + let ctx = self.native_context.clone(); + let result = orchard_truncate_checkpoint(ctx.try_borrow_mut().unwrap().pin_mut(), (*checkpoint_id).into()); + if result == ShardStoreStatusCode::Ok { + return Ok(()); + } else if result == ShardStoreStatusCode::None { + return Ok(()); + } + return Err(Error::ShardTreeError); + } +} + +fn create_shard_tree(context: UniquePtr) -> Box { + let shard_store = OrchardCxxShardStoreImpl { + native_context: Rc::new(RefCell::new(context)), + _hash_type: Default::default() + }; + let shardtree = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap()); + Box::new(OrchardShardTreeBundleResult::from(Ok(OrchardShardTreeBundleValue{tree: shardtree}))) +} \ No newline at end of file diff --git a/components/brave_wallet/browser/zcash/rust/librustzcash/BUILD.gn b/components/brave_wallet/browser/zcash/rust/librustzcash/BUILD.gn index dade16e02910..471cfe2490cc 100644 --- a/components/brave_wallet/browser/zcash/rust/librustzcash/BUILD.gn +++ b/components/brave_wallet/browser/zcash/rust/librustzcash/BUILD.gn @@ -6,7 +6,11 @@ import("//build/rust/rust_static_library.gni") rust_static_library("zcash_protocol") { - visibility = [ ":zcash_primitives" ] + visibility = [ + ":zcash_primitives", + "//brave/components/brave_wallet/browser/zcash/rust:rust_lib", + "//brave/components/brave_wallet/browser/zcash/rust:rust_lib_cxx_generated", + ] crate_name = "zcash_protocol" crate_root = "src/components/zcash_protocol/src/lib.rs" sources = [ diff --git a/components/brave_wallet/browser/zcash/rust/orchard_block_decoder.h b/components/brave_wallet/browser/zcash/rust/orchard_block_decoder.h index 6e4d3a382ebf..aba37c512e45 100644 --- a/components/brave_wallet/browser/zcash/rust/orchard_block_decoder.h +++ b/components/brave_wallet/browser/zcash/rust/orchard_block_decoder.h @@ -10,6 +10,7 @@ #include #include +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h" #include "brave/components/brave_wallet/common/zcash_utils.h" #include "brave/components/services/brave_wallet/public/mojom/zcash_decoder.mojom.h" @@ -19,8 +20,10 @@ class OrchardBlockDecoder { public: virtual ~OrchardBlockDecoder() = default; - virtual std::optional> ScanBlock( - const ::brave_wallet::zcash::mojom::CompactBlockPtr& block) = 0; + virtual std::unique_ptr ScanBlocks( + const FrontierChainState& frontier_chain_state, + const std::vector<::brave_wallet::zcash::mojom::CompactBlockPtr>& + blocks) = 0; static std::unique_ptr FromFullViewKey( const OrchardFullViewKey& fvk); diff --git a/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.cc b/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.cc index 69afd446414f..2975404532c8 100644 --- a/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.cc +++ b/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.cc @@ -11,6 +11,7 @@ #include "base/memory/ptr_util.h" #include "brave/components/brave_wallet/browser/zcash/rust/lib.rs.h" +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.h" #include "brave/components/brave_wallet/common/zcash_utils.h" namespace brave_wallet::orchard { @@ -20,51 +21,57 @@ OrchardBlockDecoderImpl::OrchardBlockDecoderImpl(const OrchardFullViewKey& fvk) OrchardBlockDecoderImpl::~OrchardBlockDecoderImpl() = default; -std::optional> -OrchardBlockDecoderImpl::ScanBlock( - const ::brave_wallet::zcash::mojom::CompactBlockPtr& block) { - std::vector result; - for (const auto& tx : block->vtx) { - ::rust::Vec orchard_actions; - for (const auto& orchard_action : tx->orchard_actions) { - orchard::OrchardCompactAction orchard_compact_action; +std::unique_ptr +OrchardBlockDecoderImpl::ScanBlocks( + const FrontierChainState& frontier_chain_state, + const std::vector<::brave_wallet::zcash::mojom::CompactBlockPtr>& blocks) { + ::rust::Vec orchard_actions; + for (const auto& block : blocks) { + bool block_has_orchard_action = false; + for (const auto& tx : block->vtx) { + for (const auto& orchard_action : tx->orchard_actions) { + block_has_orchard_action = true; + orchard::OrchardCompactAction orchard_compact_action; - if (orchard_action->nullifier.size() != kOrchardNullifierSize || - orchard_action->cmx.size() != kOrchardCmxSize || - orchard_action->ephemeral_key.size() != kOrchardEphemeralKeySize || - orchard_action->ciphertext.size() != kOrchardCipherTextSize) { - return std::nullopt; - } + if (orchard_action->nullifier.size() != kOrchardNullifierSize || + orchard_action->cmx.size() != kOrchardCmxSize || + orchard_action->ephemeral_key.size() != kOrchardEphemeralKeySize || + orchard_action->ciphertext.size() != kOrchardCipherTextSize) { + return nullptr; + } - base::ranges::copy(orchard_action->nullifier, - orchard_compact_action.nullifier.begin()); - base::ranges::copy(orchard_action->cmx, - orchard_compact_action.cmx.begin()); - base::ranges::copy(orchard_action->ephemeral_key, - orchard_compact_action.ephemeral_key.begin()); - base::ranges::copy(orchard_action->ciphertext, - orchard_compact_action.enc_cipher_text.begin()); + base::ranges::copy(orchard_action->nullifier, + orchard_compact_action.nullifier.begin()); + base::ranges::copy(orchard_action->cmx, + orchard_compact_action.cmx.begin()); + base::ranges::copy(orchard_action->ephemeral_key, + orchard_compact_action.ephemeral_key.begin()); + base::ranges::copy(orchard_action->ciphertext, + orchard_compact_action.enc_cipher_text.begin()); + // orchard_compact_action.is_block_last_action = index == ; - orchard_actions.emplace_back(std::move(orchard_compact_action)); + orchard_actions.emplace_back(std::move(orchard_compact_action)); + } + } + if (block_has_orchard_action) { + orchard_actions.back().is_block_last_action = true; } + } - ::rust::Box<::brave_wallet::orchard::BatchOrchardDecodeBundleResult> - decode_result = ::brave_wallet::orchard::batch_decode( - full_view_key_, std::move(orchard_actions)); + ::brave_wallet::orchard::OrchardFrontierChainState chain_state; + chain_state.frontier_block_height = frontier_chain_state.frontier_block_height; + chain_state.frontier_orchard_commitment_tree_size = frontier_chain_state.frontier_orchard_tree_size; + // base::ranges::copy(frontier_chain_state.frontier_tree_state) + ::rust::Box<::brave_wallet::orchard::BatchOrchardDecodeBundleResult> + decode_result = ::brave_wallet::orchard::batch_decode( + full_view_key_, std::move(chain_state), std::move(orchard_actions)); - if (decode_result->is_ok()) { - ::rust::Box<::brave_wallet::orchard::BatchOrchardDecodeBundle> - result_bundle = decode_result->unwrap(); - for (size_t i = 0; i < result_bundle->size(); i++) { - result.emplace_back(OrchardNote( - {block->height, result_bundle->note_nullifier(full_view_key_, i), - result_bundle->note_value(i)})); - } - } else { - return std::nullopt; - } + if (decode_result->is_ok()) { + return base::WrapUnique( + new OrchardDecodedBlocksBundleImpl(decode_result->unwrap())); + } else { + return nullptr; } - return result; } // static diff --git a/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.h b/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.h index f7cbb60a5d6a..bdd6a7daf924 100644 --- a/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.h +++ b/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.h @@ -9,6 +9,7 @@ #include #include "brave/components/brave_wallet/browser/zcash/rust/orchard_block_decoder.h" +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h" namespace brave_wallet::orchard { @@ -16,8 +17,10 @@ class OrchardBlockDecoderImpl : public OrchardBlockDecoder { public: ~OrchardBlockDecoderImpl() override; - std::optional> ScanBlock( - const ::brave_wallet::zcash::mojom::CompactBlockPtr& block) override; + std::unique_ptr ScanBlocks( + const FrontierChainState& frontier_chain_state, + const std::vector<::brave_wallet::zcash::mojom::CompactBlockPtr>& block) + override; private: friend class OrchardBlockDecoder; diff --git a/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h b/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h new file mode 100644 index 000000000000..4d568c485f19 --- /dev/null +++ b/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h @@ -0,0 +1,22 @@ +// Copyright (c) 2024 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_RUST_ORCHARD_DECODED_BLOCKS_BUNDLE_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_RUST_ORCHARD_DECODED_BLOCKS_BUNDLE_H_ + +#include "brave/components/brave_wallet/browser/zcash/rust/lib.rs.h" +#include "third_party/rust/cxx/v1/cxx.h" + +namespace brave_wallet::orchard { + +class OrchardDecodedBlocksBundle { + public: + virtual ~OrchardDecodedBlocksBundle() {} + virtual std::optional> GetDiscoveredNotes() = 0; +}; + +} + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_RUST_ORCHARD_DECODED_BLOCKS_BUNDLE_H_ diff --git a/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.cc b/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.cc new file mode 100644 index 000000000000..cd56ff690f74 --- /dev/null +++ b/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.cc @@ -0,0 +1,35 @@ +// Copyright (c) 2024 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/rust/orchard_decoded_blocks_bunde_impl.h" + +namespace brave_wallet::orchard { + +OrchardDecodedBlocksBundleImpl::OrchardDecodedBlocksBundleImpl(rust::Box v) : batch_decode_result_(std::move(v)) { + +} + +OrchardDecodedBlocksBundleImpl::~OrchardDecodedBlocksBundleImpl() { + +} + +std::optional> OrchardDecodedBlocksBundleImpl::GetDiscoveredNotes() { + std::vector result; + + for (size_t i = 0; i < batch_decode_result_->size(); i++) { + result.emplace_back(OrchardNote( + {batch_decode_result_->note_block_height(i), + batch_decode_result_->note_nullifier(i), + batch_decode_result_->note_value(i)})); + } + + return result; +} + +BatchOrchardDecodeBundle& OrchardDecodedBlocksBundleImpl::GetDecodeBundle() { + return *batch_decode_result_; +} + +} // namespace brave_wallet::orchard diff --git a/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.h b/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.h new file mode 100644 index 000000000000..8643dfd2b793 --- /dev/null +++ b/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.h @@ -0,0 +1,30 @@ +// Copyright (c) 2024 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_RUST_ORCHARD_DECODED_BLOCKS_BUNDLE_IMPL_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_RUST_ORCHARD_DECODED_BLOCKS_BUNDLE_IMPL_H_ + +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h" + +#include "brave/components/brave_wallet/browser/zcash/rust/lib.rs.h" +#include "third_party/rust/cxx/v1/cxx.h" + +namespace brave_wallet::orchard { + +class OrchardDecodedBlocksBundleImpl : public OrchardDecodedBlocksBundle { + public: + explicit OrchardDecodedBlocksBundleImpl(rust::Box); + ~OrchardDecodedBlocksBundleImpl() override; + + std::optional> GetDiscoveredNotes() override; + BatchOrchardDecodeBundle& GetDecodeBundle(); + + private: + rust::Box batch_decode_result_; +}; + +} // namespace brave_wallet::orchard + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_RUST_ORCHARD_DECODED_BLOCKS_BUNDLE_IMPL_H_ diff --git a/components/brave_wallet/browser/zcash/rust/orchard_shard_tree.h b/components/brave_wallet/browser/zcash/rust/orchard_shard_tree.h new file mode 100644 index 000000000000..1c0c630f1fbb --- /dev/null +++ b/components/brave_wallet/browser/zcash/rust/orchard_shard_tree.h @@ -0,0 +1,24 @@ +// Copyright (c) 2024 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 + +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet::orchard { + +class OrchardShardTree { + public: + virtual ~OrchardShardTree() {} + + virtual bool ApplyScanResults( + std::unique_ptr commitments) = 0; + + static std::unique_ptr Create( + std::unique_ptr<::brave_wallet::OrchardShardTreeDelegate> delegate); +}; + +} // namespace brave_wallet::orchard diff --git a/components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.cc b/components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.cc new file mode 100644 index 000000000000..8590bc0a2464 --- /dev/null +++ b/components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.cc @@ -0,0 +1,325 @@ +// Copyright (c) 2024 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 http://mozilla.org/MPL/2.0/. + +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.h" + +#include "base/memory/ptr_util.h" +#include "base/ranges/algorithm.h" +#include "brave/components/brave_wallet/browser/zcash/rust/cxx/src/shard_store.h" +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_decoded_blocks_bunde_impl.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet::orchard { + +::brave_wallet::OrchardShardAddress From(const FfiShardAddress& addr) { + return ::brave_wallet::OrchardShardAddress{addr.level, addr.index}; +} + +FfiShardAddress From(const ::brave_wallet::OrchardShardAddress& addr) { + return FfiShardAddress{addr.level, addr.index}; +} + +FfiCap From(::brave_wallet::OrchardCap& cap) { + ::rust::Vec data; + base::ranges::copy(cap.data.begin(), cap.data.end(), data.begin()); + return FfiCap{std::move(data)}; +} + +::brave_wallet::OrchardCap From(const FfiCap& cap) { + std::vector data; + base::ranges::copy(cap.data.begin(), cap.data.end(), data.begin()); + return ::brave_wallet::OrchardCap{std::move(data)}; +} + +::brave_wallet::OrchardShard From(const FfiShardTree& tree) { + std::vector data; + base::ranges::copy(tree.data.begin(), tree.data.end(), data.begin()); + return ::brave_wallet::OrchardShard(From(tree.address), tree.hash, + std::move(data), tree.contains_marked); +} + +FfiShardTree From(const ::brave_wallet::OrchardShard& tree) { + ::rust::Vec data; + base::ranges::copy(tree.shard_data.begin(), tree.shard_data.end(), + data.begin()); + + return FfiShardTree{From(tree.address), tree.root_hash, std::move(data), + tree.contains_marked}; +} + +FfiCheckpoint From(const ::brave_wallet::OrchardCheckpoint& checkpoint) { + return FfiCheckpoint{}; +} + +FfiCheckpointBundle From( + const ::brave_wallet::OrchardCheckpointBundle& checkpoint_bundle) { + return FfiCheckpointBundle(checkpoint_bundle.checkpoint_id, + From(checkpoint_bundle.checkpoint)); +} + +::brave_wallet::OrchardCheckpoint From(const FfiCheckpoint& checkpoint) { + CheckpointTreeState checkpoint_tree_state = std::nullopt; + if (!checkpoint.empty) { + checkpoint_tree_state = checkpoint.position; + } + return ::brave_wallet::OrchardCheckpoint{ + checkpoint_tree_state, + std::vector(checkpoint.mark_removed.begin(), + checkpoint.mark_removed.end())}; +} + +ShardStoreStatusCode orchard_get_shard(const ShardStoreContext& ctx, + const FfiShardAddress& addr, + FfiShardTree& input) { + auto shard = ctx.GetShard(From(addr)); + if (!shard.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!shard.value()) { + return ShardStoreStatusCode::None; + } + input = From(**shard); + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_last_shard(const ShardStoreContext& ctx, + FfiShardTree& input) { + auto shard = ctx.LastShard(0); + if (!shard.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!shard.value()) { + return ShardStoreStatusCode::None; + } + input = From(**shard); + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_put_shard(ShardStoreContext& ctx, + const FfiShardTree& tree) { + auto result = ctx.PutShard(From(tree)); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!result.value()) { + return ShardStoreStatusCode::None; + } + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_get_shard_roots( + const ShardStoreContext& ctx, + ::rust::Vec& input) { + auto shard = ctx.GetShardRoots(0); + if (!shard.has_value()) { + return ShardStoreStatusCode::Error; + } + for (const auto& root : *shard) { + input.push_back(From(root)); + } + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_truncate(ShardStoreContext& ctx, + const FfiShardAddress& address) { + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_get_cap(const ShardStoreContext& ctx, + FfiCap& input) { + auto result = ctx.GetCap(); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!result.value()) { + return ShardStoreStatusCode::None; + } + input = From(**result); + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_put_cap(ShardStoreContext& ctx, + const FfiCap& tree) { + auto result = ctx.PutCap(From(tree)); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_min_checkpoint_id(const ShardStoreContext& ctx, + uint32_t& input) { + auto result = ctx.MinCheckpointId(); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!result.value()) { + return ShardStoreStatusCode::None; + } + input = **result; + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_max_checkpoint_id(const ShardStoreContext& ctx, + uint32_t& input) { + auto result = ctx.MaxCheckpointId(); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!result.value()) { + return ShardStoreStatusCode::None; + } + input = **result; + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_add_checkpoint(ShardStoreContext& ctx, + uint32_t checkpoint_id, + const FfiCheckpoint& checkpoint) { + auto result = ctx.AddCheckpoint(checkpoint_id, From(checkpoint)); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_checkpoint_count(const ShardStoreContext& ctx, + size_t& into) { + auto result = ctx.CheckpointCount(); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } + into = *result; + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_get_checkpoint_at_depth( + const ShardStoreContext& ctx, + size_t depth, + uint32_t& into_checkpoint_id, + FfiCheckpoint& into_checkpoint) { + auto checkpoint_id = ctx.GetCheckpointAtDepth(depth); + if (!checkpoint_id.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!checkpoint_id.value()) { + return ShardStoreStatusCode::None; + } + into_checkpoint_id = **checkpoint_id; + + auto checkpoint = ctx.GetCheckpoint(into_checkpoint_id); + if (!checkpoint.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!checkpoint.value()) { + return ShardStoreStatusCode::None; + } + into_checkpoint = From((**checkpoint).checkpoint); + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_get_checkpoint(const ShardStoreContext& ctx, + uint32_t checkpoint_id, + FfiCheckpoint& input) { + auto checkpoint = ctx.GetCheckpoint(checkpoint_id); + if (!checkpoint.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!checkpoint.value()) { + return ShardStoreStatusCode::None; + } + input = From((**checkpoint).checkpoint); + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_update_checkpoint( + ShardStoreContext& ctx, + uint32_t checkpoint_id, + const FfiCheckpoint& checkpoint) { + auto result = ctx.UpdateCheckpoint(checkpoint_id, From(checkpoint)); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!result.value()) { + return ShardStoreStatusCode::None; + } + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_remove_checkpoint(ShardStoreContext& ctx, + uint32_t checkpoint_id) { + auto result = ctx.RemoveCheckpoint(checkpoint_id); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!result.value()) { + return ShardStoreStatusCode::None; + } + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_truncate_checkpoint(ShardStoreContext& ctx, + uint32_t checkpoint_id) { + auto result = ctx.TruncateCheckpoints(checkpoint_id); + if (!result.has_value()) { + return ShardStoreStatusCode::Error; + } else if (!result.value()) { + return ShardStoreStatusCode::None; + } + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_with_checkpoints( + const ShardStoreContext& ctx, + size_t limit, + rust::cxxbridge1::Fn< + ShardStoreStatusCode(uint32_t, const FfiCheckpoint& checkpoint)> fn) { + auto checkpoints = ctx.GetCheckpoints(limit); + if (!checkpoints.has_value()) { + return ShardStoreStatusCode::Error; + } else if (checkpoints->empty()) { + return ShardStoreStatusCode::None; + } + + // TODO(cypt4): Make via a call to the DB + for (const auto& checkpoint : checkpoints.value()) { + auto r = fn(checkpoint.checkpoint_id, From(checkpoint.checkpoint)); + if (r != ShardStoreStatusCode::Ok) { + return r; + } + } + return ShardStoreStatusCode::Ok; +} + +ShardStoreStatusCode orchard_get_checkpoints(const ShardStoreContext& ctx, + size_t limit, + ::rust::Vec& into) { + auto checkpoints = ctx.GetCheckpoints(limit); + if (!checkpoints.has_value()) { + return ShardStoreStatusCode::Error; + } + if (checkpoints->empty()) { + return ShardStoreStatusCode::None; + } + for (const auto& checkpoint : checkpoints.value()) { + into.push_back(From(checkpoint.checkpoint)); + } + return ShardStoreStatusCode::Ok; +} + +bool OrchardShardTreeImpl::ApplyScanResults( + std::unique_ptr commitments) { + auto* bundle_impl = + static_cast(commitments.get()); + return orcard_shard_tree_->insert_commitments(bundle_impl->GetDecodeBundle()); +} + +OrchardShardTreeImpl::OrchardShardTreeImpl(rust::Box orcard_shard_tree) : orcard_shard_tree_(std::move(orcard_shard_tree)){ +} + +OrchardShardTreeImpl::~OrchardShardTreeImpl() {} + +// static +std::unique_ptr OrchardShardTree::Create( + std::unique_ptr<::brave_wallet::OrchardShardTreeDelegate> delegate) { + auto shard_tree_result = + ::brave_wallet::orchard::create_shard_tree(std::move(delegate)); + if (!shard_tree_result->is_ok()) { + return nullptr; + } + return base::WrapUnique( + new OrchardShardTreeImpl(shard_tree_result->unwrap())); +} + +} // namespace brave_wallet::orchard diff --git a/components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.h b/components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.h new file mode 100644 index 000000000000..5b12e90b9129 --- /dev/null +++ b/components/brave_wallet/browser/zcash/rust/orchard_shard_tree_impl.h @@ -0,0 +1,26 @@ +// Copyright (c) 2024 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 http://mozilla.org/MPL/2.0/. + +#include "brave/components/brave_wallet/browser/zcash/rust/lib.rs.h" +#include "brave/components/brave_wallet/browser/zcash/rust/orchard_shard_tree.h" + +namespace brave_wallet::orchard { + +class OrchardShardTreeImpl : public OrchardShardTree { + public: + OrchardShardTreeImpl(rust::Box orcard_shard_tree); + ~OrchardShardTreeImpl() override; + + bool ApplyScanResults( + std::unique_ptr commitments) override; + + private: + static std::unique_ptr Create( + std::unique_ptr<::brave_wallet::orchard::ShardStoreContext> context); + + ::rust::Box orcard_shard_tree_; +}; + +} // namespace brave_wallet::orchard diff --git a/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.cc b/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.cc new file mode 100644 index 000000000000..7a600a080d19 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.cc @@ -0,0 +1,200 @@ +// Copyright (c) 2024 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_blocks_batch_scan_task.h" + +#include +#include + +#include "base/containers/extend.h" +#include "brave/components/brave_wallet/common/hex_utils.h" + +namespace brave_wallet { + +namespace { +constexpr uint32_t kBlockDownloadBatchSize = 10u; +} + +ZCashBlocksBatchScanTask::ZCashBlocksBatchScanTask( + ZCashShieldSyncService* sync_service, uint32_t from, uint32_t to, + ZCashBlocksBatchScanTaskCallback callback) : sync_service_(sync_service), from_(from), to_(to), callback_(std::move(callback)){ + frontier_block_height_ = from - 1; +} + +ZCashBlocksBatchScanTask::~ZCashBlocksBatchScanTask() { +} + +void ZCashBlocksBatchScanTask::Start() { + ScheduleWorkOnTask(); +} + +void ZCashBlocksBatchScanTask::ScheduleWorkOnTask() { + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&ZCashBlocksBatchScanTask::WorkOnTask, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashBlocksBatchScanTask::WorkOnTask() { + if (error_) { + std::move(callback_).Run(base::unexpected(*error_)); + return; + } + + if (!frontier_tree_state_) { + GetFrontierTreeState(); + return; + } + + if (!frontier_block_) { + GetFrontierBlock(); + return; + } + + if (downloaded_blocks_->size() != (from_ - to_)) { + DownloadBlocks(); + return; + } + + if (!scan_result_) { + ScanBlocks(); + return; + } + + if (!database_updated_) { + UpdateDatabase(); + return; + } + + std::move(callback_).Run(true); +} + +void ZCashBlocksBatchScanTask::GetFrontierTreeState() { + auto block_id = zcash::mojom::BlockID::New(frontier_block_height_, std::vector({})); + sync_service_->zcash_rpc_->GetTreeState( + sync_service_->chain_id_, std::move(block_id) , + base::BindOnce(&ZCashBlocksBatchScanTask::OnGetFrontierTreeState, + weak_ptr_factory_.GetWeakPtr())); + +} + +void ZCashBlocksBatchScanTask::OnGetFrontierTreeState( + base::expected result) { + if (!result.has_value() || !result.value()) { + error_ = ZCashShieldSyncService::Error{ ZCashShieldSyncService::ErrorCode::kFailedToReceiveTreeState, result.error() }; + ScheduleWorkOnTask(); + return; + } + frontier_tree_state_ = result.value().Clone(); + ScheduleWorkOnTask(); +} + +void ZCashBlocksBatchScanTask::GetFrontierBlock() { + sync_service_->zcash_rpc_->GetCompactBlocks( + sync_service_->chain_id_, frontier_block_height_, frontier_block_height_, + base::BindOnce(&ZCashBlocksBatchScanTask::OnGetFrontierBlock, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashBlocksBatchScanTask::OnGetFrontierBlock( + base::expected, std::string> result) { + if (!result.has_value() || result.value().size() != 1) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kFailedToDownloadBlocks, result.error()}; + ScheduleWorkOnTask(); + return; + } + + frontier_block_ = std::move(result.value()[0]); + ScheduleWorkOnTask(); +} + +void ZCashBlocksBatchScanTask::DownloadBlocks() { + uint32_t start_index = downloaded_blocks_ ? from_ + downloaded_blocks_->size() : from_; + uint32_t end_index = std::min(start_index + kBlockDownloadBatchSize - 1, to_); + auto expected_size = end_index - start_index; + + sync_service_->zcash_rpc_->GetCompactBlocks( + sync_service_->chain_id_, start_index, end_index, + base::BindOnce(&ZCashBlocksBatchScanTask::OnBlocksDownloaded, + weak_ptr_factory_.GetWeakPtr(), expected_size)); +} + +void ZCashBlocksBatchScanTask::OnBlocksDownloaded( + size_t expected_size, + base::expected, std::string> + result) { + if (!result.has_value() || expected_size != result.value().size()) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kFailedToDownloadBlocks, result.error()}; + ScheduleWorkOnTask(); + return; + } + + base::Extend(downloaded_blocks_.value(), std::move(result.value())); + ScheduleWorkOnTask(); +} + +void ZCashBlocksBatchScanTask::ScanBlocks() { + if (!downloaded_blocks_ || downloaded_blocks_->empty()) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kScannerError, ""}; + ScheduleWorkOnTask(); + return; + } + + if (!frontier_block_.value() || !frontier_tree_state_.value() || !frontier_block_.value()->chain_metadata) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kScannerError, ""}; + ScheduleWorkOnTask(); + return; + } + + FrontierChainState frontier_chain_state { + frontier_block_.value()->height, + frontier_block_.value()->chain_metadata->orchard_commitment_tree_size, + frontier_tree_state_.value()->orchardTree + }; + + latest_scanned_block_ = downloaded_blocks_->back().Clone(); + + sync_service_->block_scanner_->ScanBlocks( + frontier_chain_state, std::move(downloaded_blocks_.value()), + base::BindOnce(&ZCashBlocksBatchScanTask::OnBlocksScanned, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashBlocksBatchScanTask::OnBlocksScanned( + base::expected + result) { + if (!result.has_value()) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kScannerError, ""}; + ScheduleWorkOnTask(); + return; + } + + scan_result_ = std::move(result.value()); + ScheduleWorkOnTask(); +} + +void ZCashBlocksBatchScanTask::UpdateDatabase() { + CHECK(scan_result_.has_value()); + auto latest_scanned_block_hash = ToHex((*latest_scanned_block_)->hash); + auto latest_scanned_block_height = (*latest_scanned_block_)->height; + + sync_service_->sync_state_.AsyncCall(&ZCashOrchardSyncState::UpdateNotes) + .WithArgs(sync_service_->account_id_.Clone(), std::move(scan_result_.value()), + latest_scanned_block_height, latest_scanned_block_hash) + .Then(base::BindOnce(&ZCashBlocksBatchScanTask::OnDatabaseUpdated, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashBlocksBatchScanTask::OnDatabaseUpdated( + std::optional error) { + if (error) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kFailedToUpdateDatabase, error->message}; + ScheduleWorkOnTask(); + return; + } + database_updated_ = true; + ScheduleWorkOnTask(); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.h b/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.h new file mode 100644 index 000000000000..9c3a36446fc2 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.h @@ -0,0 +1,73 @@ +// Copyright (c) 2024 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_BLOCKS_BATCH_SCAN_TASK_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_BLOCKS_BATCH_SCAN_TASK_ + +#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet { + +class ZCashBlocksBatchScanTask { + public: + using ZCashBlocksBatchScanTaskCallback = base::OnceCallback)>; + ZCashBlocksBatchScanTask(ZCashShieldSyncService* sync_service, + uint32_t from, uint32_t to, ZCashBlocksBatchScanTaskCallback callback); + ~ZCashBlocksBatchScanTask(); + + uint32_t from() const { + return from_; + } + + uint32_t to() const { + return to_; + } + + void Start(); + + private: + void WorkOnTask(); + void ScheduleWorkOnTask(); + + void GetFrontierTreeState(); + void OnGetFrontierTreeState(base::expected); + void GetFrontierBlock(); + void OnGetFrontierBlock(base::expected, std::string> result); + + void DownloadBlocks(); + void OnBlocksDownloaded(size_t expected_size, + base::expected, std::string> + result); + + void ScanBlocks(); + void OnBlocksScanned(base::expected + result); + + void UpdateDatabase(); + void OnDatabaseUpdated(std::optional error); + + raw_ptr sync_service_; + uint32_t from_ = 0; + uint32_t to_ = 0; + ZCashBlocksBatchScanTaskCallback callback_; + + uint32_t frontier_block_height_ = 0; + + std::optional error_; + std::optional frontier_tree_state_; + std::optional frontier_block_; + std::optional> downloaded_blocks_; + std::optional scan_result_; + std::optional latest_scanned_block_; + + bool database_updated_ = false; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_BLOCK_BATCH_SCAN_TASK_ diff --git a/components/brave_wallet/browser/zcash/zcash_orchard_storage.cc b/components/brave_wallet/browser/zcash/zcash_orchard_storage.cc index bea195b8c843..39b7e40e0d87 100644 --- a/components/brave_wallet/browser/zcash/zcash_orchard_storage.cc +++ b/components/brave_wallet/browser/zcash/zcash_orchard_storage.cc @@ -18,6 +18,7 @@ #include "sql/meta_table.h" #include "sql/statement.h" #include "sql/transaction.h" +#include "third_party/sqlite/src/amalgamation/sqlite3.h" namespace brave_wallet { @@ -25,6 +26,9 @@ namespace { #define kNotesTable "notes" #define kSpentNotesTable "spent_notes" #define kAccountMeta "account_meta" +#define kShardTree "shard_tree" +#define kShardTreeCheckpoints "checkpoints" +#define kCheckpointsMarksRemoved "checkpoints_mark_removed" const int kEmptyDbVersionNumber = 1; const int kCurrentVersionNumber = 2; @@ -37,6 +41,43 @@ std::optional ReadUint32(sql::Statement& statement, size_t index) { return static_cast(v); } +base::expected ReadCheckpointTreeState( + sql::Statement& statement, + size_t index) { + if (statement.GetColumnType(index) == sql::ColumnType::kNull) { + return std::nullopt; + } + auto v = ReadUint32(statement, index); + if (!v) { + return base::unexpected("Format error"); + } + return *v; +} + +// std::optional> ReadBlobData(sql::Statement& statement, +// size_t index) { +// if (statement.GetColumnType(index) == sql::ColumnType::kNull) { +// return std::nullopt; +// } +// auto blob = statement.ColumnBlob(index); +// return std::vector(blob.begin(), blob.end()); +// } + +base::expected, std::string> ReadRootHash( + sql::Statement& statement, + size_t index) { + if (statement.GetColumnType(index) == sql::ColumnType::kNull) { + return std::nullopt; + } + auto v = statement.ColumnBlob(index); + if (v.size() != kOrchardShardTreeHashSize) { + return base::unexpected("Size error"); + } + std::array result; + base::ranges::copy(v.begin(), v.end(), result.begin()); + return result; +} + } // namespace ZCashOrchardStorage::ZCashOrchardStorage(base::FilePath path_to_database) @@ -124,6 +165,33 @@ bool ZCashOrchardStorage::CreateSchema() { "account_birthday INTEGER NOT NULL," "latest_scanned_block INTEGER NOT NULL," "latest_scanned_block_hash TEXT NOT NULL);") && + database_->Execute( + "CREATE TABLE " kShardTree + " (" + "account_id TEXT NOT NULL," + "shard_index INTEGER NOT NULL," + "subtree_end_height INTEGER," + "root_hash BLOB," + "shard_data BLOB," + "contains_marked INTEGER," + "CONSTRAINT shard_index_unique UNIQUE (shard_index, account_id)," + "CONSTRAINT root_unique UNIQUE (root_hash, account_id));") && + database_->Execute("CREATE TABLE " kShardTreeCheckpoints + " (" + "account_id TEXT NOT NULL," + "checkpoint_id INTEGER PRIMARY KEY," + "position INTEGER)") && + database_->Execute("CREATE TABLE " kCheckpointsMarksRemoved + " (" + "account_id TEXT NOT NULL," + "checkpoint_id INTEGER NOT NULL," + "mark_removed_position INTEGER NOT NULL," + "FOREIGN KEY (checkpoint_id) REFERENCES " + "orchard_tree_checkpoints(checkpoint_id)" + "ON DELETE CASCADE," + "CONSTRAINT spend_position_unique UNIQUE " + "(checkpoint_id, mark_removed_position, account_id)" + ")") && transaction.Commit(); } @@ -346,7 +414,7 @@ ZCashOrchardStorage::GetSpendableNotes(mojom::AccountIdPtr account_id) { std::optional ZCashOrchardStorage::UpdateNotes( mojom::AccountIdPtr account_id, const std::vector& found_notes, - const std::vector& spent_notes, + const std::vector& found_nullifiers, const uint32_t latest_scanned_block, const std::string& latest_scanned_block_hash) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); @@ -385,7 +453,7 @@ std::optional ZCashOrchardStorage::UpdateNotes( "(account_id, spent_block_id, nullifier) " "VALUES (?, ?, ?);")); - for (const auto& spent : spent_notes) { + for (const auto& spent : found_nullifiers) { statement_populate_spent_notes.Reset(true); statement_populate_spent_notes.BindString(0, account_id->unique_key); statement_populate_spent_notes.BindInt64(1, spent.block_id); @@ -420,4 +488,847 @@ std::optional ZCashOrchardStorage::UpdateNotes( return std::nullopt; } +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetLatestShardIndex(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement resolve_max_shard_id( + database_->GetCachedStatement(SQL_FROM_HERE, + "SELECT " + "MAX(shard_index) FROM " kShardTree " " + "WHERE account_id = ?;")); + + resolve_max_shard_id.BindString(0, account_id->unique_key); + if (resolve_max_shard_id.Step()) { + if (resolve_max_shard_id.GetColumnType(0) == sql::ColumnType::kNull) { + return std::nullopt; + } + auto shard_index = ReadUint32(resolve_max_shard_id, 0); + if (!shard_index) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + return shard_index.value(); + } + + return std::nullopt; +} + +base::expected ZCashOrchardStorage::GetCap(mojom::AccountIdPtr account_id) { + return OrchardCap({}); + +} + +base::expected ZCashOrchardStorage::PutCap(mojom::AccountIdPtr account_id, + OrchardCap cap) { + return false; + +} + +base::expected +ZCashOrchardStorage::UpdateSubtreeRoots( + mojom::AccountIdPtr account_id, + uint32_t start_index, + std::vector roots) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Transaction transaction(database_.get()); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement statement_populate_roots(database_->GetCachedStatement( + SQL_FROM_HERE, + "INSERT INTO " kShardTree + " " + "(shard_index, subtree_end_height, root_hash, shard_data, account_id) " + "VALUES (?, ?, ?, ?, ?);" + + )); + + sql::Statement statement_update_roots(database_->GetCachedStatement( + SQL_FROM_HERE, + "UPDATE " kShardTree + " " + "SET subtree_end_height = :subtree_end_height, root_hash = :root_hash " + "WHERE " + "shard_index = :shard_index and account_id = :account_id;")); + + for (size_t i = 0; i < roots.size(); i++) { + if (!roots[i] || + roots[i]->complete_block_hash.size() != kOrchardShardTreeHashSize) { + return base::unexpected(Error{ErrorCode::kInternalError, "Wrong data"}); + } + + statement_populate_roots.Reset(true); + statement_populate_roots.BindInt64(0, start_index + i); + statement_populate_roots.BindInt64(1, roots[i]->complete_block_height); + statement_populate_roots.BindBlob(2, roots[i]->complete_block_hash); + statement_populate_roots.BindNull( + 3); // TODO(cypt4): Serialize hash as a leaf? + statement_populate_roots.BindString(4, account_id->unique_key); + if (!statement_populate_roots.Run()) { + if (database_->GetErrorCode() == SQLITE_CONSTRAINT) { + LOG(ERROR) << "XXXZZZ constraint!"; + statement_update_roots.Reset(true); + statement_update_roots.BindInt64(0, roots[i]->complete_block_height); + statement_update_roots.BindBlob(1, roots[i]->complete_block_hash); + statement_update_roots.BindInt64(2, start_index + i); + statement_update_roots.BindString(3, account_id->unique_key); + if (!statement_update_roots.Run()) { + LOG(ERROR) << "XXXZZZ " << database_->GetErrorMessage(); + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + } else { + LOG(ERROR) << "XXXZZZ " << database_->GetErrorMessage(); + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + } + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + + return true; +} + +base::expected +ZCashOrchardStorage::TruncateShards(mojom::AccountIdPtr account_id, + uint32_t shard_index) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Transaction transaction(database_.get()); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement remove_checkpoint_by_id(database_->GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kShardTree " " + "WHERE shard_index >= ? AND account_id = ?;")); + + remove_checkpoint_by_id.BindInt64(0, shard_index); + remove_checkpoint_by_id.BindString(1, account_id->unique_key); + + if (!remove_checkpoint_by_id.Run()) { + LOG(ERROR) << "XXXZZZ " << database_->GetErrorMessage(); + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + + return true; +} + +base::expected ZCashOrchardStorage::PutShard( + mojom::AccountIdPtr account_id, + OrchardShard shard) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + auto existing_shard = GetShard(account_id.Clone(), shard.address); + if (!existing_shard.has_value()) { + return base::unexpected(existing_shard.error()); + } + + sql::Transaction transaction(database_.get()); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + if (existing_shard.value()) { + LOG(ERROR) << "XXXZZZ insert existing shard"; + sql::Statement statement_update_shard(database_->GetCachedStatement( + SQL_FROM_HERE, + "UPDATE " kShardTree + " " + "SET root_hash = :root_hash, shard_data = :shard_data " + "WHERE shard_index = :shard_index AND account_id = :account_id;")); + + statement_update_shard.BindBlob(0, shard.root_hash); + statement_update_shard.BindBlob(1, shard.shard_data); + statement_update_shard.BindInt64(2, shard.address.index); + statement_update_shard.BindString(3, account_id->unique_key); + + if (!statement_update_shard.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + } else { + sql::Statement statement_put_shard(database_->GetCachedStatement( + SQL_FROM_HERE, + "INSERT INTO " kShardTree + " " + "(shard_index, root_hash, shard_data, account_id) " + "VALUES (:shard_index, :root_hash, :shard_data, :account_id);")); + + statement_put_shard.BindInt64(0, shard.address.index); + statement_put_shard.BindBlob(1, shard.root_hash); + statement_put_shard.BindBlob(2, shard.shard_data); + statement_put_shard.BindString(3, account_id->unique_key); + + if (!statement_put_shard.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + + return true; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetShard(mojom::AccountIdPtr account_id, + OrchardShardAddress address) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement resolve_shard_statement(database_->GetCachedStatement( + SQL_FROM_HERE, + "SELECT root_hash, shard_data, contains_marked FROM " kShardTree + " " + "WHERE account_id = ? AND shard_index = ?;")); + + resolve_shard_statement.BindString(0, account_id->unique_key); + resolve_shard_statement.BindInt64(1, address.index); + + if (!resolve_shard_statement.Step()) { + return std::nullopt; + } + + LOG(ERROR) << "XXXZZZ 1"; + auto hash = ReadRootHash(resolve_shard_statement, 0); + if (!hash.has_value()) { + return base::unexpected(Error{ErrorCode::kDbInitError, hash.error()}); + } + LOG(ERROR) << "XXXZZZ 2"; + + auto shard_data = resolve_shard_statement.ColumnBlob(1); + auto contains_marked = resolve_shard_statement.ColumnBool(2); + auto shard = OrchardShard(address, hash.value().value(), + std::vector(), contains_marked); + LOG(ERROR) << "XXXZZZ 3"; + + base::ranges::copy(shard_data, std::back_inserter(shard.shard_data)); + LOG(ERROR) << "XXXZZZ 4"; + + return shard; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::LastShard(mojom::AccountIdPtr account_id, + uint8_t shard_height) { + auto shard_index = GetLatestShardIndex(account_id.Clone()); + if (!shard_index.has_value()) { + return base::unexpected(shard_index.error()); + } + + if (!shard_index.value()) { + return std::nullopt; + } + + return GetShard( + account_id.Clone(), + OrchardShardAddress{shard_height, shard_index.value().value()}); +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetShardRoots(mojom::AccountIdPtr account_id, + uint8_t shard_level) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + std::vector result; + + sql::Statement resolve_shards_statement(database_->GetCachedStatement( + SQL_FROM_HERE, "SELECT shard_index FROM " kShardTree + " WHERE account_id = ? ORDER BY shard_index;")); + + resolve_shards_statement.BindString(0, account_id->unique_key); + + while (resolve_shards_statement.Step()) { + LOG(ERROR) << "XXXZZZ steps"; + auto shard_index = ReadUint32(resolve_shards_statement, 0); + if (!shard_index) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, ""}); + } + result.push_back(OrchardShardAddress{shard_level, shard_index.value()}); + } + LOG(ERROR) << "XXXZZZ " << database_->GetErrorMessage(); + + if (!resolve_shards_statement.is_valid()) { + LOG(ERROR) << "XXXZZZ " << database_->GetErrorMessage(); + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, ""}); + } + + return result; +} + +base::expected +ZCashOrchardStorage::AddCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id, + OrchardCheckpoint checkpoint) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + LOG(ERROR) << "XXXZZZ 1"; + sql::Statement extant_tree_state_statement(database_->GetCachedStatement( + SQL_FROM_HERE, "SELECT position FROM " kShardTreeCheckpoints " " + "WHERE checkpoint_id = ? " + "AND account_id = ?;")); + extant_tree_state_statement.BindInt64(0, checkpoint_id); + extant_tree_state_statement.BindString(1, account_id->unique_key); + + std::optional extant_tree_state_position; + if (extant_tree_state_statement.Step()) { + auto state = ReadCheckpointTreeState(extant_tree_state_statement, 0); + if (!state.has_value()) { + return base::unexpected(Error{ErrorCode::kDbInitError, state.error()}); + } + extant_tree_state_position = state.value(); + } + + sql::Transaction transaction(database_.get()); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Failed to init database "}); + } + + // Checkpoint with same id didn't exist. + if (!extant_tree_state_position) { + LOG(ERROR) << "XXXZZZ 1"; + + sql::Statement insert_checkpoint_statement(database_->GetCachedStatement( + SQL_FROM_HERE, "INSERT INTO " kShardTreeCheckpoints " " + "(account_id, checkpoint_id, position)" + "VALUES (?, ?, ?);")); + insert_checkpoint_statement.BindString(0, account_id->unique_key); + insert_checkpoint_statement.BindInt64(1, checkpoint_id); + if (checkpoint.tree_state_position) { + insert_checkpoint_statement.BindInt64( + 2, checkpoint.tree_state_position.value()); + } else { + insert_checkpoint_statement.BindNull(2); + } + if (!insert_checkpoint_statement.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + LOG(ERROR) << "XXXZZZ 1"; + + sql::Statement insert_marks_removed_statement(database_->GetCachedStatement( + SQL_FROM_HERE, "INSERT INTO " kCheckpointsMarksRemoved " " + "(account_id, checkpoint_id, mark_removed_position) " + "VALUES (?, ?, ?);")); + for (const auto& mark : checkpoint.marks_removed) { + insert_marks_removed_statement.Reset(true); + insert_marks_removed_statement.BindString(0, account_id->unique_key); + insert_marks_removed_statement.BindInt64(1, checkpoint_id); + insert_marks_removed_statement.BindInt64(2, mark); + LOG(ERROR) << "XXXZZZ 1"; + + if (!insert_marks_removed_statement.Run()) { + LOG(ERROR) << "XXXZZZ " << database_->GetErrorMessage(); + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + } + } else { + // Existing checkpoint should be the same + LOG(ERROR) << "XXXZZZ --" << extant_tree_state_position.has_value(); + + LOG(ERROR) << "XXXZZZ --" << checkpoint.tree_state_position.has_value(); + if (extant_tree_state_position.value() != checkpoint.tree_state_position) { + LOG(ERROR) << "XXXZZZ 1"; + return base::unexpected( + Error{ErrorCode::kConsistencyError, "Consistency error"}); + } + auto marks_removed_result = + GetMarksRemoved(account_id.Clone(), checkpoint_id); + if (!marks_removed_result.has_value()) { + return base::unexpected(marks_removed_result.error()); + } + + if (!marks_removed_result.value()) { + return base::unexpected( + Error{ErrorCode::kConsistencyError, "Consistency error"}); + } + + LOG(ERROR) << "XXXZZZ " << marks_removed_result.value().value().size(); + if (marks_removed_result.value().value() != checkpoint.marks_removed) { + LOG(ERROR) << "XXXZZZ 1"; + + return base::unexpected( + Error{ErrorCode::kConsistencyError, "Consistency error"}); + } + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kDbInitError, ""}); + } + + return true; +} + +base::expected +ZCashOrchardStorage::CheckpointCount(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement resolve_checkpoints_count(database_->GetCachedStatement( + SQL_FROM_HERE, "SELECT COUNT(*) FROM " kShardTreeCheckpoints)); + + if (!resolve_checkpoints_count.Step()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + auto value = ReadUint32(resolve_checkpoints_count, 0); + + if (!value) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + return *value; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::MinCheckpointId(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement resolve_min_checkpoint_id(database_->GetCachedStatement( + SQL_FROM_HERE, "SELECT MIN(checkpoint_id) FROM " kShardTreeCheckpoints)); + + if (!resolve_min_checkpoint_id.Step()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + if (resolve_min_checkpoint_id.GetColumnType(0) == sql::ColumnType::kNull) { + return std::nullopt; + } else { + return ReadUint32(resolve_min_checkpoint_id, 0); + } +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::MaxCheckpointId(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement resolve_max_checkpoint_id(database_->GetCachedStatement( + SQL_FROM_HERE, "SELECT MAX(checkpoint_id) FROM " kShardTreeCheckpoints)); + + if (!resolve_max_checkpoint_id.Step()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + if (resolve_max_checkpoint_id.GetColumnType(0) == sql::ColumnType::kNull) { + return std::nullopt; + } else { + return ReadUint32(resolve_max_checkpoint_id, 0); + } +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetCheckpointAtDepth(mojom::AccountIdPtr account_id, + uint32_t depth) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement get_checkpoint_at_depth_statement( + database_->GetCachedStatement(SQL_FROM_HERE, + "SELECT checkpoint_id, position " + "FROM " kShardTreeCheckpoints " " + "WHERE account_id = ? " + "ORDER BY checkpoint_id DESC " + "LIMIT 1 " + "OFFSET ?;")); + + get_checkpoint_at_depth_statement.BindString(0, account_id->unique_key); + get_checkpoint_at_depth_statement.BindInt64(1, depth); + + if (!get_checkpoint_at_depth_statement.Step()) { + LOG(ERROR) << "XXXZZZ " << database_->GetErrorMessage(); + + LOG(ERROR) << "XXXZZZ nullopt"; + return std::nullopt; + // LOG(ERROR) << "XXXZZZ " << database_->GetErrorMessage(); + // return base::unexpected( + // Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + auto value = ReadUint32(get_checkpoint_at_depth_statement, 0); + + if (!value) { + LOG(ERROR) << "XXXZZZ nullopt"; + return std::nullopt; + } + + return *value; +} + +base::expected>, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetMarksRemoved(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement get_marks_removed_statement( + database_->GetCachedStatement(SQL_FROM_HERE, + "SELECT mark_removed_position " + "FROM " kCheckpointsMarksRemoved " " + "WHERE checkpoint_id = ? AND " + "account_id = ?;")); + get_marks_removed_statement.BindInt64(0, checkpoint_id); + get_marks_removed_statement.BindString(1, account_id->unique_key); + + std::vector result; + while (get_marks_removed_statement.Step()) { + auto position = ReadUint32(get_marks_removed_statement, 0); + if (!position) { + return base::unexpected(Error{ErrorCode::kDbInitError, "Format error"}); + } + result.push_back(*position); + } + + return result; +} + +base::expected, + ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement get_checkpoint_statement(database_->GetCachedStatement( + SQL_FROM_HERE, + "SELECT position " + "FROM " kShardTreeCheckpoints + " " + "WHERE checkpoint_id = ? AND account_id = ?;")); + + get_checkpoint_statement.BindInt64(0, checkpoint_id); + get_checkpoint_statement.BindString(1, account_id->unique_key); + if (!get_checkpoint_statement.Step()) { + return std::nullopt; + } + auto checkpoint_position = + ReadCheckpointTreeState(get_checkpoint_statement, 0); + if (!checkpoint_position.has_value()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + + sql::Statement marks_removed_statement(database_->GetCachedStatement( + SQL_FROM_HERE, + "SELECT mark_removed_position " + "FROM " kCheckpointsMarksRemoved + " " + "WHERE checkpoint_id = ? AND account_id = ?;")); + + marks_removed_statement.BindInt64(0, checkpoint_id); + marks_removed_statement.BindString(1, account_id->unique_key); + + std::vector positions; + while (marks_removed_statement.Step()) { + auto position = ReadUint32(marks_removed_statement, 0); + if (position) { + positions.push_back(*position); + } else { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_->GetErrorMessage()}); + } + } + + return OrchardCheckpointBundle{ + checkpoint_id, + OrchardCheckpoint{*checkpoint_position, std::move(positions)}}; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetCheckpoints(mojom::AccountIdPtr account_id, + size_t limit) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement get_checkpoints_statement( + database_->GetCachedStatement(SQL_FROM_HERE, + "SELECT checkpoint_id, position " + "FROM " kShardTreeCheckpoints " " + "WHERE account_id = ? " + "ORDER BY position " + "LIMIT ?")); + + get_checkpoints_statement.BindString(0, account_id->unique_key); + get_checkpoints_statement.BindInt64(1, limit); + + std::vector checkpoints; + while (!get_checkpoints_statement.Step()) { + auto checkpoint_id = ReadUint32(get_checkpoints_statement, 0); + auto checkpoint_position = + ReadCheckpointTreeState(get_checkpoints_statement, 1); + if (!checkpoint_id) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, ""}); + } + if (!checkpoint_position.has_value()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, ""}); + } + auto found_marks_removed = + GetMarksRemoved(account_id.Clone(), *checkpoint_id); + if (!found_marks_removed.has_value()) { + return base::unexpected(found_marks_removed.error()); + } + std::vector marks_removed; + if (found_marks_removed.value()) { + marks_removed = **found_marks_removed; + } + + checkpoints.push_back(OrchardCheckpointBundle{ + *checkpoint_id, OrchardCheckpoint(checkpoint_position.value(), + std::move(marks_removed))}); + } + return checkpoints; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetMaxCheckpointedHeight(mojom::AccountIdPtr account_id, + uint32_t chain_tip_height, + uint32_t min_confirmations) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + uint32_t max_checkpointed_height = chain_tip_height - min_confirmations - 1; + + sql::Statement get_max_checkpointed_height(database_->GetCachedStatement( + SQL_FROM_HERE, "SELECT checkpoint_id FROM " kShardTreeCheckpoints " " + "WHERE checkpoint_id <= :max_checkpoint_height " + "ORDER BY checkpoint_id DESC " + "LIMIT 1")); + + get_max_checkpointed_height.BindInt64(0, max_checkpointed_height); + + if (!get_max_checkpointed_height.Step()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + auto value = ReadUint32(get_max_checkpointed_height, 0); + + if (!value) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + return *value; +} + +base::expected +ZCashOrchardStorage::RemoveCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Statement remove_checkpoint_by_id(database_->GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kShardTreeCheckpoints " " + "WHERE checkpoint_id = ? AND account_id= ?;")); + + remove_checkpoint_by_id.BindInt64(0, checkpoint_id); + remove_checkpoint_by_id.BindString(1, account_id->unique_key); + + sql::Transaction transaction(database_.get()); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Failed to init database "}); + } + + if (!remove_checkpoint_by_id.Run()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + if (!transaction.Commit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Failed to init database "}); + } + + return true; +} + +base::expected +ZCashOrchardStorage::TruncateCheckpoints(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_->GetErrorMessage()}); + } + + sql::Transaction transaction(database_.get()); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Failed to init database "}); + } + + sql::Statement truncate_checkpoints(database_->GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kShardTreeCheckpoints + " WHERE checkpoint_id >= ? and account_id = ?;")); + + truncate_checkpoints.BindInt64(0, checkpoint_id); + truncate_checkpoints.BindString(1, account_id->unique_key); + + if (!truncate_checkpoints.Run()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + if (!transaction.Commit()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_->GetErrorMessage()}); + } + + return true; +} + +// // TODO(cypt4): Rewrite on JOIN +// base::expected +// ZCashOrchardStorage::WithCheckpoints( +// mojom::AccountIdPtr account_id, +// uint32_t limit, +// WithCheckpointsCallback callback) { +// DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + +// if (!EnsureDbInit()) { +// NOTREACHED_IN_MIGRATION(); +// return base::unexpected(Error{ErrorCode::kDbInitError, +// database_->GetErrorMessage()}); +// } + +// sql::Statement get_checkpoints_statement( +// database_->GetCachedStatement(SQL_FROM_HERE, +// "SELECT checkpoint_id, position " +// "FROM " kShardTreeCheckpoints " " +// "ORDER BY position " +// "LIMIT ?")); + +// get_checkpoints_statement.BindInt64(0, limit); + +// while (!get_checkpoints_statement.Step()) { +// sql::Statement marks_removed_statement( +// database_->GetCachedStatement(SQL_FROM_HERE, +// "SELECT mark_removed_position " +// "FROM " kCheckpointsMarksRemoved " " +// "WHERE checkpoint_id = ?")); + +// std::vector positions; +// while (!marks_removed_statement.Run()) { +// auto position = ReadUint32(marks_removed_statement, 0); +// if (position) { +// positions.push_back(*position); +// } else { +// return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, +// database_->GetErrorMessage()}); +// } +// } + +// auto checkpoint_id = ReadUint32(get_checkpoints_statement, 0); +// auto checkpoint_position = ReadUint32(get_checkpoints_statement, 1); + +// if (checkpoint_id && checkpoint_position) { +// callback.Run(*checkpoint_id, OrchardCheckpoint{ false, +// *checkpoint_position, std::move(positions) }); +// } else { +// return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, +// database_->GetErrorMessage()}); +// } +// } +// return true; +// } + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_orchard_storage.h b/components/brave_wallet/browser/zcash/zcash_orchard_storage.h index 6594af6ca9b7..c03b2b9a2eef 100644 --- a/components/brave_wallet/browser/zcash/zcash_orchard_storage.h +++ b/components/brave_wallet/browser/zcash/zcash_orchard_storage.h @@ -17,6 +17,7 @@ #include "base/types/expected.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/zcash_utils.h" +#include "brave/components/services/brave_wallet/public/mojom/zcash_decoder.mojom.h" namespace sql { class Database; @@ -26,8 +27,11 @@ namespace brave_wallet { // Implements SQLite database to store found incoming notes, // nullifiers, wallet zcash accounts and commitment trees. -class ZCashOrchardStorage { +class ZCashOrchardStorage : public base::RefCounted { public: + using WithCheckpointsCallback = + base::RepeatingCallback; + struct AccountMeta { uint32_t account_birthday = 0; uint32_t latest_scanned_block_id = 0; @@ -38,7 +42,9 @@ class ZCashOrchardStorage { kDbInitError, kAccountNotFound, kFailedToExecuteStatement, - kInternalError + kInternalError, + kNoCheckpoints, + kConsistencyError }; struct Error { @@ -47,7 +53,6 @@ class ZCashOrchardStorage { }; explicit ZCashOrchardStorage(base::FilePath path_to_database); - ~ZCashOrchardStorage(); base::expected RegisterAccount( mojom::AccountIdPtr account_id, @@ -74,12 +79,72 @@ class ZCashOrchardStorage { std::optional UpdateNotes( mojom::AccountIdPtr account_id, const std::vector& notes_to_add, - const std::vector& notes_to_delete, + const std::vector& found_nullifiers, const uint32_t latest_scanned_block, const std::string& latest_scanned_block_hash); void ResetDatabase(); + // Shard tree + base::expected GetCap(mojom::AccountIdPtr account_id); + base::expected PutCap(mojom::AccountIdPtr account_id, + OrchardCap cap); + + base::expected TruncateShards(mojom::AccountIdPtr account_id, + uint32_t shard_index); + base::expected, Error> GetLatestShardIndex( + mojom::AccountIdPtr account_id); + base::expected PutShard(mojom::AccountIdPtr account_id, + OrchardShard shard); + base::expected, Error> GetShard( + mojom::AccountIdPtr account_id, + OrchardShardAddress address); + base::expected, Error> LastShard( + mojom::AccountIdPtr account_id, + uint8_t shard_height); + + base::expected CheckpointCount(mojom::AccountIdPtr account_id); + base::expected, Error> MinCheckpointId( + mojom::AccountIdPtr account_id); + base::expected, Error> MaxCheckpointId( + mojom::AccountIdPtr account_id); + base::expected, Error> GetCheckpointAtDepth( + mojom::AccountIdPtr account_id, + uint32_t depth); + base::expected, Error> GetMaxCheckpointedHeight( + mojom::AccountIdPtr account_id, + uint32_t chain_tip_height, + uint32_t min_confirmations); + base::expected RemoveCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id); + base::expected TruncateCheckpoints( + mojom::AccountIdPtr account_id, + uint32_t checkpoint_id); + base::expected AddCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id, + OrchardCheckpoint checkpoint); + base::expected, Error> GetCheckpoints( + mojom::AccountIdPtr account_id, + size_t limit); + base::expected, Error> GetCheckpoint( + mojom::AccountIdPtr account_id, + uint32_t checkpoint_id); + base::expected>, Error> GetMarksRemoved( + mojom::AccountIdPtr account_id, + uint32_t checkpoint_id); + + base::expected UpdateSubtreeRoots( + mojom::AccountIdPtr account_id, + uint32_t start_index, + std::vector roots); + base::expected, Error> GetShardRoots( + mojom::AccountIdPtr account_id, + uint8_t shard_level); + private: + friend class base::RefCounted; + + ~ZCashOrchardStorage(); + bool EnsureDbInit(); bool CreateOrUpdateDatabase(); bool CreateSchema(); diff --git a/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc b/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc index 724743447e2e..f01c2d58be22 100644 --- a/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc +++ b/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc @@ -17,6 +17,14 @@ namespace brave_wallet { +namespace { + +OrchardShardAddress AddressFromOrchardShard(const OrchardShard& item) { + return item.address; +} + +} // namespace + class OrchardStorageTest : public testing::Test { public: OrchardStorageTest() @@ -25,14 +33,14 @@ class OrchardStorageTest : public testing::Test { base::test::TaskEnvironment task_environment_; base::ScopedTempDir temp_dir_; - std::unique_ptr orchard_storage_; + scoped_refptr orchard_storage_; }; void OrchardStorageTest::SetUp() { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); base::FilePath db_path( temp_dir_.GetPath().Append(FILE_PATH_LITERAL("orchard.db"))); - orchard_storage_ = std::make_unique(db_path); + orchard_storage_ = base::WrapRefCounted(new ZCashOrchardStorage(db_path)); } TEST_F(OrchardStorageTest, AccountMeta) { @@ -350,4 +358,512 @@ TEST_F(OrchardStorageTest, HandleChainReorg) { } } +TEST_F(OrchardStorageTest, Shards) {} + +namespace { + +OrchardShard CreateSubtreeRoot(size_t index, size_t level) { + OrchardShard orchard_shard; + orchard_shard.root_hash.fill(static_cast(index)); + orchard_shard.address.index = index; + orchard_shard.address.level = level; + orchard_shard.contains_marked = false; + return orchard_shard; +} + +OrchardShard CreateShard(size_t index, size_t level) { + OrchardShard orchard_shard; + orchard_shard.root_hash.fill(static_cast(index)); + orchard_shard.address.index = index; + orchard_shard.address.level = level; + orchard_shard.contains_marked = false; + orchard_shard.shard_data = std::vector({0, 0, 0, 0}); + return orchard_shard; +} + +} // namespace + +TEST_F(OrchardStorageTest, InsertSubtreeRoots) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + std::vector level_1_roots; + { + for (int i = 0; i < 10; i++) { + level_1_roots.push_back(CreateSubtreeRoot(i, 9)); + } + EXPECT_TRUE( + orchard_storage_->PutShardRoots(account_id.Clone(), level_1_roots) + .value()); + } + + { + std::vector level_1_addrs; + base::ranges::transform(level_1_roots, std::back_inserter(level_1_addrs), + AddressFromOrchardShard); + + auto result = orchard_storage_->GetShardRoots(account_id.Clone(), 9); + EXPECT_EQ(result.value(), level_1_addrs); + } +} + +TEST_F(OrchardStorageTest, TruncateSubtreeRoots) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + std::vector level_1_roots; + { + for (int i = 0; i < 10; i++) { + level_1_roots.push_back(CreateSubtreeRoot(i, 1)); + } + EXPECT_TRUE( + orchard_storage_->PutShardRoots(account_id.Clone(), level_1_roots) + .value()); + } + + EXPECT_TRUE(orchard_storage_->TruncateShards(account_id.Clone(), 5).value()); + { + std::vector level_1_addrs; + base::ranges::transform(level_1_roots, std::back_inserter(level_1_addrs), + AddressFromOrchardShard); + + auto result = orchard_storage_->GetShardRoots(account_id.Clone(), 1); + EXPECT_EQ(result.value(), + std::vector(level_1_addrs.begin(), + level_1_addrs.begin() + 5)); + } +} + +TEST_F(OrchardStorageTest, TruncateShards) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + { + for (uint32_t i = 0; i < 10; i++) { + EXPECT_TRUE( + orchard_storage_->PutShard(account_id.Clone(), CreateShard(i, 1)) + .value()); + } + } + + EXPECT_TRUE(orchard_storage_->TruncateShards(account_id.Clone(), 5).value()); + for (uint32_t i = 0; i < 5; i++) { + EXPECT_EQ(CreateShard(i, 1), + **(orchard_storage_->GetShard(account_id.Clone(), + OrchardShardAddress(1, i)))); + } + + EXPECT_EQ(std::nullopt, *(orchard_storage_->GetShard( + account_id.Clone(), OrchardShardAddress(1, 6)))); +} + +TEST_F(OrchardStorageTest, ShardOverridesSubtreeRoot) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + std::vector level_1_roots; + { + for (int i = 0; i < 10; i++) { + level_1_roots.push_back(CreateSubtreeRoot(i, 1)); + } + EXPECT_TRUE( + orchard_storage_->PutShardRoots(account_id.Clone(), level_1_roots) + .value()); + } + + // Update existing shard + OrchardShard new_shard; + new_shard.address.index = 5; + new_shard.address.level = 1; + new_shard.root_hash.fill(5); + new_shard.shard_data = std::vector({5, 5, 5, 5}); + EXPECT_TRUE( + orchard_storage_->PutShard(account_id.Clone(), new_shard).value()); + + auto result = + orchard_storage_->GetShard(account_id.Clone(), OrchardShardAddress{1, 5}); + EXPECT_EQ(*result.value(), new_shard); +} + +TEST_F(OrchardStorageTest, InsertShards) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + EXPECT_EQ(std::nullopt, + orchard_storage_->GetLatestShardIndex(account_id.Clone()).value()); + EXPECT_EQ( + std::nullopt, + orchard_storage_->GetShard(account_id.Clone(), OrchardShardAddress{1, 0}) + .value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->LastShard(account_id.Clone(), 1).value()); + + std::vector level_1_roots; + { + for (uint8_t i = 0; i < 10; i++) { + level_1_roots.push_back(CreateSubtreeRoot(i, 1)); + } + EXPECT_TRUE( + orchard_storage_->PutShardRoots(account_id.Clone(), level_1_roots) + .value()); + } + + OrchardShard new_shard; + new_shard.address.index = 11; + new_shard.address.level = 1; + new_shard.root_hash.fill(11); + new_shard.shard_data = std::vector({1, 1, 1, 1}); + + EXPECT_TRUE( + orchard_storage_->PutShard(account_id.Clone(), new_shard).value()); + + { + auto result = orchard_storage_->GetShard(account_id.Clone(), + OrchardShardAddress{1, 11}); + EXPECT_EQ(*result.value(), new_shard); + } + + { + for (uint8_t i = 0; i < 10; i++) { + auto result = orchard_storage_->GetShard(account_id.Clone(), + OrchardShardAddress{1, i}); + EXPECT_EQ(*result.value(), level_1_roots[i]); + } + } + + EXPECT_EQ(11u, orchard_storage_->GetLatestShardIndex(account_id.Clone()) + .value() + .value()); + EXPECT_EQ(new_shard, + orchard_storage_->LastShard(account_id.Clone(), 1).value()); +} + +TEST_F(OrchardStorageTest, RemoveChekpoint) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({4, 5, 6}); + checkpoint2.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + + EXPECT_TRUE( + orchard_storage_->RemoveCheckpoint(account_id.Clone(), 1).value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value()); + EXPECT_EQ( + OrchardCheckpointBundle(2, checkpoint2), + orchard_storage_->GetCheckpoint(account_id.Clone(), 2).value().value()); +} + +TEST_F(OrchardStorageTest, CheckpointId) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + EXPECT_EQ(std::nullopt, + orchard_storage_->MinCheckpointId(account_id.Clone()).value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->MaxCheckpointId(account_id.Clone()).value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({1, 2, 3}); + checkpoint2.tree_state_position = 2; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + + OrchardCheckpoint checkpoint3; + checkpoint3.marks_removed = std::vector({5}); + checkpoint3.tree_state_position = 3; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint3) + .value()); + + OrchardCheckpoint checkpoint4; + checkpoint4.marks_removed = std::vector(); + checkpoint4.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 4, checkpoint4) + .value()); + + EXPECT_EQ(1, orchard_storage_->MinCheckpointId(account_id.Clone()).value()); + EXPECT_EQ(4, orchard_storage_->MaxCheckpointId(account_id.Clone()).value()); +} + +TEST_F(OrchardStorageTest, CheckpointAtPosition) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({4, 5, 6}); + checkpoint2.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + OrchardCheckpoint checkpoint3; + checkpoint3.marks_removed = std::vector({7, 8, 9}); + checkpoint3.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint3) + .value()); + + EXPECT_EQ(1u, orchard_storage_->GetCheckpointAtDepth(account_id.Clone(), 2) + .value() + .value()); + EXPECT_EQ( + std::nullopt, + orchard_storage_->GetCheckpointAtDepth(account_id.Clone(), 5).value()); +} + +TEST_F(OrchardStorageTest, TruncateCheckpoints_OutOfBoundry) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + EXPECT_TRUE( + orchard_storage_->TruncateCheckpoints(account_id.Clone(), 3).value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint1), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); +} + +TEST_F(OrchardStorageTest, TruncateCheckpoints) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({1, 2, 3}); + checkpoint2.tree_state_position = 2; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + + OrchardCheckpoint checkpoint3; + checkpoint3.marks_removed = std::vector({5}); + checkpoint3.tree_state_position = 3; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint3) + .value()); + + OrchardCheckpoint checkpoint4; + checkpoint4.marks_removed = std::vector(); + checkpoint4.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 4, checkpoint4) + .value()); + + EXPECT_TRUE( + orchard_storage_->TruncateCheckpoints(account_id.Clone(), 3).value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint1), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); + EXPECT_EQ( + OrchardCheckpointBundle(2, checkpoint2), + orchard_storage_->GetCheckpoint(account_id.Clone(), 2).value().value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->GetCheckpoint(account_id.Clone(), 3).value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->GetCheckpoint(account_id.Clone(), 4).value()); +} + +TEST_F(OrchardStorageTest, AddCheckpoint) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({4, 5, 6}); + checkpoint2.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + OrchardCheckpoint checkpoint3; + checkpoint3.marks_removed = std::vector(); + checkpoint3.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint3) + .value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint1), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); + EXPECT_EQ( + OrchardCheckpointBundle(2, checkpoint2), + orchard_storage_->GetCheckpoint(account_id.Clone(), 2).value().value()); + EXPECT_EQ( + OrchardCheckpointBundle(3, checkpoint3), + orchard_storage_->GetCheckpoint(account_id.Clone(), 3).value().value()); +} + +TEST_F(OrchardStorageTest, AddSameCheckpoint) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + { + OrchardCheckpoint checkpoint; + checkpoint.marks_removed = std::vector({1, 2, 3}); + checkpoint.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint) + .value()); + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint) + .value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); + } + + { + OrchardCheckpoint checkpoint; + checkpoint.marks_removed = std::vector({1, 2, 3}); + checkpoint.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint) + .value()); + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint) + .value()); + + EXPECT_EQ( + OrchardCheckpointBundle(2, checkpoint), + orchard_storage_->GetCheckpoint(account_id.Clone(), 2).value().value()); + } + + { + OrchardCheckpoint checkpoint; + checkpoint.marks_removed = std::vector(); + checkpoint.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint) + .value()); + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint) + .value()); + + EXPECT_EQ( + OrchardCheckpointBundle(3, checkpoint), + orchard_storage_->GetCheckpoint(account_id.Clone(), 3).value().value()); + } +} + +TEST_F(OrchardStorageTest, AddChekpoint_ErrorOnConflict) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE(orchard_storage_->RegisterAccount(account_id.Clone(), 100, "hash") + .has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + OrchardCheckpoint checkpoint_different_marks_removed = checkpoint1; + checkpoint_different_marks_removed.marks_removed = + std::vector({1, 2}); + EXPECT_FALSE(orchard_storage_ + ->AddCheckpoint(account_id.Clone(), 1, + checkpoint_different_marks_removed) + .has_value()); + + OrchardCheckpoint checkpoint_different_position1 = checkpoint1; + checkpoint_different_position1.tree_state_position = 7; + EXPECT_FALSE( + orchard_storage_ + ->AddCheckpoint(account_id.Clone(), 1, checkpoint_different_position1) + .has_value()); + + OrchardCheckpoint checkpoint_different_position2 = checkpoint1; + checkpoint_different_position2.tree_state_position = std::nullopt; + EXPECT_FALSE( + orchard_storage_ + ->AddCheckpoint(account_id.Clone(), 1, checkpoint_different_position2) + .has_value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint1), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_orchard_sync_state.cc b/components/brave_wallet/browser/zcash/zcash_orchard_sync_state.cc new file mode 100644 index 000000000000..beadf72bcfac --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_orchard_sync_state.cc @@ -0,0 +1,307 @@ +/* Copyright (c) 2024 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_orchard_sync_state.h" + +#include "base/containers/extend.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet { + +namespace { + +OrchardShardTreeDelegate::Error From(ZCashOrchardStorage::Error) { + return OrchardShardTreeDelegate::Error::kStorageError; +} + +// std::optional> GetRootHash( +// const zcash::mojom::SubtreeRootPtr& root) { +// if (root->root_hash.size() != kOrchardShardTreeHashSize) { +// return std::nullopt; +// } +// std::array result; +// base::ranges::copy(root->root_hash, result.begin()); +// return result; +// } + +} // namespace + +class OrchardShardTreeDelegateImpl : public OrchardShardTreeDelegate { + public: + OrchardShardTreeDelegateImpl(mojom::AccountIdPtr account_id, + scoped_refptr storage) + : account_id_(std::move(account_id)), storage_(storage) {} + + ~OrchardShardTreeDelegateImpl() override {} + + base::expected, Error> GetCap() const override { + auto result = storage_->GetCap(account_id_.Clone()); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(*result); + } + + base::expected PutCap(OrchardCap cap) override { + auto result = storage_->PutCap(account_id_.Clone(), cap); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return *result; + } + + base::expected Truncate(uint32_t block_height) override { + auto result = storage_->TruncateShards(account_id_.Clone(), block_height); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return *result; + } + + base::expected, Error> GetLatestShardIndex() + const override { + auto result = storage_->GetLatestShardIndex(account_id_.Clone()); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return *result; + } + + base::expected PutShard(OrchardShard shard) override { + auto result = storage_->PutShard(account_id_.Clone(), shard); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return *result; + } + + base::expected, Error> GetShard( + OrchardShardAddress address) const override { + auto result = storage_->GetShard(account_id_.Clone(), address); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(*result); + } + + base::expected, Error> LastShard( + uint8_t shard_height) const override { + auto result = storage_->LastShard(account_id_.Clone(), shard_height); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(*result); + } + + base::expected CheckpointCount() const override { + auto result = storage_->CheckpointCount(account_id_.Clone()); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return *result; + } + + base::expected, Error> MinCheckpointId() + const override { + auto result = storage_->MinCheckpointId(account_id_.Clone()); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected, Error> MaxCheckpointId() + const override { + auto result = storage_->MaxCheckpointId(account_id_.Clone()); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected, Error> GetCheckpointAtDepth( + uint32_t depth) const override { + auto result = storage_->GetCheckpointAtDepth(account_id_.Clone(), depth); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected, Error> GetCheckpoint( + uint32_t checkpoint_id) const override { + auto result = storage_->GetCheckpoint(account_id_.Clone(), checkpoint_id); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected, Error> GetCheckpoints( + size_t limit) const override { + auto result = storage_->GetCheckpoints(account_id_.Clone(), limit); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected AddCheckpoint( + uint32_t id, + OrchardCheckpoint checkpoint) override { + auto result = storage_->AddCheckpoint(account_id_.Clone(), id, checkpoint); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected TruncateCheckpoints( + uint32_t checkpoint_id) override { + auto result = + storage_->TruncateCheckpoints(account_id_.Clone(), checkpoint_id); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected RemoveCheckpoint( + uint32_t checkpoint_id) override { + auto result = + storage_->RemoveCheckpoint(account_id_.Clone(), checkpoint_id); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected RemoveCheckpointAt(uint32_t depth) override { + return false; + } + + base::expected, Error> GetShardRoots( + uint8_t shard_level) const override { + auto result = storage_->GetShardRoots(account_id_.Clone(), shard_level); + if (!result.has_value()) { + return base::unexpected(From(result.error())); + } + return std::move(result.value()); + } + + base::expected UpdateCheckpoint( + uint32_t id, + OrchardCheckpoint checkpoint) override { + // RemoveCheckpoint(id); + // AddCheckpoint(id, checkpoint); + return false; + } + + private: + mojom::AccountIdPtr account_id_; + scoped_refptr storage_; +}; + +ZCashOrchardSyncState::ZCashOrchardSyncState(base::FilePath path_to_database) { + storage_ = base::MakeRefCounted(path_to_database); +} + +ZCashOrchardSyncState::~ZCashOrchardSyncState() {} + +OrchardShardTreeManager* ZCashOrchardSyncState::GetOrCreateShardTreeManager( + const mojom::AccountIdPtr& account_id) { + if (shard_tree_managers_.find(account_id) == shard_tree_managers_.end()) { + shard_tree_managers_[account_id.Clone()] = OrchardShardTreeManager::Create( + std::make_unique(account_id.Clone(), + storage_)); + } + return shard_tree_managers_[account_id.Clone()].get(); +} + +base::expected +ZCashOrchardSyncState::RegisterAccount( + mojom::AccountIdPtr account_id, + uint64_t account_birthday_block, + const std::string& account_bithday_block_hash) { + return storage_->RegisterAccount(std::move(account_id), + account_birthday_block, + account_bithday_block_hash); +} + +base::expected +ZCashOrchardSyncState::GetAccountMeta(mojom::AccountIdPtr account_id) { + return storage_->GetAccountMeta(std::move(account_id)); +} + +std::optional +ZCashOrchardSyncState::HandleChainReorg(mojom::AccountIdPtr account_id, + uint32_t reorg_block_id, + const std::string& reorg_block_hash) { + return storage_->HandleChainReorg(std::move(account_id), reorg_block_id, + reorg_block_hash); +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardSyncState::GetSpendableNotes(mojom::AccountIdPtr account_id) { + return storage_->GetSpendableNotes(std::move(account_id)); +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardSyncState::GetNullifiers(mojom::AccountIdPtr account_id) { + return storage_->GetNullifiers(std::move(account_id)); +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardSyncState::GetLatestShardIndex(mojom::AccountIdPtr account_id) { + return storage_->GetLatestShardIndex(std::move(account_id)); +} + +base::expected +ZCashOrchardSyncState::UpdateSubtreeRoots( + mojom::AccountIdPtr account_id, + uint32_t start_index, + std::vector roots) { + return storage_->UpdateSubtreeRoots(std::move(account_id), start_index, + std::move(roots)); +} + +std::optional ZCashOrchardSyncState::UpdateNotes( + mojom::AccountIdPtr account_id, + OrchardBlockScanner::Result block_scanner_results, + const uint32_t latest_scanned_block, + const std::string& latest_scanned_block_hash) { + auto existing_notes = storage_->GetSpendableNotes(account_id.Clone()); + if (!existing_notes.has_value()) { + return existing_notes.error(); + } + + std::vector notes_to_add = + block_scanner_results.discovered_notes; + base::Extend(existing_notes.value(), notes_to_add); + + std::vector nf_to_add; + + for (const auto& nf : block_scanner_results.found_nullifiers) { + if (std::find_if(existing_notes.value().begin(), + existing_notes.value().end(), [&nf](const auto& v) { + return v.nullifier == nf.nullifier; + }) != existing_notes.value().end()) { + nf_to_add.push_back(nf); + } + } + + GetOrCreateShardTreeManager(account_id.Clone()) + ->InsertCommitments(std::move(block_scanner_results)); + + return storage_->UpdateNotes(std::move(account_id), notes_to_add, + std::move(nf_to_add), latest_scanned_block, + latest_scanned_block_hash); +} + +void ZCashOrchardSyncState::ResetDatabase() { + storage_->ResetDatabase(); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_orchard_sync_state.h b/components/brave_wallet/browser/zcash/zcash_orchard_sync_state.h new file mode 100644 index 000000000000..d92edcca5368 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_orchard_sync_state.h @@ -0,0 +1,62 @@ +/* Copyright (c) 2024 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 + +#include "brave/components/brave_wallet/browser/internal/orchard_shard_tree_manager.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_orchard_storage.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet { + +class ZCashOrchardSyncState { + public: + ZCashOrchardSyncState(base::FilePath path_to_database); + ~ZCashOrchardSyncState(); + + base::expected + RegisterAccount(mojom::AccountIdPtr account_id, + uint64_t account_birthday_block, + const std::string& account_bithday_block_hash); + + base::expected + GetAccountMeta(mojom::AccountIdPtr account_id); + + std::optional HandleChainReorg( + mojom::AccountIdPtr account_id, + uint32_t reorg_block_id, + const std::string& reorg_block_hash); + + base::expected, ZCashOrchardStorage::Error> + GetSpendableNotes(mojom::AccountIdPtr account_id); + + base::expected, ZCashOrchardStorage::Error> + GetNullifiers(mojom::AccountIdPtr account_id); + + std::optional UpdateNotes( + mojom::AccountIdPtr account_id, + OrchardBlockScanner::Result block_scanner_results, + const uint32_t latest_scanned_block, + const std::string& latest_scanned_block_hash); + + void ResetDatabase(); + + base::expected, ZCashOrchardStorage::Error> + GetLatestShardIndex(mojom::AccountIdPtr account_id); + base::expected UpdateSubtreeRoots( + mojom::AccountIdPtr account_id, + uint32_t start_index, + std::vector roots); + + private: + OrchardShardTreeManager* GetOrCreateShardTreeManager( + const mojom::AccountIdPtr& account_id); + + scoped_refptr storage_; + std::map> + shard_tree_managers_; +}; + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_rpc.cc b/components/brave_wallet/browser/zcash/zcash_rpc.cc index 34c512a745b8..449ad1b6ed80 100644 --- a/components/brave_wallet/browser/zcash/zcash_rpc.cc +++ b/components/brave_wallet/browser/zcash/zcash_rpc.cc @@ -259,6 +259,23 @@ const GURL MakeGetCompactBlocksURL(const GURL& base_url) { return base_url.ReplaceComponents(replacements); } +const GURL MakeGetSubtreeRootsURL(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/GetSubtreeRoots"}); + replacements.SetPathStr(path); + + return base_url.ReplaceComponents(replacements); +} + std::string MakeGetTreeStateURLParams( const zcash::mojom::BlockIDPtr& block_id) { ::zcash::BlockID request; @@ -336,6 +353,16 @@ std::string MakeGetCompactBlocksParams(uint32_t block_start, return GetPrefixedProtobuf(range.SerializeAsString()); } +std::string MakeGetSubtreeRootsParams(uint32_t start, uint32_t entries) { + ::zcash::GetSubtreeRootsArg arg; + + arg.set_startindex(start); + arg.set_maxentries(entries); + arg.set_shieldedprotocol(::zcash::ShieldedProtocol::orchard); + + return GetPrefixedProtobuf(arg.SerializeAsString()); +} + std::unique_ptr MakeGRPCLoader( const GURL& url, const std::string& body) { @@ -517,6 +544,36 @@ void ZCashRpc::GetCompactBlocks(const std::string& chain_id, (*it)->DownloadAsStream(url_loader_factory_.get(), handler_it->get()); } +void ZCashRpc::GetSubtreeRoots(const std::string& chain_id, + uint32_t start, + uint32_t entries, + GetSubtreeRootsCallback callback) { + GURL request_url = MakeGetSubtreeRootsURL(GetNetworkURL(chain_id)); + + if (!request_url.is_valid()) { + std::move(callback).Run( + base::unexpected(l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR))); + return; + } + + auto url_loader = + MakeGRPCLoader(request_url, MakeGetSubtreeRootsParams(start, entries)); + + UrlLoadersList::iterator it = url_loaders_list_.insert( + url_loaders_list_.begin(), std::move(url_loader)); + + StreamHandlersList::iterator handler_it = stream_handlers_list_.insert( + stream_handlers_list_.begin(), + std::make_unique()); + + static_cast(handler_it->get()) + ->set_callback(base::BindOnce(&ZCashRpc::OnGetSubtreeRootsResponse, + weak_ptr_factory_.GetWeakPtr(), + std::move(callback), it, handler_it)); + + (*it)->DownloadAsStream(url_loader_factory_.get(), handler_it->get()); +} + void ZCashRpc::OnGetCompactBlocksResponse( ZCashRpc::GetCompactBlocksCallback callback, UrlLoadersList::iterator it, @@ -537,6 +594,26 @@ void ZCashRpc::OnGetCompactBlocksResponse( weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } +void ZCashRpc::OnGetSubtreeRootsResponse( + ZCashRpc::GetSubtreeRootsCallback callback, + UrlLoadersList::iterator it, + StreamHandlersList::iterator handler_it, + base::expected, std::string> result) { + url_loaders_list_.erase(it); + stream_handlers_list_.erase(handler_it); + + if (!result.has_value()) { + std::move(callback).Run( + base::unexpected(l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR))); + return; + } + + GetDecoder()->ParseSubtreeRoots( + *result, + base::BindOnce(&ZCashRpc::OnParseSubtreeRoots, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + void ZCashRpc::OnGetUtxosResponse(ZCashRpc::GetUtxoListCallback callback, UrlLoadersList::iterator it, std::unique_ptr response_body) { @@ -571,6 +648,16 @@ void ZCashRpc::OnParseCompactBlocks( } } +void ZCashRpc::OnParseSubtreeRoots( + GetSubtreeRootsCallback callback, + std::optional> subtree_roots) { + if (subtree_roots) { + std::move(callback).Run(std::move(subtree_roots.value())); + } else { + std::move(callback).Run(base::unexpected("Cannot parse subtree roots")); + } +} + template void ZCashRpc::OnParseResult( base::OnceCallback)> callback, diff --git a/components/brave_wallet/browser/zcash/zcash_rpc.h b/components/brave_wallet/browser/zcash/zcash_rpc.h index 3234c255ad00..f657932d5d53 100644 --- a/components/brave_wallet/browser/zcash/zcash_rpc.h +++ b/components/brave_wallet/browser/zcash/zcash_rpc.h @@ -44,6 +44,8 @@ class ZCashRpc { base::expected)>; using GetCompactBlocksCallback = base::OnceCallback, std::string>)>; + using GetSubtreeRootsCallback = base::OnceCallback, std::string>)>; ZCashRpc(NetworkManager* network_manager, scoped_refptr url_loader_factory); @@ -81,6 +83,11 @@ class ZCashRpc { uint32_t to, GetCompactBlocksCallback callback); + virtual void GetSubtreeRoots(const std::string& chain_id, + uint32_t start, + uint32_t entries, + GetSubtreeRootsCallback); + private: friend class base::RefCountedThreadSafe; @@ -119,6 +126,12 @@ class ZCashRpc { StreamHandlersList::iterator handler_it, base::expected, std::string> result); + void OnGetSubtreeRootsResponse( + ZCashRpc::GetSubtreeRootsCallback callback, + UrlLoadersList::iterator it, + StreamHandlersList::iterator handler_it, + base::expected, std::string> result); + template void OnParseResult(base::OnceCallback)>, T value); @@ -127,6 +140,10 @@ class ZCashRpc { GetCompactBlocksCallback callback, std::optional> compact_blocks); + void OnParseSubtreeRoots( + GetSubtreeRootsCallback callback, + std::optional> subtree_roots); + mojo::AssociatedRemote& GetDecoder(); GURL GetNetworkURL(const std::string& chain_id); diff --git a/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.cc b/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.cc new file mode 100644 index 000000000000..0175a5085c66 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.cc @@ -0,0 +1,130 @@ +// Copyright (c) 2024 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 http://mozilla.org/MPL/2.0/. + +#include + +#include "brave/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.h" + +namespace brave_wallet { + +namespace { +constexpr uint32_t kBatchSize = 1024; +} // namespace + +ZCashScanBlocksTask::ZCashScanBlocksTask( + ZCashShieldSyncService* sync_service, + ZCashScanBlocksTaskObserver observer) : sync_service_(sync_service), observer_(std::move(observer)) { +} + +ZCashScanBlocksTask::~ZCashScanBlocksTask() { +} + +void ZCashScanBlocksTask::Start() { + ScheduleWorkOnTask(); +} + +void ZCashScanBlocksTask::ScheduleWorkOnTask() { + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&ZCashScanBlocksTask::WorkOnTask, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashScanBlocksTask::WorkOnTask() { + if (error_) { + observer_.Run(base::unexpected(*error_)); + return; + } + + if (!latest_scanned_block_) { + GetLatestScannedBlock(); + return; + } + + if (!chain_tip_block_) { + GetChainTip(); + return; + } + + if (!scan_ranges_) { + PrepareScanRanges(); + return; + } + + ScanRanges(); +} + +void ZCashScanBlocksTask::PrepareScanRanges() { + uint32_t from = latest_scanned_block_.value() + 1; + uint32_t to = chain_tip_block_.value(); + initial_ranges_count_ = std::ceil((double)(to - from) / kBatchSize); + scan_ranges_ = std::deque(); + for (size_t i = 0; i < initial_ranges_count_.value(); i++) { + scan_ranges_->push_back(ScanRange{ static_cast(from + i * kBatchSize), std::min (to, static_cast(from + (i + 1)* kBatchSize)) }); + } +} + +void ZCashScanBlocksTask::GetLatestScannedBlock() { + sync_service_->sync_state_.AsyncCall(&ZCashOrchardSyncState::GetAccountMeta) + .WithArgs(sync_service_->account_id_.Clone()) + .Then(base::BindOnce(&ZCashScanBlocksTask::OnGetAccountMeta, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashScanBlocksTask::OnGetAccountMeta( base::expected + result) { + if (!result.has_value()) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kFailedToRetrieveAccount, ""}; + ScheduleWorkOnTask(); + return; + } + + latest_scanned_block_ = result.value().latest_scanned_block_id; + ScheduleWorkOnTask(); +} + +void ZCashScanBlocksTask::GetChainTip() { + sync_service_->zcash_rpc_->GetLatestBlock( + sync_service_->chain_id_, base::BindOnce(&ZCashScanBlocksTask::OnGetChainTip, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashScanBlocksTask::OnGetChainTip( + base::expected result) { + if (!result.has_value()) { + error_ = ZCashShieldSyncService::Error{ ZCashShieldSyncService::ErrorCode::kFailedToUpdateChainTip, result.error() }; + ScheduleWorkOnTask(); + return; + } + + chain_tip_block_ = (*result)->height; + ScheduleWorkOnTask(); +} + + +void ZCashScanBlocksTask::ScanRanges() { + CHECK(scan_ranges_); + if (scan_ranges_->empty()) { + return; + } + auto scan_range = scan_ranges_->front(); + scan_ranges_->pop_front(); + current_block_range_ = std::make_unique( + sync_service_, scan_range.from, scan_range.to, + base::BindOnce(&ZCashScanBlocksTask::OnScanningRangeComplete, + weak_ptr_factory_.GetWeakPtr())); + current_block_range_->Start(); +} + +void ZCashScanBlocksTask::OnScanningRangeComplete(base::expected result) { + if (!result.has_value()) { + error_ = result.error(); + ScheduleWorkOnTask(); + return; + } + + ScanRanges(); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.h b/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.h new file mode 100644 index 000000000000..4a4c5c5420ee --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.h @@ -0,0 +1,59 @@ +// Copyright (c) 2024 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 http://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_SCAN_BLOCKS_TASK_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_SCAN_BLOCKS_TASK_H_ + +#include "brave/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" + +namespace brave_wallet { + +class ZCashScanBlocksTask { + public: + using ZCashScanBlocksTaskObserver = + base::RepeatingCallback)>; + + ZCashScanBlocksTask(ZCashShieldSyncService* sync_service, ZCashScanBlocksTaskObserver observer); + ~ZCashScanBlocksTask(); + + void Start(); + + private: + struct ScanRange { + uint32_t from; + uint32_t to; + }; + void ScheduleWorkOnTask(); + void WorkOnTask(); + + void GetLatestScannedBlock(); + void OnGetAccountMeta(base::expected + result); + + void GetChainTip(); + void OnGetChainTip(base::expected result); + + void PrepareScanRanges(); + + void ScanRanges(); + void OnScanningRangeComplete(base::expected result); + + raw_ptr sync_service_; + ZCashScanBlocksTaskObserver observer_; + + std::optional error_; + std::optional latest_scanned_block_; + std::optional chain_tip_block_; + std::optional> scan_ranges_; + std::optional initial_ranges_count_; + std::unique_ptr current_block_range_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_SCAN_BLOCKS_TASK_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_shield_sync_service.cc b/components/brave_wallet/browser/zcash/zcash_shield_sync_service.cc index eaf114185ff5..4a5475a76e39 100644 --- a/components/brave_wallet/browser/zcash/zcash_shield_sync_service.cc +++ b/components/brave_wallet/browser/zcash/zcash_shield_sync_service.cc @@ -9,6 +9,9 @@ #include #include "base/task/thread_pool.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.h" #include "brave/components/brave_wallet/common/common_utils.h" #include "brave/components/brave_wallet/common/hex_utils.h" #include "brave/components/brave_wallet/common/zcash_utils.h" @@ -33,8 +36,14 @@ size_t GetCode(ZCashShieldSyncService::ErrorCode error) { return 5; case ZCashShieldSyncService::ErrorCode::kFailedToRetrieveAccount: return 6; - case ZCashShieldSyncService::ErrorCode::kScannerError: + case ZCashShieldSyncService::ErrorCode::kFailedToVerifyChainState: return 7; + case ZCashShieldSyncService::ErrorCode::kFailedToUpdateSubtreeRoots: + return 8; + case ZCashShieldSyncService::ErrorCode::kDatabaseError: + return 9; + case ZCashShieldSyncService::ErrorCode::kScannerError: + return 10; } } @@ -51,13 +60,13 @@ ZCashShieldSyncService::OrchardBlockScannerProxy::~OrchardBlockScannerProxy() = default; void ZCashShieldSyncService::OrchardBlockScannerProxy::ScanBlocks( - std::vector known_notes, + FrontierChainState frontier_chain_state, std::vector blocks, base::OnceCallback)> callback) { background_block_scanner_.AsyncCall(&OrchardBlockScanner::ScanBlocks) - .WithArgs(std::move(known_notes), std::move(blocks)) + .WithArgs(std::move(frontier_chain_state), std::move(blocks)) .Then(std::move(callback)); } @@ -75,7 +84,7 @@ ZCashShieldSyncService::ZCashShieldSyncService( observer_(std::move(observer)) { chain_id_ = GetNetworkForZCashKeyring(account_id->keyring_id); block_scanner_ = std::make_unique(fvk); - background_orchard_storage_.emplace( + sync_state_.emplace( base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), db_dir_path_); } @@ -118,48 +127,42 @@ void ZCashShieldSyncService::WorkOnTask() { return; } - if (!chain_tip_block_) { - UpdateChainTip(); - return; - } - if (!account_meta_) { GetOrCreateAccount(); return; } - if (!latest_scanned_block_) { - VerifyChainState(*account_meta_); + if (!chain_state_verified_) { + VerifyChainState(); return; } - if (!spendable_notes_) { - UpdateSpendableNotes(); + if (!subtree_roots_updated_) { + UpdateSubtreeRoots(); return; } - if (observer_) { - observer_->OnSyncStatusUpdate(account_id_, current_sync_status_.Clone()); - } - - if (!downloaded_blocks_ && *latest_scanned_block_ < *chain_tip_block_) { - DownloadBlocks(); + if (!spendable_notes_) { + UpdateSpendableNotes(); return; } - if (downloaded_blocks_) { - ScanBlocks(); + if (!block_scanner_) { + StartBlockScanning(); return; } - if (observer_) { - observer_->OnSyncStop(account_id_); - stopped_ = true; + if (latest_scanned_block_result_ && + latest_scanned_block_result_->IsFinished()) { + if (observer_) { + observer_->OnSyncStop(account_id_); + stopped_ = true; + } } } void ZCashShieldSyncService::GetOrCreateAccount() { - background_orchard_storage_.AsyncCall(&ZCashOrchardStorage::GetAccountMeta) + sync_state_.AsyncCall(&ZCashOrchardSyncState::GetAccountMeta) .WithArgs(account_id_.Clone()) .Then(base::BindOnce(&ZCashShieldSyncService::OnGetAccountMeta, weak_ptr_factory_.GetWeakPtr())); @@ -189,7 +192,7 @@ void ZCashShieldSyncService::OnGetAccountMeta( } void ZCashShieldSyncService::InitAccount() { - background_orchard_storage_.AsyncCall(&ZCashOrchardStorage::RegisterAccount) + sync_state_.AsyncCall(&ZCashOrchardSyncState::RegisterAccount) .WithArgs(account_id_.Clone(), account_birthday_->value, account_birthday_->hash) .Then(base::BindOnce(&ZCashShieldSyncService::OnAccountInit, @@ -207,99 +210,9 @@ void ZCashShieldSyncService::OnAccountInit( ScheduleWorkOnTask(); } -void ZCashShieldSyncService::VerifyChainState( - ZCashOrchardStorage::AccountMeta account_meta) { - // If block chain has removed blocks we already scanned then we need to handle - // chain reorg. - if (*chain_tip_block_ < account_meta.latest_scanned_block_id) { - // Assume that chain reorg can't affect more than kChainReorgBlockDelta - // blocks So we can just fallback on this number from the chain tip block. - GetTreeStateForChainReorg(*chain_tip_block_ - kChainReorgBlockDelta); - return; - } - // Retrieve block info for last scanned block id to check whether block hash - // is the same - auto block_id = zcash::mojom::BlockID::New( - account_meta.latest_scanned_block_id, std::vector()); - zcash_rpc_->GetTreeState( - chain_id_, std::move(block_id), - base::BindOnce( - &ZCashShieldSyncService::OnGetTreeStateForChainVerification, - weak_ptr_factory_.GetWeakPtr(), std::move(account_meta))); -} - -void ZCashShieldSyncService::OnGetTreeStateForChainVerification( - ZCashOrchardStorage::AccountMeta account_meta, - base::expected tree_state) { - if (!tree_state.has_value() || !tree_state.value()) { - error_ = Error{ErrorCode::kFailedToReceiveTreeState, tree_state.error()}; - ScheduleWorkOnTask(); - return; - } - auto backend_block_hash = RevertHex(tree_state.value()->hash); - if (backend_block_hash != account_meta.latest_scanned_block_hash) { - // Assume that chain reorg can't affect more than kChainReorgBlockDelta - // blocks So we can just fallback on this number. - uint32_t new_block_id = - account_meta.latest_scanned_block_id > kChainReorgBlockDelta - ? account_meta.latest_scanned_block_id - kChainReorgBlockDelta - : 0; - GetTreeStateForChainReorg(new_block_id); - return; - } - - // Restore latest scanned block from the database so we can continue - // scanning from previous point. - latest_scanned_block_ = account_meta.latest_scanned_block_id; - ScheduleWorkOnTask(); -} - -void ZCashShieldSyncService::GetTreeStateForChainReorg( - uint32_t new_block_height) { - // Query block info by block height - auto block_id = - zcash::mojom::BlockID::New(new_block_height, std::vector()); - zcash_rpc_->GetTreeState( - chain_id_, std::move(block_id), - base::BindOnce(&ZCashShieldSyncService::OnGetTreeStateForChainReorg, - weak_ptr_factory_.GetWeakPtr(), new_block_height)); -} - -void ZCashShieldSyncService::OnGetTreeStateForChainReorg( - uint32_t new_block_height, - base::expected tree_state) { - if (!tree_state.has_value() || !tree_state.value() || - new_block_height != (*tree_state)->height) { - error_ = Error{ErrorCode::kFailedToReceiveTreeState, tree_state.error()}; - ScheduleWorkOnTask(); - return; - } else { - // Reorg database so records related to removed blocks are wiped out - background_orchard_storage_ - .AsyncCall(&ZCashOrchardStorage::HandleChainReorg) - .WithArgs(account_id_.Clone(), (*tree_state)->height, - (*tree_state)->hash) - .Then(base::BindOnce( - &ZCashShieldSyncService::OnDatabaseUpdatedForChainReorg, - weak_ptr_factory_.GetWeakPtr(), (*tree_state)->height)); - } -} - -void ZCashShieldSyncService::OnDatabaseUpdatedForChainReorg( - uint32_t new_block_height, - std::optional error) { - if (error) { - error_ = Error{ErrorCode::kFailedToUpdateDatabase, error->message}; - ScheduleWorkOnTask(); - return; - } - - latest_scanned_block_ = new_block_height; - ScheduleWorkOnTask(); -} - void ZCashShieldSyncService::UpdateSpendableNotes() { - background_orchard_storage_.AsyncCall(&ZCashOrchardStorage::GetSpendableNotes) + spendable_notes_ = std::nullopt; + sync_state_.AsyncCall(&ZCashOrchardSyncState::GetSpendableNotes) .WithArgs(account_id_.Clone()) .Then(base::BindOnce(&ZCashShieldSyncService::OnGetSpendableNotes, weak_ptr_factory_.GetWeakPtr())); @@ -311,105 +224,97 @@ void ZCashShieldSyncService::OnGetSpendableNotes( if (!result.has_value()) { error_ = Error{ErrorCode::kFailedToRetrieveSpendableNotes, result.error().message}; + ScheduleWorkOnTask(); + return; + } + + spendable_notes_ = result.value(); + + if (latest_scanned_block_result_) { + current_sync_status_ = mojom::ZCashShieldSyncStatus::New( + latest_scanned_block_result_->start_block, + latest_scanned_block_result_->end_block, + latest_scanned_block_result_->total_ranges, + latest_scanned_block_result_->ready_ranges, spendable_notes_->size(), + GetSpendableBalance()); } else { - spendable_notes_ = result.value(); current_sync_status_ = mojom::ZCashShieldSyncStatus::New( - latest_scanned_block_.value(), chain_tip_block_.value(), + latest_scanned_block_.value(), latest_scanned_block_.value(), 0, 0, spendable_notes_->size(), GetSpendableBalance()); } - ScheduleWorkOnTask(); -} -void ZCashShieldSyncService::UpdateChainTip() { - zcash_rpc_->GetLatestBlock( - chain_id_, base::BindOnce(&ZCashShieldSyncService::OnGetLatestBlock, - weak_ptr_factory_.GetWeakPtr())); -} - -void ZCashShieldSyncService::OnGetLatestBlock( - base::expected result) { - if (!result.has_value()) { - error_ = Error{ErrorCode::kFailedToUpdateChainTip, result.error()}; - } else { - chain_tip_block_ = (*result)->height; + if (observer_) { + observer_->OnSyncStatusUpdate(account_id_, current_sync_status_.Clone()); } + ScheduleWorkOnTask(); } -void ZCashShieldSyncService::DownloadBlocks() { - zcash_rpc_->GetCompactBlocks( - chain_id_, *latest_scanned_block_ + 1, - std::min(*chain_tip_block_, *latest_scanned_block_ + kScanBatchSize), - base::BindOnce(&ZCashShieldSyncService::OnBlocksDownloaded, - weak_ptr_factory_.GetWeakPtr())); +void ZCashShieldSyncService::VerifyChainState() { + CHECK(!verify_chain_state_task_.get()); + verify_chain_state_task_ = std::make_unique( + this, base::BindOnce(&ZCashShieldSyncService::OnChainStateVerified, + weak_ptr_factory_.GetWeakPtr())); + verify_chain_state_task_->Start(); } -void ZCashShieldSyncService::OnBlocksDownloaded( - base::expected, std::string> - result) { +void ZCashShieldSyncService::OnChainStateVerified( + base::expected result) { if (!result.has_value()) { - error_ = Error{ErrorCode::kFailedToDownloadBlocks, result.error()}; - } else { - downloaded_blocks_ = std::move(result.value()); + error_ = result.error(); + ScheduleWorkOnTask(); + return; } - ScheduleWorkOnTask(); -} -void ZCashShieldSyncService::ScanBlocks() { - if (!downloaded_blocks_ || downloaded_blocks_->empty()) { - error_ = Error{ErrorCode::kScannerError, ""}; + if (!result.value()) { + error_ = Error{ErrorCode::kFailedToVerifyChainState, ""}; ScheduleWorkOnTask(); return; } - auto last_block_hash = ToHex(downloaded_blocks_->back()->hash); - auto last_block_height = downloaded_blocks_->back()->height; + chain_state_verified_ = true; + ScheduleWorkOnTask(); +} - block_scanner_->ScanBlocks( - *spendable_notes_, std::move(downloaded_blocks_.value()), - base::BindOnce(&ZCashShieldSyncService::OnBlocksScanned, - weak_ptr_factory_.GetWeakPtr(), last_block_height, - last_block_hash)); +void ZCashShieldSyncService::UpdateSubtreeRoots() { + CHECK(!update_subtree_roots_task_); + update_subtree_roots_task_ = std::make_unique( + this, + base::BindOnce(&ZCashShieldSyncService::OnSubtreeRootsUpdated, weak_ptr_factory_.GetWeakPtr())); + update_subtree_roots_task_->Start(); } -void ZCashShieldSyncService::OnBlocksScanned( - uint32_t last_block_height, - std::string last_block_hash, - base::expected - result) { - downloaded_blocks_ = std::nullopt; - if (!result.has_value()) { - error_ = Error{ErrorCode::kScannerError, ""}; +void ZCashShieldSyncService::OnSubtreeRootsUpdated( + bool result) { + if (!result) { + error_ = Error{ErrorCode::kFailedToUpdateSubtreeRoots, ""}; ScheduleWorkOnTask(); - } else { - UpdateNotes(result->discovered_notes, result->spent_notes, - last_block_height, last_block_hash); + return; } + + subtree_roots_updated_ = true; + ScheduleWorkOnTask(); } -void ZCashShieldSyncService::UpdateNotes( - const std::vector& found_notes, - const std::vector& notes_to_delete, - uint32_t latest_scanned_block, - std::string latest_scanned_block_hash) { - background_orchard_storage_.AsyncCall(&ZCashOrchardStorage::UpdateNotes) - .WithArgs(account_id_.Clone(), found_notes, notes_to_delete, - latest_scanned_block, latest_scanned_block_hash) - .Then(base::BindOnce(&ZCashShieldSyncService::UpdateNotesComplete, - weak_ptr_factory_.GetWeakPtr(), - latest_scanned_block)); +void ZCashShieldSyncService::StartBlockScanning() { + CHECK(!scan_blocks_task_); + scan_blocks_task_ = std::make_unique( + this, base::BindRepeating(&ZCashShieldSyncService::OnScanRangeResult, + weak_ptr_factory_.GetWeakPtr())); + scan_blocks_task_->Start(); } -void ZCashShieldSyncService::UpdateNotesComplete( - uint32_t new_latest_scanned_block, - std::optional error) { - if (error) { - error_ = Error{ErrorCode::kFailedToUpdateDatabase, error->message}; - } else { - latest_scanned_block_ = new_latest_scanned_block; - spendable_notes_ = std::nullopt; +void ZCashShieldSyncService::OnScanRangeResult( + base::expected result) { + if (!result.has_value()) { + scan_blocks_task_.reset(); + error_ = result.error(); + ScheduleWorkOnTask(); + return; } - ScheduleWorkOnTask(); + + latest_scanned_block_result_ = result.value(); + UpdateSpendableNotes(); } uint32_t ZCashShieldSyncService::GetSpendableBalance() { @@ -422,7 +327,7 @@ uint32_t ZCashShieldSyncService::GetSpendableBalance() { } void ZCashShieldSyncService::Reset() { - background_orchard_storage_.AsyncCall(&ZCashOrchardStorage::ResetDatabase); + // sync_state_.AsyncCall(&ZCashOrchardStorage::ResetDatabase); } } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h b/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h index 1de6a8f3e271..8a9d07bf808d 100644 --- a/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h +++ b/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h @@ -15,13 +15,18 @@ #include "base/threading/sequence_bound.h" #include "base/types/expected.h" #include "brave/components/brave_wallet/browser/internal/orchard_block_scanner.h" -#include "brave/components/brave_wallet/browser/zcash/zcash_orchard_storage.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_orchard_sync_state.h" #include "brave/components/brave_wallet/browser/zcash/zcash_rpc.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "mojo/public/cpp/bindings/remote.h" namespace brave_wallet { +class ZCashBlocksBatchScanTask; +class ZCashScanBlocksTask; +class ZCashUpdateSubtreeRootsTask; +class ZCashVerifyChainStateTask; + // ZCashScanService downloads and scans blockchain blocks to find // spendable notes related to the account. // Provided full view key allows to decode orchard compact actions @@ -36,6 +41,9 @@ class ZCashShieldSyncService { kFailedToReceiveTreeState, kFailedToInitAccount, kFailedToRetrieveAccount, + kFailedToVerifyChainState, + kFailedToUpdateSubtreeRoots, + kDatabaseError, kScannerError, }; @@ -44,6 +52,15 @@ class ZCashShieldSyncService { std::string message; }; + struct ScanRangeResult { + uint32_t start_block = 0; + uint32_t end_block = 0; + size_t total_ranges = 0; + size_t ready_ranges = 0; + + bool IsFinished() { return total_ranges == ready_ranges; } + }; + class Observer { public: virtual ~Observer() {} @@ -61,7 +78,7 @@ class ZCashShieldSyncService { explicit OrchardBlockScannerProxy(OrchardFullViewKey full_view_key); virtual ~OrchardBlockScannerProxy(); virtual void ScanBlocks( - std::vector known_notes, + FrontierChainState frontier_chain_state, std::vector blocks, base::OnceCallback)> @@ -90,6 +107,10 @@ class ZCashShieldSyncService { private: FRIEND_TEST_ALL_PREFIXES(ZCashShieldSyncServiceTest, ScanBlocks); + friend class ZCashBlocksBatchScanTask; + friend class ZCashScanBlocksTask; + friend class ZCashUpdateSubtreeRootsTask; + friend class ZCashVerifyChainStateTask; void SetOrchardBlockScannerProxyForTesting( std::unique_ptr block_scanner); @@ -113,48 +134,24 @@ class ZCashShieldSyncService { // Chain reorg flow // Chain reorg happens when latest blocks are removed from the blockchain // We assume that there is a limit of reorg depth - kChainReorgBlockDelta + void VerifyChainState(); + void OnChainStateVerified( + base::expected result); - // Verifies that last known scanned block hash is unchanged - void VerifyChainState(ZCashOrchardStorage::AccountMeta account_meta); - void OnGetTreeStateForChainVerification( - ZCashOrchardStorage::AccountMeta account_meta, - base::expected tree_state); - - // Resolves block hash for the block we are going to fallback - void GetTreeStateForChainReorg(uint32_t new_block_id); - void OnGetTreeStateForChainReorg( - uint32_t new_block_height, - base::expected tree_state); - void OnDatabaseUpdatedForChainReorg( - uint32_t new_block_height, - std::optional error); + void UpdateSubtreeRoots(); + void OnSubtreeRootsUpdated(bool result); // Update spendable notes state void UpdateSpendableNotes(); void OnGetSpendableNotes(base::expected, ZCashOrchardStorage::Error> result); - // Download, scan, update flow - // Download next bunch of blocks - void DownloadBlocks(); - void OnBlocksDownloaded( - base::expected, std::string> - result); - // Process a bunch of downloaded blocks to resolve related notes and - // nullifiers - void ScanBlocks(); - void OnBlocksScanned(uint32_t last_block_height, - std::string last_block_hash, - base::expected result); - void UpdateNotes(const std::vector& found_notes, - const std::vector& notes_to_delete, - uint32_t latest_scanned_block, - std::string latest_scanned_block_hash); - void UpdateNotesComplete(uint32_t new_latest_scanned_block, - std::optional error); + void StartBlockScanning(); + void OnScanRangeResult( + base::expected result); uint32_t GetSpendableBalance(); + std::optional error() { return error_; } // Params @@ -166,15 +163,22 @@ class ZCashShieldSyncService { base::WeakPtr observer_; std::string chain_id_; - base::SequenceBound background_orchard_storage_; + base::SequenceBound sync_state_; std::unique_ptr block_scanner_; std::optional account_meta_; // Latest scanned block - std::optional latest_scanned_block_; - // Latest block in the blockchain - std::optional chain_tip_block_; - std::optional> downloaded_blocks_; + std::optional latest_scanned_block_; + + std::unique_ptr verify_chain_state_task_; + bool chain_state_verified_ = false; + + std::unique_ptr update_subtree_roots_task_; + bool subtree_roots_updated_ = false; + + std::unique_ptr scan_blocks_task_; + std::optional latest_scanned_block_result_; + // Local cache of spendable notes to fast check on discovered nullifiers std::optional> spendable_notes_; std::optional error_; diff --git a/components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.cc b/components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.cc new file mode 100644 index 000000000000..33160d5421d5 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.cc @@ -0,0 +1,86 @@ +/* Copyright (c) 2024 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_update_subtree_roots_task.h" + +namespace brave_wallet { + +namespace { +constexpr size_t kSubTreeRootsResolveBatchSize = 1024; +} // namespace + +ZCashUpdateSubtreeRootsTask::ZCashUpdateSubtreeRootsTask( + ZCashShieldSyncService* sync_service, ZCashUpdateSubtreeRootsTaskCallback callback) : sync_service_(sync_service), callback_(std::move(callback)) { +} + +ZCashUpdateSubtreeRootsTask::~ZCashUpdateSubtreeRootsTask() { +} + +void ZCashUpdateSubtreeRootsTask::Start() { + sync_service_->sync_state_.AsyncCall(&ZCashOrchardSyncState::GetLatestShardIndex) + .WithArgs(sync_service_->account_id_.Clone()) + .Then(base::BindOnce(&ZCashUpdateSubtreeRootsTask::OnGetLatestShardIndex, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashUpdateSubtreeRootsTask::OnGetLatestShardIndex( + base::expected, ZCashOrchardStorage::Error> result) { + if (!result.has_value()) { + std::move(callback_).Run(false); + return; + } + + auto latest_shard_index = result.value().value(); + sync_service_->zcash_rpc_->GetSubtreeRoots( + sync_service_->chain_id_, latest_shard_index + 1, kSubTreeRootsResolveBatchSize, + base::BindOnce(&ZCashUpdateSubtreeRootsTask::OnGetSubtreeRoots, + weak_ptr_factory_.GetWeakPtr(), latest_shard_index + 1)); +} + +void ZCashUpdateSubtreeRootsTask::GetSubtreeRoots( + uint32_t start_index) { + sync_service_->zcash_rpc_->GetSubtreeRoots( + sync_service_->chain_id_, start_index, kSubTreeRootsResolveBatchSize, + base::BindOnce(&ZCashUpdateSubtreeRootsTask::OnGetSubtreeRoots, + weak_ptr_factory_.GetWeakPtr(), start_index)); +} + +void ZCashUpdateSubtreeRootsTask::OnGetSubtreeRoots( + uint32_t start_index, + base::expected, std::string> + result) { + if (!result.has_value()) { + std::move(callback_).Run(false); + return; + } + + std::optional next_start_index; + if (result->size() == kSubTreeRootsResolveBatchSize) { + next_start_index = start_index + kSubTreeRootsResolveBatchSize; + } + + sync_service_->sync_state_.AsyncCall(&ZCashOrchardSyncState::UpdateSubtreeRoots) + .WithArgs(sync_service_->account_id_.Clone(), start_index, std::move(result.value())) + .Then(base::BindOnce(&ZCashUpdateSubtreeRootsTask::OnSubtreeRootsUpdated, + weak_ptr_factory_.GetWeakPtr(), next_start_index)); +} + + +void ZCashUpdateSubtreeRootsTask::OnSubtreeRootsUpdated( + std::optional next_start_index, + base::expected result) { + if (!result.has_value()) { + std::move(callback_).Run(false); + return; + } + + if (next_start_index) { + GetSubtreeRoots(*next_start_index); + } else { + std::move(callback_).Run(true); + } +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.h b/components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.h new file mode 100644 index 000000000000..ba61ac41b0fc --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_update_subtree_roots_task.h @@ -0,0 +1,42 @@ +/* Copyright (c) 2024 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_UPDATE_SUBTREE_ROOTS_TASK_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_UPDATE_SUBTREE_ROOTS_TASK_H_ + +#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" + +namespace brave_wallet { + +class ZCashUpdateSubtreeRootsTask { + public: + using ZCashUpdateSubtreeRootsTaskCallback = base::OnceCallback; + ZCashUpdateSubtreeRootsTask(ZCashShieldSyncService* sync_service, ZCashUpdateSubtreeRootsTaskCallback callback); + ~ZCashUpdateSubtreeRootsTask(); + + void Start(); + + private: + void OnGetLatestShardIndex( + base::expected, ZCashOrchardStorage::Error> result); + void GetSubtreeRoots( + uint32_t start_index); + void OnGetSubtreeRoots( + uint32_t start_index, + base::expected, std::string> + result); + void OnSubtreeRootsUpdated( + std::optional next_start_index, + base::expected result); + + raw_ptr sync_service_; // Owns this + ZCashUpdateSubtreeRootsTaskCallback callback_; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_UPDATE_SUBTREE_ROOTS_TASK_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.cc b/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.cc new file mode 100644 index 000000000000..4ebfba2a6c74 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.cc @@ -0,0 +1,172 @@ +/* Copyright (c) 2024 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_verify_chain_state_task.h" + +namespace brave_wallet { + +ZCashVerifyChainStateTask::ZCashVerifyChainStateTask(ZCashShieldSyncService* sync_service, ZCashVerifyChainStateTaskCallback callback) : sync_service_(sync_service), callback_(std::move(callback)) { +} +ZCashVerifyChainStateTask::~ZCashVerifyChainStateTask() { +} + +void ZCashVerifyChainStateTask::Start() { + ScheduleWorkOnTask(); +} + +void ZCashVerifyChainStateTask::WorkOnTask() { + if (error_) { + std::move(callback_).Run(base::unexpected(*error_)); + return; + } + + if (!account_meta_) { + GetAccountMeta(); + return; + } + + if (!chain_tip_block_) { + GetChainTipBlock(); + return; + } + + if (!chain_state_verified_) { + VerifyChainState(); + return; + } + + std::move(callback_).Run(chain_state_verified_); +} + +void ZCashVerifyChainStateTask::ScheduleWorkOnTask() { + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&ZCashVerifyChainStateTask::WorkOnTask, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashVerifyChainStateTask::GetAccountMeta() { + sync_service_->sync_state_.AsyncCall(&ZCashOrchardSyncState::GetAccountMeta) + .WithArgs(sync_service_->account_id_.Clone()) + .Then(base::BindOnce(&ZCashVerifyChainStateTask::OnGetAccountMeta, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashVerifyChainStateTask::OnGetAccountMeta( + base::expected + result) { + if (result.has_value()) { + error_ = ZCashShieldSyncService::Error{ ZCashShieldSyncService::ErrorCode::kFailedToRetrieveAccount, "" }; + ScheduleWorkOnTask(); + return; + } + + account_meta_ = result.value(); + ScheduleWorkOnTask(); +} + +void ZCashVerifyChainStateTask::GetChainTipBlock() { + sync_service_->zcash_rpc_->GetLatestBlock( + sync_service_->chain_id_, base::BindOnce(&ZCashVerifyChainStateTask::OnGetChainTipBlock, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashVerifyChainStateTask::OnGetChainTipBlock( + base::expected result) { + if (!result.has_value()) { + error_ = ZCashShieldSyncService::Error{ ZCashShieldSyncService::ErrorCode::kFailedToUpdateChainTip, result.error() }; + ScheduleWorkOnTask(); + return; + } + + chain_tip_block_ = (*result)->height; + ScheduleWorkOnTask(); +} + +void ZCashVerifyChainStateTask::VerifyChainState() { + // If block chain has removed blocks we already scanned then we need to handle + // chain reorg. + if (*chain_tip_block_ < account_meta_->latest_scanned_block_id) { + // Assume that chain reorg can't affect more than kChainReorgBlockDelta + // blocks So we can just fallback on this number from the chain tip block. + GetTreeStateForChainReorg(*chain_tip_block_ - kChainReorgBlockDelta); + return; + } + // Retrieve block info for last scanned block id to check whether block hash + // is the same + auto block_id = zcash::mojom::BlockID::New( + account_meta_->latest_scanned_block_id, std::vector()); + sync_service_->zcash_rpc_->GetTreeState( + sync_service_->chain_id_, std::move(block_id), + base::BindOnce( + &ZCashVerifyChainStateTask::OnGetTreeStateForChainVerification, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashVerifyChainStateTask::OnGetTreeStateForChainVerification( + base::expected tree_state) { + if (!tree_state.has_value() || !tree_state.value()) { + error_ = ZCashShieldSyncService::Error{ ZCashShieldSyncService::ErrorCode::kFailedToReceiveTreeState, tree_state.error() }; + ScheduleWorkOnTask(); + return; + } + auto backend_block_hash = RevertHex(tree_state.value()->hash); + if (backend_block_hash != account_meta_->latest_scanned_block_hash) { + // Assume that chain reorg can't affect more than kChainReorgBlockDelta + // blocks So we can just fallback on this number. + uint32_t new_block_id = + account_meta_->latest_scanned_block_id > kChainReorgBlockDelta + ? account_meta_->latest_scanned_block_id - kChainReorgBlockDelta + : 0; + GetTreeStateForChainReorg(new_block_id); + return; + } + + chain_state_verified_ = true; + ScheduleWorkOnTask(); +} + +void ZCashVerifyChainStateTask::GetTreeStateForChainReorg( + uint32_t new_block_height) { + // Query block info by block height + auto block_id = + zcash::mojom::BlockID::New(new_block_height, std::vector()); + sync_service_->zcash_rpc_->GetTreeState( + sync_service_->chain_id_, std::move(block_id), + base::BindOnce(&ZCashVerifyChainStateTask::OnGetTreeStateForChainReorg, + weak_ptr_factory_.GetWeakPtr(), new_block_height)); +} + +void ZCashVerifyChainStateTask::OnGetTreeStateForChainReorg( + uint32_t new_block_height, + base::expected tree_state) { + if (!tree_state.has_value() || !tree_state.value() || + new_block_height != (*tree_state)->height) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kFailedToReceiveTreeState, tree_state.error()}; + ScheduleWorkOnTask(); + return; + } else { + // Reorg database so records related to removed blocks are wiped out + sync_service_->sync_state_.AsyncCall(&ZCashOrchardSyncState::HandleChainReorg) + .WithArgs(sync_service_->account_id_.Clone(), (*tree_state)->height, + (*tree_state)->hash) + .Then(base::BindOnce( + &ZCashVerifyChainStateTask::OnDatabaseUpdatedForChainReorg, + weak_ptr_factory_.GetWeakPtr())); + } +} + +void ZCashVerifyChainStateTask::OnDatabaseUpdatedForChainReorg( + std::optional error) { + if (error) { + error_ = ZCashShieldSyncService::Error{ZCashShieldSyncService::ErrorCode::kFailedToUpdateDatabase, error->message}; + ScheduleWorkOnTask(); + return; + } + + chain_state_verified_ = true; + ScheduleWorkOnTask(); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.h b/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.h new file mode 100644 index 000000000000..128f28b078d3 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.h @@ -0,0 +1,62 @@ +/* Copyright (c) 2024 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_VERIFY_CHAIN_STATE_TASK_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_VERIFY_CHAIN_STATE_TASK_ + +#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" + +namespace brave_wallet { + +class ZCashVerifyChainStateTask { + public: + using ZCashVerifyChainStateTaskCallback = base::OnceCallback)>; + ZCashVerifyChainStateTask(ZCashShieldSyncService* sync_service, ZCashVerifyChainStateTaskCallback callback); + ~ZCashVerifyChainStateTask(); + + void Start(); + + private: + + void WorkOnTask(); + void ScheduleWorkOnTask(); + + void GetAccountMeta(); + void OnGetAccountMeta(base::expected + result); + + void GetChainTipBlock(); + void OnGetChainTipBlock(base::expected result); + + void VerifyChainState(); + + // Verifies that last known scanned block hash is unchanged + void GetTreeStateForLatestScannedBlock(); + void OnGetTreeStateForChainVerification( + base::expected tree_state); + + // Resolves block hash for the block we are going to fallback + void GetTreeStateForChainReorg(uint32_t new_block_id); + void OnGetTreeStateForChainReorg( + uint32_t new_block_height, + base::expected tree_state); + void OnDatabaseUpdatedForChainReorg( + std::optional error); + + raw_ptr sync_service_; /* Owns this */ + ZCashVerifyChainStateTaskCallback callback_; + + std::optional error_; + std::optional account_meta_; + // Latest block in the blockchain + std::optional chain_tip_block_; + bool chain_state_verified_ = false; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_VERIFY_CHAIN_STATE_TASK_ diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index 11119c54aad1..884ecc87dfd5 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -1734,8 +1734,10 @@ enum ZCashAddressValidationResult { }; struct ZCashShieldSyncStatus { - uint64 current_block; - uint64 chain_tip; + uint64 start_block; + uint64 end_block; + uint64 total_ranges; + uint64 scanned_ranges; uint32 notes_found; uint64 spendable_balance; }; diff --git a/components/brave_wallet/common/zcash_utils.cc b/components/brave_wallet/common/zcash_utils.cc index ac1e8406db1c..d73678d03b61 100644 --- a/components/brave_wallet/common/zcash_utils.cc +++ b/components/brave_wallet/common/zcash_utils.cc @@ -117,6 +117,54 @@ DecodedZCashAddress::DecodedZCashAddress(DecodedZCashAddress&& other) = default; DecodedZCashAddress& DecodedZCashAddress::operator=( DecodedZCashAddress&& other) = default; +OrchardCheckpoint::OrchardCheckpoint() {} +OrchardCheckpoint::OrchardCheckpoint(CheckpointTreeState tree_state_position, + std::vector marks_removed) + : tree_state_position(tree_state_position), + marks_removed(std::move(marks_removed)) {} +OrchardCheckpoint::~OrchardCheckpoint() {} +OrchardCheckpoint::OrchardCheckpoint(const OrchardCheckpoint& other) = default; +OrchardCheckpoint& OrchardCheckpoint::operator=( + const OrchardCheckpoint& other) = default; +OrchardCheckpoint::OrchardCheckpoint(OrchardCheckpoint&& other) = default; +OrchardCheckpoint& OrchardCheckpoint::operator=(OrchardCheckpoint&& other) = + default; + +OrchardCheckpointBundle::OrchardCheckpointBundle(uint32_t checkpoint_id, + OrchardCheckpoint checkpoint) + : checkpoint_id(checkpoint_id), checkpoint(std::move(checkpoint)) {} +OrchardCheckpointBundle::~OrchardCheckpointBundle() {} +OrchardCheckpointBundle::OrchardCheckpointBundle( + const OrchardCheckpointBundle& other) = default; +OrchardCheckpointBundle& OrchardCheckpointBundle::operator=( + const OrchardCheckpointBundle& other) = default; +OrchardCheckpointBundle::OrchardCheckpointBundle( + OrchardCheckpointBundle&& other) = default; +OrchardCheckpointBundle& OrchardCheckpointBundle::operator=( + OrchardCheckpointBundle&& other) = default; + +OrchardShard::OrchardShard() {} +OrchardShard::OrchardShard(OrchardShardAddress address, + OrchardShardRootHash root_hash, + std::vector shard_data, + bool contains_marked) + : address(std::move(address)), + root_hash(std::move(root_hash)), + shard_data(std::move(shard_data)), + contains_marked(contains_marked) {} +OrchardShard::~OrchardShard() = default; +OrchardShard::OrchardShard(const OrchardShard& other) = default; +OrchardShard& OrchardShard::operator=(const OrchardShard& other) = default; +OrchardShard::OrchardShard(OrchardShard&& other) = default; +OrchardShard& OrchardShard::operator=(OrchardShard&& other) = default; + +OrchardCap::OrchardCap(std::vector data) : data(std::move(data)) {} +OrchardCap::~OrchardCap() {} +OrchardCap::OrchardCap(const OrchardCap& other) = default; +OrchardCap& OrchardCap::operator=(const OrchardCap& other) = default; +OrchardCap::OrchardCap(OrchardCap&& other) = default; +OrchardCap& OrchardCap::operator=(OrchardCap&& other) = default; + bool OutputZCashAddressSupported(const std::string& address, bool is_testnet) { auto decoded_address = DecodeZCashAddress(address); if (!decoded_address) { diff --git a/components/brave_wallet/common/zcash_utils.h b/components/brave_wallet/common/zcash_utils.h index 77e0e88ef94a..9126f05fb62e 100644 --- a/components/brave_wallet/common/zcash_utils.h +++ b/components/brave_wallet/common/zcash_utils.h @@ -12,6 +12,7 @@ #include #include "base/containers/span.h" +#include "base/types/expected.h" namespace brave_wallet { @@ -30,6 +31,9 @@ inline constexpr size_t kOrchardNullifierSize = 32u; inline constexpr size_t kOrchardCmxSize = 32u; inline constexpr size_t kOrchardEphemeralKeySize = 32u; inline constexpr size_t kOrchardCipherTextSize = 52u; +inline constexpr size_t kOrchardShardTreeHashSize = 32u; +inline constexpr uint8_t kOrchardShardSubtreeHeight = 8; +inline constexpr uint8_t kOrchardShardTreeHeight = 32; using OrchardFullViewKey = std::array; @@ -56,6 +60,8 @@ enum class OrchardAddressKind { Internal }; +enum class ShardTreeRetention { Ephemeral, Marked, Checkpoint }; + using ParsedAddress = std::pair>; struct DecodedZCashAddress { @@ -95,6 +101,138 @@ struct OrchardNote { bool operator==(const OrchardNote& other) const = default; }; +using CheckpointTreeState = std::optional; +using OrchardShardRootHash = std::array; + +struct OrchardCheckpoint { + OrchardCheckpoint(); + OrchardCheckpoint(CheckpointTreeState, std::vector); + ~OrchardCheckpoint(); + OrchardCheckpoint(const OrchardCheckpoint& other); + OrchardCheckpoint& operator=(const OrchardCheckpoint& other); + OrchardCheckpoint(OrchardCheckpoint&& other); + OrchardCheckpoint& operator=(OrchardCheckpoint&& other); + + bool operator==(const OrchardCheckpoint& other) const = default; + + CheckpointTreeState tree_state_position; + std::vector marks_removed; +}; + +struct OrchardCheckpointBundle { + OrchardCheckpointBundle(uint32_t checkpoint_id, OrchardCheckpoint); + ~OrchardCheckpointBundle(); + OrchardCheckpointBundle(const OrchardCheckpointBundle& other); + OrchardCheckpointBundle& operator=(const OrchardCheckpointBundle& other); + OrchardCheckpointBundle(OrchardCheckpointBundle&& other); + OrchardCheckpointBundle& operator=(OrchardCheckpointBundle&& other); + + bool operator==(const OrchardCheckpointBundle& other) const = default; + + uint32_t checkpoint_id; + OrchardCheckpoint checkpoint; +}; + +struct OrchardShardAddress { + uint8_t level = 0; + uint32_t index = 0; + + bool operator==(const OrchardShardAddress& other) const = default; +}; + +struct OrchardCap { + OrchardCap(std::vector data); + ~OrchardCap(); + + OrchardCap(const OrchardCap& other); + OrchardCap& operator=(const OrchardCap& other); + OrchardCap(OrchardCap&& other); + OrchardCap& operator=(OrchardCap&& other); + + std::array root_hash; + std::vector data; +}; + +struct OrchardShard { + OrchardShard(); + OrchardShard(OrchardShardAddress shard_addr, + OrchardShardRootHash shard_hash, + std::vector shard_data, + bool contains_marked); + ~OrchardShard(); + + OrchardShard(const OrchardShard& other); + OrchardShard& operator=(const OrchardShard& other); + OrchardShard(OrchardShard&& other); + OrchardShard& operator=(OrchardShard&& other); + + bool operator==(const OrchardShard& other) const = default; + + OrchardShardAddress address; + OrchardShardRootHash root_hash; + std::vector shard_data; + size_t subtree_end_height = 0; + bool contains_marked = false; +}; + +struct OrchardCommitment { + uint32_t block_id = 0; + uint32_t index = 0; + std::array cmu; + ShardTreeRetention retention; +}; + +struct FrontierChainState { + uint32_t frontier_block_height; + uint32_t frontier_orchard_tree_size; + std::string frontier_tree_state; +}; + +class OrchardShardTreeDelegate { + public: + enum Error { kStorageError = 0, kConsistensyError = 1 }; + + virtual ~OrchardShardTreeDelegate() {} + + virtual base::expected, Error> GetCap() const = 0; + virtual base::expected PutCap(OrchardCap shard) = 0; + + virtual base::expected, Error> GetLatestShardIndex() + const = 0; + virtual base::expected PutShard(OrchardShard shard) = 0; + virtual base::expected, Error> GetShard( + OrchardShardAddress address) const = 0; + virtual base::expected, Error> LastShard( + uint8_t shard_height) const = 0; + virtual base::expected Truncate(uint32_t block_height) = 0; + + virtual base::expected TruncateCheckpoints( + uint32_t checkpoint_id) = 0; + virtual base::expected CheckpointCount() const = 0; + virtual base::expected, Error> MinCheckpointId() + const = 0; + virtual base::expected, Error> MaxCheckpointId() + const = 0; + virtual base::expected, Error> GetCheckpointAtDepth( + uint32_t depth) const = 0; + virtual base::expected, Error> + GetCheckpoint(uint32_t checkpoint_id) const = 0; + virtual base::expected, Error> + GetCheckpoints(size_t limit) const = 0; + virtual base::expected RemoveCheckpointAt(uint32_t depth) = 0; + virtual base::expected RemoveCheckpoint( + uint32_t checkpoint_id) = 0; + virtual base::expected AddCheckpoint( + uint32_t id, + OrchardCheckpoint checkpoint) = 0; + virtual base::expected UpdateCheckpoint( + uint32_t id, + OrchardCheckpoint checkpoint) = 0; + + virtual base::expected, Error> GetShardRoots( + uint8_t shard_level) const = 0; +}; + bool OutputZCashAddressSupported(const std::string& address, bool is_testnet); // https://zips.z.cash/zip-0317 uint64_t CalculateZCashTxFee(const uint32_t tx_input_count, diff --git a/components/brave_wallet_ui/page/screens/dev-zcash/dev-zcash.tsx b/components/brave_wallet_ui/page/screens/dev-zcash/dev-zcash.tsx index 0a3727bebecf..b22fc8890763 100644 --- a/components/brave_wallet_ui/page/screens/dev-zcash/dev-zcash.tsx +++ b/components/brave_wallet_ui/page/screens/dev-zcash/dev-zcash.tsx @@ -130,8 +130,8 @@ const GetBalanceSection = (props: GetBalanceSectionProps) => { status: ZCashShieldSyncStatus) => { if (props.accountId.uniqueKey === accountId.uniqueKey) { setSyncStatusResult("Current block " + - status.currentBlock + "/" + - status.chainTip); + status.endBlock + "-" + + status.startBlock + " " + status.scannedRanges + "/" + status.totalRanges); setShieldedBalanceValue("Found balance: " + status.spendableBalance); } }, diff --git a/components/services/brave_wallet/public/mojom/zcash_decoder.mojom b/components/services/brave_wallet/public/mojom/zcash_decoder.mojom index df368e94e250..0b0f996bc981 100644 --- a/components/services/brave_wallet/public/mojom/zcash_decoder.mojom +++ b/components/services/brave_wallet/public/mojom/zcash_decoder.mojom @@ -71,6 +71,12 @@ struct CompactBlock { ChainMetadata chain_metadata; }; +struct SubtreeRoot { + array root_hash; + array complete_block_hash; + uint32 complete_block_height; +}; + interface ZCashDecoder { ParseBlockID(string data) => (BlockID? value); ParseGetAddressUtxos(string data) => (GetAddressUtxosResponse? value); @@ -78,5 +84,6 @@ interface ZCashDecoder { ParseRawTransaction(string data) => (RawTransaction? tx); ParseTreeState(string data) => (TreeState? tree_state); ParseCompactBlocks(array data) => (array? compact_blocks); + ParseSubtreeRoots(array data) => (array? subtree_roots); }; diff --git a/components/services/brave_wallet/public/proto/zcash_grpc_data.proto b/components/services/brave_wallet/public/proto/zcash_grpc_data.proto index 89ae1120db39..73657e8b68bb 100644 --- a/components/services/brave_wallet/public/proto/zcash_grpc_data.proto +++ b/components/services/brave_wallet/public/proto/zcash_grpc_data.proto @@ -110,3 +110,19 @@ message CompactBlock { repeated CompactTx vtx = 7; ChainMetadata chainMetadata = 8; } + +enum ShieldedProtocol { + sapling = 0; + orchard = 1; +} + +message GetSubtreeRootsArg { + uint32 startIndex = 1; + ShieldedProtocol shieldedProtocol = 2; + uint32 maxEntries = 3; +} +message SubtreeRoot { + bytes rootHash = 2; + bytes completingBlockHash = 3; + uint64 completingBlockHeight = 4; +} diff --git a/components/services/brave_wallet/zcash/zcash_decoder.cc b/components/services/brave_wallet/zcash/zcash_decoder.cc index e51cfa4857c2..54c2c1491831 100644 --- a/components/services/brave_wallet/zcash/zcash_decoder.cc +++ b/components/services/brave_wallet/zcash/zcash_decoder.cc @@ -138,4 +138,19 @@ void ZCashDecoder::ParseCompactBlocks(const std::vector& data, std::move(callback).Run(std::move(parsed_blocks)); } +void ZCashDecoder::ParseSubtreeRoots(const std::vector& data, + ParseSubtreeRootsCallback callback) { + std::vector roots; + for (const auto& data_block : data) { + ::zcash::SubtreeRoot result; + auto serialized_message = ResolveSerializedMessage(data_block); + if (!serialized_message || + !result.ParseFromString(serialized_message.value()) || + serialized_message->empty()) { + std::move(callback).Run(std::nullopt); + return; + } + } +} + } // namespace brave_wallet diff --git a/components/services/brave_wallet/zcash/zcash_decoder.h b/components/services/brave_wallet/zcash/zcash_decoder.h index b7136ccb0e50..cfba78838283 100644 --- a/components/services/brave_wallet/zcash/zcash_decoder.h +++ b/components/services/brave_wallet/zcash/zcash_decoder.h @@ -36,6 +36,8 @@ class ZCashDecoder : public zcash::mojom::ZCashDecoder { ParseTreeStateCallback callback) override; void ParseCompactBlocks(const std::vector& data, ParseCompactBlocksCallback callback) override; + void ParseSubtreeRoots(const std::vector& data, + ParseSubtreeRootsCallback callback) override; }; } // namespace brave_wallet