From 2dde562f7bd8e39fe874f6576cacd4f395f09d1e Mon Sep 17 00:00:00 2001 From: oisupov Date: Wed, 15 Jan 2025 21:25:23 +0400 Subject: [PATCH] Implement shard tree sync process --- components/brave_wallet/browser/BUILD.gn | 6 + components/brave_wallet/browser/test/BUILD.gn | 3 + .../zcash/zcash_blocks_batch_scan_task.cc | 257 ++++++++ .../zcash/zcash_blocks_batch_scan_task.h | 85 +++ .../zcash_blocks_batch_scan_task_unittest.cc | 456 ++++++++++++++ .../browser/zcash/zcash_scan_blocks_task.cc | 171 ++++++ .../browser/zcash/zcash_scan_blocks_task.h | 86 +++ .../zcash/zcash_scan_blocks_task_unittest.cc | 561 ++++++++++++++++++ .../zcash/zcash_shield_sync_service.cc | 332 +++-------- .../browser/zcash/zcash_shield_sync_service.h | 99 ++-- .../zcash_shield_sync_service_unittest.cc | 55 +- .../browser/zcash/zcash_test_utils.cc | 26 + .../browser/zcash/zcash_test_utils.h | 28 + .../browser/zcash/zcash_transaction_utils.cc | 3 +- .../zcash/zcash_verify_chain_state_task.cc | 234 ++++++++ .../zcash/zcash_verify_chain_state_task.h | 71 +++ .../zcash_verify_chain_state_task_unittest.cc | 370 ++++++++++++ .../browser/zcash/zcash_wallet_service.cc | 102 +++- .../browser/zcash/zcash_wallet_service.h | 35 +- .../zcash/zcash_wallet_service_unittest.cc | 2 +- .../brave_wallet/common/brave_wallet.mojom | 16 +- components/brave_wallet/common/zcash_utils.h | 4 +- .../slices/endpoints/zcash.endpoints.ts | 2 +- .../page/screens/dev-zcash/dev-zcash.tsx | 70 ++- 24 files changed, 2669 insertions(+), 405 deletions(-) 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_blocks_batch_scan_task_unittest.cc 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_scan_blocks_task_unittest.cc 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 create mode 100644 components/brave_wallet/browser/zcash/zcash_verify_chain_state_task_unittest.cc diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index 64c6fc3aa3e2..859ce7f277b1 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -314,12 +314,18 @@ 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_transaction_task.cc", "zcash/zcash_create_shield_transaction_task.h", "zcash/zcash_get_zcash_chain_tip_status_task.cc", "zcash/zcash_get_zcash_chain_tip_status_task.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_verify_chain_state_task.cc", + "zcash/zcash_verify_chain_state_task.h", ] deps += [ diff --git a/components/brave_wallet/browser/test/BUILD.gn b/components/brave_wallet/browser/test/BUILD.gn index 0cc66b28836c..329457db4d1a 100644 --- a/components/brave_wallet/browser/test/BUILD.gn +++ b/components/brave_wallet/browser/test/BUILD.gn @@ -163,7 +163,10 @@ source_set("brave_wallet_unit_tests") { "//brave/components/brave_wallet/browser/internal/orchard_bundle_manager_unittest.cc", "//brave/components/brave_wallet/browser/internal/orchard_storage/orchard_storage_unittest.cc", "//brave/components/brave_wallet/browser/internal/orchard_sync_state_unittest.cc", + "//brave/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task_unittest.cc", + "//brave/components/brave_wallet/browser/zcash/zcash_scan_blocks_task_unittest.cc", "//brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service_unittest.cc", + "//brave/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task_unittest.cc", ] deps += [ "//brave/components/brave_wallet/browser/internal:orchard_bundle", 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..0b32c94932ee --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.cc @@ -0,0 +1,257 @@ +// 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 +#include + +#include "base/containers/extend.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_wallet_service.h" +#include "brave/components/brave_wallet/common/hex_utils.h" + +namespace brave_wallet { + +namespace { +constexpr uint32_t kBlockDownloadBatchSize = 10u; +} + +ZCashBlocksBatchScanTask::ZCashBlocksBatchScanTask( + ZCashActionContext& context, + ZCashShieldSyncService::OrchardBlockScannerProxy& scanner, + uint32_t from, + uint32_t to, + ZCashBlocksBatchScanTaskCallback callback) + : context_(context), + scanner_(scanner), + from_(from), + to_(to), + callback_(std::move(callback)) { + CHECK_GT(from, kNu5BlockUpdate); + CHECK_GE(to, from); + frontier_block_height_ = from - 1; +} + +ZCashBlocksBatchScanTask::~ZCashBlocksBatchScanTask() = default; + +void ZCashBlocksBatchScanTask::Start() { + CHECK(!started_); + started_ = true; + 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 (!scan_result_ && (!downloaded_blocks_ || + downloaded_blocks_->size() != (to_ - from_ + 1))) { + 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({})); + context_->zcash_rpc->GetTreeState( + context_->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, + base::StrCat({"Frontier tree state failed, ", result.error()})}; + ScheduleWorkOnTask(); + return; + } + frontier_tree_state_ = result.value().Clone(); + ScheduleWorkOnTask(); +} + +void ZCashBlocksBatchScanTask::GetFrontierBlock() { + context_->zcash_rpc->GetCompactBlocks( + context_->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 + 1; + + context_->zcash_rpc->GetCompactBlocks( + context_->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) { + CHECK(frontier_block_); + CHECK(frontier_tree_state_); + if (!result.has_value()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToDownloadBlocks, + result.error()}; + ScheduleWorkOnTask(); + return; + } + + if (expected_size != result.value().size()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToDownloadBlocks, + "Expected block count doesn't match actual"}; + ScheduleWorkOnTask(); + return; + } + + if (!downloaded_blocks_) { + downloaded_blocks_ = std::vector(); + } + 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, "No blocks to scan"}; + ScheduleWorkOnTask(); + return; + } + + if (!frontier_block_.value() || !frontier_tree_state_.value() || + !frontier_block_.value()->chain_metadata) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kScannerError, "Frontier error"}; + ScheduleWorkOnTask(); + return; + } + + OrchardTreeState tree_state; + { + auto frontier_bytes = PrefixedHexStringToBytes( + base::StrCat({"0x", frontier_tree_state_.value()->orchardTree})); + + if (!frontier_bytes) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kScannerError, + "Failed to parse tree state"}; + ScheduleWorkOnTask(); + return; + } + + tree_state.block_height = frontier_block_.value()->height; + tree_state.tree_size = + frontier_block_.value()->chain_metadata->orchard_commitment_tree_size; + tree_state.frontier = std::move(*frontier_bytes); + } + + latest_scanned_block_ = downloaded_blocks_->back().Clone(); + + scanner_->ScanBlocks( + std::move(tree_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, + "Failed to scan blocks"}; + ScheduleWorkOnTask(); + return; + } + + scan_result_ = std::move(result.value()); + ScheduleWorkOnTask(); +} + +void ZCashBlocksBatchScanTask::UpdateDatabase() { + CHECK(scan_result_.has_value()); + CHECK(latest_scanned_block_.has_value()); + auto latest_scanned_block_hash = ToHex((*latest_scanned_block_)->hash); + auto latest_scanned_block_height = (*latest_scanned_block_)->height; + + context_->sync_state->AsyncCall(&OrchardSyncState::ApplyScanResults) + .WithArgs(context_->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( + base::expected result) { + if (!result.has_value()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToUpdateDatabase, + result.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..c4ce9d89aacf --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task.h @@ -0,0 +1,85 @@ +// 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_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_BLOCKS_BATCH_SCAN_TASK_H_ + +#include +#include + +#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" +#include "brave/components/brave_wallet/common/zcash_utils.h" + +namespace brave_wallet { + +// Scans single scan range. +class ZCashBlocksBatchScanTask { + public: + using ZCashBlocksBatchScanTaskCallback = base::OnceCallback)>; + ZCashBlocksBatchScanTask( + ZCashActionContext& context, + ZCashShieldSyncService::OrchardBlockScannerProxy& scanner, + 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( + base::expected result); + + raw_ref context_; + raw_ref scanner_; + 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 started_ = false; + bool database_updated_ = false; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_BLOCKS_BATCH_SCAN_TASK_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task_unittest.cc b/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task_unittest.cc new file mode 100644 index 000000000000..9cf404996a14 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_blocks_batch_scan_task_unittest.cc @@ -0,0 +1,456 @@ +/* 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 + +#include "base/files/scoped_temp_dir.h" +#include "base/task/thread_pool.h" +#include "base/test/bind.h" +#include "base/test/mock_callback.h" +#include "brave/components/brave_wallet/browser/internal/orchard_test_utils.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_rpc.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_test_utils.h" +#include "brave/components/brave_wallet/common/common_utils.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Eq; +using testing::SaveArg; +using testing::Truly; +using testing::WithArg; + +namespace brave_wallet { + +namespace { + +class MockZCashRPC : public ZCashRpc { + public: + MockZCashRPC() : ZCashRpc(nullptr, nullptr) {} + ~MockZCashRPC() override = default; + + MOCK_METHOD3(GetTreeState, + void(const std::string& chain_id, + zcash::mojom::BlockIDPtr block, + GetTreeStateCallback callback)); + + MOCK_METHOD4(GetCompactBlocks, + void(const std::string& chain_id, + uint32_t from, + uint32_t to, + GetCompactBlocksCallback callback)); +}; + +} // namespace + +class ZCashBlocksBatchScanTest : public testing::Test { + public: + ZCashBlocksBatchScanTest() + : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} + + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + base::FilePath db_path( + temp_dir_.GetPath().Append(FILE_PATH_LITERAL("orchard.db"))); + sync_state_.emplace(base::SequencedTaskRunner::GetCurrentDefault(), + db_path.AppendASCII("orchard.db")); + account_id_ = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + auto lambda = base::BindLambdaForTesting( + [&](base::expected + result) { EXPECT_TRUE(result.has_value()); }); + sync_state_.AsyncCall(&OrchardSyncState::RegisterAccount) + .WithArgs(account_id_.Clone(), kNu5BlockUpdate + 1) + .Then(std::move(lambda)); + + InitZCashRpc(); + } + + void InitZCashRpc() { + ON_CALL(zcash_rpc(), GetTreeState(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, zcash::mojom::BlockIDPtr block, + ZCashRpc::GetTreeStateCallback callback) { + // Valid tree state + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + })); + + ON_CALL(zcash_rpc(), GetCompactBlocks(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, uint32_t from, uint32_t to, + ZCashRpc::GetCompactBlocksCallback callback) { + // Only 600 blocks available + if (to > kNu5BlockUpdate + 600u) { + std::move(callback).Run(base::unexpected("error")); + return; + } + std::vector blocks; + for (uint32_t i = from; i <= to; i++) { + auto chain_metadata = zcash::mojom::ChainMetadata::New(); + chain_metadata->orchard_commitment_tree_size = 0; + // Create empty block for testing + blocks.push_back(zcash::mojom::CompactBlock::New( + 0u, i, std::vector({0xbb, 0xaa}), + std::vector(), 0u, std::vector(), + std::vector(), + std::move(chain_metadata))); + } + std::move(callback).Run(std::move(blocks)); + })); + } + + ZCashActionContext CreateContext() { + return ZCashActionContext(zcash_rpc_, sync_state_, account_id_, + mojom::kZCashMainnet); + } + + testing::NiceMock& zcash_rpc() { return zcash_rpc_; } + + base::test::TaskEnvironment& task_environment() { return task_environment_; } + + base::expected, OrchardStorage::Error> + GetSpendableNotes() { + std::optional< + base::expected, OrchardStorage::Error>> + result; + sync_state_.AsyncCall(&OrchardSyncState::GetSpendableNotes) + .WithArgs(account_id_.Clone()) + .Then(base::BindLambdaForTesting( + [&](base::expected, OrchardStorage::Error> + r) { result = std::move(r); })); + task_environment().RunUntilIdle(); + return result.value(); + } + + std::unique_ptr + CreateMockOrchardBlockScannerProxy() { + return std::make_unique(base::BindRepeating( + [](OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + auto account_id = MakeIndexBasedAccountId( + mojom::CoinType::ZEC, mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + OrchardBlockScanner::Result result = CreateResultForTesting( + std::move(tree_state), std::vector()); + for (const auto& block : blocks) { + if (block->height == kNu5BlockUpdate + 105) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 1)); + } else if (block->height == kNu5BlockUpdate + 205) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 2)); + } else if (block->height == kNu5BlockUpdate + 305) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 3)); + } + + if (block->height == kNu5BlockUpdate + 255) { + result.found_spends.push_back(OrchardNoteSpend( + block->height, {GenerateMockNullifier(account_id, 1)})); + } else if (block->height == kNu5BlockUpdate + 265) { + result.found_spends.push_back(OrchardNoteSpend( + block->height, {GenerateMockNullifier(account_id, 2)})); + } + + if (block->height == kNu5BlockUpdate + 405) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 4)); + } else if (block->height == kNu5BlockUpdate + 505) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 5)); + } + } + std::move(callback).Run(std::move(result)); + })); + } + + private: + base::test::TaskEnvironment task_environment_; + + base::ScopedTempDir temp_dir_; + base::SequenceBound sync_state_; + mojom::AccountIdPtr account_id_; + testing::NiceMock zcash_rpc_; +}; + +TEST_F(ZCashBlocksBatchScanTest, SingleBlockDecoded) { + ZCashActionContext context = CreateContext(); + + std::vector decoded_blocks; + auto block_scanner = + std::make_unique(base::BindRepeating( + [](std::vector* decoded_blocks, OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + OrchardBlockScanner::Result result = CreateResultForTesting( + std::move(tree_state), std::vector()); + for (const auto& block : blocks) { + decoded_blocks->push_back(block->height); + } + std::move(callback).Run(std::move(result)); + }, + &decoded_blocks)); + + base::MockCallback + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(decoded_blocks.size(), 1u); + EXPECT_EQ(decoded_blocks[0], kNu5BlockUpdate + 1); + }); + + auto task = + ZCashBlocksBatchScanTask(context, *block_scanner, kNu5BlockUpdate + 1, + kNu5BlockUpdate + 1, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashBlocksBatchScanTest, AllBlocksDecoded) { + ZCashActionContext context = CreateContext(); + + std::vector decoded_blocks; + auto block_scanner = + std::make_unique(base::BindRepeating( + [](std::vector* decoded_blocks, OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + OrchardBlockScanner::Result result = CreateResultForTesting( + std::move(tree_state), std::vector()); + for (const auto& block : blocks) { + decoded_blocks->push_back(block->height); + } + std::move(callback).Run(std::move(result)); + }, + &decoded_blocks)); + + base::MockCallback + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + // We shouldn't have any notes added since one block is corrupted + EXPECT_EQ(decoded_blocks.size(), 400u); + for (int i = 0; i < 400; i++) { + EXPECT_EQ(decoded_blocks[i], i + 1 + kNu5BlockUpdate); + } + }); + + auto task = + ZCashBlocksBatchScanTask(context, *block_scanner, kNu5BlockUpdate + 1, + kNu5BlockUpdate + 400, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashBlocksBatchScanTest, Scan) { + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(GetSpendableNotes().value().size(), 2u); + }); + + auto task = + ZCashBlocksBatchScanTask(context, *block_scanner, kNu5BlockUpdate + 1, + kNu5BlockUpdate + 500, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashBlocksBatchScanTest, Error_PartialScan) { + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce( + [&](base::expected result) { + EXPECT_FALSE(result.has_value()); + // Since we had only 600 available blocks and requiested to scan 700 + // We shouldn't have any notes added to the database. + EXPECT_EQ(GetSpendableNotes().value().size(), 0u); + }); + + auto task = + ZCashBlocksBatchScanTask(context, *block_scanner, kNu5BlockUpdate + 1, + kNu5BlockUpdate + 700, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashBlocksBatchScanTest, Error_PartialDecoding) { + ZCashActionContext context = CreateContext(); + + auto block_scanner = + std::make_unique(base::BindRepeating( + [](OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + auto account_id = MakeIndexBasedAccountId( + mojom::CoinType::ZEC, mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + OrchardBlockScanner::Result result = CreateResultForTesting( + std::move(tree_state), std::vector()); + for (const auto& block : blocks) { + if (block->height == kNu5BlockUpdate + 105) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 1)); + } else if (block->height == kNu5BlockUpdate + 205) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 2)); + } else if (block->height == kNu5BlockUpdate + 305) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 3)); + } + + if (block->height == kNu5BlockUpdate + 355) { + std::move(callback).Run(base::unexpected( + OrchardBlockScanner::ErrorCode::kDecoderError)); + return; + } + } + std::move(callback).Run(std::move(result)); + })); + + base::MockCallback + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce( + [&](base::expected result) { + EXPECT_FALSE(result.has_value()); + // We shouldn't have any notes added since one block is corrupted + EXPECT_EQ(GetSpendableNotes().value().size(), 0u); + }); + + auto task = + ZCashBlocksBatchScanTask(context, *block_scanner, kNu5BlockUpdate + 1, + kNu5BlockUpdate + 400, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashBlocksBatchScanTest, NetworkError_Blocks) { + ON_CALL(zcash_rpc(), GetCompactBlocks(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, uint32_t from, uint32_t to, + ZCashRpc::GetCompactBlocksCallback callback) { + std::move(callback).Run(base::unexpected("error")); + })); + + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce( + [&](base::expected result) { + EXPECT_FALSE(result.has_value()); + }); + + auto task = + ZCashBlocksBatchScanTask(context, *block_scanner, kNu5BlockUpdate + 1, + kNu5BlockUpdate + 200, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashBlocksBatchScanTest, NetworkError_TreeState) { + ON_CALL(zcash_rpc(), GetTreeState(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, zcash::mojom::BlockIDPtr block, + ZCashRpc::GetTreeStateCallback callback) { + std::move(callback).Run(base::unexpected("error")); + })); + + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce( + [&](base::expected result) { + EXPECT_FALSE(result.has_value()); + }); + + auto task = + ZCashBlocksBatchScanTask(context, *block_scanner, kNu5BlockUpdate + 1, + kNu5BlockUpdate + 200, callback.Get()); + task.Start(); + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashBlocksBatchScanTest, DecodingError) { + auto block_scanner = + std::make_unique(base::BindRepeating( + [](OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + std::move(callback).Run(base::unexpected( + OrchardBlockScanner::ErrorCode::kDecoderError)); + })); + ZCashActionContext context = CreateContext(); + + base::MockCallback + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce( + [&](base::expected result) { + EXPECT_FALSE(result.has_value()); + }); + + auto task = + ZCashBlocksBatchScanTask(context, *block_scanner, kNu5BlockUpdate + 1, + kNu5BlockUpdate + 200, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); +} + +} // namespace brave_wallet 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..90967e8c45e4 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.cc @@ -0,0 +1,171 @@ +/* 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_scan_blocks_task.h" + +#include +#include + +#include "brave/components/brave_wallet/browser/zcash/zcash_wallet_service.h" + +namespace brave_wallet { + +namespace { +constexpr uint32_t kBatchSize = 1024; +} // namespace + +ZCashScanBlocksTask::ZCashScanBlocksTask( + ZCashActionContext& context, + ZCashShieldSyncService::OrchardBlockScannerProxy& scanner, + ZCashScanBlocksTaskObserver observer, + std::optional to) + : context_(context), + scanner_(scanner), + observer_(std::move(observer)), + to_(to) {} + +ZCashScanBlocksTask::~ZCashScanBlocksTask() = default; + +void ZCashScanBlocksTask::Start() { + CHECK(!started_); + started_ = true; + 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 (!account_meta_) { + GetAccountMeta(); + return; + } + + if (!chain_tip_block_) { + GetChainTip(); + return; + } + + if (!scan_ranges_) { + PrepareScanRanges(); + return; + } + + if (!scan_ranges_->empty()) { + ScanRanges(); + return; + } +} + +void ZCashScanBlocksTask::PrepareScanRanges() { + CHECK(account_meta_); + uint32_t from = account_meta_->latest_scanned_block_id + ? account_meta_->latest_scanned_block_id.value() + 1 + : account_meta_->account_birthday; + uint32_t to = to_.value_or(chain_tip_block_.value()); + + if (from > to || to > chain_tip_block_.value()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToUpdateChainTip, + "Scan range error"}; + ScheduleWorkOnTask(); + return; + } + + start_block_ = from; + end_block_ = to; + + initial_ranges_count_ = + std::ceil(static_cast((to - from + 1)) / 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 - 1))}); + } + ScheduleWorkOnTask(); +} + +void ZCashScanBlocksTask::GetAccountMeta() { + context_->sync_state->AsyncCall(&OrchardSyncState::GetAccountMeta) + .WithArgs(context_->account_id.Clone()) + .Then(base::BindOnce(&ZCashScanBlocksTask::OnGetAccountMeta, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashScanBlocksTask::OnGetAccountMeta( + base::expected, + OrchardStorage::Error> result) { + if (!result.has_value() || !result.value()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToRetrieveAccount, + "Failed to retrieve account"}; + ScheduleWorkOnTask(); + return; + } + + account_meta_ = **result; + ScheduleWorkOnTask(); +} + +void ZCashScanBlocksTask::GetChainTip() { + context_->zcash_rpc->GetLatestBlock( + context_->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_); + CHECK(!scan_ranges_->empty()); + auto scan_range = scan_ranges_->front(); + scan_ranges_->pop_front(); + current_block_range_ = std::make_unique( + context_.get(), scanner_.get(), 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; + } + ZCashShieldSyncService::ScanRangeResult scan_ranges_result; + scan_ranges_result.end_block = end_block_.value(); + scan_ranges_result.start_block = start_block_.value(); + scan_ranges_result.total_ranges = initial_ranges_count_.value(); + scan_ranges_result.ready_ranges = + initial_ranges_count_.value() - scan_ranges_.value().size(); + observer_.Run(std::move(scan_ranges_result)); + + ScheduleWorkOnTask(); +} + +} // 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..1924f1bc90e3 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_scan_blocks_task.h @@ -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/. */ + +#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 +#include +#include + +#include "base/threading/sequence_bound.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 { + +// ZCashScanBlocksTask scans blocks from the last scanned block to the provided +// right border. Splits this range to subranges and uses a bunch of smaller +// tasks to process. Current implementation uses sequential scanning. Parallel +// implementation TBD. Notifies client with the progress. See also +// ZCashBlocksBatchScanTask. +class ZCashScanBlocksTask { + public: + using ZCashScanBlocksTaskObserver = base::RepeatingCallback)>; + + ZCashScanBlocksTask(ZCashActionContext& context, + ZCashShieldSyncService::OrchardBlockScannerProxy& scanner, + ZCashScanBlocksTaskObserver observer, + // Right border for scanning, chain tip is used + // if std::nullopt provided + std::optional to); + ~ZCashScanBlocksTask(); + + void Start(); + + private: + struct ScanRange { + uint32_t from; + uint32_t to; + }; + void ScheduleWorkOnTask(); + void WorkOnTask(); + + void GetAccountMeta(); + void OnGetAccountMeta( + base::expected, + OrchardStorage::Error> result); + + void GetChainTip(); + void OnGetChainTip( + base::expected result); + + void PrepareScanRanges(); + + void ScanRanges(); + void OnScanningRangeComplete( + base::expected result); + + raw_ref context_; + raw_ref scanner_; + + ZCashScanBlocksTaskObserver observer_; + + std::optional to_; + std::optional start_block_; + std::optional end_block_; + + bool started_ = false; + + std::optional error_; + std::optional account_meta_; + 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_scan_blocks_task_unittest.cc b/components/brave_wallet/browser/zcash/zcash_scan_blocks_task_unittest.cc new file mode 100644 index 000000000000..7645b03a6f5e --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_scan_blocks_task_unittest.cc @@ -0,0 +1,561 @@ +/* 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_scan_blocks_task.h" + +#include +#include + +#include "base/files/scoped_temp_dir.h" +#include "base/task/thread_pool.h" +#include "base/test/bind.h" +#include "base/test/mock_callback.h" +#include "brave/components/brave_wallet/browser/internal/orchard_test_utils.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_rpc.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_test_utils.h" +#include "brave/components/brave_wallet/common/common_utils.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Eq; +using testing::SaveArg; +using testing::Truly; +using testing::WithArg; + +namespace brave_wallet { + +namespace { + +constexpr uint32_t kExpectedBatchSize = 1024; +constexpr uint32_t kChainTipHeight = kNu5BlockUpdate + 10000; + +class MockZCashRPC : public ZCashRpc { + public: + MockZCashRPC() : ZCashRpc(nullptr, nullptr) {} + ~MockZCashRPC() override = default; + + MOCK_METHOD2(GetLatestBlock, + void(const std::string& chain_id, + GetLatestBlockCallback callback)); + + MOCK_METHOD3(GetTreeState, + void(const std::string& chain_id, + zcash::mojom::BlockIDPtr block, + GetTreeStateCallback callback)); + + MOCK_METHOD4(GetCompactBlocks, + void(const std::string& chain_id, + uint32_t from, + uint32_t to, + GetCompactBlocksCallback callback)); +}; + +} // namespace + +class ZCashScanBlocksTaskTest : public testing::Test { + public: + ZCashScanBlocksTaskTest() + : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} + + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + base::FilePath db_path( + temp_dir_.GetPath().Append(FILE_PATH_LITERAL("orchard.db"))); + sync_state_.emplace(base::SequencedTaskRunner::GetCurrentDefault(), + db_path.AppendASCII("orchard.db")); + account_id_ = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + auto lambda = base::BindLambdaForTesting( + [&](base::expected + result) { EXPECT_TRUE(result.has_value()); }); + sync_state_.AsyncCall(&OrchardSyncState::RegisterAccount) + .WithArgs(account_id_.Clone(), kNu5BlockUpdate + 1) + .Then(std::move(lambda)); + + InitZCashRpc(); + } + + void InitZCashRpc() { + ON_CALL(zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + std::move(callback).Run(zcash::mojom::BlockID::New( + kChainTipHeight, std::vector({}))); + })); + + ON_CALL(zcash_rpc(), GetTreeState(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, zcash::mojom::BlockIDPtr block, + ZCashRpc::GetTreeStateCallback callback) { + // Valid tree state + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + })); + + ON_CALL(zcash_rpc(), GetCompactBlocks(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, uint32_t from, uint32_t to, + ZCashRpc::GetCompactBlocksCallback callback) { + std::vector blocks; + for (uint32_t i = from; i <= to; i++) { + auto chain_metadata = zcash::mojom::ChainMetadata::New(); + chain_metadata->orchard_commitment_tree_size = 0; + // Create empty block for testing + blocks.push_back(zcash::mojom::CompactBlock::New( + 0u, i, std::vector({0xbb, 0xaa}), + std::vector(), 0u, std::vector(), + std::vector(), + std::move(chain_metadata))); + } + std::move(callback).Run(std::move(blocks)); + })); + } + + ZCashActionContext CreateContext() { + return ZCashActionContext(zcash_rpc_, sync_state_, account_id_, + mojom::kZCashMainnet); + } + + testing::NiceMock& zcash_rpc() { return zcash_rpc_; } + + base::test::TaskEnvironment& task_environment() { return task_environment_; } + + base::expected, OrchardStorage::Error> + GetSpendableNotes() { + std::optional< + base::expected, OrchardStorage::Error>> + result; + sync_state_.AsyncCall(&OrchardSyncState::GetSpendableNotes) + .WithArgs(account_id_.Clone()) + .Then(base::BindLambdaForTesting( + [&](base::expected, OrchardStorage::Error> + r) { result = std::move(r); })); + task_environment().RunUntilIdle(); + return result.value(); + } + + std::unique_ptr + CreateMockOrchardBlockScannerProxy() { + return std::make_unique(base::BindRepeating( + [](OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + auto account_id = MakeIndexBasedAccountId( + mojom::CoinType::ZEC, mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + OrchardBlockScanner::Result result = CreateResultForTesting( + std::move(tree_state), std::vector()); + for (const auto& block : blocks) { + // 3 notes in the blockchain found in the first batch + if (block->height == kNu5BlockUpdate + 105) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 1)); + } else if (block->height == kNu5BlockUpdate + 205) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 2)); + } else if (block->height == kNu5BlockUpdate + 305) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 3)); + } + + // First 2 notes are spent in the second batch + if (block->height == kNu5BlockUpdate + kExpectedBatchSize + 255) { + result.found_spends.push_back(OrchardNoteSpend( + block->height, {GenerateMockNullifier(account_id, 1)})); + } else if (block->height == + kNu5BlockUpdate + kExpectedBatchSize + 265) { + result.found_spends.push_back(OrchardNoteSpend( + block->height, {GenerateMockNullifier(account_id, 2)})); + } + + // Another 2 additional notes found in the third batch + if (block->height == + kNu5BlockUpdate + kExpectedBatchSize * 2 + 105) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 4)); + } else if (block->height == + kNu5BlockUpdate + kExpectedBatchSize * 2 + 205) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 5)); + } + + // Another 2 additional notes found far away from 3 previous batches + if (block->height == + kNu5BlockUpdate + kExpectedBatchSize * 7 + 105) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 6)); + } else if (block->height == + kNu5BlockUpdate + kExpectedBatchSize * 7 + 205) { + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, block->height, 7)); + } + } + std::move(callback).Run(std::move(result)); + })); + } + + private: + base::test::TaskEnvironment task_environment_; + + base::ScopedTempDir temp_dir_; + base::SequenceBound sync_state_; + mojom::AccountIdPtr account_id_; + testing::NiceMock zcash_rpc_; +}; + +TEST_F(ZCashScanBlocksTaskTest, ScanRanges) { + auto block_scanner = + std::make_unique(base::BindRepeating( + [](OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + if (blocks[0]->height == kNu5BlockUpdate + 1) { + EXPECT_EQ(blocks.size(), kExpectedBatchSize); + EXPECT_EQ(blocks.back()->height, + kNu5BlockUpdate + kExpectedBatchSize); + } else if (blocks[0]->height == + kNu5BlockUpdate + kExpectedBatchSize + 1) { + EXPECT_EQ(blocks.size(), kExpectedBatchSize); + EXPECT_EQ(blocks.back()->height, + kNu5BlockUpdate + kExpectedBatchSize * 2); + } else if (blocks[0]->height == + kNu5BlockUpdate + kExpectedBatchSize * 2 + 1) { + EXPECT_EQ(blocks.size(), kExpectedBatchSize); + EXPECT_EQ(blocks.back()->height, + kNu5BlockUpdate + kExpectedBatchSize * 3); + } else if (blocks[0]->height == + kNu5BlockUpdate + kExpectedBatchSize * 3 + 1) { + EXPECT_EQ(blocks.size(), 15u); + EXPECT_EQ(blocks.back()->height, + kNu5BlockUpdate + kExpectedBatchSize * 3 + 15); + } else { + NOTREACHED(); + } + OrchardBlockScanner::Result result = CreateResultForTesting( + std::move(tree_state), std::vector()); + std::move(callback).Run(std::move(result)); + })); + + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(4) + .WillRepeatedly( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + }); + + auto task = + ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + kNu5BlockUpdate + kExpectedBatchSize * 3 + 15); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, ScanSingle) { + auto block_scanner = + std::make_unique(base::BindRepeating( + [](OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + auto account_id = MakeIndexBasedAccountId( + mojom::CoinType::ZEC, mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + OrchardBlockScanner::Result result = CreateResultForTesting( + std::move(tree_state), std::vector()); + EXPECT_EQ(blocks.size(), 1u); + EXPECT_EQ(blocks[0]->height, kNu5BlockUpdate + 1u); + result.discovered_notes.push_back( + GenerateMockOrchardNote(account_id, blocks[0]->height, 1)); + std::move(callback).Run(std::move(result)); + })); + + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillRepeatedly( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + if (result->ready_ranges == 1u) { + auto notes1 = GetSpendableNotes(); + EXPECT_EQ(1u, notes1.value().size()); + } + }); + + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + kNu5BlockUpdate + 1); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, ScanLimited) { + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(3) + .WillRepeatedly( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + if (result->ready_ranges == 1u) { + auto notes1 = GetSpendableNotes(); + EXPECT_EQ(3u, notes1.value().size()); + } else if (result->ready_ranges == 2u) { + auto notes2 = GetSpendableNotes(); + EXPECT_EQ(1u, notes2.value().size()); + } else if (result->ready_ranges == 3u) { + auto notes3 = GetSpendableNotes(); + EXPECT_EQ(3u, notes3.value().size()); + } + }); + + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + kNu5BlockUpdate + kExpectedBatchSize * 3); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, ScanUnlimited) { + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(10) + .WillRepeatedly( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + if (result->ready_ranges == 1u) { + auto notes1 = GetSpendableNotes(); + EXPECT_EQ(3u, notes1.value().size()); + } else if (result->ready_ranges == 2u) { + auto notes2 = GetSpendableNotes(); + EXPECT_EQ(1u, notes2.value().size()); + } else if (result->ready_ranges == 3u) { + auto notes3 = GetSpendableNotes(); + EXPECT_EQ(3u, notes3.value().size()); + } else if (result->ready_ranges == 8u) { + auto notes8 = GetSpendableNotes(); + EXPECT_EQ(5u, notes8.value().size()); + } + }); + + // Scan without right border + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + std::nullopt); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, PartialScanningDueError) { + ON_CALL(zcash_rpc(), GetCompactBlocks(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, uint32_t from, uint32_t to, + ZCashRpc::GetCompactBlocksCallback callback) { + std::vector blocks; + // Blocks after the second batch are failing + if (from > kNu5BlockUpdate + kExpectedBatchSize + 10) { + std::move(callback).Run(base::unexpected("error")); + return; + } + for (uint32_t i = from; i <= to; i++) { + auto chain_metadata = zcash::mojom::ChainMetadata::New(); + chain_metadata->orchard_commitment_tree_size = 0; + // Create empty block for testing + blocks.push_back(zcash::mojom::CompactBlock::New( + 0u, i, std::vector({0xbb, 0xaa}), + std::vector(), 0u, std::vector(), + std::vector(), + std::move(chain_metadata))); + } + std::move(callback).Run(std::move(blocks)); + })); + + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(2) + .WillRepeatedly( + [&](base::expected result) { + // First batch completes, second batch fails + EXPECT_TRUE(!result.has_value() || result->ready_ranges == 1u); + if (result.has_value()) { + auto notes1 = GetSpendableNotes(); + EXPECT_EQ(3u, notes1.value().size()); + } + }); + + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + kNu5BlockUpdate + kExpectedBatchSize * 3); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, ChainTipMismatch) { + ON_CALL(zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + std::move(callback).Run(zcash::mojom::BlockID::New( + kChainTipHeight - 200, std::vector({}))); + })); + + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce([&](base::expected result) { + EXPECT_FALSE(result.has_value()); + }); + + // Scan with right border less than actual chain tip + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + kChainTipHeight); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, NetworkError_LatestBlock) { + ON_CALL(zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + std::move(callback).Run(base::unexpected("error")); + })); + + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce([&](base::expected result) { + EXPECT_FALSE(result.has_value()); + }); + + // Scan without right border + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + std::nullopt); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, NetworkError_CompactBlocks) { + ON_CALL(zcash_rpc(), GetCompactBlocks(_, _, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, uint32_t from, uint32_t to, + ZCashRpc::GetCompactBlocksCallback callback) { + std::move(callback).Run(base::unexpected("error")); + })); + + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce([&](base::expected result) { + EXPECT_FALSE(result.has_value()); + }); + + // Scan without right border + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + std::nullopt); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, NetworkError_TreeState) { + ON_CALL(zcash_rpc(), GetTreeState(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, zcash::mojom::BlockIDPtr block, + ZCashRpc::GetTreeStateCallback callback) { + std::move(callback).Run(base::unexpected("error")); + })); + + auto block_scanner = CreateMockOrchardBlockScannerProxy(); + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce([&](base::expected result) { + EXPECT_FALSE(result.has_value()); + }); + + // Scan without right border + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + std::nullopt); + task.Start(); + + task_environment().RunUntilIdle(); +} + +TEST_F(ZCashScanBlocksTaskTest, DecodingError) { + auto block_scanner = + std::make_unique(base::BindRepeating( + [](OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> callback) { + std::move(callback).Run(base::unexpected( + OrchardBlockScanner::ErrorCode::kDecoderError)); + })); + ZCashActionContext context = CreateContext(); + + base::MockCallback callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillOnce([&](base::expected result) { + EXPECT_FALSE(result.has_value()); + }); + + // Scan without right border + auto task = ZCashScanBlocksTask(context, *block_scanner, callback.Get(), + std::nullopt); + task.Start(); + + task_environment().RunUntilIdle(); +} + +} // namespace brave_wallet 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 fffc45cca813..568f691b60c6 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,8 @@ #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_verify_chain_state_task.h" #include "brave/components/brave_wallet/browser/zcash/zcash_wallet_service.h" #include "brave/components/brave_wallet/common/common_utils.h" #include "brave/components/brave_wallet/common/hex_utils.h" @@ -18,25 +20,8 @@ namespace brave_wallet { namespace { -size_t GetCode(ZCashShieldSyncService::ErrorCode error) { - switch (error) { - case ZCashShieldSyncService::ErrorCode::kFailedToDownloadBlocks: - return 0; - case ZCashShieldSyncService::ErrorCode::kFailedToUpdateDatabase: - return 1; - case ZCashShieldSyncService::ErrorCode::kFailedToUpdateChainTip: - return 2; - case ZCashShieldSyncService::ErrorCode::kFailedToRetrieveSpendableNotes: - return 3; - case ZCashShieldSyncService::ErrorCode::kFailedToReceiveTreeState: - return 4; - case ZCashShieldSyncService::ErrorCode::kFailedToInitAccount: - return 5; - case ZCashShieldSyncService::ErrorCode::kFailedToRetrieveAccount: - return 6; - case ZCashShieldSyncService::ErrorCode::kScannerError: - return 7; - } +int GetCode(ZCashShieldSyncService::ErrorCode error) { + return static_cast(error); } } // namespace @@ -63,18 +48,13 @@ void ZCashShieldSyncService::OrchardBlockScannerProxy::ScanBlocks( } ZCashShieldSyncService::ZCashShieldSyncService( - ZCashRpc& zcash_rpc, - base::SequenceBound& zcash_orchard_sync_state, - const mojom::AccountIdPtr& account_id, + ZCashActionContext context, const mojom::ZCashAccountShieldBirthdayPtr& account_birthday, const OrchardFullViewKey& fvk, base::WeakPtr observer) - : zcash_rpc_(zcash_rpc), - zcash_orchard_sync_state_(zcash_orchard_sync_state), - account_id_(account_id.Clone()), + : context_(std::move(context)), account_birthday_(account_birthday.Clone()), observer_(std::move(observer)) { - chain_id_ = GetNetworkForZCashKeyring(account_id->keyring_id); block_scanner_ = std::make_unique(fvk); } @@ -85,10 +65,11 @@ void ZCashShieldSyncService::SetOrchardBlockScannerProxyForTesting( block_scanner_ = std::move(block_scanner); } -void ZCashShieldSyncService::StartSyncing() { +void ZCashShieldSyncService::StartSyncing(std::optional to) { + to_ = to; ScheduleWorkOnTask(); if (observer_) { - observer_->OnSyncStart(account_id_); + observer_->OnSyncStart(context_.account_id); } } @@ -103,56 +84,41 @@ void ZCashShieldSyncService::ScheduleWorkOnTask() { } void ZCashShieldSyncService::WorkOnTask() { - if (stopped_) { - return; - } - if (error_) { + verify_chain_state_task_.reset(); + scan_blocks_task_.reset(); + if (observer_) { observer_->OnSyncError( - account_id_, + context_.account_id, base::NumberToString(GetCode(error_->code)) + ": " + error_->message); } return; } - if (!chain_tip_block_) { - UpdateChainTip(); - return; - } - if (!account_meta_) { GetOrCreateAccount(); return; } - if (!latest_scanned_block_) { - VerifyChainState(*account_meta_); - return; - } - - if (!spendable_notes_) { - UpdateSpendableNotes(); - return; - } - - if (observer_) { - observer_->OnSyncStatusUpdate(account_id_, current_sync_status_.Clone()); - } - - if (!downloaded_blocks_ && *latest_scanned_block_ < *chain_tip_block_) { - DownloadBlocks(); + if (!chain_state_verified_) { + VerifyChainState(); return; + } else { + verify_chain_state_task_.reset(); } - if (downloaded_blocks_) { - ScanBlocks(); + if (!scan_blocks_task_) { + StartBlockScanning(); return; } - if (observer_) { - observer_->OnSyncStop(account_id_); - stopped_ = true; + if (latest_scanned_block_result_ && + latest_scanned_block_result_->IsFinished()) { + scan_blocks_task_.reset(); + if (observer_) { + observer_->OnSyncStop(context_.account_id); + } } } @@ -165,7 +131,7 @@ void ZCashShieldSyncService::GetOrCreateAccount() { } sync_state() .AsyncCall(&OrchardSyncState::GetAccountMeta) - .WithArgs(account_id_.Clone()) + .WithArgs(context_.account_id.Clone()) .Then(base::BindOnce(&ZCashShieldSyncService::OnGetAccountMeta, weak_ptr_factory_.GetWeakPtr())); } @@ -174,15 +140,16 @@ void ZCashShieldSyncService::OnGetAccountMeta( base::expected, OrchardStorage::Error> result) { if (!result.has_value()) { - error_ = Error{ErrorCode::kFailedToRetrieveAccount, result.error().message}; + error_ = Error{ErrorCode::kFailedToInitAccount, "Database error"}; ScheduleWorkOnTask(); + return; } if (!result.value()) { InitAccount(); return; } - account_meta_ = *result; - if (account_meta_->latest_scanned_block_id.value() && + account_meta_ = **result; + if (account_meta_->latest_scanned_block_id && (account_meta_->latest_scanned_block_id.value() < account_meta_->account_birthday)) { error_ = Error{ErrorCode::kFailedToRetrieveAccount, ""}; @@ -193,7 +160,7 @@ void ZCashShieldSyncService::OnGetAccountMeta( void ZCashShieldSyncService::InitAccount() { sync_state() .AsyncCall(&OrchardSyncState::RegisterAccount) - .WithArgs(account_id_.Clone(), account_birthday_->value) + .WithArgs(context_.account_id.Clone(), account_birthday_->value) .Then(base::BindOnce(&ZCashShieldSyncService::OnAccountInit, weak_ptr_factory_.GetWeakPtr())); } @@ -208,113 +175,68 @@ void ZCashShieldSyncService::OnAccountInit( ScheduleWorkOnTask(); } -void ZCashShieldSyncService::VerifyChainState( - OrchardStorage::AccountMeta account_meta) { - if (account_meta.account_birthday < kNu5BlockUpdate) { - error_ = Error{ErrorCode::kFailedToRetrieveAccount, - "Wrong birthday block height"}; - ScheduleWorkOnTask(); - return; - } - if (!account_meta.latest_scanned_block_id) { - latest_scanned_block_ = account_meta.account_birthday - 1; - ScheduleWorkOnTask(); - return; - } - // 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.value()) { - // 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.value(), 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::VerifyChainState() { + CHECK(!verify_chain_state_task_.get()); + verify_chain_state_task_ = std::make_unique( + context_, base::BindOnce(&ZCashShieldSyncService::OnChainStateVerified, + weak_ptr_factory_.GetWeakPtr())); + verify_chain_state_task_->Start(); } -void ZCashShieldSyncService::OnGetTreeStateForChainVerification( - OrchardStorage::AccountMeta account_meta, - base::expected tree_state) { - if (!tree_state.has_value() || !tree_state.value()) { - error_ = Error{ErrorCode::kFailedToReceiveTreeState, tree_state.error()}; +void ZCashShieldSyncService::OnChainStateVerified( + base::expected result) { + if (!result.has_value()) { + error_ = result.error(); ScheduleWorkOnTask(); return; } - auto backend_block_hash = RevertHex(tree_state.value()->hash); - if (backend_block_hash != account_meta.latest_scanned_block_hash.value()) { - // 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.value() > kChainReorgBlockDelta - ? account_meta.latest_scanned_block_id.value() - - kChainReorgBlockDelta - : 0; - GetTreeStateForChainReorg(new_block_id); + + if (!result.value()) { + error_ = Error{ErrorCode::kFailedToVerifyChainState, ""}; + ScheduleWorkOnTask(); 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; + chain_state_verified_ = true; 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::StartBlockScanning() { + CHECK(!scan_blocks_task_); + scan_blocks_task_ = std::make_unique( + context_, *block_scanner_, + base::BindRepeating(&ZCashShieldSyncService::OnScanRangeResult, + weak_ptr_factory_.GetWeakPtr()), + to_); + scan_blocks_task_->Start(); } -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()}; +void ZCashShieldSyncService::OnScanRangeResult( + base::expected result) { + if (!result.has_value()) { + error_ = result.error(); ScheduleWorkOnTask(); return; - } else { - // Reorg database so records related to removed blocks are wiped out - sync_state() - .AsyncCall(&OrchardSyncState::HandleChainReorg) - .WithArgs(account_id_.Clone(), (*tree_state)->height, - (*tree_state)->hash) - .Then(base::BindOnce( - &ZCashShieldSyncService::OnDatabaseUpdatedForChainReorg, - weak_ptr_factory_.GetWeakPtr(), (*tree_state)->height)); } + + latest_scanned_block_result_ = result.value(); + UpdateSpendableNotes(); } -void ZCashShieldSyncService::OnDatabaseUpdatedForChainReorg( - uint32_t new_block_height, - base::expected result) { - if (!result.has_value()) { - error_ = Error{ErrorCode::kFailedToUpdateDatabase, result.error().message}; - ScheduleWorkOnTask(); - return; +uint32_t ZCashShieldSyncService::GetSpendableBalance() { + CHECK(spendable_notes_.has_value()); + uint32_t balance = 0; + for (const auto& note : spendable_notes_.value()) { + balance += note.amount; } - - latest_scanned_block_ = new_block_height; - ScheduleWorkOnTask(); + return balance; } void ZCashShieldSyncService::UpdateSpendableNotes() { + spendable_notes_ = std::nullopt; sync_state() .AsyncCall(&OrchardSyncState::GetSpendableNotes) - .WithArgs(account_id_.Clone()) + .WithArgs(context_.account_id.Clone()) .Then(base::BindOnce(&ZCashShieldSyncService::OnGetSpendableNotes, weak_ptr_factory_.GetWeakPtr())); } @@ -324,121 +246,39 @@ void ZCashShieldSyncService::OnGetSpendableNotes( if (!result.has_value()) { error_ = Error{ErrorCode::kFailedToRetrieveSpendableNotes, result.error().message}; - } else { - spendable_notes_ = result.value(); - current_sync_status_ = mojom::ZCashShieldSyncStatus::New( - latest_scanned_block_.value(), chain_tip_block_.value(), - 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; - } - 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::OnBlocksDownloaded( - base::expected, std::string> - result) { - if (!result.has_value()) { - error_ = Error{ErrorCode::kFailedToDownloadBlocks, result.error()}; - } else { - downloaded_blocks_ = std::move(result.value()); - } - ScheduleWorkOnTask(); -} - -void ZCashShieldSyncService::ScanBlocks() { - if (!downloaded_blocks_ || downloaded_blocks_->empty()) { - error_ = Error{ErrorCode::kScannerError, ""}; ScheduleWorkOnTask(); return; } - auto last_block_hash = ToHex(downloaded_blocks_->back()->hash); - auto last_block_height = downloaded_blocks_->back()->height; + spendable_notes_ = result.value(); - block_scanner_->ScanBlocks( - OrchardTreeState(), std::move(downloaded_blocks_.value()), - base::BindOnce(&ZCashShieldSyncService::OnBlocksScanned, - weak_ptr_factory_.GetWeakPtr(), last_block_height, - last_block_hash)); -} - -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, ""}; - ScheduleWorkOnTask(); + 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 { - UpdateNotes(std::move(result.value()), last_block_height, last_block_hash); + current_sync_status_ = mojom::ZCashShieldSyncStatus::New( + latest_scanned_block_.value_or(0), latest_scanned_block_.value_or(0), 0, + 0, spendable_notes_->size(), GetSpendableBalance()); } -} - -void ZCashShieldSyncService::UpdateNotes( - OrchardBlockScanner::Result result, - uint32_t latest_scanned_block, - std::string latest_scanned_block_hash) { - sync_state() - .AsyncCall(&OrchardSyncState::ApplyScanResults) - .WithArgs(account_id_.Clone(), std::move(result), latest_scanned_block, - latest_scanned_block_hash) - .Then(base::BindOnce(&ZCashShieldSyncService::UpdateNotesComplete, - weak_ptr_factory_.GetWeakPtr(), - latest_scanned_block)); -} -void ZCashShieldSyncService::UpdateNotesComplete( - uint32_t new_latest_scanned_block, - base::expected result) { - if (!result.has_value()) { - error_ = Error{ErrorCode::kFailedToUpdateDatabase, result.error().message}; - } else { - latest_scanned_block_ = new_latest_scanned_block; - spendable_notes_ = std::nullopt; + if (observer_) { + observer_->OnSyncStatusUpdate(context_.account_id, + current_sync_status_.Clone()); } - ScheduleWorkOnTask(); -} -uint32_t ZCashShieldSyncService::GetSpendableBalance() { - CHECK(spendable_notes_.has_value()); - uint32_t balance = 0; - for (const auto& note : spendable_notes_.value()) { - balance += note.amount; - } - return balance; + ScheduleWorkOnTask(); } ZCashRpc& ZCashShieldSyncService::zcash_rpc() { - return zcash_rpc_.get(); + return context_.zcash_rpc.get(); } base::SequenceBound& ZCashShieldSyncService::sync_state() { - return zcash_orchard_sync_state_.get(); + return context_.sync_state.get(); } } // 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 af0284d67d0a..dec96eda3dca 100644 --- a/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h +++ b/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h @@ -11,18 +11,16 @@ #include #include -#include "base/memory/scoped_refptr.h" -#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/internal/orchard_sync_state.h" -#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" -#include "mojo/public/cpp/bindings/remote.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_action_context.h" namespace brave_wallet { class OrchardStorage; -class ZCashRpc; +class ZCashScanBlocksTask; +class ZCashVerifyChainStateTask; // ZCashShieldSyncService downloads and scans blockchain blocks to find // spendable notes related to the account. @@ -30,7 +28,7 @@ class ZCashRpc; // related to the account. class ZCashShieldSyncService { public: - enum class ErrorCode { + enum ErrorCode { kFailedToDownloadBlocks, kFailedToUpdateDatabase, kFailedToUpdateChainTip, @@ -38,6 +36,9 @@ class ZCashShieldSyncService { kFailedToReceiveTreeState, kFailedToInitAccount, kFailedToRetrieveAccount, + kFailedToVerifyChainState, + kFailedToUpdateSubtreeRoots, + kDatabaseError, kScannerError, }; @@ -46,6 +47,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() {} @@ -74,17 +84,15 @@ class ZCashShieldSyncService { }; ZCashShieldSyncService( - ZCashRpc& zcash_rpc, - base::SequenceBound& zcash_orchard_sync_state, - const mojom::AccountIdPtr& account_id, + ZCashActionContext context, const mojom::ZCashAccountShieldBirthdayPtr& account_birthday, - const std::array& fvk, + const OrchardFullViewKey& fvk, base::WeakPtr observer); virtual ~ZCashShieldSyncService(); bool IsStarted(); - void StartSyncing(); + void StartSyncing(std::optional to); mojom::ZCashShieldSyncStatusPtr GetSyncStatus(); @@ -106,79 +114,52 @@ class ZCashShieldSyncService { void OnAccountInit( base::expected error); - // Get last known block in the blockchain - void UpdateChainTip(); - void OnGetLatestBlock( - base::expected result); - // 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(OrchardStorage::AccountMeta account_meta); - void OnGetTreeStateForChainVerification( - OrchardStorage::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, - base::expected result); + uint32_t GetSpendableBalance(); // Update spendable notes state void UpdateSpendableNotes(); void OnGetSpendableNotes( base::expected, OrchardStorage::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(OrchardBlockScanner::Result result, - uint32_t latest_scanned_block, - std::string latest_scanned_block_hash); - void UpdateNotesComplete( - uint32_t new_latest_scanned_block, - base::expected result); + void StartBlockScanning(); + void OnScanRangeResult( + base::expected result); ZCashRpc& zcash_rpc(); base::SequenceBound& sync_state(); - uint32_t GetSpendableBalance(); std::optional error() { return error_; } // Params - raw_ref zcash_rpc_; - raw_ref> zcash_orchard_sync_state_; - mojom::AccountIdPtr account_id_; + ZCashActionContext context_; // Birthday of the account will be used to resolve initial scan range. mojom::ZCashAccountShieldBirthdayPtr account_birthday_; - base::FilePath db_dir_path_; base::WeakPtr observer_; - std::string chain_id_; + std::optional to_; 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; + + bool subtree_roots_updated_ = false; + + std::unique_ptr scan_blocks_task_; + bool scanning_finished_ = false; + + 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_shield_sync_service_unittest.cc b/components/brave_wallet/browser/zcash/zcash_shield_sync_service_unittest.cc index 050fffdd6cb7..4261beb533aa 100644 --- a/components/brave_wallet/browser/zcash/zcash_shield_sync_service_unittest.cc +++ b/components/brave_wallet/browser/zcash/zcash_shield_sync_service_unittest.cc @@ -95,35 +95,6 @@ class MockZCashShieldSyncServiceObserver } // namespace -class MockOrchardBlockScannerProxy - : public ZCashShieldSyncService::OrchardBlockScannerProxy { - public: - using Callback = base::RepeatingCallback, - base::OnceCallback)> - callback)>; - - explicit MockOrchardBlockScannerProxy(Callback callback) - : OrchardBlockScannerProxy({}), callback_(callback) {} - - ~MockOrchardBlockScannerProxy() override = default; - - void ScanBlocks( - OrchardTreeState tree_state, - std::vector blocks, - base::OnceCallback)> - callback) override { - callback_.Run(std::move(tree_state), std::move(blocks), - std::move(callback)); - } - - private: - Callback callback_; -}; - class ZCashShieldSyncServiceTest : public testing::Test { public: ZCashShieldSyncServiceTest() @@ -155,8 +126,9 @@ class ZCashShieldSyncServiceTest : public testing::Test { OrchardFullViewKey fvk; sync_service_ = std::make_unique( - zcash_rpc_, sync_state_, account_id, account_birthday, fvk, - observer_->GetWeakPtr()); + ZCashActionContext(zcash_rpc_, sync_state_, account_id, + mojom::kZCashMainnet), + account_birthday, fvk, observer_->GetWeakPtr()); // Ensure previous OrchardStorage is destroyed on background thread task_environment_.RunUntilIdle(); @@ -198,11 +170,11 @@ class ZCashShieldSyncServiceTest : public testing::Test { // First 2 notes are spent if (block->height == kNu5BlockUpdate + 255) { - result.found_spends.push_back(OrchardNoteSpend{ - block->height, GenerateMockNullifier(account_id, 1)}); + result.found_spends.push_back(OrchardNoteSpend( + block->height, {GenerateMockNullifier(account_id, 1)})); } else if (block->height == kNu5BlockUpdate + 265) { - result.found_spends.push_back(OrchardNoteSpend{ - block->height, GenerateMockNullifier(account_id, 2)}); + result.found_spends.push_back(OrchardNoteSpend( + block->height, {GenerateMockNullifier(account_id, 2)})); } } std::move(callback).Run(std::move(result)); @@ -252,17 +224,20 @@ TEST_F(ZCashShieldSyncServiceTest, ScanBlocks) { ZCashRpc::GetCompactBlocksCallback callback) { std::vector blocks; for (uint32_t i = from; i <= to; i++) { + auto chain_metadata = zcash::mojom::ChainMetadata::New(); + chain_metadata->orchard_commitment_tree_size = 0; // Create empty block for testing blocks.push_back(zcash::mojom::CompactBlock::New( 0u, i, std::vector({0xbb, 0xaa}), std::vector(), 0u, std::vector(), - std::vector(), nullptr)); + std::vector(), + std::move(chain_metadata))); } std::move(callback).Run(std::move(blocks)); })); { - sync_service()->StartSyncing(); + sync_service()->StartSyncing(std::nullopt); task_environment_.RunUntilIdle(); auto sync_status = sync_service()->GetSyncStatus(); @@ -326,7 +301,7 @@ TEST_F(ZCashShieldSyncServiceTest, ScanBlocks) { })); { - sync_service()->StartSyncing(); + sync_service()->StartSyncing(std::nullopt); task_environment_.RunUntilIdle(); auto sync_status = sync_service()->GetSyncStatus(); @@ -368,7 +343,7 @@ TEST_F(ZCashShieldSyncServiceTest, ScanBlocks) { }))); { - sync_service()->StartSyncing(); + sync_service()->StartSyncing(std::nullopt); task_environment_.RunUntilIdle(); auto sync_status = sync_service()->GetSyncStatus(); @@ -431,7 +406,7 @@ TEST_F(ZCashShieldSyncServiceTest, ScanBlocks) { }))); { - sync_service()->StartSyncing(); + sync_service()->StartSyncing(std::nullopt); task_environment_.RunUntilIdle(); auto sync_status = sync_service()->GetSyncStatus(); diff --git a/components/brave_wallet/browser/zcash/zcash_test_utils.cc b/components/brave_wallet/browser/zcash/zcash_test_utils.cc index eff09718bd42..f19ab312a1bd 100644 --- a/components/brave_wallet/browser/zcash/zcash_test_utils.cc +++ b/components/brave_wallet/browser/zcash/zcash_test_utils.cc @@ -6,10 +6,26 @@ #include "brave/components/brave_wallet/browser/zcash/zcash_test_utils.h" #include +#include #include namespace brave_wallet { +// MockOrchardBlockScannerProxy +MockOrchardBlockScannerProxy::MockOrchardBlockScannerProxy(Callback callback) + : OrchardBlockScannerProxy({}), callback_(callback) {} + +MockOrchardBlockScannerProxy::~MockOrchardBlockScannerProxy() = default; + +void MockOrchardBlockScannerProxy::ScanBlocks( + OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> + callback) { + callback_.Run(std::move(tree_state), std::move(blocks), std::move(callback)); +} + OrchardNullifier GenerateMockNullifier(const mojom::AccountIdPtr& account_id, uint8_t seed) { std::array nullifier; @@ -42,4 +58,14 @@ void SortByBlockId(std::vector& vec) { }); } +std::vector GetZCashUtxo(size_t seed) { + auto utxo = zcash::mojom::ZCashUtxo::New(); + utxo->address = base::NumberToString(seed); + utxo->value_zat = seed; + utxo->tx_id = std::vector(32u, 1u); + std::vector result; + result.push_back(std::move(utxo)); + return result; +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_test_utils.h b/components/brave_wallet/browser/zcash/zcash_test_utils.h index f0db39a75ced..ed9d5dbed78d 100644 --- a/components/brave_wallet/browser/zcash/zcash_test_utils.h +++ b/components/brave_wallet/browser/zcash/zcash_test_utils.h @@ -8,11 +8,37 @@ #include +#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/zcash_utils.h" namespace brave_wallet { +class MockOrchardBlockScannerProxy + : public ZCashShieldSyncService::OrchardBlockScannerProxy { + public: + using Callback = base::RepeatingCallback blocks, + base::OnceCallback)> + callback)>; + + explicit MockOrchardBlockScannerProxy(Callback callback); + + ~MockOrchardBlockScannerProxy() override; + + void ScanBlocks( + OrchardTreeState tree_state, + std::vector blocks, + base::OnceCallback)> + callback) override; + + private: + Callback callback_; +}; + std::array GenerateMockNullifier( const mojom::AccountIdPtr& account_id, uint8_t seed); @@ -27,6 +53,8 @@ OrchardNote GenerateMockOrchardNote(const mojom::AccountIdPtr& account_id, void SortByBlockId(std::vector& vec); +std::vector GetZCashUtxo(size_t seed); + } // namespace brave_wallet #endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_TEST_UTILS_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_transaction_utils.cc b/components/brave_wallet/browser/zcash/zcash_transaction_utils.cc index 3df20dc2d1ae..bb30dc8dd770 100644 --- a/components/brave_wallet/browser/zcash/zcash_transaction_utils.cc +++ b/components/brave_wallet/browser/zcash/zcash_transaction_utils.cc @@ -10,7 +10,6 @@ namespace brave_wallet { namespace { -#if BUILDFLAG(ENABLE_ORCHARD) uint64_t CalculateInputsAmount( const std::vector& inputs) { @@ -21,6 +20,8 @@ uint64_t CalculateInputsAmount( return total_value; } +#if BUILDFLAG(ENABLE_ORCHARD) + uint64_t CalculateInputsAmount(const std::vector& notes) { uint64_t total_value = 0; for (const auto& note : notes) { 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..960cb8f71436 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.cc @@ -0,0 +1,234 @@ +/* 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" + +#include +#include +#include + +#include "brave/components/brave_wallet/browser/zcash/zcash_wallet_service.h" + +namespace brave_wallet { + +ZCashVerifyChainStateTask::ZCashVerifyChainStateTask( + ZCashActionContext& context, + ZCashVerifyChainStateTaskCallback callback) + : context_(context), callback_(std::move(callback)) {} +ZCashVerifyChainStateTask::~ZCashVerifyChainStateTask() = default; + +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() { + context_->sync_state->AsyncCall(&OrchardSyncState::GetAccountMeta) + .WithArgs(context_->account_id.Clone()) + .Then(base::BindOnce(&ZCashVerifyChainStateTask::OnGetAccountMeta, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashVerifyChainStateTask::OnGetAccountMeta( + base::expected, + OrchardStorage::Error> result) { + if (!result.has_value()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToRetrieveAccount, + result.error().message}; + ScheduleWorkOnTask(); + return; + } + + if (!result.value()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToRetrieveAccount, + "Account doesn't exist"}; + ScheduleWorkOnTask(); + return; + } + + if (result.value()->account_birthday < kNu5BlockUpdate) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToRetrieveAccount, + "Wrong birthday block height"}; + ScheduleWorkOnTask(); + return; + } + + account_meta_ = **result; + ScheduleWorkOnTask(); +} + +void ZCashVerifyChainStateTask::GetChainTipBlock() { + context_->zcash_rpc->GetLatestBlock( + context_->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() { + // Skip chain state verification if no blocks were scanned yet + if (!account_meta_->latest_scanned_block_id) { + chain_state_verified_ = true; + ScheduleWorkOnTask(); + return; + } + + // 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.value()) { + // 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.value(), std::vector()); + context_->zcash_rpc->GetTreeState( + context_->chain_id, std::move(block_id), + base::BindOnce( + &ZCashVerifyChainStateTask::OnGetTreeStateForChainVerification, + weak_ptr_factory_.GetWeakPtr())); +} + +void ZCashVerifyChainStateTask::OnGetTreeStateForChainVerification( + base::expected tree_state) { + CHECK(account_meta_.has_value()); + CHECK(account_meta_->latest_scanned_block_hash.has_value()); + CHECK(account_meta_->latest_scanned_block_id.has_value()); + if (!tree_state.has_value() || !tree_state.value()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToReceiveTreeState, + base::StrCat({"Verification tree state failed, ", tree_state.error()})}; + ScheduleWorkOnTask(); + return; + } + auto backend_block_hash = RevertHex(tree_state.value()->hash); + if (!backend_block_hash) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToReceiveTreeState, + "Wrong block hash format"}; + ScheduleWorkOnTask(); + return; + } + if (backend_block_hash.value() != + account_meta_->latest_scanned_block_hash.value()) { + // 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.value() > kChainReorgBlockDelta + ? account_meta_->latest_scanned_block_id.value() - + 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()); + context_->zcash_rpc->GetTreeState( + context_->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, + base::StrCat({"Reorg tree state failed, ", tree_state.error()})}; + ScheduleWorkOnTask(); + return; + } else { + auto reverted_hex = RevertHex((*tree_state)->hash); + if (!reverted_hex) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToReceiveTreeState, + "Wrong block hash format"}; + ScheduleWorkOnTask(); + return; + } + // Reorg database so records related to removed blocks are wiped out + context_->sync_state->AsyncCall(&OrchardSyncState::HandleChainReorg) + .WithArgs(context_->account_id.Clone(), (*tree_state)->height, + *reverted_hex) + .Then(base::BindOnce( + &ZCashVerifyChainStateTask::OnDatabaseUpdatedForChainReorg, + weak_ptr_factory_.GetWeakPtr())); + } +} + +void ZCashVerifyChainStateTask::OnDatabaseUpdatedForChainReorg( + base::expected result) { + if (!result.has_value()) { + error_ = ZCashShieldSyncService::Error{ + ZCashShieldSyncService::ErrorCode::kFailedToUpdateDatabase, + result.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..fe88e2f073b3 --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task.h @@ -0,0 +1,71 @@ +/* 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_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_ZCASH_ZCASH_VERIFY_CHAIN_STATE_TASK_H_ + +#include + +#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" + +namespace brave_wallet { + +// Ensures that the hash of the latest scanned block remains unchanged for the +// reason of a possible chain reorganization event. If the hash has changed, the +// latest scanned block is rolled back, and outdated data is removed from the +// Orchard database. +class ZCashVerifyChainStateTask { + public: + using ZCashVerifyChainStateTaskCallback = base::OnceCallback)>; + ZCashVerifyChainStateTask(ZCashActionContext& context, + ZCashVerifyChainStateTaskCallback callback); + ~ZCashVerifyChainStateTask(); + + void Start(); + + private: + void WorkOnTask(); + void ScheduleWorkOnTask(); + + void GetAccountMeta(); + void OnGetAccountMeta( + base::expected, + OrchardStorage::Error> 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( + base::expected result); + + raw_ref context_; + 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_H_ diff --git a/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task_unittest.cc b/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task_unittest.cc new file mode 100644 index 000000000000..4b98b7f28eec --- /dev/null +++ b/components/brave_wallet/browser/zcash/zcash_verify_chain_state_task_unittest.cc @@ -0,0 +1,370 @@ +/* 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" + +#include +#include + +#include "base/files/scoped_temp_dir.h" +#include "base/task/thread_pool.h" +#include "base/test/bind.h" +#include "base/test/mock_callback.h" +#include "brave/components/brave_wallet/browser/internal/orchard_sync_state.h" +#include "brave/components/brave_wallet/browser/internal/orchard_test_utils.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_rpc.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_test_utils.h" +#include "brave/components/brave_wallet/common/common_utils.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Eq; +using testing::SaveArg; +using testing::Truly; +using testing::WithArg; + +namespace brave_wallet { + +namespace { + +constexpr uint32_t kLatestScannedBlock = kNu5BlockUpdate + 10000u; +constexpr char kLatestScannedBlockHash[] = "0x00bbaa"; + +class MockZCashRPC : public ZCashRpc { + public: + MockZCashRPC() : ZCashRpc(nullptr, nullptr) {} + ~MockZCashRPC() override = default; + + MOCK_METHOD2(GetLatestBlock, + void(const std::string& chain_id, + GetLatestBlockCallback callback)); + + MOCK_METHOD3(GetTreeState, + void(const std::string& chain_id, + zcash::mojom::BlockIDPtr block, + GetTreeStateCallback callback)); +}; + +} // namespace + +class ZCashVerifyChainStateTaskTest : public testing::Test { + public: + ZCashVerifyChainStateTaskTest() + : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} + + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + base::FilePath db_path( + temp_dir_.GetPath().Append(FILE_PATH_LITERAL("orchard.db"))); + sync_state_.emplace(base::SequencedTaskRunner::GetCurrentDefault(), + db_path.AppendASCII("orchard.db")); + account_id_ = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + + InitSyncState(); + } + + ZCashActionContext CreateContext() { + return ZCashActionContext(zcash_rpc_, sync_state_, account_id_, + mojom::kZCashMainnet); + } + + testing::NiceMock& zcash_rpc() { return zcash_rpc_; } + + base::test::TaskEnvironment& task_environment() { return task_environment_; } + + base::expected, OrchardStorage::Error> + GetSpendableNotes() { + std::optional< + base::expected, OrchardStorage::Error>> + result; + sync_state_.AsyncCall(&OrchardSyncState::GetSpendableNotes) + .WithArgs(account_id_.Clone()) + .Then(base::BindLambdaForTesting( + [&](base::expected, OrchardStorage::Error> + r) { result = std::move(r); })); + task_environment().RunUntilIdle(); + return result.value(); + } + + base::expected, + OrchardStorage::Error> + GetAccountMeta() { + std::optional, + OrchardStorage::Error>> + result; + sync_state_.AsyncCall(&OrchardSyncState::GetAccountMeta) + .WithArgs(account_id_.Clone()) + .Then(base::BindLambdaForTesting( + [&](base::expected, + OrchardStorage::Error> r) { + result = std::move(r); + })); + task_environment().RunUntilIdle(); + return result.value(); + } + + void InitSyncState() { + auto lambda = base::BindLambdaForTesting( + [&](base::expected + result) { EXPECT_TRUE(result.has_value()); }); + sync_state_.AsyncCall(&OrchardSyncState::RegisterAccount) + .WithArgs(account_id_.Clone(), kNu5BlockUpdate + 1) + .Then(std::move(lambda)); + + OrchardBlockScanner::Result result = CreateResultForTesting( + OrchardTreeState(), std::vector()); + result.discovered_notes.push_back(GenerateMockOrchardNote( + account_id_, kLatestScannedBlock - kChainReorgBlockDelta - 2, 1)); + result.discovered_notes.push_back(GenerateMockOrchardNote( + account_id_, kLatestScannedBlock - kChainReorgBlockDelta - 1, 2)); + result.discovered_notes.push_back(GenerateMockOrchardNote( + account_id_, kLatestScannedBlock - kChainReorgBlockDelta + 1, 3)); + result.discovered_notes.push_back(GenerateMockOrchardNote( + account_id_, kLatestScannedBlock - kChainReorgBlockDelta + 2, 4)); + + sync_state_.AsyncCall(&OrchardSyncState::ApplyScanResults) + .WithArgs(account_id_.Clone(), std::move(result), kLatestScannedBlock, + kLatestScannedBlockHash) + .Then(base::BindLambdaForTesting( + [&](base::expected + r) { EXPECT_TRUE(r.has_value()); })); + task_environment().RunUntilIdle(); + ASSERT_EQ(GetSpendableNotes().value().size(), 4u); + } + + private: + base::test::TaskEnvironment task_environment_; + + base::ScopedTempDir temp_dir_; + base::SequenceBound sync_state_; + mojom::AccountIdPtr account_id_; + testing::NiceMock zcash_rpc_; +}; + +TEST_F(ZCashVerifyChainStateTaskTest, Reorg_NewChaintip) { + ON_CALL(zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + std::move(callback).Run(zcash::mojom::BlockID::New( + kLatestScannedBlock + 1000u, std::vector({}))); + })); + + ON_CALL(zcash_rpc(), GetTreeState(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, zcash::mojom::BlockIDPtr block, + ZCashRpc::GetTreeStateCallback callback) { + if (block->height == kLatestScannedBlock) { + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb00", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + return; + } + // Valid tree state + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + })); + + ZCashActionContext context = CreateContext(); + + base::MockCallback< + ZCashVerifyChainStateTask::ZCashVerifyChainStateTaskCallback> + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillRepeatedly( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + }); + + auto task = ZCashVerifyChainStateTask(context, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); + + auto meta_result = GetAccountMeta(); + EXPECT_TRUE(meta_result.has_value()); + EXPECT_EQ(meta_result.value()->latest_scanned_block_id.value(), + kLatestScannedBlock); + EXPECT_EQ(meta_result.value()->latest_scanned_block_hash.value(), + kLatestScannedBlockHash); + EXPECT_EQ(GetSpendableNotes().value().size(), 4u); +} + +TEST_F(ZCashVerifyChainStateTaskTest, NoReorg) { + ON_CALL(zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + std::move(callback).Run(zcash::mojom::BlockID::New( + kLatestScannedBlock + 1000u, std::vector({}))); + })); + + ON_CALL(zcash_rpc(), GetTreeState(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, zcash::mojom::BlockIDPtr block, + ZCashRpc::GetTreeStateCallback callback) { + if (block->height == kLatestScannedBlock) { + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb00", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + return; + } + // Valid tree state + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + })); + + ZCashActionContext context = CreateContext(); + + base::MockCallback< + ZCashVerifyChainStateTask::ZCashVerifyChainStateTaskCallback> + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillRepeatedly( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + }); + + auto task = ZCashVerifyChainStateTask(context, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); + + auto meta_result = GetAccountMeta(); + EXPECT_TRUE(meta_result.has_value()); + EXPECT_EQ(meta_result.value()->latest_scanned_block_id.value(), + kLatestScannedBlock); + EXPECT_EQ(meta_result.value()->latest_scanned_block_hash.value(), + kLatestScannedBlockHash); + EXPECT_EQ(GetSpendableNotes().value().size(), 4u); +} + +TEST_F(ZCashVerifyChainStateTaskTest, Reorg_ChainTipBeforeLatestScannedBlock) { + ON_CALL(zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + std::move(callback).Run(zcash::mojom::BlockID::New( + kLatestScannedBlock - 1u, std::vector({}))); + })); + + ON_CALL(zcash_rpc(), GetTreeState(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, zcash::mojom::BlockIDPtr block, + ZCashRpc::GetTreeStateCallback callback) { + if (block->height == kLatestScannedBlock) { + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb00", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + return; + } + if (block->height == + kLatestScannedBlock - 1 - kChainReorgBlockDelta) { + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb0022", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + return; + } + // Valid tree state + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + })); + + ZCashActionContext context = CreateContext(); + + base::MockCallback< + ZCashVerifyChainStateTask::ZCashVerifyChainStateTaskCallback> + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillRepeatedly( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + }); + + auto task = ZCashVerifyChainStateTask(context, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); + + auto meta_result = GetAccountMeta(); + EXPECT_TRUE(meta_result.has_value()); + EXPECT_EQ(meta_result.value()->latest_scanned_block_id.value(), + kLatestScannedBlock - 1 - kChainReorgBlockDelta); + EXPECT_EQ(meta_result.value()->latest_scanned_block_hash.value(), + "0x2200bbaa"); + EXPECT_EQ(GetSpendableNotes().value().size(), 2u); +} + +TEST_F(ZCashVerifyChainStateTaskTest, Reorg_ChainTipAfterLatestScannedBlock) { + ON_CALL(zcash_rpc(), GetLatestBlock(_, _)) + .WillByDefault( + ::testing::Invoke([](const std::string& chain_id, + ZCashRpc::GetLatestBlockCallback callback) { + std::move(callback).Run(zcash::mojom::BlockID::New( + kLatestScannedBlock + 1000u, std::vector({}))); + })); + + ON_CALL(zcash_rpc(), GetTreeState(_, _, _)) + .WillByDefault(::testing::Invoke( + [](const std::string& chain_id, zcash::mojom::BlockIDPtr block, + ZCashRpc::GetTreeStateCallback callback) { + if (block->height == kLatestScannedBlock) { + // Hash differs from the latest scanned block hash + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb0011", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + return; + } + // New tree state for the new latest scanned block + if (block->height == kLatestScannedBlock - kChainReorgBlockDelta) { + // Hash differs from the latest scanned block hash + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb0022", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + return; + } + // Valid tree state + auto tree_state = zcash::mojom::TreeState::New( + chain_id, block->height, "aabb", 0, "", ""); + std::move(callback).Run(std::move(tree_state)); + })); + + ZCashActionContext context = CreateContext(); + + base::MockCallback< + ZCashVerifyChainStateTask::ZCashVerifyChainStateTaskCallback> + callback; + EXPECT_CALL(callback, Run(testing::_)) + .Times(1) + .WillRepeatedly( + [&](base::expected result) { + EXPECT_TRUE(result.has_value()); + }); + + auto task = ZCashVerifyChainStateTask(context, callback.Get()); + task.Start(); + + task_environment().RunUntilIdle(); + + auto meta_result = GetAccountMeta(); + EXPECT_TRUE(meta_result.has_value()); + EXPECT_EQ(meta_result.value()->latest_scanned_block_id.value(), + kLatestScannedBlock - kChainReorgBlockDelta); + EXPECT_EQ(meta_result.value()->latest_scanned_block_hash.value(), + "0x2200bbaa"); + EXPECT_EQ(GetSpendableNotes().value().size(), 2u); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service.cc b/components/brave_wallet/browser/zcash/zcash_wallet_service.cc index 8a0d8060cfa8..a6e3c867ccb1 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service.cc +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service.cc @@ -134,6 +134,7 @@ void ZCashWalletService::GetZCashAccountInfo( void ZCashWalletService::MakeAccountShielded( mojom::AccountIdPtr account_id, + uint32_t account_birthday_block, MakeAccountShieldedCallback callback) { #if BUILDFLAG(ENABLE_ORCHARD) if (IsZCashShieldedTransactionsEnabled()) { @@ -146,7 +147,12 @@ void ZCashWalletService::MakeAccountShielded( return; } } - GetLatestBlockForAccountBirthday(account_id.Clone(), std::move(callback)); + if (account_birthday_block == 0) { + GetLatestBlockForAccountBirthday(account_id.Clone(), std::move(callback)); + } else { + GetTreeStateForAccountBirthday( + std::move(account_id), account_birthday_block, std::move(callback)); + } return; } #endif @@ -154,6 +160,7 @@ void ZCashWalletService::MakeAccountShielded( } void ZCashWalletService::StartShieldSync(mojom::AccountIdPtr account_id, + uint32_t to, StartShieldSyncCallback callback) { #if BUILDFLAG(ENABLE_ORCHARD) if (IsZCashShieldedTransactionsEnabled()) { @@ -176,10 +183,12 @@ void ZCashWalletService::StartShieldSync(mojom::AccountIdPtr account_id, shield_sync_services_[account_id.Clone()] = std::make_unique( - *zcash_rpc_, sync_state_, account_id, account_birthday, fvk.value(), - weak_ptr_factory_.GetWeakPtr()); + CreateActionContext( + account_id, GetNetworkForZCashKeyring(account_id->keyring_id)), + account_birthday, fvk.value(), weak_ptr_factory_.GetWeakPtr()); - shield_sync_services_[account_id.Clone()]->StartSyncing(); + shield_sync_services_[account_id.Clone()]->StartSyncing( + to == 0 ? std::nullopt : std::optional(to)); std::move(callback).Run(std::nullopt); return; @@ -207,16 +216,6 @@ void ZCashWalletService::StopShieldSync(mojom::AccountIdPtr account_id, std::move(callback).Run("Not supported"); } -ZCashActionContext ZCashWalletService::CreateActionContext( - const mojom::AccountIdPtr& account_id, - const std::string chain_id) { - return ZCashActionContext(*zcash_rpc_, -#if BUILDFLAG(ENABLE_ORCHARD) - sync_state_, -#endif - account_id.Clone(), chain_id); -} - void ZCashWalletService::GetChainTipStatus(mojom::AccountIdPtr account_id, const std::string& chain_id, GetChainTipStatusCallback callback) { @@ -364,7 +363,7 @@ void ZCashWalletService::CompleteTransactionDone( auto tx = ZCashSerializer::SerializeRawTransaction(result.value()); zcash_rpc_->SendTransaction( - chain_id, tx, + chain_id, std::move(tx), base::BindOnce(&ZCashWalletService::OnSendTransactionResult, weak_ptr_factory_.GetWeakPtr(), std::move(callback), std::move(result.value()))); @@ -594,8 +593,30 @@ void ZCashWalletService::ShieldAllFunds(const std::string& chain_id, #endif } +void ZCashWalletService::ResetSyncState(mojom::AccountIdPtr account_id, + ResetSyncStateCallback callback) { #if BUILDFLAG(ENABLE_ORCHARD) + if (IsZCashShieldedTransactionsEnabled()) { + if (shield_sync_services_.find(account_id) != shield_sync_services_.end()) { + std::move(callback).Run("Sync in progress"); + return; + } + sync_state_.AsyncCall(&OrchardSyncState::ResetAccountSyncState) + .WithArgs(account_id.Clone()) + .Then(base::BindOnce(&ZCashWalletService::OnResetSyncState, + weak_ptr_factory_.GetWeakPtr(), + std::move(callback))); + } else { + std::move(callback).Run( + l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); + } +#else + std::move(callback).Run( + l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); +#endif +} +#if BUILDFLAG(ENABLE_ORCHARD) void ZCashWalletService::CreateShieldAllTransaction( const std::string& chain_id, mojom::AccountIdPtr account_id, @@ -662,19 +683,28 @@ void ZCashWalletService::OnGetLatestBlockForAccountBirthday( return; } + GetTreeStateForAccountBirthday(std::move(account_id), (*result)->height, + std::move(callback)); +} + +void ZCashWalletService::GetTreeStateForAccountBirthday( + mojom::AccountIdPtr account_id, + uint32_t block_id, + MakeAccountShieldedCallback callback) { // Get block info for the block that is back from latest block for // kChainReorgBlockDelta to ensure account birthday won't be affected by chain // reorg. - if ((*result)->height < kChainReorgBlockDelta) { + if (block_id < kChainReorgBlockDelta) { std::move(callback).Run("Failed to retrieve latest block"); return; } - auto block_id = zcash::mojom::BlockID::New( - (*result)->height - kChainReorgBlockDelta, std::vector()); + auto block_id_param = zcash::mojom::BlockID::New( + block_id - kChainReorgBlockDelta, std::vector()); zcash_rpc_->GetTreeState( - GetNetworkForZCashKeyring(account_id->keyring_id), std::move(block_id), + GetNetworkForZCashKeyring(account_id->keyring_id), + std::move(block_id_param), base::BindOnce(&ZCashWalletService::OnGetTreeStateForAccountBirthday, weak_ptr_factory_.GetWeakPtr(), account_id.Clone(), std::move(callback))); @@ -732,6 +762,20 @@ void ZCashWalletService::OnSyncStatusUpdate( } } +void ZCashWalletService::OnResetSyncState( + ResetSyncStateCallback callback, + base::expected result) { + if (result.has_value()) { + std::move(callback).Run( + result.value() == OrchardStorage::Result::kSuccess + ? std::nullopt + : std::optional("Account data wasn't deleted")); + return; + } + + std::move(callback).Run(result.error().message); +} + void ZCashWalletService::OnGetChainTipStatusResult( GetChainTipStatusCallback callback, base::expected result) { @@ -777,11 +821,11 @@ void ZCashWalletService::CreateTransactionTaskDone( } #if BUILDFLAG(ENABLE_ORCHARD) -void ZCashWalletService::CreateTransactionTaskDone( - ZCashCreateShieldTransactionTask* task) { - CHECK(create_shield_transaction_tasks_.remove_if( - [task](auto& item) { return item.get() == task; })); -} + void ZCashWalletService::CreateTransactionTaskDone( + ZCashCreateShieldTransactionTask* task) { + CHECK(create_shield_transaction_tasks_.remove_if( + [task](auto& item) { return item.get() == task; })); + } #endif // BUILDFLAG(ENABLE_ORCHARD) void ZCashWalletService::ResolveBalanceTaskDone(ZCashResolveBalanceTask* task) { @@ -826,4 +870,14 @@ void ZCashWalletService::Reset() { #endif // BUILDFLAG(ENABLE_ORCHARD) } +ZCashActionContext ZCashWalletService::CreateActionContext( + const mojom::AccountIdPtr& account_id, + const std::string chain_id) { + return ZCashActionContext(*zcash_rpc_, +#if BUILDFLAG(ENABLE_ORCHARD) + sync_state_, +#endif + account_id.Clone(), chain_id); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service.h b/components/brave_wallet/browser/zcash/zcash_wallet_service.h index ec9b87dbb61c..263e8563d69e 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service.h +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service.h @@ -19,15 +19,16 @@ #include "brave/components/brave_wallet/browser/keyring_service_observer_base.h" #include "brave/components/brave_wallet/browser/zcash/zcash_action_context.h" #include "brave/components/brave_wallet/browser/zcash/zcash_rpc.h" -#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" #include "brave/components/brave_wallet/browser/zcash/zcash_transaction.h" #include "brave/components/brave_wallet/browser/zcash/zcash_transaction_complete_manager.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/buildflags.h" #include "brave/components/brave_wallet/common/zcash_utils.h" +#include "brave/components/services/brave_wallet/public/mojom/zcash_decoder.mojom.h" #if BUILDFLAG(ENABLE_ORCHARD) #include "brave/components/brave_wallet/browser/internal/orchard_sync_state.h" +#include "brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h" #endif namespace brave_wallet { @@ -85,9 +86,12 @@ class ZCashWalletService : public mojom::ZCashWalletService, GetZCashAccountInfoCallback callback) override; void MakeAccountShielded(mojom::AccountIdPtr account_id, + uint32_t account_birthday_block, MakeAccountShieldedCallback callback) override; + // Starts Orchard pool syncing for the provided account. void StartShieldSync(mojom::AccountIdPtr account_id, + uint32_t to, StartShieldSyncCallback callback) override; void StopShieldSync(mojom::AccountIdPtr account_id, StopShieldSyncCallback callback) override; @@ -108,6 +112,9 @@ class ZCashWalletService : public mojom::ZCashWalletService, mojom::AccountIdPtr account_id, ShieldAllFundsCallback callback) override; + void ResetSyncState(mojom::AccountIdPtr account_id, + ResetSyncStateCallback callback) override; + void RunDiscovery(mojom::AccountIdPtr account_id, RunDiscoveryCallback callback); @@ -159,12 +166,10 @@ class ZCashWalletService : public mojom::ZCashWalletService, void Reset(); private: - friend class ZCashCreateShieldTransactionTask; friend class ZCashCreateTransparentTransactionTask; friend class ZCashDiscoverNextUnusedZCashAddressTask; friend class ZCashGetZCashChainTipStatusTask; friend class ZCashResolveBalanceTask; - friend class ZCashShieldSyncService; friend class ZCashTransactionCompleteManager; friend class ZCashTxManager; @@ -189,15 +194,16 @@ class ZCashWalletService : public mojom::ZCashWalletService, std::string> result); void WorkOnGetUtxos(scoped_refptr context); + void OnTransactionResolvedForStatus( + GetTransactionStatusCallback callback, + base::expected result); + void OnDiscoveryDoneForBalance(mojom::AccountIdPtr account_id, std::string chain_id, GetBalanceCallback callback, RunDiscoveryResult discovery_result); void OnUtxosResolvedForBalance(GetBalanceCallback initial_callback, base::expected result); - void OnTransactionResolvedForStatus( - GetTransactionStatusCallback callback, - base::expected result); void OnSendTransactionResult( SignAndPostTransactionCallback callback, @@ -221,7 +227,6 @@ class ZCashWalletService : public mojom::ZCashWalletService, void CreateShieldAllTransaction(const std::string& chain_id, mojom::AccountIdPtr account_id, CreateTransactionCallback callback); - void CreateShieldAllTransactionTaskDone( const std::string& chain_id, mojom::AccountIdPtr account_id, @@ -240,6 +245,9 @@ class ZCashWalletService : public mojom::ZCashWalletService, mojom::AccountIdPtr account_id, MakeAccountShieldedCallback callback, base::expected result); + void GetTreeStateForAccountBirthday(mojom::AccountIdPtr account_id, + uint32_t block_id, + MakeAccountShieldedCallback callback); void OnGetTreeStateForAccountBirthday( mojom::AccountIdPtr account_id, MakeAccountShieldedCallback callback, @@ -260,6 +268,10 @@ class ZCashWalletService : public mojom::ZCashWalletService, const mojom::AccountIdPtr& account_id, const mojom::ZCashShieldSyncStatusPtr& status) override; + void OnResetSyncState( + ResetSyncStateCallback callback, + base::expected result); + base::SequenceBound& sync_state(); void OnGetChainTipStatusResult( @@ -288,16 +300,15 @@ class ZCashWalletService : public mojom::ZCashWalletService, #if BUILDFLAG(ENABLE_ORCHARD) base::SequenceBound sync_state_; - std::list> - create_shield_transaction_tasks_; - std::list> - get_zcash_chain_tip_status_tasks_; std::map> shield_sync_services_; + std::list> + get_zcash_chain_tip_status_tasks_; + std::list> + create_shield_transaction_tasks_; #endif mojo::RemoteSet observers_; - mojo::ReceiverSet receivers_; mojo::Receiver keyring_observer_receiver_{this}; diff --git a/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc b/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc index e3d15b7f1c45..c59c3bfcafe9 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc @@ -934,7 +934,7 @@ TEST_F(ZCashWalletServiceUnitTest, MakeAccountShielded) { EXPECT_CALL(make_account_shielded_callback, Run(Eq(std::nullopt))); zcash_wallet_service_->MakeAccountShielded( - account_id_1.Clone(), make_account_shielded_callback.Get()); + account_id_1.Clone(), 0, make_account_shielded_callback.Get()); task_environment_.RunUntilIdle(); } diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index 52da80735d26..2a4c1c7e4034 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -1857,8 +1857,12 @@ enum ZCashAddressValidationResult { }; struct ZCashShieldSyncStatus { - uint64 current_block; - uint64 chain_tip; + uint32 start_block; + uint32 end_block; + + uint32 total_ranges; + uint32 scanned_ranges; + uint32 notes_found; uint64 spendable_balance; }; @@ -1886,14 +1890,14 @@ interface ZCashWalletService { (ZCashAddressValidationResult result); ShieldAllFunds(string network_id, AccountId account_id) => (string? tx_id, string? error_message); - GetChainTipStatus(AccountId account_id, string chain_id) => - (ZCashChainTipStatus? status, string? error_message); AddObserver(pending_remote observer); - MakeAccountShielded(AccountId account_id) => (string? error_message); - StartShieldSync(AccountId account_id) => (string? error_message); + MakeAccountShielded(AccountId account_id, uint32 account_birthday_block /* 0 if use last available block */) => (string? error_message); + StartShieldSync(AccountId account_id, uint32 to /* 0 if latest available block is used */) => (string? error_message); StopShieldSync(AccountId account_id) => (string? error_message); + ResetSyncState(AccountId account_id) => (string? error_message); + GetChainTipStatus(AccountId account_id, string chain_id) => (ZCashChainTipStatus? status, string? error_message); }; enum TransactionStatus { diff --git a/components/brave_wallet/common/zcash_utils.h b/components/brave_wallet/common/zcash_utils.h index 4a0b043c6b84..647b25e61281 100644 --- a/components/brave_wallet/common/zcash_utils.h +++ b/components/brave_wallet/common/zcash_utils.h @@ -105,9 +105,9 @@ struct OrchardOutput { static std::optional FromValue(const base::Value::Dict& value); }; -// Structure describes note nullifier that marks some note as spent +// Describes note nullifier that marks some note as spent. struct OrchardNoteSpend { - // Block id where spent nullifier was met + // Block id where spent nullifier was met. uint32_t block_id = 0; std::array nullifier; diff --git a/components/brave_wallet_ui/common/slices/endpoints/zcash.endpoints.ts b/components/brave_wallet_ui/common/slices/endpoints/zcash.endpoints.ts index 8862d648f694..341a1a6ac039 100644 --- a/components/brave_wallet_ui/common/slices/endpoints/zcash.endpoints.ts +++ b/components/brave_wallet_ui/common/slices/endpoints/zcash.endpoints.ts @@ -22,7 +22,7 @@ export const zcashEndpoints = ({ const { zcashWalletService } = baseQuery(undefined).data const { errorMessage } = await zcashWalletService.makeAccountShielded( - args + args, 0 ) if (errorMessage) { 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 f303bf46bf5e..982a2ad44f80 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 @@ -7,7 +7,6 @@ import getAPIProxy from '../../../common/async/bridge' import * as React from 'react' import styled from 'styled-components' -import { useState } from 'react' import { BraveWallet } from '../../../constants/types' import { LoadingSkeleton // @@ -55,20 +54,24 @@ interface GetBalanceSectionProps { } const GetBalanceSection = (props: GetBalanceSectionProps) => { - const [loading, setLoading] = useState(true) - const [balance, setBalance] = useState< + const [loading, setLoading] = React.useState(true) + const [balance, setBalance] = React.useState< BraveWallet.ZCashBalance | undefined >() - const [shieldResult, setShieldResult] = useState() + const [shieldResult, setShieldResult] = React.useState() const [ makeAccountShieldableResult, - setMakeAccountShieldableResult] = useState() - const [syncStatusResult, setSyncStatusResult] = useState(); - const [shieldedBalanceValue, setShieldedBalanceValue] = useState(); + setMakeAccountShieldableResult] = React.useState() + const [syncStatusResult, setSyncStatusResult] = React.useState(); + const [shieldedBalanceValue, setShieldedBalanceValue] = + React.useState(); + const [accountBirthdayValue, setAccountBirthdayValue] = + React.useState(); + const [syncBlockLimit, setSyncBlockLimit] = React.useState(); const makeAccountShielded = async() => { const result = await getAPIProxy().zcashWalletService.makeAccountShielded( - props.accountId); + props.accountId, Number(accountBirthdayValue || '0')); setMakeAccountShieldableResult(result.errorMessage || 'Done'); } @@ -76,7 +79,8 @@ const GetBalanceSection = (props: GetBalanceSectionProps) => { const startOrchardSync = async() => { setSyncStatusResult('') const result = - await getAPIProxy().zcashWalletService.startShieldSync(props.accountId); + await getAPIProxy().zcashWalletService.startShieldSync( + props.accountId, Number(syncBlockLimit || '0')); if (result.errorMessage) { setSyncStatusResult("Sync error " + result.errorMessage); @@ -91,6 +95,14 @@ const GetBalanceSection = (props: GetBalanceSectionProps) => { } } + const resetAccountSyncState = async() => { + const result = + await getAPIProxy().zcashWalletService.resetSyncState(props.accountId); + if (result.errorMessage) { + setSyncStatusResult("Stop error " + result.errorMessage); + } + } + const shieldAllFunds = async () => { let { txId, @@ -130,8 +142,9 @@ 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); } }, @@ -163,9 +176,28 @@ const GetBalanceSection = (props: GetBalanceSectionProps) => { /> ) : ( <> + setAccountBirthdayValue(ev.target.value)} + spellCheck={false} + /> + setSyncBlockLimit(ev.target.value)} + spellCheck={false} + /> + @@ -203,8 +235,8 @@ interface GetZCashAccountInfoSectionProps { const GetZCashAccountInfoSection: React.FC< GetZCashAccountInfoSectionProps > = ({ accountId }) => { - const [loading, setLoading] = useState(true) - const [zcashAccountInfo, setZCashAccountInfo] = useState< + const [loading, setLoading] = React.useState(true) + const [zcashAccountInfo, setZCashAccountInfo] = React.useState< BraveWallet.ZCashAccountInfo | undefined >() @@ -251,6 +283,18 @@ const GetZCashAccountInfoSection: React.FC< {zcashAccountInfo?.unifiedAddress || '-'} +
+ Orchard address: + + {zcashAccountInfo?.orchardAddress || '-'} + +
+
+ Account shielded birthday: + + {String(zcashAccountInfo?.accountShieldBirthday?.value) || '-'} + +
Next Receive Address: {keyId(zcashAccountInfo?.nextTransparentReceiveAddress.keyId)}