From eebde0482013e7e64451865cb8d20302e9f2f8f3 Mon Sep 17 00:00:00 2001 From: oisupov Date: Wed, 13 Nov 2024 22:09:02 +0400 Subject: [PATCH] Upgrade ZCashOrchardStorage for shard tree purposes Resolves https://github.com/brave/brave-browser/issues/42281 --- .../browser/internal/orchard_block_scanner.cc | 14 +- .../browser/internal/orchard_block_scanner.h | 4 +- .../orchard_block_scanner_unittest.cc | 2 +- components/brave_wallet/browser/test/BUILD.gn | 2 - .../zcash/rust/orchard_block_decoder_impl.cc | 11 +- .../browser/zcash/zcash_orchard_storage.cc | 1106 ++++++++++++++++- .../browser/zcash/zcash_orchard_storage.h | 111 +- .../zcash/zcash_orchard_storage_unittest.cc | 604 ++++++++- .../zcash/zcash_shield_sync_service.cc | 24 +- .../browser/zcash/zcash_shield_sync_service.h | 2 +- .../zcash_shield_sync_service_unittest.cc | 8 +- .../browser/zcash/zcash_test_utils.cc | 18 +- .../browser/zcash/zcash_test_utils.h | 2 +- .../zcash/zcash_wallet_service_unittest.cc | 4 +- components/brave_wallet/common/zcash_utils.cc | 148 +++ components/brave_wallet/common/zcash_utils.h | 175 ++- .../public/mojom/zcash_decoder.mojom | 6 + 17 files changed, 2114 insertions(+), 127 deletions(-) diff --git a/components/brave_wallet/browser/internal/orchard_block_scanner.cc b/components/brave_wallet/browser/internal/orchard_block_scanner.cc index 186d740b0c34..ad4baaf1b730 100644 --- a/components/brave_wallet/browser/internal/orchard_block_scanner.cc +++ b/components/brave_wallet/browser/internal/orchard_block_scanner.cc @@ -10,7 +10,7 @@ namespace brave_wallet { OrchardBlockScanner::Result::Result() = default; OrchardBlockScanner::Result::Result(std::vector discovered_notes, - std::vector spent_notes) + std::vector spent_notes) : discovered_notes(std::move(discovered_notes)), spent_notes(std::move(spent_notes)) {} @@ -31,7 +31,7 @@ base::expected OrchardBlockScanner::ScanBlocks( std::vector known_notes, std::vector blocks) { - std::vector found_nullifiers; + std::vector found_spends; std::vector found_notes; for (const auto& block : blocks) { @@ -64,15 +64,15 @@ OrchardBlockScanner::ScanBlocks( [&action_nullifier](const auto& v) { return v.nullifier == action_nullifier; }) != known_notes.end()) { - OrchardNullifier nullifier; - nullifier.block_id = block->height; - nullifier.nullifier = action_nullifier; - found_nullifiers.push_back(std::move(nullifier)); + OrchardNoteSpend spend; + spend.block_id = block->height; + spend.nullifier = action_nullifier; + found_spends.push_back(std::move(spend)); } } } } - return Result({std::move(found_notes), std::move(found_nullifiers)}); + return Result({std::move(found_notes), std::move(found_spends)}); } } // namespace brave_wallet diff --git a/components/brave_wallet/browser/internal/orchard_block_scanner.h b/components/brave_wallet/browser/internal/orchard_block_scanner.h index e5380b02edd4..a2fa0d018653 100644 --- a/components/brave_wallet/browser/internal/orchard_block_scanner.h +++ b/components/brave_wallet/browser/internal/orchard_block_scanner.h @@ -26,7 +26,7 @@ class OrchardBlockScanner { struct Result { Result(); Result(std::vector discovered_notes, - std::vector spent_notes); + std::vector spent_notes); Result(const Result&); Result& operator=(const Result&); ~Result(); @@ -34,7 +34,7 @@ class OrchardBlockScanner { // New notes have been discovered std::vector discovered_notes; // Nullifiers for the previously discovered notes - std::vector spent_notes; + std::vector spent_notes; }; explicit OrchardBlockScanner(const OrchardFullViewKey& full_view_key); diff --git a/components/brave_wallet/browser/internal/orchard_block_scanner_unittest.cc b/components/brave_wallet/browser/internal/orchard_block_scanner_unittest.cc index 9fae8b28942c..dbf713735299 100644 --- a/components/brave_wallet/browser/internal/orchard_block_scanner_unittest.cc +++ b/components/brave_wallet/browser/internal/orchard_block_scanner_unittest.cc @@ -499,7 +499,7 @@ TEST(OrchardBlockScanner, FoundKnownNullifiers) { PrefixedHexStringToBytes( "0x1b32edbbe4d18f28876de262518ad31122701f8c0a52e98047a337876e7eea19") .value(); - OrchardNullifier nf; + OrchardNoteSpend nf; base::ranges::copy(nullifier_bytes, nf.nullifier.begin()); nf.block_id = 10; diff --git a/components/brave_wallet/browser/test/BUILD.gn b/components/brave_wallet/browser/test/BUILD.gn index 1cb94f2a57e1..cc16c3ddf3b9 100644 --- a/components/brave_wallet/browser/test/BUILD.gn +++ b/components/brave_wallet/browser/test/BUILD.gn @@ -164,8 +164,6 @@ source_set("brave_wallet_unit_tests") { "//brave/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc", "//brave/components/brave_wallet/browser/zcash/zcash_shield_sync_service_unittest.cc", ] - deps += - [ "//brave/components/brave_wallet/browser/internal:orchard_bundle" ] } if (!is_ios) { diff --git a/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.cc b/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.cc index 69afd446414f..eeafe7863494 100644 --- a/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.cc +++ b/components/brave_wallet/browser/zcash/rust/orchard_block_decoder_impl.cc @@ -56,9 +56,14 @@ OrchardBlockDecoderImpl::ScanBlock( ::rust::Box<::brave_wallet::orchard::BatchOrchardDecodeBundle> result_bundle = decode_result->unwrap(); for (size_t i = 0; i < result_bundle->size(); i++) { - result.emplace_back(OrchardNote( - {block->height, result_bundle->note_nullifier(full_view_key_, i), - result_bundle->note_value(i)})); + result.emplace_back( + OrchardNote({{}, + block->height, + result_bundle->note_nullifier(full_view_key_, i), + result_bundle->note_value(i), + 0, + {}, + {}})); } } else { return std::nullopt; diff --git a/components/brave_wallet/browser/zcash/zcash_orchard_storage.cc b/components/brave_wallet/browser/zcash/zcash_orchard_storage.cc index b7511f4d8dbd..234d64fdf254 100644 --- a/components/brave_wallet/browser/zcash/zcash_orchard_storage.cc +++ b/components/brave_wallet/browser/zcash/zcash_orchard_storage.cc @@ -15,7 +15,6 @@ #include "base/strings/stringprintf.h" #include "brave/components/brave_wallet/common/hex_utils.h" #include "sql/meta_table.h" -#include "sql/statement.h" #include "sql/transaction.h" namespace brave_wallet { @@ -24,6 +23,10 @@ namespace { #define kNotesTable "notes" #define kSpentNotesTable "spent_notes" #define kAccountMeta "account_meta" +#define kShardTree "shard_tree" +#define kShardTreeCap "shard_tree_cap" +#define kShardTreeCheckpoints "checkpoints" +#define kCheckpointsMarksRemoved "checkpoints_mark_removed" const int kEmptyDbVersionNumber = 1; const int kCurrentVersionNumber = 2; @@ -36,8 +39,42 @@ std::optional ReadUint32(sql::Statement& statement, size_t index) { return static_cast(v); } +base::expected ReadCheckpointTreeState( + sql::Statement& statement, + size_t index) { + if (statement.GetColumnType(index) == sql::ColumnType::kNull) { + return std::nullopt; + } + auto v = ReadUint32(statement, index); + if (!v) { + return base::unexpected("Format error"); + } + return *v; +} + +base::expected, std::string> ReadRootHash( + sql::Statement& statement, + size_t index) { + if (statement.GetColumnType(index) == sql::ColumnType::kNull) { + return std::nullopt; + } + auto v = statement.ColumnBlob(index); + if (v.size() != kOrchardShardTreeHashSize) { + return base::unexpected("Size error"); + } + std::array result; + base::ranges::copy(v.begin(), v.end(), result.begin()); + return result; +} + } // namespace +ZCashOrchardStorage::AccountMeta::AccountMeta() = default; +ZCashOrchardStorage::AccountMeta::~AccountMeta() = default; +ZCashOrchardStorage::AccountMeta::AccountMeta(const AccountMeta&) = default; +ZCashOrchardStorage::AccountMeta& ZCashOrchardStorage::AccountMeta::operator=( + const AccountMeta&) = default; + ZCashOrchardStorage::ZCashOrchardStorage(base::FilePath path_to_database) : db_file_path_(std::move(path_to_database)) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); @@ -108,8 +145,12 @@ bool ZCashOrchardStorage::CreateSchema() { "id INTEGER PRIMARY KEY AUTOINCREMENT," "account_id TEXT NOT NULL," "amount INTEGER NOT NULL," + "addr BLOB NOT NULL," "block_id INTEGER NOT NULL," - "nullifier BLOB NOT NULL UNIQUE);") && + "commitment_tree_position INTEGER," + "nullifier BLOB NOT NULL UNIQUE," + "rho BLOB NOT NULL," + "rseed BLOB NOT NULL);") && database_.Execute("CREATE TABLE " kSpentNotesTable " (" "id INTEGER PRIMARY KEY AUTOINCREMENT," @@ -120,8 +161,38 @@ bool ZCashOrchardStorage::CreateSchema() { " (" "account_id TEXT NOT NULL PRIMARY KEY," "account_birthday INTEGER NOT NULL," - "latest_scanned_block INTEGER NOT NULL," - "latest_scanned_block_hash TEXT NOT NULL);") && + "latest_scanned_block INTEGER," + "latest_scanned_block_hash TEXT);") && + database_.Execute( + "CREATE TABLE " kShardTree + " (" + "account_id TEXT NOT NULL," + "shard_index INTEGER NOT NULL," + "subtree_end_height INTEGER," + "root_hash BLOB," + "shard_data BLOB," + "CONSTRAINT shard_index_unique UNIQUE (shard_index, account_id)," + "CONSTRAINT root_unique UNIQUE (root_hash, account_id));") && + database_.Execute("CREATE TABLE " kShardTreeCheckpoints + " (" + "account_id TEXT NOT NULL," + "checkpoint_id INTEGER PRIMARY KEY," + "position INTEGER)") && + database_.Execute("CREATE TABLE " kCheckpointsMarksRemoved + " (" + "account_id TEXT NOT NULL," + "checkpoint_id INTEGER NOT NULL," + "mark_removed_position INTEGER NOT NULL," + "FOREIGN KEY (checkpoint_id) REFERENCES " + "orchard_tree_checkpoints(checkpoint_id)" + "ON DELETE CASCADE," + "CONSTRAINT spend_position_unique UNIQUE " + "(checkpoint_id, mark_removed_position, account_id)" + ")") && + database_.Execute("CREATE TABLE " kShardTreeCap + " (" + "account_id TEXT NOT NULL," + "cap_data BLOB NOT NULL)") && transaction.Commit(); } @@ -131,33 +202,28 @@ bool ZCashOrchardStorage::UpdateSchema() { } base::expected -ZCashOrchardStorage::RegisterAccount( - mojom::AccountIdPtr account_id, - uint32_t account_birthday_block, - const std::string& account_birthday_block_hash) { +ZCashOrchardStorage::RegisterAccount(mojom::AccountIdPtr account_id, + uint32_t account_birthday_block) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (!EnsureDbInit()) { return base::unexpected( - Error{ErrorCode::kDbInitError, "Failed to init database "}); + Error{ErrorCode::kDbInitError, "Failed to init database"}); } sql::Transaction transaction(&database_); if (!transaction.Begin()) { return base::unexpected( - Error{ErrorCode::kDbInitError, "Failed to init database "}); + Error{ErrorCode::kDbInitError, "Failed to start transaction"}); } sql::Statement register_account_statement(database_.GetCachedStatement( SQL_FROM_HERE, "INSERT INTO " kAccountMeta " " - "(account_id, account_birthday, latest_scanned_block, " - "latest_scanned_block_hash) " - "VALUES (?, ?, ?, ?)")); + "(account_id, account_birthday) " + "VALUES (?, ?)")); register_account_statement.BindString(0, account_id->unique_key); register_account_statement.BindInt64(1, account_birthday_block); - register_account_statement.BindInt64(2, account_birthday_block); - register_account_statement.BindString(3, account_birthday_block_hash); if (!register_account_statement.Run()) { return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, @@ -165,12 +231,13 @@ ZCashOrchardStorage::RegisterAccount( } if (!transaction.Commit()) { - return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + return base::unexpected(Error{ErrorCode::kFailedToCommitTransaction, database_.GetErrorMessage()}); } - return AccountMeta{account_birthday_block, account_birthday_block, - account_birthday_block_hash}; + AccountMeta meta; + meta.account_birthday = account_birthday_block; + return meta; } base::expected @@ -196,16 +263,26 @@ ZCashOrchardStorage::GetAccountMeta(mojom::AccountIdPtr account_id) { AccountMeta account_meta; auto account_birthday = ReadUint32(resolve_account_statement, 0); - auto latest_scanned_block = ReadUint32(resolve_account_statement, 1); - if (!account_birthday || !latest_scanned_block) { + if (!account_birthday) { return base::unexpected( Error{ErrorCode::kInternalError, "Database format error"}); } + account_meta.account_birthday = account_birthday.value(); + + if (resolve_account_statement.GetColumnType(1) != sql::ColumnType::kNull) { + auto latest_scanned_block = ReadUint32(resolve_account_statement, 1); + if (!latest_scanned_block) { + return base::unexpected( + Error{ErrorCode::kInternalError, "Database format error"}); + } + account_meta.latest_scanned_block_id = latest_scanned_block.value(); + } + + if (resolve_account_statement.GetColumnType(2) != sql::ColumnType::kNull) { + account_meta.latest_scanned_block_hash = + resolve_account_statement.ColumnString(2); + } - account_meta.account_birthday = *account_birthday; - account_meta.latest_scanned_block_id = *latest_scanned_block; - account_meta.latest_scanned_block_hash = - resolve_account_statement.ColumnString(2); return account_meta; } @@ -265,7 +342,69 @@ std::optional ZCashOrchardStorage::HandleChainReorg( return std::nullopt; } -base::expected, ZCashOrchardStorage::Error> +base::expected +ZCashOrchardStorage::ResetAccountSyncState(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + // Clear cap + sql::Statement clear_cap_statement(database_.GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kShardTreeCap " WHERE account_id = ?;")); + clear_cap_statement.BindString(0, account_id->unique_key); + + // Clear shards + sql::Statement clear_shards_statement(database_.GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kShardTree " WHERE account_id = ?;")); + clear_shards_statement.BindString(0, account_id->unique_key); + + // Clear discovered notes + sql::Statement clear_discovered_notes(database_.GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kNotesTable " WHERE account_id = ?;")); + clear_discovered_notes.BindString(0, account_id->unique_key); + + // Clear spent notes + sql::Statement clear_spent_notes(database_.GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kSpentNotesTable " WHERE account_id = ?;")); + clear_spent_notes.BindString(0, account_id->unique_key); + + // Clear checkpoints + sql::Statement clear_checkpoints_statement(database_.GetCachedStatement( + SQL_FROM_HERE, + "DELETE FROM " kShardTreeCheckpoints " WHERE account_id = ?;")); + clear_checkpoints_statement.BindString(0, account_id->unique_key); + + // Update account meta + sql::Statement update_account_meta(database_.GetCachedStatement( + SQL_FROM_HERE, "UPDATE " kAccountMeta " " + "SET latest_scanned_block = NULL, " + "latest_scanned_block_hash = NULL WHERE account_id = ?;")); + update_account_meta.BindString(0, account_id->unique_key); + + sql::Transaction transaction(&database_); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + if (!clear_cap_statement.Run() || !clear_shards_statement.Run() || + !clear_discovered_notes.Run() || !clear_spent_notes.Run() || + !clear_checkpoints_statement.Run() || !update_account_meta.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kFailedToCommitTransaction, + database_.GetErrorMessage()}); + } + + return true; +} + +base::expected, ZCashOrchardStorage::Error> ZCashOrchardStorage::GetNullifiers(mojom::AccountIdPtr account_id) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); @@ -281,18 +420,18 @@ ZCashOrchardStorage::GetNullifiers(mojom::AccountIdPtr account_id) { resolve_note_spents.BindString(0, account_id->unique_key); - std::vector result; + std::vector result; while (resolve_note_spents.Step()) { - OrchardNullifier nf; + OrchardNoteSpend spend; auto block_id = ReadUint32(resolve_note_spents, 0); if (!block_id) { return base::unexpected( Error{ErrorCode::kDbInitError, "Wrong database format"}); } - nf.block_id = block_id.value(); + spend.block_id = block_id.value(); auto nullifier = resolve_note_spents.ColumnBlob(1); - base::ranges::copy(nullifier, nf.nullifier.begin()); - result.push_back(std::move(nf)); + base::ranges::copy(nullifier, spend.nullifier.begin()); + result.push_back(std::move(spend)); } if (!resolve_note_spents.Succeeded()) { return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, @@ -313,8 +452,9 @@ ZCashOrchardStorage::GetSpendableNotes(mojom::AccountIdPtr account_id) { sql::Statement resolve_unspent_notes(database_.GetCachedStatement( SQL_FROM_HERE, "SELECT " - "notes.block_id, notes.amount," - "notes.nullifier FROM " kNotesTable + "notes.block_id, notes.commitment_tree_position, notes.amount," + "notes.rho, notes.rseed," + "notes.nullifier, notes.addr FROM " kNotesTable " " "LEFT OUTER JOIN spent_notes " "ON notes.nullifier = spent_notes.nullifier AND notes.account_id = " @@ -327,15 +467,31 @@ ZCashOrchardStorage::GetSpendableNotes(mojom::AccountIdPtr account_id) { while (resolve_unspent_notes.Step()) { OrchardNote note; auto block_id = ReadUint32(resolve_unspent_notes, 0); - auto amount = ReadUint32(resolve_unspent_notes, 1); - if (!block_id || !amount) { + auto commitment_tree_position = ReadUint32(resolve_unspent_notes, 1); + auto amount = ReadUint32(resolve_unspent_notes, 2); + if (!block_id || !amount || !commitment_tree_position) { return base::unexpected( Error{ErrorCode::kDbInitError, "Wrong database format"}); } + auto rho = ReadSizedBlob(resolve_unspent_notes, 3); + auto rseed = ReadSizedBlob(resolve_unspent_notes, 4); + auto nf = ReadSizedBlob(resolve_unspent_notes, 5); + auto addr = ReadSizedBlob(resolve_unspent_notes, 6); + + if (!rho.has_value() || !rho.value() || !rseed.has_value() || + !rseed.value() || !nf.has_value() || !nf.value() || !addr.has_value() || + !addr.value()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Wrong database format"}); + } + note.block_id = block_id.value(); note.amount = amount.value(); - auto nullifier = resolve_unspent_notes.ColumnBlob(2); - base::ranges::copy(nullifier, note.nullifier.begin()); + note.orchard_commitment_tree_position = commitment_tree_position.value(); + note.rho = **rho; + note.seed = **rseed; + note.nullifier = **nf; + note.addr = **addr; result.push_back(std::move(note)); } return result; @@ -344,7 +500,7 @@ ZCashOrchardStorage::GetSpendableNotes(mojom::AccountIdPtr account_id) { std::optional ZCashOrchardStorage::UpdateNotes( mojom::AccountIdPtr account_id, const std::vector& found_notes, - const std::vector& spent_notes, + const std::vector& found_nullifiers, const uint32_t latest_scanned_block, const std::string& latest_scanned_block_hash) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); @@ -362,15 +518,22 @@ std::optional ZCashOrchardStorage::UpdateNotes( // Insert found notes to the notes table sql::Statement statement_populate_notes(database_.GetCachedStatement( SQL_FROM_HERE, "INSERT INTO " kNotesTable " " - "(account_id, amount, block_id, nullifier) " - "VALUES (?, ?, ?, ?);")); + "(account_id, amount, block_id, commitment_tree_position, " + "nullifier, rho, rseed, addr) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);")); for (const auto& note : found_notes) { statement_populate_notes.Reset(true); statement_populate_notes.BindString(0, account_id->unique_key); statement_populate_notes.BindInt64(1, note.amount); statement_populate_notes.BindInt64(2, note.block_id); - statement_populate_notes.BindBlob(3, note.nullifier); + statement_populate_notes.BindInt64(3, + note.orchard_commitment_tree_position); + statement_populate_notes.BindBlob(4, note.nullifier); + statement_populate_notes.BindBlob(5, note.rho); + statement_populate_notes.BindBlob(6, note.seed); + statement_populate_notes.BindBlob(7, note.addr); + if (!statement_populate_notes.Run()) { return Error{ErrorCode::kFailedToExecuteStatement, database_.GetErrorMessage()}; @@ -383,7 +546,7 @@ std::optional ZCashOrchardStorage::UpdateNotes( "(account_id, spent_block_id, nullifier) " "VALUES (?, ?, ?);")); - for (const auto& spent : spent_notes) { + for (const auto& spent : found_nullifiers) { statement_populate_spent_notes.Reset(true); statement_populate_spent_notes.BindString(0, account_id->unique_key); statement_populate_spent_notes.BindInt64(1, spent.block_id); @@ -418,4 +581,865 @@ std::optional ZCashOrchardStorage::UpdateNotes( return std::nullopt; } +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetLatestShardIndex(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement resolve_max_shard_id( + database_.GetCachedStatement(SQL_FROM_HERE, + "SELECT " + "MAX(shard_index) FROM " kShardTree " " + "WHERE account_id = ?;")); + + resolve_max_shard_id.BindString(0, account_id->unique_key); + if (resolve_max_shard_id.Step()) { + if (resolve_max_shard_id.GetColumnType(0) == sql::ColumnType::kNull) { + return std::nullopt; + } + auto shard_index = ReadUint32(resolve_max_shard_id, 0); + if (!shard_index) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + return shard_index.value(); + } + + return std::nullopt; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetCap(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement resolve_cap( + database_.GetCachedStatement(SQL_FROM_HERE, + "SELECT " + "cap_data FROM " kShardTreeCap " " + "WHERE account_id = ?;")); + resolve_cap.BindString(0, account_id->unique_key); + + if (!resolve_cap.Step()) { + if (!resolve_cap.Succeeded()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + return std::nullopt; + } + + OrchardCap result; + auto blob = resolve_cap.ColumnBlob(0); + result.data.reserve(blob.size()); + base::ranges::copy(blob, std::back_inserter(result.data)); + + return result; +} + +base::expected ZCashOrchardStorage::PutCap( + mojom::AccountIdPtr account_id, + OrchardCap cap) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Transaction transaction(&database_); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + auto existing_cap = GetCap(account_id.Clone()); + if (!existing_cap.has_value()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, existing_cap.error().message}); + } + + sql::Statement stmnt; + if (!existing_cap.value()) { + stmnt.Assign(database_.GetCachedStatement(SQL_FROM_HERE, + "INSERT INTO " kShardTreeCap " " + "(account_id, cap_data) " + "VALUES (?, ?);")); + stmnt.BindString(0, account_id->unique_key); + stmnt.BindBlob(1, cap.data); + } else { + stmnt.Assign(database_.GetCachedStatement( + SQL_FROM_HERE, "UPDATE " kShardTreeCap " " + "SET " + "cap_data = ? WHERE account_id = ?;")); + stmnt.BindBlob(0, cap.data); + stmnt.BindString(1, account_id->unique_key); + } + + if (!stmnt.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + + return true; +} + +base::expected +ZCashOrchardStorage::UpdateSubtreeRoots( + mojom::AccountIdPtr account_id, + uint32_t start_index, + std::vector roots) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Transaction transaction(&database_); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement statement_populate_roots(database_.GetCachedStatement( + SQL_FROM_HERE, + "INSERT INTO " kShardTree + " " + "(shard_index, subtree_end_height, root_hash, shard_data, account_id) " + "VALUES (?, ?, ?, ?, ?);" + + )); + + sql::Statement statement_update_roots(database_.GetCachedStatement( + SQL_FROM_HERE, + "UPDATE " kShardTree + " " + "SET subtree_end_height = :subtree_end_height, root_hash = :root_hash " + "WHERE " + "shard_index = :shard_index and account_id = :account_id;")); + + for (size_t i = 0; i < roots.size(); i++) { + if (!roots[i] || + roots[i]->complete_block_hash.size() != kOrchardCompleteBlockHashSize) { + return base::unexpected(Error{ErrorCode::kInternalError, "Wrong data"}); + } + + statement_populate_roots.Reset(true); + statement_populate_roots.BindInt64(0, start_index + i); + statement_populate_roots.BindInt64(1, roots[i]->complete_block_height); + statement_populate_roots.BindBlob(2, roots[i]->complete_block_hash); + statement_populate_roots.BindNull( + 3); // TODO(cypt4): Serialize hash as a leaf + statement_populate_roots.BindString(4, account_id->unique_key); + if (!statement_populate_roots.Run()) { + if (database_.GetErrorCode() == 19 /*SQLITE_CONSTRAINT*/) { + statement_update_roots.Reset(true); + statement_update_roots.BindInt64(0, roots[i]->complete_block_height); + statement_update_roots.BindBlob(1, roots[i]->complete_block_hash); + statement_update_roots.BindInt64(2, start_index + i); + statement_update_roots.BindString(3, account_id->unique_key); + if (!statement_update_roots.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + } else { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + } + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + + return true; +} + +base::expected +ZCashOrchardStorage::TruncateShards(mojom::AccountIdPtr account_id, + uint32_t shard_index) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Transaction transaction(&database_); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement remove_checkpoint_by_id(database_.GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kShardTree " " + "WHERE shard_index >= ? AND account_id = ?;")); + + remove_checkpoint_by_id.BindInt64(0, shard_index); + remove_checkpoint_by_id.BindString(1, account_id->unique_key); + + if (!remove_checkpoint_by_id.Run()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + + return true; +} + +base::expected ZCashOrchardStorage::PutShard( + mojom::AccountIdPtr account_id, + OrchardShard shard) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + auto existing_shard = GetShard(account_id.Clone(), shard.address); + if (!existing_shard.has_value()) { + return base::unexpected(existing_shard.error()); + } + + sql::Transaction transaction(&database_); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + if (existing_shard.value()) { + sql::Statement statement_update_shard(database_.GetCachedStatement( + SQL_FROM_HERE, + "UPDATE " kShardTree + " " + "SET root_hash = :root_hash, shard_data = :shard_data " + "WHERE shard_index = :shard_index AND account_id = :account_id;")); + + if (!shard.root_hash) { + statement_update_shard.BindNull(0); + } else { + statement_update_shard.BindBlob(0, shard.root_hash.value()); + } + statement_update_shard.BindBlob(1, shard.shard_data); + statement_update_shard.BindInt64(2, shard.address.index); + statement_update_shard.BindString(3, account_id->unique_key); + + if (!statement_update_shard.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + } else { + sql::Statement statement_put_shard(database_.GetCachedStatement( + SQL_FROM_HERE, + "INSERT INTO " kShardTree + " " + "(shard_index, root_hash, shard_data, account_id) " + "VALUES (:shard_index, :root_hash, :shard_data, :account_id);")); + + statement_put_shard.BindInt64(0, shard.address.index); + if (!shard.root_hash) { + statement_put_shard.BindNull(1); + } else { + statement_put_shard.BindBlob(1, shard.root_hash.value()); + } + statement_put_shard.BindBlob(2, shard.shard_data); + statement_put_shard.BindString(3, account_id->unique_key); + + if (!statement_put_shard.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + + return true; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetShard(mojom::AccountIdPtr account_id, + OrchardShardAddress address) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement resolve_shard_statement(database_.GetCachedStatement( + SQL_FROM_HERE, "SELECT root_hash, shard_data FROM " kShardTree " " + "WHERE account_id = ? AND shard_index = ?;")); + + resolve_shard_statement.BindString(0, account_id->unique_key); + resolve_shard_statement.BindInt64(1, address.index); + + if (!resolve_shard_statement.Step()) { + if (!resolve_shard_statement.Succeeded()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + return std::nullopt; + } + + auto hash = ReadRootHash(resolve_shard_statement, 0); + if (!hash.has_value()) { + return base::unexpected(Error{ErrorCode::kDbInitError, hash.error()}); + } + + auto shard_data = resolve_shard_statement.ColumnBlob(1); + auto shard = OrchardShard(address, hash.value(), std::vector()); + + base::ranges::copy(shard_data, std::back_inserter(shard.shard_data)); + + return shard; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::LastShard(mojom::AccountIdPtr account_id, + uint8_t shard_height) { + auto shard_index = GetLatestShardIndex(account_id.Clone()); + if (!shard_index.has_value()) { + return base::unexpected(shard_index.error()); + } + + if (!shard_index.value()) { + return std::nullopt; + } + + return GetShard( + account_id.Clone(), + OrchardShardAddress{shard_height, shard_index.value().value()}); +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetShardRoots(mojom::AccountIdPtr account_id, + uint8_t shard_level) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + std::vector result; + + sql::Statement resolve_shards_statement(database_.GetCachedStatement( + SQL_FROM_HERE, "SELECT shard_index FROM " kShardTree + " WHERE account_id = ? ORDER BY shard_index;")); + + resolve_shards_statement.BindString(0, account_id->unique_key); + + while (resolve_shards_statement.Step()) { + auto shard_index = ReadUint32(resolve_shards_statement, 0); + if (!shard_index) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, ""}); + } + result.push_back(OrchardShardAddress{shard_level, shard_index.value()}); + } + + if (!resolve_shards_statement.is_valid()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, ""}); + } + + return result; +} + +base::expected +ZCashOrchardStorage::AddCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id, + OrchardCheckpoint checkpoint) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement extant_tree_state_statement(database_.GetCachedStatement( + SQL_FROM_HERE, "SELECT position FROM " kShardTreeCheckpoints " " + "WHERE checkpoint_id = ? " + "AND account_id = ?;")); + extant_tree_state_statement.BindInt64(0, checkpoint_id); + extant_tree_state_statement.BindString(1, account_id->unique_key); + + std::optional extant_tree_state_position; + if (extant_tree_state_statement.Step()) { + auto state = ReadCheckpointTreeState(extant_tree_state_statement, 0); + if (!state.has_value()) { + return base::unexpected(Error{ErrorCode::kDbInitError, state.error()}); + } + extant_tree_state_position = state.value(); + } + + sql::Transaction transaction(&database_); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Failed to init database "}); + } + + // Checkpoint with same id didn't exist. + if (!extant_tree_state_position) { + sql::Statement insert_checkpoint_statement(database_.GetCachedStatement( + SQL_FROM_HERE, "INSERT INTO " kShardTreeCheckpoints " " + "(account_id, checkpoint_id, position)" + "VALUES (?, ?, ?);")); + insert_checkpoint_statement.BindString(0, account_id->unique_key); + insert_checkpoint_statement.BindInt64(1, checkpoint_id); + if (checkpoint.tree_state_position) { + insert_checkpoint_statement.BindInt64( + 2, checkpoint.tree_state_position.value()); + } else { + insert_checkpoint_statement.BindNull(2); + } + if (!insert_checkpoint_statement.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + + sql::Statement insert_marks_removed_statement(database_.GetCachedStatement( + SQL_FROM_HERE, "INSERT INTO " kCheckpointsMarksRemoved " " + "(account_id, checkpoint_id, mark_removed_position) " + "VALUES (?, ?, ?);")); + for (const auto& mark : checkpoint.marks_removed) { + insert_marks_removed_statement.Reset(true); + insert_marks_removed_statement.BindString(0, account_id->unique_key); + insert_marks_removed_statement.BindInt64(1, checkpoint_id); + insert_marks_removed_statement.BindInt64(2, mark); + + if (!insert_marks_removed_statement.Run()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + } + } else { + // Existing checkpoint should be the same + if (extant_tree_state_position.value() != checkpoint.tree_state_position) { + return base::unexpected( + Error{ErrorCode::kConsistencyError, "Consistency error"}); + } + auto marks_removed_result = + GetMarksRemoved(account_id.Clone(), checkpoint_id); + if (!marks_removed_result.has_value()) { + return base::unexpected(marks_removed_result.error()); + } + + if (!marks_removed_result.value()) { + return base::unexpected( + Error{ErrorCode::kConsistencyError, "Consistency error"}); + } + + if (marks_removed_result.value().value() != checkpoint.marks_removed) { + return base::unexpected( + Error{ErrorCode::kConsistencyError, "Consistency error"}); + } + } + + if (!transaction.Commit()) { + return base::unexpected(Error{ErrorCode::kDbInitError, ""}); + } + + return true; +} + +base::expected +ZCashOrchardStorage::CheckpointCount(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement resolve_checkpoints_count(database_.GetCachedStatement( + SQL_FROM_HERE, + "SELECT COUNT(*) FROM " kShardTreeCheckpoints " WHERE account_id = ?;")); + resolve_checkpoints_count.BindString(0, account_id->unique_key); + if (!resolve_checkpoints_count.Step()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } + + auto value = ReadUint32(resolve_checkpoints_count, 0); + + if (!value) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } + + return *value; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::MinCheckpointId(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement resolve_min_checkpoint_id(database_.GetCachedStatement( + SQL_FROM_HERE, "SELECT MIN(checkpoint_id) FROM " kShardTreeCheckpoints + " WHERE account_id = ?;")); + + resolve_min_checkpoint_id.BindString(0, account_id->unique_key); + + if (!resolve_min_checkpoint_id.Step()) { + if (!resolve_min_checkpoint_id.Succeeded()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } else { + return std::nullopt; + } + } + + if (resolve_min_checkpoint_id.GetColumnType(0) == sql::ColumnType::kNull) { + return std::nullopt; + } else { + return ReadUint32(resolve_min_checkpoint_id, 0); + } +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::MaxCheckpointId(mojom::AccountIdPtr account_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement resolve_max_checkpoint_id(database_.GetCachedStatement( + SQL_FROM_HERE, "SELECT MAX(checkpoint_id) FROM " kShardTreeCheckpoints + " WHERE account_id = ?;")); + resolve_max_checkpoint_id.BindString(0, account_id->unique_key); + + if (!resolve_max_checkpoint_id.Step()) { + if (!resolve_max_checkpoint_id.Succeeded()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } else { + return std::nullopt; + } + } + + if (resolve_max_checkpoint_id.GetColumnType(0) == sql::ColumnType::kNull) { + return std::nullopt; + } else { + return ReadUint32(resolve_max_checkpoint_id, 0); + } +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetCheckpointAtDepth(mojom::AccountIdPtr account_id, + uint32_t depth) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement get_checkpoint_at_depth_statement( + database_.GetCachedStatement(SQL_FROM_HERE, + "SELECT checkpoint_id, position " + "FROM " kShardTreeCheckpoints " " + "WHERE account_id = ? " + "ORDER BY checkpoint_id DESC " + "LIMIT 1 " + "OFFSET ?;")); + + get_checkpoint_at_depth_statement.BindString(0, account_id->unique_key); + get_checkpoint_at_depth_statement.BindInt64(1, depth); + + if (!get_checkpoint_at_depth_statement.Step()) { + if (!get_checkpoint_at_depth_statement.Succeeded()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } + return std::nullopt; + } + + auto value = ReadUint32(get_checkpoint_at_depth_statement, 0); + + if (!value) { + return std::nullopt; + } + + return *value; +} + +base::expected>, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetMarksRemoved(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement get_marks_removed_statement( + database_.GetCachedStatement(SQL_FROM_HERE, + "SELECT mark_removed_position " + "FROM " kCheckpointsMarksRemoved " " + "WHERE checkpoint_id = ? AND " + "account_id = ?;")); + get_marks_removed_statement.BindInt64(0, checkpoint_id); + get_marks_removed_statement.BindString(1, account_id->unique_key); + + std::vector result; + while (get_marks_removed_statement.Step()) { + auto position = ReadUint32(get_marks_removed_statement, 0); + if (!position) { + return base::unexpected(Error{ErrorCode::kDbInitError, "Format error"}); + } + result.push_back(*position); + } + + return result; +} + +base::expected, + ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement get_checkpoint_statement(database_.GetCachedStatement( + SQL_FROM_HERE, + "SELECT position " + "FROM " kShardTreeCheckpoints + " " + "WHERE checkpoint_id = ? AND account_id = ?;")); + + get_checkpoint_statement.BindInt64(0, checkpoint_id); + get_checkpoint_statement.BindString(1, account_id->unique_key); + if (!get_checkpoint_statement.Step()) { + return std::nullopt; + } + auto checkpoint_position = + ReadCheckpointTreeState(get_checkpoint_statement, 0); + if (!checkpoint_position.has_value()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + + sql::Statement marks_removed_statement(database_.GetCachedStatement( + SQL_FROM_HERE, + "SELECT mark_removed_position " + "FROM " kCheckpointsMarksRemoved + " " + "WHERE checkpoint_id = ? AND account_id = ?;")); + + marks_removed_statement.BindInt64(0, checkpoint_id); + marks_removed_statement.BindString(1, account_id->unique_key); + + std::vector positions; + while (marks_removed_statement.Step()) { + auto position = ReadUint32(marks_removed_statement, 0); + if (position) { + positions.push_back(*position); + } else { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, + database_.GetErrorMessage()}); + } + } + + return OrchardCheckpointBundle{ + checkpoint_id, + OrchardCheckpoint{*checkpoint_position, std::move(positions)}}; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetCheckpoints(mojom::AccountIdPtr account_id, + size_t limit) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement get_checkpoints_statement( + database_.GetCachedStatement(SQL_FROM_HERE, + "SELECT checkpoint_id, position " + "FROM " kShardTreeCheckpoints " " + "WHERE account_id = ? " + "ORDER BY position " + "LIMIT ?")); + + get_checkpoints_statement.BindString(0, account_id->unique_key); + get_checkpoints_statement.BindInt64(1, limit); + + std::vector checkpoints; + while (get_checkpoints_statement.Step()) { + auto checkpoint_id = ReadUint32(get_checkpoints_statement, 0); + auto checkpoint_position = + ReadCheckpointTreeState(get_checkpoints_statement, 1); + if (!checkpoint_id) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, ""}); + } + if (!checkpoint_position.has_value()) { + return base::unexpected(Error{ErrorCode::kFailedToExecuteStatement, ""}); + } + auto found_marks_removed = + GetMarksRemoved(account_id.Clone(), *checkpoint_id); + if (!found_marks_removed.has_value()) { + return base::unexpected(found_marks_removed.error()); + } + std::vector marks_removed; + if (found_marks_removed.value()) { + marks_removed = **found_marks_removed; + } + + checkpoints.push_back(OrchardCheckpointBundle{ + *checkpoint_id, OrchardCheckpoint(checkpoint_position.value(), + std::move(marks_removed))}); + } + return checkpoints; +} + +base::expected, ZCashOrchardStorage::Error> +ZCashOrchardStorage::GetMaxCheckpointedHeight(mojom::AccountIdPtr account_id, + uint32_t chain_tip_height, + uint32_t min_confirmations) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + uint32_t max_checkpointed_height = chain_tip_height - min_confirmations - 1; + + sql::Statement get_max_checkpointed_height(database_.GetCachedStatement( + SQL_FROM_HERE, "SELECT checkpoint_id FROM " kShardTreeCheckpoints " " + "WHERE checkpoint_id <= ? AND " + "account_id = ? " + "ORDER BY checkpoint_id DESC " + "LIMIT 1")); + + get_max_checkpointed_height.BindInt64(0, max_checkpointed_height); + get_max_checkpointed_height.BindString(1, account_id->unique_key); + + if (!get_max_checkpointed_height.Step()) { + if (!get_max_checkpointed_height.Succeeded()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } else { + return std::nullopt; + } + } + + auto value = ReadUint32(get_max_checkpointed_height, 0); + + if (!value) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } + + return *value; +} + +base::expected +ZCashOrchardStorage::RemoveCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Statement remove_checkpoint_by_id(database_.GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kShardTreeCheckpoints " " + "WHERE checkpoint_id = ? AND account_id= ?;")); + + remove_checkpoint_by_id.BindInt64(0, checkpoint_id); + remove_checkpoint_by_id.BindString(1, account_id->unique_key); + + sql::Transaction transaction(&database_); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Failed to init database "}); + } + + if (!remove_checkpoint_by_id.Run()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } + + if (!transaction.Commit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Failed to init database "}); + } + + return true; +} + +base::expected +ZCashOrchardStorage::TruncateCheckpoints(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDbInit()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, database_.GetErrorMessage()}); + } + + sql::Transaction transaction(&database_); + if (!transaction.Begin()) { + return base::unexpected( + Error{ErrorCode::kDbInitError, "Failed to init database "}); + } + + sql::Statement truncate_checkpoints(database_.GetCachedStatement( + SQL_FROM_HERE, "DELETE FROM " kShardTreeCheckpoints + " WHERE checkpoint_id >= ? and account_id = ?;")); + + truncate_checkpoints.BindInt64(0, checkpoint_id); + truncate_checkpoints.BindString(1, account_id->unique_key); + + if (!truncate_checkpoints.Run()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } + + if (!transaction.Commit()) { + return base::unexpected( + Error{ErrorCode::kNoCheckpoints, database_.GetErrorMessage()}); + } + + return true; +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_orchard_storage.h b/components/brave_wallet/browser/zcash/zcash_orchard_storage.h index 01c7d889c4ac..62a2e7b8e0f5 100644 --- a/components/brave_wallet/browser/zcash/zcash_orchard_storage.h +++ b/components/brave_wallet/browser/zcash/zcash_orchard_storage.h @@ -16,7 +16,9 @@ #include "base/types/expected.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/zcash_utils.h" +#include "brave/components/services/brave_wallet/public/mojom/zcash_decoder.mojom.h" #include "sql/database.h" +#include "sql/statement.h" namespace sql { class Database; @@ -24,21 +26,58 @@ class Database; namespace brave_wallet { +template +base::expected>, std::string> +ReadSizedBlob(sql::Statement& statement, size_t position) { + auto columns = statement.ColumnCount(); + CHECK(columns >= 0); + if (position >= static_cast(columns)) { + return base::unexpected("Position mismatch"); + } + + if (statement.GetColumnType(position) == sql::ColumnType::kNull) { + return std::nullopt; + } + + if (statement.GetColumnType(position) != sql::ColumnType::kBlob) { + return base::unexpected("Type mismatch"); + } + + auto blob = statement.ColumnBlob(position); + if (blob.size() != T) { + return base::unexpected("Size mismatch"); + } + + std::array to; + base::ranges::copy_n(blob.begin(), to.size(), to.begin()); + return to; +} + // Implements SQLite database to store found incoming notes, // nullifiers, wallet zcash accounts and commitment trees. class ZCashOrchardStorage { public: + using WithCheckpointsCallback = + base::RepeatingCallback; + struct AccountMeta { + AccountMeta(); + ~AccountMeta(); + AccountMeta(const AccountMeta&); + AccountMeta& operator=(const AccountMeta&); uint32_t account_birthday = 0; - uint32_t latest_scanned_block_id = 0; - std::string latest_scanned_block_hash; + std::optional latest_scanned_block_id; + std::optional latest_scanned_block_hash; }; enum class ErrorCode { kDbInitError, kAccountNotFound, kFailedToExecuteStatement, - kInternalError + kFailedToCommitTransaction, + kInternalError, + kNoCheckpoints, + kConsistencyError }; struct Error { @@ -51,10 +90,11 @@ class ZCashOrchardStorage { base::expected RegisterAccount( mojom::AccountIdPtr account_id, - uint32_t account_birthday_block, - const std::string& account_bithday_block_hash); + uint32_t account_birthday_block); base::expected GetAccountMeta( mojom::AccountIdPtr account_id); + base::expected ResetAccountSyncState( + mojom::AccountIdPtr account_id); // Removes database records which are under effect of chain reorg // Removes spendable notes and nullifiers with block_height > reorg_block @@ -67,18 +107,75 @@ class ZCashOrchardStorage { base::expected, ZCashOrchardStorage::Error> GetSpendableNotes(mojom::AccountIdPtr account_id); // Returns a list of discovered nullifiers - base::expected, Error> GetNullifiers( + base::expected, Error> GetNullifiers( mojom::AccountIdPtr account_id); // Updates database with discovered spendable notes and nullifiers // Also updates account info with latest scanned block info std::optional UpdateNotes( mojom::AccountIdPtr account_id, const std::vector& notes_to_add, - const std::vector& notes_to_delete, + const std::vector& found_nullifiers, const uint32_t latest_scanned_block, const std::string& latest_scanned_block_hash); void ResetDatabase(); + // Shard tree + base::expected, Error> GetCap( + mojom::AccountIdPtr account_id); + base::expected PutCap(mojom::AccountIdPtr account_id, + OrchardCap cap); + + base::expected TruncateShards(mojom::AccountIdPtr account_id, + uint32_t shard_index); + base::expected, Error> GetLatestShardIndex( + mojom::AccountIdPtr account_id); + base::expected PutShard(mojom::AccountIdPtr account_id, + OrchardShard shard); + base::expected, Error> GetShard( + mojom::AccountIdPtr account_id, + OrchardShardAddress address); + base::expected, Error> LastShard( + mojom::AccountIdPtr account_id, + uint8_t shard_height); + + base::expected CheckpointCount(mojom::AccountIdPtr account_id); + base::expected, Error> MinCheckpointId( + mojom::AccountIdPtr account_id); + base::expected, Error> MaxCheckpointId( + mojom::AccountIdPtr account_id); + base::expected, Error> GetCheckpointAtDepth( + mojom::AccountIdPtr account_id, + uint32_t depth); + base::expected, Error> GetMaxCheckpointedHeight( + mojom::AccountIdPtr account_id, + uint32_t chain_tip_height, + uint32_t min_confirmations); + base::expected RemoveCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id); + base::expected TruncateCheckpoints( + mojom::AccountIdPtr account_id, + uint32_t checkpoint_id); + base::expected AddCheckpoint(mojom::AccountIdPtr account_id, + uint32_t checkpoint_id, + OrchardCheckpoint checkpoint); + base::expected, Error> GetCheckpoints( + mojom::AccountIdPtr account_id, + size_t limit); + base::expected, Error> GetCheckpoint( + mojom::AccountIdPtr account_id, + uint32_t checkpoint_id); + base::expected>, Error> GetMarksRemoved( + mojom::AccountIdPtr account_id, + uint32_t checkpoint_id); + + base::expected UpdateSubtreeRoots( + mojom::AccountIdPtr account_id, + uint32_t start_index, + std::vector roots); + base::expected, Error> GetShardRoots( + mojom::AccountIdPtr account_id, + uint8_t shard_level); + private: bool EnsureDbInit(); bool CreateOrUpdateDatabase(); diff --git a/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc b/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc index 724743447e2e..581c25897b41 100644 --- a/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc +++ b/components/brave_wallet/browser/zcash/zcash_orchard_storage_unittest.cc @@ -49,36 +49,33 @@ TEST_F(OrchardStorageTest, AccountMeta) { } EXPECT_TRUE( - orchard_storage_->RegisterAccount(account_id_1.Clone(), 100, "hash") - .has_value()); + orchard_storage_->RegisterAccount(account_id_1.Clone(), 100).has_value()); { auto result = orchard_storage_->GetAccountMeta(account_id_1.Clone()); EXPECT_TRUE(result.has_value()); EXPECT_EQ(result->account_birthday, 100u); - EXPECT_EQ(result->latest_scanned_block_id, 100u); - EXPECT_EQ(result->latest_scanned_block_hash, "hash"); + EXPECT_FALSE(result->latest_scanned_block_id); + EXPECT_FALSE(result->latest_scanned_block_hash); } { // Failed to insert same account - EXPECT_EQ( - orchard_storage_->RegisterAccount(account_id_1.Clone(), 200, "hash") - .error() - .error_code, - ZCashOrchardStorage::ErrorCode::kFailedToExecuteStatement); + EXPECT_EQ(orchard_storage_->RegisterAccount(account_id_1.Clone(), 200) + .error() + .error_code, + ZCashOrchardStorage::ErrorCode::kFailedToExecuteStatement); } // Insert second account EXPECT_TRUE( - orchard_storage_->RegisterAccount(account_id_2.Clone(), 200, "hash") - .has_value()); + orchard_storage_->RegisterAccount(account_id_2.Clone(), 200).has_value()); { auto result = orchard_storage_->GetAccountMeta(account_id_2.Clone()); EXPECT_TRUE(result.has_value()); EXPECT_EQ(result->account_birthday, 200u); - EXPECT_EQ(result->latest_scanned_block_id, 200u); - EXPECT_EQ(result->latest_scanned_block_hash, "hash"); + EXPECT_FALSE(result->latest_scanned_block_id); + EXPECT_FALSE(result->latest_scanned_block_hash); } } @@ -91,11 +88,9 @@ TEST_F(OrchardStorageTest, PutDiscoveredNotes) { mojom::AccountKind::kDerived, 1); EXPECT_TRUE( - orchard_storage_->RegisterAccount(account_id_1.Clone(), 100, "hash") - .has_value()); + orchard_storage_->RegisterAccount(account_id_1.Clone(), 100).has_value()); EXPECT_TRUE( - orchard_storage_->RegisterAccount(account_id_2.Clone(), 100, "hash") - .has_value()); + orchard_storage_->RegisterAccount(account_id_2.Clone(), 100).has_value()); // Update notes for account 1 { @@ -147,27 +142,30 @@ TEST_F(OrchardStorageTest, PutDiscoveredNotes) { // Update notes for account 1 { std::vector notes; - std::vector nullifiers; + std::vector spends; // Add 1 note, spend 1 note notes.push_back(GenerateMockOrchardNote(account_id_1, 201, 3)); - nullifiers.push_back(GenerateMockNullifier(account_id_1, 203, 1)); + spends.push_back( + OrchardNoteSpend{203, GenerateMockNullifier(account_id_1, 1)}); - orchard_storage_->UpdateNotes(account_id_1.Clone(), notes, nullifiers, 300, + orchard_storage_->UpdateNotes(account_id_1.Clone(), notes, spends, 300, "hash300"); } // Update notes for account 2 { std::vector notes; - std::vector nullifiers; + std::vector spends; // Add 1 note, spend 2 notes notes.push_back(GenerateMockOrchardNote(account_id_2, 211, 4)); - nullifiers.push_back(GenerateMockNullifier(account_id_2, 222, 2)); - nullifiers.push_back(GenerateMockNullifier(account_id_2, 233, 3)); + spends.push_back( + OrchardNoteSpend{222, GenerateMockNullifier(account_id_2, 2)}); + spends.push_back( + OrchardNoteSpend{233, GenerateMockNullifier(account_id_2, 3)}); - orchard_storage_->UpdateNotes(account_id_2.Clone(), notes, nullifiers, 300, + orchard_storage_->UpdateNotes(account_id_2.Clone(), notes, spends, 300, "hash300"); } @@ -218,16 +216,14 @@ TEST_F(OrchardStorageTest, HandleChainReorg) { mojom::AccountKind::kDerived, 1); EXPECT_TRUE( - orchard_storage_->RegisterAccount(account_id_1.Clone(), 100, "hash") - .has_value()); + orchard_storage_->RegisterAccount(account_id_1.Clone(), 100).has_value()); EXPECT_TRUE( - orchard_storage_->RegisterAccount(account_id_2.Clone(), 100, "hash") - .has_value()); + orchard_storage_->RegisterAccount(account_id_2.Clone(), 100).has_value()); // Update notes for account 1 { std::vector notes; - std::vector nullifiers; + std::vector spends; // Add 4 notes, spend 2 notes notes.push_back(GenerateMockOrchardNote(account_id_1, 101, 1)); @@ -236,17 +232,19 @@ TEST_F(OrchardStorageTest, HandleChainReorg) { notes.push_back(GenerateMockOrchardNote(account_id_1, 104, 4)); notes.push_back(GenerateMockOrchardNote(account_id_1, 304, 5)); - nullifiers.push_back(GenerateMockNullifier(account_id_1, 102, 2)); - nullifiers.push_back(GenerateMockNullifier(account_id_1, 103, 3)); + spends.push_back( + OrchardNoteSpend{102, GenerateMockNullifier(account_id_1, 2)}); + spends.push_back( + OrchardNoteSpend{103, GenerateMockNullifier(account_id_1, 3)}); - orchard_storage_->UpdateNotes(account_id_1.Clone(), notes, nullifiers, 450, + orchard_storage_->UpdateNotes(account_id_1.Clone(), notes, spends, 450, "hash450"); } // Update notes for account 2 { std::vector notes; - std::vector nullifiers; + std::vector spends; // Add 4 notes, spend 2 notes notes.push_back(GenerateMockOrchardNote(account_id_2, 211, 1)); @@ -254,10 +252,12 @@ TEST_F(OrchardStorageTest, HandleChainReorg) { notes.push_back(GenerateMockOrchardNote(account_id_2, 213, 3)); notes.push_back(GenerateMockOrchardNote(account_id_2, 414, 4)); - nullifiers.push_back(GenerateMockNullifier(account_id_2, 322, 2)); - nullifiers.push_back(GenerateMockNullifier(account_id_2, 333, 3)); + spends.push_back( + OrchardNoteSpend{322, GenerateMockNullifier(account_id_2, 2)}); + spends.push_back( + OrchardNoteSpend{333, GenerateMockNullifier(account_id_2, 3)}); - orchard_storage_->UpdateNotes(account_id_2.Clone(), notes, nullifiers, 500, + orchard_storage_->UpdateNotes(account_id_2.Clone(), notes, spends, 500, "hash500"); } @@ -350,4 +350,536 @@ TEST_F(OrchardStorageTest, HandleChainReorg) { } } +TEST_F(OrchardStorageTest, Shards) {} + +namespace { + +zcash::mojom::SubtreeRootPtr CreateSubtreeRoot(size_t level, size_t index) { + zcash::mojom::SubtreeRootPtr root = zcash::mojom::SubtreeRoot::New(); + root->root_hash = std::vector(kOrchardShardTreeHashSize, index); + root->complete_block_hash = + std::vector(kOrchardCompleteBlockHashSize, index); + root->complete_block_height = 0; + return root; +} + +OrchardShard CreateShard(size_t index, size_t level) { + OrchardShard orchard_shard; + orchard_shard.root_hash = OrchardShardRootHash(); + orchard_shard.root_hash->fill(static_cast(index)); + orchard_shard.address.index = index; + orchard_shard.address.level = level; + orchard_shard.shard_data = std::vector({0, 0, 0, 0}); + return orchard_shard; +} + +} // namespace + +TEST_F(OrchardStorageTest, InsertSubtreeRoots_BlockHashConflict) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + std::vector level_1_roots; + level_1_roots.push_back(CreateSubtreeRoot(9, 0)); + level_1_roots.push_back(CreateSubtreeRoot(9, 0)); + EXPECT_FALSE( + orchard_storage_ + ->UpdateSubtreeRoots(account_id.Clone(), 0, std::move(level_1_roots)) + .has_value()); +} + +TEST_F(OrchardStorageTest, InsertSubtreeRoots) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + { + std::vector level_1_roots; + for (uint32_t i = 0; i < 10; i++) { + level_1_roots.push_back(CreateSubtreeRoot(9, i)); + } + EXPECT_TRUE(orchard_storage_ + ->UpdateSubtreeRoots(account_id.Clone(), 0, + std::move(level_1_roots)) + .value()); + } + + { + std::vector level_1_addrs; + for (uint32_t i = 0; i < 10; i++) { + level_1_addrs.push_back(OrchardShardAddress{9, i}); + } + auto result = orchard_storage_->GetShardRoots(account_id.Clone(), 9); + + EXPECT_EQ(result.value(), level_1_addrs); + } +} + +TEST_F(OrchardStorageTest, TruncateSubtreeRoots) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + { + std::vector level_1_roots; + for (int i = 0; i < 10; i++) { + level_1_roots.push_back(CreateSubtreeRoot(1, i)); + } + EXPECT_TRUE(orchard_storage_ + ->UpdateSubtreeRoots(account_id.Clone(), 0, + std::move(level_1_roots)) + .value()); + } + + EXPECT_TRUE(orchard_storage_->TruncateShards(account_id.Clone(), 5).value()); + { + std::vector addresses_after_truncate; + for (uint32_t i = 0; i < 5; i++) { + addresses_after_truncate.push_back(OrchardShardAddress{1, i}); + } + auto result = orchard_storage_->GetShardRoots(account_id.Clone(), 1); + EXPECT_EQ(result.value(), addresses_after_truncate); + } +} + +TEST_F(OrchardStorageTest, TruncateShards) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + { + for (uint32_t i = 0; i < 10; i++) { + EXPECT_TRUE( + orchard_storage_->PutShard(account_id.Clone(), CreateShard(i, 1)) + .value()); + } + } + + EXPECT_TRUE(orchard_storage_->TruncateShards(account_id.Clone(), 5).value()); + for (uint32_t i = 0; i < 5; i++) { + EXPECT_EQ(CreateShard(i, 1), + **(orchard_storage_->GetShard(account_id.Clone(), + OrchardShardAddress(1, i)))); + } + + EXPECT_EQ(std::nullopt, *(orchard_storage_->GetShard( + account_id.Clone(), OrchardShardAddress(1, 6)))); +} + +TEST_F(OrchardStorageTest, ShardOverridesSubtreeRoot) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + { + std::vector level_1_roots; + for (uint32_t i = 0; i < 10; i++) { + level_1_roots.push_back(CreateSubtreeRoot(1, i)); + } + EXPECT_TRUE(orchard_storage_ + ->UpdateSubtreeRoots(account_id.Clone(), 0, + std::move(level_1_roots)) + .value()); + } + + // Update existing shard + OrchardShard new_shard; + new_shard.root_hash = OrchardShardRootHash(); + new_shard.address.index = 5; + new_shard.address.level = 1; + new_shard.root_hash->fill(5); + new_shard.shard_data = std::vector({5, 5, 5, 5}); + EXPECT_TRUE( + orchard_storage_->PutShard(account_id.Clone(), new_shard).value()); + + auto result = + orchard_storage_->GetShard(account_id.Clone(), OrchardShardAddress{1, 5}); + EXPECT_EQ(*result.value(), new_shard); +} + +TEST_F(OrchardStorageTest, InsertShards) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + EXPECT_EQ(std::nullopt, + orchard_storage_->GetLatestShardIndex(account_id.Clone()).value()); + EXPECT_EQ( + std::nullopt, + orchard_storage_->GetShard(account_id.Clone(), OrchardShardAddress{1, 0}) + .value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->LastShard(account_id.Clone(), 1).value()); + + { + std::vector level_1_roots; + for (uint32_t i = 0; i < 10; i++) { + level_1_roots.push_back(CreateSubtreeRoot(1, i)); + } + EXPECT_TRUE(orchard_storage_ + ->UpdateSubtreeRoots(account_id.Clone(), 0, + std::move(level_1_roots)) + .value()); + } + + OrchardShard new_shard; + new_shard.root_hash = OrchardShardRootHash(); + new_shard.address.index = 11; + new_shard.address.level = 1; + new_shard.root_hash->fill(11); + new_shard.shard_data = std::vector({1, 1, 1, 1}); + + EXPECT_TRUE( + orchard_storage_->PutShard(account_id.Clone(), new_shard).value()); + + { + auto result = orchard_storage_->GetShard(account_id.Clone(), + OrchardShardAddress{1, 11}); + EXPECT_EQ(*result.value(), new_shard); + } + + { + for (uint32_t i = 0; i < 10; i++) { + auto result = orchard_storage_->GetShard(account_id.Clone(), + OrchardShardAddress{1, i}); + auto root = CreateSubtreeRoot(1, i); + EXPECT_EQ(std::vector(std::begin(*result.value()->root_hash), + std::end(*result.value()->root_hash)), + root->root_hash); + } + } + + EXPECT_EQ(11u, orchard_storage_->GetLatestShardIndex(account_id.Clone()) + .value() + .value()); + EXPECT_EQ(new_shard, + orchard_storage_->LastShard(account_id.Clone(), 1).value()); +} + +TEST_F(OrchardStorageTest, RemoveChekpoint) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({4, 5, 6}); + checkpoint2.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + + EXPECT_TRUE( + orchard_storage_->RemoveCheckpoint(account_id.Clone(), 1).value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value()); + EXPECT_EQ( + OrchardCheckpointBundle(2, checkpoint2), + orchard_storage_->GetCheckpoint(account_id.Clone(), 2).value().value()); +} + +TEST_F(OrchardStorageTest, CheckpointId) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + EXPECT_EQ(std::nullopt, + orchard_storage_->MinCheckpointId(account_id.Clone()).value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->MaxCheckpointId(account_id.Clone()).value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({1, 2, 3}); + checkpoint2.tree_state_position = 2; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + + OrchardCheckpoint checkpoint3; + checkpoint3.marks_removed = std::vector({5}); + checkpoint3.tree_state_position = 3; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint3) + .value()); + + OrchardCheckpoint checkpoint4; + checkpoint4.marks_removed = std::vector(); + checkpoint4.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 4, checkpoint4) + .value()); + + EXPECT_EQ(1, orchard_storage_->MinCheckpointId(account_id.Clone()).value()); + EXPECT_EQ(4, orchard_storage_->MaxCheckpointId(account_id.Clone()).value()); +} + +TEST_F(OrchardStorageTest, CheckpointAtPosition) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({4, 5, 6}); + checkpoint2.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + OrchardCheckpoint checkpoint3; + checkpoint3.marks_removed = std::vector({7, 8, 9}); + checkpoint3.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint3) + .value()); + + EXPECT_EQ(1u, orchard_storage_->GetCheckpointAtDepth(account_id.Clone(), 2) + .value() + .value()); + EXPECT_EQ( + std::nullopt, + orchard_storage_->GetCheckpointAtDepth(account_id.Clone(), 5).value()); +} + +TEST_F(OrchardStorageTest, TruncateCheckpoints_OutOfBoundry) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + EXPECT_TRUE( + orchard_storage_->TruncateCheckpoints(account_id.Clone(), 3).value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint1), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); +} + +TEST_F(OrchardStorageTest, TruncateCheckpoints) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({1, 2, 3}); + checkpoint2.tree_state_position = 2; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + + OrchardCheckpoint checkpoint3; + checkpoint3.marks_removed = std::vector({5}); + checkpoint3.tree_state_position = 3; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint3) + .value()); + + OrchardCheckpoint checkpoint4; + checkpoint4.marks_removed = std::vector(); + checkpoint4.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 4, checkpoint4) + .value()); + + EXPECT_TRUE( + orchard_storage_->TruncateCheckpoints(account_id.Clone(), 3).value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint1), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); + EXPECT_EQ( + OrchardCheckpointBundle(2, checkpoint2), + orchard_storage_->GetCheckpoint(account_id.Clone(), 2).value().value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->GetCheckpoint(account_id.Clone(), 3).value()); + EXPECT_EQ(std::nullopt, + orchard_storage_->GetCheckpoint(account_id.Clone(), 4).value()); +} + +TEST_F(OrchardStorageTest, AddCheckpoint) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + OrchardCheckpoint checkpoint2; + checkpoint2.marks_removed = std::vector({4, 5, 6}); + checkpoint2.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint2) + .value()); + OrchardCheckpoint checkpoint3; + checkpoint3.marks_removed = std::vector(); + checkpoint3.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint3) + .value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint1), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); + EXPECT_EQ( + OrchardCheckpointBundle(2, checkpoint2), + orchard_storage_->GetCheckpoint(account_id.Clone(), 2).value().value()); + EXPECT_EQ( + OrchardCheckpointBundle(3, checkpoint3), + orchard_storage_->GetCheckpoint(account_id.Clone(), 3).value().value()); +} + +TEST_F(OrchardStorageTest, AddSameCheckpoint) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + { + OrchardCheckpoint checkpoint; + checkpoint.marks_removed = std::vector({1, 2, 3}); + checkpoint.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint) + .value()); + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint) + .value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); + } + + { + OrchardCheckpoint checkpoint; + checkpoint.marks_removed = std::vector({1, 2, 3}); + checkpoint.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint) + .value()); + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 2, checkpoint) + .value()); + + EXPECT_EQ( + OrchardCheckpointBundle(2, checkpoint), + orchard_storage_->GetCheckpoint(account_id.Clone(), 2).value().value()); + } + + { + OrchardCheckpoint checkpoint; + checkpoint.marks_removed = std::vector(); + checkpoint.tree_state_position = std::nullopt; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint) + .value()); + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 3, checkpoint) + .value()); + + EXPECT_EQ( + OrchardCheckpointBundle(3, checkpoint), + orchard_storage_->GetCheckpoint(account_id.Clone(), 3).value().value()); + } +} + +TEST_F(OrchardStorageTest, AddChekpoint_ErrorOnConflict) { + auto account_id = MakeIndexBasedAccountId(mojom::CoinType::ZEC, + mojom::KeyringId::kZCashMainnet, + mojom::AccountKind::kDerived, 0); + EXPECT_TRUE( + orchard_storage_->RegisterAccount(account_id.Clone(), 100).has_value()); + + OrchardCheckpoint checkpoint1; + checkpoint1.marks_removed = std::vector({1, 2, 3}); + checkpoint1.tree_state_position = 4; + EXPECT_TRUE( + orchard_storage_->AddCheckpoint(account_id.Clone(), 1, checkpoint1) + .value()); + + OrchardCheckpoint checkpoint_different_marks_removed = checkpoint1; + checkpoint_different_marks_removed.marks_removed = + std::vector({1, 2}); + EXPECT_FALSE(orchard_storage_ + ->AddCheckpoint(account_id.Clone(), 1, + checkpoint_different_marks_removed) + .has_value()); + + OrchardCheckpoint checkpoint_different_position1 = checkpoint1; + checkpoint_different_position1.tree_state_position = 7; + EXPECT_FALSE( + orchard_storage_ + ->AddCheckpoint(account_id.Clone(), 1, checkpoint_different_position1) + .has_value()); + + OrchardCheckpoint checkpoint_different_position2 = checkpoint1; + checkpoint_different_position2.tree_state_position = std::nullopt; + EXPECT_FALSE( + orchard_storage_ + ->AddCheckpoint(account_id.Clone(), 1, checkpoint_different_position2) + .has_value()); + + EXPECT_EQ( + OrchardCheckpointBundle(1, checkpoint1), + orchard_storage_->GetCheckpoint(account_id.Clone(), 1).value().value()); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/zcash/zcash_shield_sync_service.cc b/components/brave_wallet/browser/zcash/zcash_shield_sync_service.cc index d86c959ee66a..6891fcb00c10 100644 --- a/components/brave_wallet/browser/zcash/zcash_shield_sync_service.cc +++ b/components/brave_wallet/browser/zcash/zcash_shield_sync_service.cc @@ -167,8 +167,9 @@ void ZCashShieldSyncService::OnGetAccountMeta( result) { if (result.has_value()) { account_meta_ = *result; - if (account_meta_->latest_scanned_block_id < - account_meta_->account_birthday) { + if (account_meta_->latest_scanned_block_id.value() && + (account_meta_->latest_scanned_block_id.value() < + account_meta_->account_birthday)) { error_ = Error{ErrorCode::kFailedToRetrieveAccount, ""}; } ScheduleWorkOnTask(); @@ -188,8 +189,7 @@ void ZCashShieldSyncService::OnGetAccountMeta( void ZCashShieldSyncService::InitAccount() { orchard_storage() .AsyncCall(&ZCashOrchardStorage::RegisterAccount) - .WithArgs(account_id_.Clone(), account_birthday_->value, - account_birthday_->hash) + .WithArgs(account_id_.Clone(), account_birthday_->value) .Then(base::BindOnce(&ZCashShieldSyncService::OnAccountInit, weak_ptr_factory_.GetWeakPtr())); } @@ -207,6 +207,11 @@ void ZCashShieldSyncService::OnAccountInit( void ZCashShieldSyncService::VerifyChainState( ZCashOrchardStorage::AccountMeta account_meta) { + 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) { @@ -218,7 +223,7 @@ void ZCashShieldSyncService::VerifyChainState( // Retrieve block info for last scanned block id to check whether block hash // is the same auto block_id = zcash::mojom::BlockID::New( - account_meta.latest_scanned_block_id, std::vector()); + account_meta.latest_scanned_block_id.value(), std::vector()); zcash_rpc().GetTreeState( chain_id_, std::move(block_id), base::BindOnce( @@ -235,12 +240,13 @@ void ZCashShieldSyncService::OnGetTreeStateForChainVerification( return; } auto backend_block_hash = RevertHex(tree_state.value()->hash); - if (backend_block_hash != account_meta.latest_scanned_block_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 > kChainReorgBlockDelta - ? account_meta.latest_scanned_block_id - kChainReorgBlockDelta + account_meta.latest_scanned_block_id.value() > kChainReorgBlockDelta + ? account_meta.latest_scanned_block_id.value() - + kChainReorgBlockDelta : 0; GetTreeStateForChainReorg(new_block_id); return; @@ -388,7 +394,7 @@ void ZCashShieldSyncService::OnBlocksScanned( void ZCashShieldSyncService::UpdateNotes( const std::vector& found_notes, - const std::vector& notes_to_delete, + const std::vector& notes_to_delete, uint32_t latest_scanned_block, std::string latest_scanned_block_hash) { orchard_storage() 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 93fa2df7d003..b6be7cb9c5d2 100644 --- a/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h +++ b/components/brave_wallet/browser/zcash/zcash_shield_sync_service.h @@ -148,7 +148,7 @@ class ZCashShieldSyncService { base::expected result); void UpdateNotes(const std::vector& found_notes, - const std::vector& notes_to_delete, + const std::vector& notes_to_delete, uint32_t latest_scanned_block, std::string latest_scanned_block_hash); void UpdateNotesComplete(uint32_t new_latest_scanned_block, 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 7eb609896870..ff98043303d2 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 @@ -186,10 +186,10 @@ class ZCashShieldSyncServiceTest : public testing::Test { // First 2 notes are spent if (block->height == 255) { result.spent_notes.push_back( - GenerateMockNullifier(account_id, block->height, 1)); + GenerateMockNoteSpend(account_id, block->height, 1)); } else if (block->height == 265) { result.spent_notes.push_back( - GenerateMockNullifier(account_id, block->height, 2)); + GenerateMockNoteSpend(account_id, block->height, 2)); } } std::move(callback).Run(result); @@ -312,7 +312,7 @@ TEST_F(ZCashShieldSyncServiceTest, ScanBlocks) { // First 2 notes are spent if (block->height == 855) { result.spent_notes.push_back( - GenerateMockNullifier(account_id, block->height, 3)); + GenerateMockNoteSpend(account_id, block->height, 3)); } } std::move(callback).Run(result); @@ -424,7 +424,7 @@ TEST_F(ZCashShieldSyncServiceTest, ScanBlocks) { // Add a nullifier for previous note if (block->height == 905) { result.spent_notes.push_back( - GenerateMockNullifier(account_id, block->height, 3)); + GenerateMockNoteSpend(account_id, block->height, 3)); } } std::move(callback).Run(result); diff --git a/components/brave_wallet/browser/zcash/zcash_test_utils.cc b/components/brave_wallet/browser/zcash/zcash_test_utils.cc index 9eaea7f89f0d..eff09718bd42 100644 --- a/components/brave_wallet/browser/zcash/zcash_test_utils.cc +++ b/components/brave_wallet/browser/zcash/zcash_test_utils.cc @@ -10,26 +10,30 @@ namespace brave_wallet { -std::array GenerateMockNullifier( - const mojom::AccountIdPtr& account_id, - uint8_t seed) { +OrchardNullifier GenerateMockNullifier(const mojom::AccountIdPtr& account_id, + uint8_t seed) { std::array nullifier; nullifier.fill(seed); nullifier[0] = account_id->account_index; return nullifier; } -OrchardNullifier GenerateMockNullifier(const mojom::AccountIdPtr& account_id, +OrchardNoteSpend GenerateMockNoteSpend(const mojom::AccountIdPtr& account_id, uint32_t block_id, uint8_t seed) { - return OrchardNullifier{block_id, GenerateMockNullifier(account_id, seed)}; + return OrchardNoteSpend{block_id, GenerateMockNullifier(account_id, seed)}; } OrchardNote GenerateMockOrchardNote(const mojom::AccountIdPtr& account_id, uint32_t block_id, uint8_t seed) { - return OrchardNote{block_id, GenerateMockNullifier(account_id, seed), - static_cast(seed * 10)}; + return OrchardNote{{}, + block_id, + GenerateMockNullifier(account_id, seed), + static_cast(seed * 10), + 0, + {}, + {}}; } void SortByBlockId(std::vector& vec) { diff --git a/components/brave_wallet/browser/zcash/zcash_test_utils.h b/components/brave_wallet/browser/zcash/zcash_test_utils.h index 7131666efb06..a8ec64abccdc 100644 --- a/components/brave_wallet/browser/zcash/zcash_test_utils.h +++ b/components/brave_wallet/browser/zcash/zcash_test_utils.h @@ -17,7 +17,7 @@ std::array GenerateMockNullifier( const mojom::AccountIdPtr& account_id, uint8_t seed); -OrchardNullifier GenerateMockNullifier(const mojom::AccountIdPtr& account_id, +OrchardNoteSpend GenerateMockNoteSpend(const mojom::AccountIdPtr& account_id, uint32_t block_id, uint8_t seed); 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 3a91071b0214..ee7a23706296 100644 --- a/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc +++ b/components/brave_wallet/browser/zcash/zcash_wallet_service_unittest.cc @@ -331,7 +331,7 @@ TEST_F(ZCashWalletServiceUnitTest, GetBalanceWithShielded) { orchard_storage() .AsyncCall(&ZCashOrchardStorage::UpdateNotes) .WithArgs(account_id.Clone(), std::vector({note}), - std::vector(), 50000, "hash50000") + std::vector(), 50000, "hash50000") .Then(std::move(update_notes_callback)); task_environment_.RunUntilIdle(); @@ -416,7 +416,7 @@ TEST_F(ZCashWalletServiceUnitTest, GetBalanceWithShielded_FeatureDisabled) { orchard_storage() .AsyncCall(&ZCashOrchardStorage::UpdateNotes) .WithArgs(account_id.Clone(), std::vector({note}), - std::vector(), 50000, "hash50000") + std::vector(), 50000, "hash50000") .Then(std::move(update_notes_callback)); task_environment_.RunUntilIdle(); diff --git a/components/brave_wallet/common/zcash_utils.cc b/components/brave_wallet/common/zcash_utils.cc index 997ec72b76d9..cfd33d403319 100644 --- a/components/brave_wallet/common/zcash_utils.cc +++ b/components/brave_wallet/common/zcash_utils.cc @@ -108,6 +108,108 @@ std::vector GetNetworkPrefix(bool is_testnet) { } // namespace +// static + +// static +base::Value::Dict OrchardNote::ToValue() const { + base::Value::Dict dict; + + dict.Set("addr", base::HexEncode(addr.data(), addr.size())); + dict.Set("block_id", base::NumberToString(block_id)); + dict.Set("nullifier", base::HexEncode(nullifier.data(), nullifier.size())); + dict.Set("amount", base::NumberToString(amount)); + dict.Set("orchard_commitment_tree_position", + base::NumberToString(orchard_commitment_tree_position)); + dict.Set("rho", base::HexEncode(rho.data(), rho.size())); + dict.Set("seed", base::HexEncode(seed.data(), seed.size())); + + return dict; +} + +// static +std::optional OrchardNote::FromValue( + const base::Value::Dict& value) { + OrchardNote result; + if (!ReadHexByteArrayTo(value, "addr", result.addr)) { + return std::nullopt; + } + + if (!ReadUint32StringTo(value, "block_id", result.block_id)) { + return std::nullopt; + } + + if (!ReadHexByteArrayTo(value, "nullifier", + result.nullifier)) { + return std::nullopt; + } + + if (!ReadUint32StringTo(value, "amount", result.amount)) { + return std::nullopt; + } + + if (!ReadUint32StringTo(value, "orchard_commitment_tree_position", + result.orchard_commitment_tree_position)) { + return std::nullopt; + } + + if (!ReadHexByteArrayTo(value, "rho", result.rho)) { + return std::nullopt; + } + + if (!ReadHexByteArrayTo(value, "seed", result.seed)) { + return std::nullopt; + } + + return result; +} + +// static +base::Value::Dict OrchardInput::ToValue() const { + base::Value::Dict dict; + + // Do not serialize witness ATM since it is calculated before post + dict.Set("note", note.ToValue()); + + return dict; +} + +// static +std::optional OrchardInput::FromValue( + const base::Value::Dict& value) { + OrchardInput result; + + auto* note_dict = value.FindDict("note"); + if (!note_dict) { + return std::nullopt; + } + auto note = OrchardNote::FromValue(*note_dict); + if (!note) { + return std::nullopt; + } + + result.note = *note; + + return result; +} + +OrchardTreeState::OrchardTreeState() {} +OrchardTreeState::~OrchardTreeState() {} +OrchardTreeState::OrchardTreeState(const OrchardTreeState&) = default; + +OrchardNoteWitness::OrchardNoteWitness() = default; +OrchardNoteWitness::~OrchardNoteWitness() = default; +OrchardNoteWitness::OrchardNoteWitness(const OrchardNoteWitness& other) = + default; + +OrchardInput::OrchardInput() = default; +OrchardInput::~OrchardInput() = default; +OrchardInput::OrchardInput(const OrchardInput& other) = default; + +OrchardSpendsBundle::OrchardSpendsBundle() = default; +OrchardSpendsBundle::~OrchardSpendsBundle() = default; +OrchardSpendsBundle::OrchardSpendsBundle(const OrchardSpendsBundle& other) = + default; + DecodedZCashAddress::DecodedZCashAddress() = default; DecodedZCashAddress::~DecodedZCashAddress() = default; DecodedZCashAddress::DecodedZCashAddress(const DecodedZCashAddress& other) = @@ -154,6 +256,52 @@ std::optional OrchardOutput::FromValue( return result; } +OrchardCheckpoint::OrchardCheckpoint() {} +OrchardCheckpoint::OrchardCheckpoint(CheckpointTreeState tree_state_position, + std::vector marks_removed) + : tree_state_position(tree_state_position), + marks_removed(std::move(marks_removed)) {} +OrchardCheckpoint::~OrchardCheckpoint() {} +OrchardCheckpoint::OrchardCheckpoint(const OrchardCheckpoint& other) = default; +OrchardCheckpoint& OrchardCheckpoint::operator=( + const OrchardCheckpoint& other) = default; +OrchardCheckpoint::OrchardCheckpoint(OrchardCheckpoint&& other) = default; +OrchardCheckpoint& OrchardCheckpoint::operator=(OrchardCheckpoint&& other) = + default; + +OrchardCheckpointBundle::OrchardCheckpointBundle(uint32_t checkpoint_id, + OrchardCheckpoint checkpoint) + : checkpoint_id(checkpoint_id), checkpoint(std::move(checkpoint)) {} +OrchardCheckpointBundle::~OrchardCheckpointBundle() {} +OrchardCheckpointBundle::OrchardCheckpointBundle( + const OrchardCheckpointBundle& other) = default; +OrchardCheckpointBundle& OrchardCheckpointBundle::operator=( + const OrchardCheckpointBundle& other) = default; +OrchardCheckpointBundle::OrchardCheckpointBundle( + OrchardCheckpointBundle&& other) = default; +OrchardCheckpointBundle& OrchardCheckpointBundle::operator=( + OrchardCheckpointBundle&& other) = default; + +OrchardShard::OrchardShard() {} +OrchardShard::OrchardShard(OrchardShardAddress address, + std::optional root_hash, + std::vector shard_data) + : address(std::move(address)), + root_hash(std::move(root_hash)), + shard_data(std::move(shard_data)) {} +OrchardShard::~OrchardShard() = default; +OrchardShard::OrchardShard(const OrchardShard& other) = default; +OrchardShard& OrchardShard::operator=(const OrchardShard& other) = default; +OrchardShard::OrchardShard(OrchardShard&& other) = default; +OrchardShard& OrchardShard::operator=(OrchardShard&& other) = default; + +OrchardCap::OrchardCap() = default; +OrchardCap::~OrchardCap() = default; +OrchardCap::OrchardCap(const OrchardCap& other) = default; +OrchardCap& OrchardCap::operator=(const OrchardCap& other) = default; +OrchardCap::OrchardCap(OrchardCap&& other) = default; +OrchardCap& OrchardCap::operator=(OrchardCap&& other) = default; + bool OutputZCashAddressSupported(const std::string& address, bool is_testnet) { auto decoded_address = DecodeZCashAddress(address); if (!decoded_address) { diff --git a/components/brave_wallet/common/zcash_utils.h b/components/brave_wallet/common/zcash_utils.h index d10ff3c955c3..61f3415e5870 100644 --- a/components/brave_wallet/common/zcash_utils.h +++ b/components/brave_wallet/common/zcash_utils.h @@ -13,6 +13,7 @@ #include #include "base/containers/span.h" +#include "base/types/expected.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" namespace brave_wallet { @@ -35,10 +36,24 @@ inline constexpr size_t kOrchardCipherTextSize = 52u; inline constexpr size_t kOrchardMemoSize = 512u; inline constexpr uint64_t kZCashFullAmount = std::numeric_limits::max(); +inline constexpr size_t kOrchardShardTreeHashSize = 32u; +inline constexpr uint8_t kOrchardShardSubtreeHeight = 8; +inline constexpr uint8_t kOrchardShardTreeHeight = 32; +inline constexpr uint8_t kOrchardNoteRhoSize = 32; +inline constexpr uint8_t kOrchardNoteRSeedSize = 32; +inline constexpr uint8_t kOrchardSpendingKeySize = 32; +inline constexpr size_t kOrchardCompleteBlockHashSize = 32u; using OrchardFullViewKey = std::array; using OrchardMemo = std::array; using OrchardAddrRawPart = std::array; +using OrchardRho = std::array; +using OrchardRseed = std::array; +using OrchardMerkleHash = std::array; +using OrchardNullifier = std::array; +using OrchardShardRootHash = std::array; +using OrchardCommitmentValue = std::array; +using OrchardSpendingKey = std::array; // Reduce current scanning position on this value if reorg is found // All Zcash network participants basically assume rollbacks longer than 100 @@ -89,21 +104,173 @@ struct OrchardOutput { }; // Structure describes note nullifier that marks some note as spent -struct OrchardNullifier { +struct OrchardNoteSpend { // Block id where spent nullifier was met uint32_t block_id = 0; std::array nullifier; - bool operator==(const OrchardNullifier& other) const = default; + bool operator==(const OrchardNoteSpend& other) const = default; }; -// Structure describes found spendable note +// Describes spendable note. +// Spendable note contains related position +// in the Orchard commitment tree, amount and data required +// for costructing zk-proof for spending. struct OrchardNote { + OrchardAddrRawPart addr; uint32_t block_id = 0; - std::array nullifier; + OrchardNullifier nullifier; uint32_t amount = 0; + uint32_t orchard_commitment_tree_position = 0; + OrchardRho rho; + OrchardRseed seed; bool operator==(const OrchardNote& other) const = default; + base::Value::Dict ToValue() const; + static std::optional FromValue(const base::Value::Dict& value); +}; + +// Note witness is a Merkle path in the Orchard commitment tree from the +// note to the tree root according some selected anchor(selected right border in +// the commitment tree). +struct OrchardNoteWitness { + OrchardNoteWitness(); + ~OrchardNoteWitness(); + OrchardNoteWitness(const OrchardNoteWitness& other); + + uint32_t position; + std::vector merkle_path; + bool operator==(const OrchardNoteWitness& other) const = default; +}; + +// Data required for constructing note spending. +struct OrchardInput { + OrchardInput(); + ~OrchardInput(); + OrchardInput(const OrchardInput& other); + + OrchardNote note; + std::optional witness; + + base::Value::Dict ToValue() const; + static std::optional FromValue(const base::Value::Dict& value); +}; + +// Bundle of Orchard inputs along with keys needed for signing. +struct OrchardSpendsBundle { + OrchardSpendsBundle(); + ~OrchardSpendsBundle(); + OrchardSpendsBundle(const OrchardSpendsBundle& other); + + OrchardSpendingKey sk; + OrchardFullViewKey fvk; + std::vector inputs; +}; + +// Leaf position of checkpoint. +using CheckpointTreeState = std::optional; + +// Checkpointed leafs are not pruned so they could be used +// as anchors for building shielded transactions. +// Last Orchard commitment in a block is used as a checkpoint. +struct OrchardCheckpoint { + OrchardCheckpoint(); + OrchardCheckpoint(CheckpointTreeState, std::vector); + ~OrchardCheckpoint(); + OrchardCheckpoint(const OrchardCheckpoint& other); + OrchardCheckpoint& operator=(const OrchardCheckpoint& other); + OrchardCheckpoint(OrchardCheckpoint&& other); + OrchardCheckpoint& operator=(OrchardCheckpoint&& other); + + bool operator==(const OrchardCheckpoint& other) const = default; + + CheckpointTreeState tree_state_position; + // List of note positions that were spent at this checkpoint. + std::vector marks_removed; +}; + +struct OrchardCheckpointBundle { + OrchardCheckpointBundle(uint32_t checkpoint_id, OrchardCheckpoint); + ~OrchardCheckpointBundle(); + OrchardCheckpointBundle(const OrchardCheckpointBundle& other); + OrchardCheckpointBundle& operator=(const OrchardCheckpointBundle& other); + OrchardCheckpointBundle(OrchardCheckpointBundle&& other); + OrchardCheckpointBundle& operator=(OrchardCheckpointBundle&& other); + + bool operator==(const OrchardCheckpointBundle& other) const = default; + + // The block height serves as the checkpoint identifier. + uint32_t checkpoint_id; + OrchardCheckpoint checkpoint; +}; + +// Address of a subtree in the shard tree. +struct OrchardShardAddress { + uint8_t level = 0; + uint32_t index = 0; + + bool operator==(const OrchardShardAddress& other) const = default; +}; + +// Top part of the shard tree from the root to the shard roots level +// Used for optimization purposes in the shard tree crate. +struct OrchardCap { + OrchardCap(); + ~OrchardCap(); + + OrchardCap(const OrchardCap& other); + OrchardCap& operator=(const OrchardCap& other); + OrchardCap(OrchardCap&& other); + OrchardCap& operator=(OrchardCap&& other); + + std::vector data; +}; + +// Subtree with root selected from the shard roots level. +struct OrchardShard { + OrchardShard(); + OrchardShard(OrchardShardAddress shard_addr, + std::optional shard_hash, + std::vector shard_data); + ~OrchardShard(); + + OrchardShard(const OrchardShard& other); + OrchardShard& operator=(const OrchardShard& other); + OrchardShard(OrchardShard&& other); + OrchardShard& operator=(OrchardShard&& other); + + bool operator==(const OrchardShard& other) const = default; + + // Subtree root address. + OrchardShardAddress address; + // Root hash exists only on completed shards. + std::optional root_hash; + std::vector shard_data; + // Right-most position of the subtree leaf. + size_t subtree_end_height = 0; +}; + +struct OrchardCommitment { + OrchardCommitmentValue cmu; + bool is_marked; + std::optional checkpoint_id; +}; + +// Compact representation of the Merkle tree on some point. +// Since batch inserting may contain gaps between scan ranges we insert +// frontier which allows to calculate node hashes and witnesses(merkle path from +// leaf to the tree root) even when previous scan ranges are not completed. +struct OrchardTreeState { + OrchardTreeState(); + ~OrchardTreeState(); + OrchardTreeState(const OrchardTreeState&); + + // Tree state is linked to the end of some block. + uint32_t block_height = 0u; + // Number of leafs at the position. + uint32_t tree_size = 0u; + // https://docs.aztec.network/protocol-specs/l1-smart-contracts/frontier + std::vector frontier; }; bool OutputZCashAddressSupported(const std::string& address, bool is_testnet); diff --git a/components/services/brave_wallet/public/mojom/zcash_decoder.mojom b/components/services/brave_wallet/public/mojom/zcash_decoder.mojom index df368e94e250..2dfa39ef247b 100644 --- a/components/services/brave_wallet/public/mojom/zcash_decoder.mojom +++ b/components/services/brave_wallet/public/mojom/zcash_decoder.mojom @@ -71,6 +71,12 @@ struct CompactBlock { ChainMetadata chain_metadata; }; +struct SubtreeRoot { + array root_hash; + array complete_block_hash; + uint32 complete_block_height; +}; + interface ZCashDecoder { ParseBlockID(string data) => (BlockID? value); ParseGetAddressUtxos(string data) => (GetAddressUtxosResponse? value);