diff options
| -rw-r--r-- | docs/MONZERO_ASSETS_V1_SPEC.md | 51 | ||||
| -rw-r--r-- | src/blockchain_db/asset_db.cpp | 92 | ||||
| -rw-r--r-- | src/blockchain_db/asset_db.h | 25 | ||||
| -rw-r--r-- | src/blockchain_db/blockchain_db.cpp | 5 | ||||
| -rw-r--r-- | src/blockchain_db/blockchain_db.h | 14 | ||||
| -rw-r--r-- | src/blockchain_db/lmdb/db_lmdb.cpp | 156 | ||||
| -rw-r--r-- | src/blockchain_db/lmdb/db_lmdb.h | 11 | ||||
| -rw-r--r-- | src/blockchain_db/testdb.h | 6 | ||||
| -rw-r--r-- | src/cryptonote_basic/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/cryptonote_basic/asset_wire.cpp | 355 | ||||
| -rw-r--r-- | src/cryptonote_basic/asset_wire.h | 68 | ||||
| -rw-r--r-- | tests/unit_tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | tests/unit_tests/asset_wire.cpp | 169 | ||||
| -rw-r--r-- | tests/unit_tests/blockchain_db.cpp | 224 |
14 files changed, 1171 insertions, 7 deletions
diff --git a/docs/MONZERO_ASSETS_V1_SPEC.md b/docs/MONZERO_ASSETS_V1_SPEC.md index 0d1f928e9..8ef42f6db 100644 --- a/docs/MONZERO_ASSETS_V1_SPEC.md +++ b/docs/MONZERO_ASSETS_V1_SPEC.md @@ -276,13 +276,54 @@ members, duplicate or zero output IDs, malformed points, key-image tampering, and any pseudo input without exactly one matching proof. Key images must also be unique inside one transaction. -This still does **not** provide global double-spend prevention: production -integration must resolve every claimed ring member against the authoritative -asset-output database and reject key images already spent by earlier blocks or -the mempool. Canonical wire serialization and reorg-safe output/key-image -indexes are also outstanding. Until those layers exist and are reviewed, these +The inactive database prototype now persists authoritative asset outputs and +spent asset key images in separate LMDB indexes. Ring claims can be resolved +against those records before CLSAG verification, and block detach removes +outputs and key images at or above the detached height. Restart, transaction +abort, duplicate-key-image, and chain-pop tests cover this storage layer. + +This still does **not** provide end-to-end global double-spend prevention. The +active transaction and mempool paths do not yet call the verifier or reserve +key images. The canonical wire and atomic state-application prototypes below +are not yet embedded in native transactions or invoked by active block and +mempool validation. Until those layers are integrated and reviewed, these proofs cannot make an asset transaction valid on any Monzero network. +### 6.3 Inactive canonical transaction payload + +The source tree contains a version-1 canonical binary payload prototype. It is +still detached from active transaction parsing. Its byte order and field order +are fixed as follows: + +1. one-byte payload version and one-byte network type; +2. 32-byte carrier-prefix hash; +3. one-byte issuance-present flag, followed when set by a two-byte + little-endian issuance length and the canonical authenticated issuance; +4. one-byte balance-group count, then for each group: asset ID, counted pseudo + inputs, counted destination-key/commitment pairs, counted burn commitments, + and counted Bulletproof+ objects; +5. one-byte ownership-proof count, then each asset ID, pseudo commitment, key + image, exactly 16 ring members, and the canonical CLSAG fields (`c1`, `D`, + and exactly 16 responses). The redundant CLSAG `I` field is reconstructed + from the separately encoded key image and is not serialized. + +All integer lengths and output indexes use explicit little-endian encoding; +all point, hash, UUID, and signature values use their fixed byte arrays. The +decoder rejects truncation, trailing bytes, unsupported versions, invalid +flags, noncanonical ring sizes, and counts over the per-group and aggregate +limits. The payload is capped at 256 KiB, eight asset groups, 64 total inputs, +64 total destinations, and 64 ownership proofs. A fixed 492-byte test vector +has canonical fast-hash +`7ad38e0c9b90d1d458f69df1ca5c4c27689ba0c86e0fbcb08a2149166b3d999b`. + +Output identities are derived from a domain label, network UUID, carrier hash, +asset ID, global output index, destination key, and commitment. State +application validates the entire payload, resolves every ring member, checks +collection authority and collisions, then atomically writes issuance records, +spent key images, and outputs. The carrier is defined as the native transaction +prefix hash with this envelope omitted; the active transaction representation +and exact stripping procedure remain an activation prerequisite. + ## 7. Metadata Consensus stores only bounded identity and commitment fields. Descriptions, diff --git a/src/blockchain_db/asset_db.cpp b/src/blockchain_db/asset_db.cpp index d80b61028..800ea78fb 100644 --- a/src/blockchain_db/asset_db.cpp +++ b/src/blockchain_db/asset_db.cpp @@ -3,6 +3,8 @@ #include <algorithm> #include <cstring> +#include "ringct/rctOps.h" + namespace cryptonote { namespace assets @@ -92,5 +94,95 @@ bool apply_block_extensions_to_db(BlockchainDB& db, asset_ids = std::move(candidate_ids); return true; } + +bool verify_asset_ownership_against_db(const BlockchainDB& db, + const asset_ownership_proof& proof, network_type expected_network, + const crypto::hash& carrier_prefix_hash, std::string* error) +{ + if (db.has_asset_key_image(proof.key_image)) + return fail(error, "asset key image is already spent"); + + for (const asset_ring_member& member : proof.ring) + { + asset_output_data_t stored{}; + if (!db.get_asset_output(member.output_id, stored)) + return fail(error, "asset ownership ring references an unknown output"); + if (stored.asset_id != member.asset_id || member.asset_id != proof.asset_id) + return fail(error, "asset ownership ring member has the wrong asset id"); + if (!rct::equalKeys(stored.destination, member.public_output.dest)) + return fail(error, "asset ownership ring destination does not match consensus state"); + if (!rct::equalKeys(stored.commitment, member.public_output.mask)) + return fail(error, "asset ownership ring commitment does not match consensus state"); + } + return verify_asset_ownership_proof( + proof, expected_network, carrier_prefix_hash, error); +} + +bool apply_asset_transaction_to_db(BlockchainDB& db, + const asset_transaction_payload& payload, network_type expected_network, + const crypto::hash& expected_carrier_prefix_hash, uint64_t height, + std::vector<crypto::hash>& output_ids, std::string* error) +{ + asset_registry registry; + if (!load_registry_from_db(db, expected_network, registry, error) + || !verify_asset_transaction_payload(payload, registry.known_assets(), + expected_network, expected_carrier_prefix_hash, error)) + return false; + for (const asset_ownership_proof& proof : payload.ownership_proofs) + if (!verify_asset_ownership_against_db(db, proof, expected_network, + expected_carrier_prefix_hash, error)) + return false; + + crypto::hash issued_id{}; + std::vector<uint8_t> encoded_issuance; + if (payload.issuance) + { + if (!registry.apply_issuance(*payload.issuance, height, issued_id, error) + || !encode_issuance_payload(*payload.issuance, encoded_issuance, error)) + return false; + } + + std::vector<crypto::hash> candidate_ids; + uint32_t global_output_index = 0; + for (size_t group = 0; group < payload.balances.size(); ++group) + { + const confidential_asset_balance& balance = payload.balances[group]; + for (size_t index = 0; index < balance.outputs.size(); ++index) + { + confidential_asset_output output{ + payload.output_destinations[group][index], balance.outputs[index]}; + crypto::hash output_id{}; + if (!derive_asset_output_id(expected_network, expected_carrier_prefix_hash, + balance.asset_id, global_output_index++, output, output_id, error)) + return false; + asset_output_data_t existing{}; + if (db.get_asset_output(output_id, existing)) + return fail(error, "asset output identity already exists"); + candidate_ids.push_back(output_id); + } + } + + if (payload.issuance) + { + const blobdata_ref bytes{reinterpret_cast<const char*>(encoded_issuance.data()), + encoded_issuance.size()}; + db.add_asset_record(issued_id, height, bytes); + } + for (const asset_ownership_proof& proof : payload.ownership_proofs) + db.add_asset_key_image(proof.key_image, height); + size_t candidate_index = 0; + for (size_t group = 0; group < payload.balances.size(); ++group) + { + const confidential_asset_balance& balance = payload.balances[group]; + for (size_t index = 0; index < balance.outputs.size(); ++index) + { + const asset_output_data_t output{balance.asset_id, + payload.output_destinations[group][index], balance.outputs[index], height}; + db.add_asset_output(candidate_ids[candidate_index++], output); + } + } + output_ids = std::move(candidate_ids); + return true; +} } } diff --git a/src/blockchain_db/asset_db.h b/src/blockchain_db/asset_db.h index 0df5721e0..a43d86585 100644 --- a/src/blockchain_db/asset_db.h +++ b/src/blockchain_db/asset_db.h @@ -4,7 +4,9 @@ #include <vector> #include "blockchain_db.h" +#include "cryptonote_basic/asset_confidential.h" #include "cryptonote_basic/asset_types.h" +#include "cryptonote_basic/asset_wire.h" namespace cryptonote { @@ -27,5 +29,28 @@ namespace assets uint64_t height, std::vector<crypto::hash>& asset_ids, std::string* error = nullptr); + + // Resolve every claimed ring member against consensus storage and reject + // key images already spent by an earlier accepted asset transaction before + // performing the cryptographic ownership check. + bool verify_asset_ownership_against_db( + const BlockchainDB& db, + const asset_ownership_proof& proof, + network_type expected_network, + const crypto::hash& carrier_prefix_hash, + std::string* error = nullptr); + + // The caller owns the outer blockchain write transaction. Every semantic, + // ownership, collision, and collection-authority check completes before the + // first write so an accepted payload changes registry, output, and spent-key + // state atomically. + bool apply_asset_transaction_to_db( + BlockchainDB& db, + const asset_transaction_payload& payload, + network_type expected_network, + const crypto::hash& expected_carrier_prefix_hash, + uint64_t height, + std::vector<crypto::hash>& output_ids, + std::string* error = nullptr); } } diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp index b1c4a4136..38203ddb2 100644 --- a/src/blockchain_db/blockchain_db.cpp +++ b/src/blockchain_db/blockchain_db.cpp @@ -318,7 +318,10 @@ void BlockchainDB::pop_block(block& blk, std::vector<transaction>& txs) // Asset records created by the detached block are consensus state and must // disappear in the same write transaction as the native block and tx data. - remove_asset_records_from_height(height() - 1); + const uint64_t detached_height = height() - 1; + remove_asset_records_from_height(detached_height); + remove_asset_outputs_from_height(detached_height); + remove_asset_key_images_from_height(detached_height); remove_block(); diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h index c176a8ef5..a52f2bdad 100644 --- a/src/blockchain_db/blockchain_db.h +++ b/src/blockchain_db/blockchain_db.h @@ -128,6 +128,14 @@ struct output_data_t uint64_t height; //!< the height of the block which created the output rct::key commitment; //!< the output's amount commitment (for spend verification) }; + +struct asset_output_data_t +{ + crypto::hash asset_id; + rct::key destination; + rct::key commitment; + uint64_t height; +}; #pragma pack(pop) #pragma pack(push, 1) @@ -1791,6 +1799,12 @@ public: virtual bool get_asset_record(const crypto::hash &asset_id, uint64_t &height, cryptonote::blobdata &payload) const = 0; virtual void remove_asset_records_from_height(uint64_t height) = 0; virtual bool for_all_asset_records(std::function<bool(const crypto::hash&, uint64_t, const cryptonote::blobdata_ref&)>) const = 0; + virtual void add_asset_output(const crypto::hash &output_id, const asset_output_data_t &output) = 0; + virtual bool get_asset_output(const crypto::hash &output_id, asset_output_data_t &output) const = 0; + virtual void add_asset_key_image(const crypto::key_image &key_image, uint64_t height) = 0; + virtual bool has_asset_key_image(const crypto::key_image &key_image) const = 0; + virtual void remove_asset_outputs_from_height(uint64_t height) = 0; + virtual void remove_asset_key_images_from_height(uint64_t height) = 0; // diff --git a/src/blockchain_db/lmdb/db_lmdb.cpp b/src/blockchain_db/lmdb/db_lmdb.cpp index 0c3a4f470..4ba91b72b 100644 --- a/src/blockchain_db/lmdb/db_lmdb.cpp +++ b/src/blockchain_db/lmdb/db_lmdb.cpp @@ -58,7 +58,7 @@ using epee::string_tools::pod_to_hex; using namespace crypto; // Increase when the DB structure changes -#define VERSION 6 +#define VERSION 7 namespace { @@ -239,6 +239,10 @@ const char* const LMDB_ALT_BLOCKS = "alt_blocks"; const char* const LMDB_ASSET_RECORDS = "asset_records"; const char* const LMDB_ASSET_HEIGHTS = "asset_heights"; +const char* const LMDB_ASSET_OUTPUTS = "asset_outputs"; +const char* const LMDB_ASSET_OUTPUT_HEIGHTS = "asset_output_heights"; +const char* const LMDB_ASSET_KEY_IMAGES = "asset_key_images"; +const char* const LMDB_ASSET_KEY_IMAGE_HEIGHTS = "asset_key_image_heights"; const char* const LMDB_HF_STARTING_HEIGHTS = "hf_starting_heights"; const char* const LMDB_HF_VERSIONS = "hf_versions"; @@ -1511,6 +1515,10 @@ void BlockchainLMDB::open(const std::string& filename, const int db_flags) lmdb_db_open(txn, LMDB_ASSET_RECORDS, MDB_CREATE, m_asset_records, "Failed to open db handle for m_asset_records"); lmdb_db_open(txn, LMDB_ASSET_HEIGHTS, MDB_INTEGERKEY | MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED, m_asset_heights, "Failed to open db handle for m_asset_heights"); + lmdb_db_open(txn, LMDB_ASSET_OUTPUTS, MDB_CREATE, m_asset_outputs, "Failed to open db handle for m_asset_outputs"); + lmdb_db_open(txn, LMDB_ASSET_OUTPUT_HEIGHTS, MDB_INTEGERKEY | MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED, m_asset_output_heights, "Failed to open db handle for m_asset_output_heights"); + lmdb_db_open(txn, LMDB_ASSET_KEY_IMAGES, MDB_CREATE, m_asset_key_images, "Failed to open db handle for m_asset_key_images"); + lmdb_db_open(txn, LMDB_ASSET_KEY_IMAGE_HEIGHTS, MDB_INTEGERKEY | MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED, m_asset_key_image_heights, "Failed to open db handle for m_asset_key_image_heights"); // this subdb is dropped on sight, so it may not be present when we open the DB. // Since we use MDB_CREATE, we'll get an exception if we open read-only and it does not exist. @@ -1538,6 +1546,10 @@ void BlockchainLMDB::open(const std::string& filename, const int db_flags) mdb_set_compare(txn, m_alt_blocks, compare_hash32); mdb_set_compare(txn, m_asset_records, compare_hash32); mdb_set_dupsort(txn, m_asset_heights, compare_hash32); + mdb_set_compare(txn, m_asset_outputs, compare_hash32); + mdb_set_dupsort(txn, m_asset_output_heights, compare_hash32); + mdb_set_compare(txn, m_asset_key_images, compare_hash32); + mdb_set_dupsort(txn, m_asset_key_image_heights, compare_hash32); mdb_set_compare(txn, m_properties, compare_string); if (!(mdb_flags & MDB_RDONLY)) @@ -1714,6 +1726,14 @@ void BlockchainLMDB::reset() throw0(DB_ERROR(lmdb_error("Failed to drop m_asset_records: ", result).c_str())); if (auto result = mdb_drop(txn, m_asset_heights, 0)) throw0(DB_ERROR(lmdb_error("Failed to drop m_asset_heights: ", result).c_str())); + if (auto result = mdb_drop(txn, m_asset_outputs, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_asset_outputs: ", result).c_str())); + if (auto result = mdb_drop(txn, m_asset_output_heights, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_asset_output_heights: ", result).c_str())); + if (auto result = mdb_drop(txn, m_asset_key_images, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_asset_key_images: ", result).c_str())); + if (auto result = mdb_drop(txn, m_asset_key_image_heights, 0)) + throw0(DB_ERROR(lmdb_error("Failed to drop m_asset_key_image_heights: ", result).c_str())); // init with current version MDB_val_str(k, "version"); @@ -2550,6 +2570,125 @@ bool BlockchainLMDB::for_all_asset_records(std::function<bool(const crypto::hash return ret; } +void BlockchainLMDB::add_asset_output(const crypto::hash &output_id, const asset_output_data_t &output) +{ + check_open(); + TXN_BLOCK_PREFIX(0); + MDB_val key = {sizeof(output_id), const_cast<crypto::hash*>(&output_id)}; + MDB_val value = {sizeof(output), const_cast<asset_output_data_t*>(&output)}; + int result = mdb_put(*txn_ptr, m_asset_outputs, &key, &value, MDB_NOOVERWRITE); + if (result == MDB_KEYEXIST) + throw1(DB_ERROR("Attempting to add an asset output that already exists")); + if (result) + throw1(DB_ERROR(lmdb_error("Error adding asset output: ", result).c_str())); + MDB_val_copy<uint64_t> height_key(output.height); + MDB_val id_value = {sizeof(output_id), const_cast<crypto::hash*>(&output_id)}; + if ((result = mdb_put(*txn_ptr, m_asset_output_heights, &height_key, &id_value, MDB_NODUPDATA))) + throw1(DB_ERROR(lmdb_error("Error indexing asset output height: ", result).c_str())); + TXN_BLOCK_POSTFIX_SUCCESS(); +} + +bool BlockchainLMDB::get_asset_output(const crypto::hash &output_id, asset_output_data_t &output) const +{ + check_open(); + TXN_PREFIX_RDONLY(); + MDB_val key = {sizeof(output_id), const_cast<crypto::hash*>(&output_id)}, value; + const int result = mdb_get(m_txn, m_asset_outputs, &key, &value); + if (result == MDB_NOTFOUND) + return false; + if (result) + throw0(DB_ERROR(lmdb_error("Error retrieving asset output: ", result).c_str())); + if (value.mv_size != sizeof(output)) + throw0(DB_ERROR("Asset output record has an invalid size")); + std::memcpy(&output, value.mv_data, sizeof(output)); + return true; +} + +void BlockchainLMDB::add_asset_key_image(const crypto::key_image &key_image, uint64_t height) +{ + check_open(); + TXN_BLOCK_PREFIX(0); + MDB_val key = {sizeof(key_image), const_cast<crypto::key_image*>(&key_image)}; + MDB_val_copy<uint64_t> value(height); + int result = mdb_put(*txn_ptr, m_asset_key_images, &key, &value, MDB_NOOVERWRITE); + if (result == MDB_KEYEXIST) + throw1(KEY_IMAGE_EXISTS("Attempting to spend an asset key image that already exists")); + if (result) + throw1(DB_ERROR(lmdb_error("Error adding asset key image: ", result).c_str())); + MDB_val_copy<uint64_t> height_key(height); + MDB_val image_value = {sizeof(key_image), const_cast<crypto::key_image*>(&key_image)}; + if ((result = mdb_put(*txn_ptr, m_asset_key_image_heights, &height_key, &image_value, MDB_NODUPDATA))) + throw1(DB_ERROR(lmdb_error("Error indexing asset key image height: ", result).c_str())); + TXN_BLOCK_POSTFIX_SUCCESS(); +} + +bool BlockchainLMDB::has_asset_key_image(const crypto::key_image &key_image) const +{ + check_open(); + TXN_PREFIX_RDONLY(); + MDB_val key = {sizeof(key_image), const_cast<crypto::key_image*>(&key_image)}, value; + const int result = mdb_get(m_txn, m_asset_key_images, &key, &value); + if (result == MDB_NOTFOUND) + return false; + if (result) + throw0(DB_ERROR(lmdb_error("Error retrieving asset key image: ", result).c_str())); + return true; +} + +void BlockchainLMDB::remove_asset_outputs_from_height(uint64_t height) +{ + check_open(); + TXN_BLOCK_PREFIX(0); + MDB_cursor *cursor = nullptr; + int result = mdb_cursor_open(*txn_ptr, m_asset_output_heights, &cursor); + if (result) + throw1(DB_ERROR(lmdb_error("Error opening asset output height cursor: ", result).c_str())); + MDB_val_copy<uint64_t> key(height); + MDB_val value; + result = mdb_cursor_get(cursor, &key, &value, MDB_SET_RANGE); + while (result == MDB_SUCCESS) + { + MDB_val output_key = {value.mv_size, value.mv_data}; + const int deleted = mdb_del(*txn_ptr, m_asset_outputs, &output_key, nullptr); + if (deleted != MDB_SUCCESS && deleted != MDB_NOTFOUND) + throw1(DB_ERROR(lmdb_error("Error removing asset output: ", deleted).c_str())); + if ((result = mdb_cursor_del(cursor, 0)) != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error removing asset output height: ", result).c_str())); + result = mdb_cursor_get(cursor, &key, &value, MDB_NEXT); + } + mdb_cursor_close(cursor); + if (result != MDB_NOTFOUND) + throw1(DB_ERROR(lmdb_error("Error iterating asset output heights: ", result).c_str())); + TXN_BLOCK_POSTFIX_SUCCESS(); +} + +void BlockchainLMDB::remove_asset_key_images_from_height(uint64_t height) +{ + check_open(); + TXN_BLOCK_PREFIX(0); + MDB_cursor *cursor = nullptr; + int result = mdb_cursor_open(*txn_ptr, m_asset_key_image_heights, &cursor); + if (result) + throw1(DB_ERROR(lmdb_error("Error opening asset key-image height cursor: ", result).c_str())); + MDB_val_copy<uint64_t> key(height); + MDB_val value; + result = mdb_cursor_get(cursor, &key, &value, MDB_SET_RANGE); + while (result == MDB_SUCCESS) + { + MDB_val image_key = {value.mv_size, value.mv_data}; + const int deleted = mdb_del(*txn_ptr, m_asset_key_images, &image_key, nullptr); + if (deleted != MDB_SUCCESS && deleted != MDB_NOTFOUND) + throw1(DB_ERROR(lmdb_error("Error removing asset key image: ", deleted).c_str())); + if ((result = mdb_cursor_del(cursor, 0)) != MDB_SUCCESS) + throw1(DB_ERROR(lmdb_error("Error removing asset key-image height: ", result).c_str())); + result = mdb_cursor_get(cursor, &key, &value, MDB_NEXT); + } + mdb_cursor_close(cursor); + if (result != MDB_NOTFOUND) + throw1(DB_ERROR(lmdb_error("Error iterating asset key-image heights: ", result).c_str())); + TXN_BLOCK_POSTFIX_SUCCESS(); +} + bool BlockchainLMDB::block_exists(const crypto::hash& h, uint64_t *height) const { LOG_PRINT_L3("BlockchainLMDB::" << __func__); @@ -5831,6 +5970,19 @@ void BlockchainLMDB::migrate_5_6() txn.commit(); } +void BlockchainLMDB::migrate_6_7() +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + mdb_txn_safe txn(false); + if (const int result = mdb_txn_begin(m_env, nullptr, 0, txn)) + throw0(DB_ERROR(lmdb_error("Failed to create transaction for DB v7 migration: ", result).c_str())); + MDB_val_str(key, "version"); + MDB_val_copy<uint32_t> value(7); + if (const int result = mdb_put(txn, m_properties, &key, &value, 0)) + throw0(DB_ERROR(lmdb_error("Failed to update DB version to 7: ", result).c_str())); + txn.commit(); +} + void BlockchainLMDB::migrate(const uint32_t oldversion) { if (oldversion < 1) @@ -5845,6 +5997,8 @@ void BlockchainLMDB::migrate(const uint32_t oldversion) migrate_4_5(); if (oldversion < 6) migrate_5_6(); + if (oldversion < 7) + migrate_6_7(); } } // namespace cryptonote diff --git a/src/blockchain_db/lmdb/db_lmdb.h b/src/blockchain_db/lmdb/db_lmdb.h index d116bcec5..c7851c117 100644 --- a/src/blockchain_db/lmdb/db_lmdb.h +++ b/src/blockchain_db/lmdb/db_lmdb.h @@ -315,6 +315,12 @@ public: virtual bool get_asset_record(const crypto::hash &asset_id, uint64_t &height, cryptonote::blobdata &payload) const; virtual void remove_asset_records_from_height(uint64_t height); virtual bool for_all_asset_records(std::function<bool(const crypto::hash&, uint64_t, const cryptonote::blobdata_ref&)>) const; + virtual void add_asset_output(const crypto::hash &output_id, const asset_output_data_t &output); + virtual bool get_asset_output(const crypto::hash &output_id, asset_output_data_t &output) const; + virtual void add_asset_key_image(const crypto::key_image &key_image, uint64_t height); + virtual bool has_asset_key_image(const crypto::key_image &key_image) const; + virtual void remove_asset_outputs_from_height(uint64_t height); + virtual void remove_asset_key_images_from_height(uint64_t height); virtual uint64_t add_block( const std::pair<block, blobdata>& blk , size_t block_weight @@ -449,6 +455,7 @@ private: // migrate from DB version 5 to 6 void migrate_5_6(); + void migrate_6_7(); void cleanup_batch(); @@ -479,6 +486,10 @@ private: MDB_dbi m_asset_records; MDB_dbi m_asset_heights; + MDB_dbi m_asset_outputs; + MDB_dbi m_asset_output_heights; + MDB_dbi m_asset_key_images; + MDB_dbi m_asset_key_image_heights; MDB_dbi m_hf_starting_heights; MDB_dbi m_hf_versions; diff --git a/src/blockchain_db/testdb.h b/src/blockchain_db/testdb.h index fb97ccee4..c14348245 100644 --- a/src/blockchain_db/testdb.h +++ b/src/blockchain_db/testdb.h @@ -170,6 +170,12 @@ public: virtual bool get_asset_record(const crypto::hash&, uint64_t&, cryptonote::blobdata&) const override { return false; } virtual void remove_asset_records_from_height(uint64_t) override {} virtual bool for_all_asset_records(std::function<bool(const crypto::hash&, uint64_t, const cryptonote::blobdata_ref&)>) const override { return true; } + virtual void add_asset_output(const crypto::hash&, const asset_output_data_t&) override {} + virtual bool get_asset_output(const crypto::hash&, asset_output_data_t&) const override { return false; } + virtual void add_asset_key_image(const crypto::key_image&, uint64_t) override {} + virtual bool has_asset_key_image(const crypto::key_image&) const override { return false; } + virtual void remove_asset_outputs_from_height(uint64_t) override {} + virtual void remove_asset_key_images_from_height(uint64_t) override {} }; } diff --git a/src/cryptonote_basic/CMakeLists.txt b/src/cryptonote_basic/CMakeLists.txt index 84e86803c..c8b41a03e 100644 --- a/src/cryptonote_basic/CMakeLists.txt +++ b/src/cryptonote_basic/CMakeLists.txt @@ -46,6 +46,7 @@ target_link_libraries(cryptonote_format_utils_basic set(cryptonote_basic_sources asset_confidential.cpp + asset_wire.cpp account.cpp asset_types.cpp connection_context.cpp diff --git a/src/cryptonote_basic/asset_wire.cpp b/src/cryptonote_basic/asset_wire.cpp new file mode 100644 index 000000000..a0d77bf33 --- /dev/null +++ b/src/cryptonote_basic/asset_wire.cpp @@ -0,0 +1,355 @@ +#include "asset_wire.h" + +#include <cstring> +#include <limits> + +#include "ringct/rctOps.h" + +namespace cryptonote +{ +namespace assets +{ +namespace +{ + constexpr char OUTPUT_ID_DOMAIN[] = "MonzeroAssetOutputIdV1"; + + bool fail(std::string* error, const std::string& message) + { + if (error) + *error = message; + return false; + } + + class writer + { + public: + template<typename T> void pod(const T& value) + { + const auto* begin = reinterpret_cast<const uint8_t*>(&value); + bytes.insert(bytes.end(), begin, begin + sizeof(value)); + } + void count(size_t value) { pod(static_cast<uint8_t>(value)); } + void u16(uint16_t value) + { + bytes.push_back(static_cast<uint8_t>(value)); + bytes.push_back(static_cast<uint8_t>(value >> 8)); + } + void u32(uint32_t value) + { + for (unsigned shift = 0; shift < 32; shift += 8) + bytes.push_back(static_cast<uint8_t>(value >> shift)); + } + std::vector<uint8_t> bytes; + }; + + class reader + { + public: + explicit reader(const std::vector<uint8_t>& source) : source_(source) {} + template<typename T> bool pod(T& value) + { + if (offset_ > source_.size() || sizeof(value) > source_.size() - offset_) + return false; + std::memcpy(&value, source_.data() + offset_, sizeof(value)); + offset_ += sizeof(value); + return true; + } + bool count(size_t limit, size_t& value) + { + uint8_t encoded = 0; + if (!pod(encoded) || encoded > limit) + return false; + value = encoded; + return true; + } + bool u16(uint16_t& value) + { + uint8_t low = 0, high = 0; + if (!pod(low) || !pod(high)) + return false; + value = static_cast<uint16_t>(low) | (static_cast<uint16_t>(high) << 8); + return true; + } + bool done() const { return offset_ == source_.size(); } + private: + const std::vector<uint8_t>& source_; + size_t offset_ = 0; + }; + + void write_keys(writer& out, const rct::keyV& keys) + { + out.count(keys.size()); + for (const rct::key& key : keys) + out.pod(key); + } + + bool read_keys(reader& in, size_t limit, rct::keyV& keys) + { + size_t count = 0; + if (!in.count(limit, count)) + return false; + keys.resize(count); + for (rct::key& key : keys) + if (!in.pod(key)) + return false; + return true; + } + + void write_range_proof(writer& out, const rct::BulletproofPlus& proof) + { + write_keys(out, proof.V); + out.pod(proof.A); out.pod(proof.A1); out.pod(proof.B); + out.pod(proof.r1); out.pod(proof.s1); out.pod(proof.d1); + write_keys(out, proof.L); + write_keys(out, proof.R); + } + + bool read_range_proof(reader& in, rct::BulletproofPlus& proof) + { + return read_keys(in, MAX_CONFIDENTIAL_ASSET_OUTPUTS, proof.V) + && in.pod(proof.A) && in.pod(proof.A1) && in.pod(proof.B) + && in.pod(proof.r1) && in.pod(proof.s1) && in.pod(proof.d1) + && read_keys(in, 16, proof.L) && read_keys(in, 16, proof.R) + && !proof.L.empty() && proof.L.size() == proof.R.size(); + } +} + +bool validate_asset_transaction_payload_shape(const asset_transaction_payload& payload, + std::string* error) +{ + if (payload.version != ASSET_TRANSACTION_WIRE_VERSION) + return fail(error, "unsupported asset transaction wire version"); + if (payload.network != MAINNET && payload.network != TESTNET && payload.network != STAGENET) + return fail(error, "asset transaction requires a public network"); + if (payload.carrier_prefix_hash == crypto::null_hash) + return fail(error, "asset transaction has a zero carrier hash"); + if (payload.balances.empty() || payload.balances.size() > MAX_ASSET_BALANCE_GROUPS) + return fail(error, "asset transaction has an invalid balance-group count"); + if (payload.output_destinations.size() != payload.balances.size()) + return fail(error, "asset transaction output destinations do not match balance groups"); + if (payload.ownership_proofs.size() > MAX_ASSET_OWNERSHIP_PROOFS) + return fail(error, "asset transaction has too many ownership proofs"); + size_t total_inputs = 0, total_destinations = 0; + for (size_t group = 0; group < payload.balances.size(); ++group) + { + const confidential_asset_balance& balance = payload.balances[group]; + if (balance.pseudo_inputs.empty() + || balance.pseudo_inputs.size() > MAX_CONFIDENTIAL_ASSET_INPUTS + || balance.outputs.size() + balance.burns.size() > MAX_CONFIDENTIAL_ASSET_OUTPUTS + || balance.range_proofs.empty() + || balance.range_proofs.size() > MAX_ASSET_RANGE_PROOFS) + return fail(error, "asset balance group exceeds canonical limits"); + if (payload.output_destinations[group].size() != balance.outputs.size()) + return fail(error, "asset output destination count does not match commitments"); + total_inputs += balance.pseudo_inputs.size(); + total_destinations += balance.outputs.size() + balance.burns.size(); + if (total_inputs > MAX_ASSET_TOTAL_INPUTS + || total_destinations > MAX_ASSET_TOTAL_DESTINATIONS) + return fail(error, "asset transaction exceeds aggregate input or destination limits"); + } + for (const asset_ownership_proof& proof : payload.ownership_proofs) + if (proof.ring.size() != CONFIDENTIAL_ASSET_RING_SIZE + || proof.signature.s.size() != CONFIDENTIAL_ASSET_RING_SIZE) + return fail(error, "asset ownership proof has a noncanonical ring size"); + return true; +} + +bool encode_asset_transaction_payload(const asset_transaction_payload& payload, + std::vector<uint8_t>& encoded, std::string* error) +{ + if (!validate_asset_transaction_payload_shape(payload, error)) + return false; + writer out; + out.pod(payload.version); + out.pod(static_cast<uint8_t>(payload.network)); + out.pod(payload.carrier_prefix_hash); + out.pod(static_cast<uint8_t>(payload.issuance ? 1 : 0)); + if (payload.issuance) + { + std::vector<uint8_t> issuance; + if (!encode_issuance_payload(*payload.issuance, issuance, error)) + return false; + if (issuance.size() > std::numeric_limits<uint16_t>::max()) + return fail(error, "asset issuance payload is too large"); + out.u16(static_cast<uint16_t>(issuance.size())); + out.bytes.insert(out.bytes.end(), issuance.begin(), issuance.end()); + } + out.count(payload.balances.size()); + for (size_t group = 0; group < payload.balances.size(); ++group) + { + const confidential_asset_balance& balance = payload.balances[group]; + out.pod(balance.asset_id); + out.count(balance.pseudo_inputs.size()); + for (const confidential_pseudo_input& input : balance.pseudo_inputs) + { + out.pod(input.source_asset_id); + out.pod(input.commitment); + } + out.count(balance.outputs.size()); + for (size_t index = 0; index < balance.outputs.size(); ++index) + { + out.pod(payload.output_destinations[group][index]); + out.pod(balance.outputs[index]); + } + write_keys(out, balance.burns); + out.count(balance.range_proofs.size()); + for (const rct::BulletproofPlus& proof : balance.range_proofs) + write_range_proof(out, proof); + } + out.count(payload.ownership_proofs.size()); + for (const asset_ownership_proof& proof : payload.ownership_proofs) + { + out.pod(proof.asset_id); + out.pod(proof.pseudo_input); + out.pod(proof.key_image); + for (const asset_ring_member& member : proof.ring) + { + out.pod(member.asset_id); out.pod(member.output_id); + out.pod(member.public_output.dest); out.pod(member.public_output.mask); + } + out.pod(proof.signature.c1); + out.pod(proof.signature.D); + for (const rct::key& response : proof.signature.s) + out.pod(response); + } + if (out.bytes.size() > MAX_ASSET_WIRE_BYTES) + return fail(error, "asset transaction payload exceeds maximum size"); + encoded = std::move(out.bytes); + return true; +} + +bool decode_asset_transaction_payload(const std::vector<uint8_t>& encoded, + asset_transaction_payload& payload, std::string* error) +{ + if (encoded.empty() || encoded.size() > MAX_ASSET_WIRE_BYTES) + return fail(error, "asset transaction payload has an invalid size"); + reader in(encoded); + asset_transaction_payload candidate; + uint8_t network = 0, has_issuance = 0; + if (!in.pod(candidate.version) || !in.pod(network) + || !in.pod(candidate.carrier_prefix_hash) || !in.pod(has_issuance) + || has_issuance > 1) + return fail(error, "truncated asset transaction header"); + candidate.network = static_cast<network_type>(network); + if (has_issuance) + { + uint16_t size = 0; + if (!in.u16(size)) + return fail(error, "truncated asset issuance length"); + std::vector<uint8_t> issuance(size); + for (uint8_t& byte : issuance) + if (!in.pod(byte)) + return fail(error, "truncated asset issuance payload"); + issuance_payload decoded; + if (!decode_issuance_payload(issuance, decoded, error)) + return false; + candidate.issuance = decoded; + } + size_t groups = 0; + if (!in.count(MAX_ASSET_BALANCE_GROUPS, groups) || groups == 0) + return fail(error, "invalid asset balance-group count"); + candidate.balances.resize(groups); + candidate.output_destinations.resize(groups); + for (size_t group = 0; group < groups; ++group) + { + confidential_asset_balance& balance = candidate.balances[group]; + size_t inputs = 0, outputs = 0, proofs = 0; + if (!in.pod(balance.asset_id) || !in.count(MAX_CONFIDENTIAL_ASSET_INPUTS, inputs) || inputs == 0) + return fail(error, "truncated or invalid asset inputs"); + balance.pseudo_inputs.resize(inputs); + for (confidential_pseudo_input& input : balance.pseudo_inputs) + if (!in.pod(input.source_asset_id) || !in.pod(input.commitment)) + return fail(error, "truncated asset pseudo input"); + if (!in.count(MAX_CONFIDENTIAL_ASSET_OUTPUTS, outputs)) + return fail(error, "invalid asset output count"); + balance.outputs.resize(outputs); + candidate.output_destinations[group].resize(outputs); + for (size_t index = 0; index < outputs; ++index) + if (!in.pod(candidate.output_destinations[group][index]) || !in.pod(balance.outputs[index])) + return fail(error, "truncated asset output"); + if (!read_keys(in, MAX_CONFIDENTIAL_ASSET_OUTPUTS - outputs, balance.burns) + || !in.count(MAX_ASSET_RANGE_PROOFS, proofs) || proofs == 0) + return fail(error, "invalid asset burn or range-proof count"); + balance.range_proofs.resize(proofs); + for (rct::BulletproofPlus& proof : balance.range_proofs) + if (!read_range_proof(in, proof)) + return fail(error, "truncated or noncanonical asset range proof"); + } + size_t ownership_count = 0; + if (!in.count(MAX_ASSET_OWNERSHIP_PROOFS, ownership_count)) + return fail(error, "invalid asset ownership-proof count"); + candidate.ownership_proofs.resize(ownership_count); + for (asset_ownership_proof& proof : candidate.ownership_proofs) + { + if (!in.pod(proof.asset_id) || !in.pod(proof.pseudo_input) || !in.pod(proof.key_image)) + return fail(error, "truncated asset ownership proof"); + proof.ring.resize(CONFIDENTIAL_ASSET_RING_SIZE); + for (asset_ring_member& member : proof.ring) + if (!in.pod(member.asset_id) || !in.pod(member.output_id) + || !in.pod(member.public_output.dest) || !in.pod(member.public_output.mask)) + return fail(error, "truncated asset ownership ring"); + if (!in.pod(proof.signature.c1) || !in.pod(proof.signature.D)) + return fail(error, "truncated asset CLSAG header"); + proof.signature.s.resize(CONFIDENTIAL_ASSET_RING_SIZE); + for (rct::key& response : proof.signature.s) + if (!in.pod(response)) + return fail(error, "truncated asset CLSAG responses"); + std::memcpy(&proof.signature.I, &proof.key_image, sizeof(proof.signature.I)); + } + if (!in.done() || !validate_asset_transaction_payload_shape(candidate, error)) + return fail(error, in.done() ? "invalid asset transaction payload" : "trailing asset transaction bytes"); + payload = std::move(candidate); + return true; +} + +bool verify_asset_transaction_payload(const asset_transaction_payload& payload, + const std::set<crypto::hash>& known_assets, network_type expected_network, + const crypto::hash& expected_carrier_prefix_hash, std::string* error) +{ + if (!validate_asset_transaction_payload_shape(payload, error)) + return false; + if (payload.network != expected_network) + return fail(error, "asset transaction belongs to a different network"); + if (payload.carrier_prefix_hash != expected_carrier_prefix_hash) + return fail(error, "asset transaction carrier hash mismatch"); + boost::optional<issuance_descriptor> descriptor; + if (payload.issuance) + { + if (!verify_issuance_authorization(payload.issuance->descriptor, + payload.issuance->issuer_signature, error)) + return false; + descriptor = payload.issuance->descriptor; + } + for (const std::vector<rct::key>& destinations : payload.output_destinations) + for (const rct::key& destination : destinations) + if (!rct::isInMainSubgroup(destination)) + return fail(error, "asset transaction contains an invalid destination key"); + return verify_confidential_asset_transaction_with_ownership( + payload.balances, payload.ownership_proofs, known_assets, descriptor, + expected_network, expected_carrier_prefix_hash, error); +} + +bool derive_asset_output_id(network_type network, + const crypto::hash& carrier_prefix_hash, + const crypto::hash& asset_id, uint32_t output_index, + const confidential_asset_output& output, crypto::hash& output_id, + std::string* error) +{ + if (network != MAINNET && network != TESTNET && network != STAGENET) + return fail(error, "asset output identity requires a public network"); + if (carrier_prefix_hash == crypto::null_hash || asset_id == crypto::null_hash) + return fail(error, "asset output identity has a zero carrier or asset id"); + if (!rct::isInMainSubgroup(output.destination) + || !rct::isInMainSubgroup(output.commitment)) + return fail(error, "asset output identity contains an invalid point"); + writer bytes; + bytes.bytes.insert(bytes.bytes.end(), OUTPUT_ID_DOMAIN, + OUTPUT_ID_DOMAIN + sizeof(OUTPUT_ID_DOMAIN) - 1); + bytes.pod(get_config(network).NETWORK_ID); + bytes.pod(carrier_prefix_hash); bytes.pod(asset_id); + bytes.u32(output_index); bytes.pod(output.destination); bytes.pod(output.commitment); + output_id = crypto::cn_fast_hash(bytes.bytes.data(), bytes.bytes.size()); + return true; +} +} +} diff --git a/src/cryptonote_basic/asset_wire.h b/src/cryptonote_basic/asset_wire.h new file mode 100644 index 000000000..e5f63a01b --- /dev/null +++ b/src/cryptonote_basic/asset_wire.h @@ -0,0 +1,68 @@ +#pragma once + +#include <string> +#include <vector> + +#include <boost/optional.hpp> + +#include "asset_confidential.h" + +namespace cryptonote +{ +namespace assets +{ + constexpr uint8_t ASSET_TRANSACTION_WIRE_VERSION = 1; + constexpr size_t MAX_ASSET_BALANCE_GROUPS = 8; + constexpr size_t MAX_ASSET_TOTAL_INPUTS = 64; + constexpr size_t MAX_ASSET_TOTAL_DESTINATIONS = 64; + constexpr size_t MAX_ASSET_OWNERSHIP_PROOFS = 64; + constexpr size_t MAX_ASSET_RANGE_PROOFS = 16; + constexpr size_t MAX_ASSET_WIRE_BYTES = 256 * 1024; + + struct confidential_asset_output + { + rct::key destination{}; + rct::key commitment{}; + }; + + // Inactive canonical payload prototype. The carrier hash is computed from + // the native transaction prefix with this envelope omitted; activation code + // must enforce that procedure to avoid a self-referential hash. + struct asset_transaction_payload + { + uint8_t version = ASSET_TRANSACTION_WIRE_VERSION; + network_type network = UNDEFINED; + crypto::hash carrier_prefix_hash{}; + boost::optional<issuance_payload> issuance; + std::vector<confidential_asset_balance> balances; + std::vector<std::vector<rct::key>> output_destinations; + std::vector<asset_ownership_proof> ownership_proofs; + }; + + bool validate_asset_transaction_payload_shape( + const asset_transaction_payload& payload, + std::string* error = nullptr); + bool encode_asset_transaction_payload( + const asset_transaction_payload& payload, + std::vector<uint8_t>& encoded, + std::string* error = nullptr); + bool decode_asset_transaction_payload( + const std::vector<uint8_t>& encoded, + asset_transaction_payload& payload, + std::string* error = nullptr); + bool verify_asset_transaction_payload( + const asset_transaction_payload& payload, + const std::set<crypto::hash>& known_assets, + network_type expected_network, + const crypto::hash& expected_carrier_prefix_hash, + std::string* error = nullptr); + bool derive_asset_output_id( + network_type network, + const crypto::hash& carrier_prefix_hash, + const crypto::hash& asset_id, + uint32_t output_index, + const confidential_asset_output& output, + crypto::hash& output_id, + std::string* error = nullptr); +} +} diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index b3328fe74..52df31b4a 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -31,6 +31,7 @@ set(unit_tests_sources apply_permutation.cpp address_from_url.cpp asset_confidential.cpp + asset_wire.cpp asset_types.cpp base58.cpp blockchain_db.cpp diff --git a/tests/unit_tests/asset_wire.cpp b/tests/unit_tests/asset_wire.cpp new file mode 100644 index 000000000..d7c26101e --- /dev/null +++ b/tests/unit_tests/asset_wire.cpp @@ -0,0 +1,169 @@ +#include "gtest/gtest.h" + +#include "cryptonote_basic/asset_wire.h" +#include "ringct/bulletproofs_plus.h" +#include "ringct/rctOps.h" +#include "string_tools.h" + +namespace +{ + cryptonote::assets::asset_transaction_payload make_payload() + { + cryptonote::assets::asset_transaction_payload payload; + payload.network = cryptonote::TESTNET; + payload.carrier_prefix_hash.data[0] = 0xc1; + + crypto::public_key issuer{}; + crypto::secret_key issuer_secret{}; + crypto::generate_keys(issuer, issuer_secret); + cryptonote::assets::issuance_payload issuance; + issuance.descriptor.network = cryptonote::TESTNET; + issuance.descriptor.issuer_key = issuer; + issuance.descriptor.issuance_nonce.data[0] = 0xc2; + issuance.descriptor.atomic_supply = 10; + issuance.descriptor.display_decimals = 0; + issuance.descriptor.metadata_reference = "ipfs://monzero-wire-vector"; + crypto::hash authorization{}; + if (!cryptonote::assets::derive_issuance_authorization_hash( + issuance.descriptor, authorization)) + throw std::runtime_error("failed to derive issuance authorization"); + crypto::generate_signature(authorization, issuer, issuer_secret, + issuance.issuer_signature); + payload.issuance = issuance; + + crypto::hash id{}; + if (!cryptonote::assets::derive_asset_id(issuance.descriptor, id)) + throw std::runtime_error("failed to derive asset id"); + cryptonote::assets::confidential_asset_balance balance; + balance.asset_id = id; + balance.pseudo_inputs.push_back({id, rct::commit(10, rct::zero())}); + balance.outputs.push_back(rct::commit(10, rct::zero())); + balance.range_proofs.push_back( + rct::bulletproof_plus_PROVE(10, rct::zero())); + payload.balances.push_back(balance); + rct::key secret{}, destination{}; + rct::skpkGen(secret, destination); + payload.output_destinations.push_back({destination}); + return payload; + } +} + +TEST(asset_wire, canonical_round_trip_preserves_verified_issuance) +{ + const auto original = make_payload(); + std::vector<uint8_t> encoded, reencoded; + std::string error; + ASSERT_TRUE(cryptonote::assets::encode_asset_transaction_payload( + original, encoded, &error)) << error; + cryptonote::assets::asset_transaction_payload decoded; + ASSERT_TRUE(cryptonote::assets::decode_asset_transaction_payload( + encoded, decoded, &error)) << error; + ASSERT_TRUE(cryptonote::assets::encode_asset_transaction_payload( + decoded, reencoded, &error)) << error; + ASSERT_EQ(encoded, reencoded); + ASSERT_TRUE(decoded.issuance); + ASSERT_TRUE(cryptonote::assets::verify_confidential_asset_transaction( + decoded.balances, {}, decoded.issuance->descriptor, &error)) << error; + ASSERT_TRUE(cryptonote::assets::verify_asset_transaction_payload( + decoded, {}, cryptonote::TESTNET, decoded.carrier_prefix_hash, &error)) << error; + EXPECT_FALSE(cryptonote::assets::verify_asset_transaction_payload( + decoded, {}, cryptonote::MAINNET, decoded.carrier_prefix_hash, &error)); + crypto::hash other_carrier = decoded.carrier_prefix_hash; + other_carrier.data[1] = 1; + EXPECT_FALSE(cryptonote::assets::verify_asset_transaction_payload( + decoded, {}, cryptonote::TESTNET, other_carrier, &error)); +} + +TEST(asset_wire, rejects_every_truncation_trailing_bytes_and_noncanonical_counts) +{ + const auto payload = make_payload(); + std::vector<uint8_t> encoded; + std::string error; + ASSERT_TRUE(cryptonote::assets::encode_asset_transaction_payload( + payload, encoded, &error)) << error; + for (size_t size = 0; size < encoded.size(); ++size) + { + cryptonote::assets::asset_transaction_payload decoded; + const std::vector<uint8_t> truncated(encoded.begin(), encoded.begin() + size); + EXPECT_FALSE(cryptonote::assets::decode_asset_transaction_payload( + truncated, decoded, &error)) << "accepted truncation at " << size; + } + auto trailing = encoded; + trailing.push_back(0); + cryptonote::assets::asset_transaction_payload decoded; + EXPECT_FALSE(cryptonote::assets::decode_asset_transaction_payload( + trailing, decoded, &error)); + auto unsupported = encoded; + unsupported[0] = 2; + EXPECT_FALSE(cryptonote::assets::decode_asset_transaction_payload( + unsupported, decoded, &error)); + auto excessive = payload; + excessive.balances.resize(cryptonote::assets::MAX_ASSET_BALANCE_GROUPS + 1, + payload.balances.front()); + excessive.output_destinations.resize(excessive.balances.size(), + payload.output_destinations.front()); + EXPECT_FALSE(cryptonote::assets::encode_asset_transaction_payload( + excessive, trailing, &error)); +} + +TEST(asset_wire, deterministic_output_identity_binds_every_field) +{ + const auto payload = make_payload(); + cryptonote::assets::confidential_asset_output output{ + payload.output_destinations.front().front(), + payload.balances.front().outputs.front()}; + crypto::hash first{}, repeated{}, changed{}; + std::string error; + ASSERT_TRUE(cryptonote::assets::derive_asset_output_id( + cryptonote::TESTNET, payload.carrier_prefix_hash, payload.balances.front().asset_id, 0, + output, first, &error)) << error; + ASSERT_TRUE(cryptonote::assets::derive_asset_output_id( + cryptonote::TESTNET, payload.carrier_prefix_hash, payload.balances.front().asset_id, 0, + output, repeated, &error)); + EXPECT_EQ(first, repeated); + ASSERT_TRUE(cryptonote::assets::derive_asset_output_id( + cryptonote::MAINNET, payload.carrier_prefix_hash, payload.balances.front().asset_id, 0, + output, changed, &error)); + EXPECT_NE(first, changed); + ASSERT_TRUE(cryptonote::assets::derive_asset_output_id( + cryptonote::TESTNET, payload.carrier_prefix_hash, payload.balances.front().asset_id, 1, + output, changed, &error)); + EXPECT_NE(first, changed); + output.commitment = rct::commit(9, rct::zero()); + ASSERT_TRUE(cryptonote::assets::derive_asset_output_id( + cryptonote::TESTNET, payload.carrier_prefix_hash, payload.balances.front().asset_id, 0, + output, changed, &error)); + EXPECT_NE(first, changed); +} + +TEST(asset_wire, fixed_wire_vector_has_stable_size_and_digest) +{ + cryptonote::assets::asset_transaction_payload payload; + payload.network = cryptonote::STAGENET; + payload.carrier_prefix_hash.data[0] = 0xe1; + cryptonote::assets::confidential_asset_balance balance; + balance.asset_id.data[0] = 0xe2; + balance.pseudo_inputs.push_back({balance.asset_id, rct::key{}}); + balance.outputs.push_back(rct::key{}); + rct::BulletproofPlus proof; + proof.A = rct::key{}; + proof.A1 = rct::key{}; + proof.B = rct::key{}; + proof.r1 = rct::key{}; + proof.s1 = rct::key{}; + proof.d1 = rct::key{}; + proof.V.resize(1); + proof.L.resize(1); + proof.R.resize(1); + balance.range_proofs.push_back(proof); + payload.balances.push_back(balance); + payload.output_destinations.push_back({rct::key{}}); + std::vector<uint8_t> encoded; + std::string error; + ASSERT_TRUE(cryptonote::assets::encode_asset_transaction_payload( + payload, encoded, &error)) << error; + ASSERT_EQ(492u, encoded.size()); + const crypto::hash digest = crypto::cn_fast_hash(encoded.data(), encoded.size()); + ASSERT_EQ("7ad38e0c9b90d1d458f69df1ca5c4c27689ba0c86e0fbcb08a2149166b3d999b", + epee::string_tools::pod_to_hex(digest)); +} diff --git a/tests/unit_tests/blockchain_db.cpp b/tests/unit_tests/blockchain_db.cpp index f5149a41e..a2261bc60 100644 --- a/tests/unit_tests/blockchain_db.cpp +++ b/tests/unit_tests/blockchain_db.cpp @@ -39,7 +39,11 @@ #include "blockchain_db/blockchain_db.h" #include "blockchain_db/asset_db.h" #include "blockchain_db/lmdb/db_lmdb.h" +#include "cryptonote_basic/asset_confidential.h" #include "cryptonote_basic/cryptonote_format_utils.h" +#include "device/device.hpp" +#include "ringct/bulletproofs_plus.h" +#include "ringct/rctSigs.h" using namespace cryptonote; using epee::string_tools::pod_to_hex; @@ -69,6 +73,69 @@ assets::transaction_extension make_asset_extension(const crypto::hash& carrier) return extension; } +assets::asset_ownership_proof make_db_ownership_proof( + const crypto::hash& id, const crypto::hash& carrier) +{ + constexpr size_t real = 5; + assets::asset_ownership_proof proof; + proof.asset_id = id; + rct::ctkeyV public_ring; + rct::key spend_secret{}, input_mask{}; + const rct::key amount = rct::d2h(10); + for (size_t index = 0; index < assets::CONFIDENTIAL_ASSET_RING_SIZE; ++index) + { + assets::asset_ring_member member; + member.asset_id = id; + member.output_id.data[0] = static_cast<unsigned char>(index + 1); + rct::key ignored; + rct::skpkGen(ignored, member.public_output.dest); + rct::skpkGen(ignored, member.public_output.mask); + proof.ring.push_back(member); + } + rct::skpkGen(spend_secret, proof.ring[real].public_output.dest); + input_mask = rct::skGen(); + rct::addKeys2(proof.ring[real].public_output.mask, input_mask, amount, rct::H); + for (const auto& member : proof.ring) + public_ring.push_back(member.public_output); + const rct::key pseudo_mask = rct::skGen(); + rct::addKeys2(proof.pseudo_input, pseudo_mask, amount, rct::H); + rct::key message; + std::string error; + if (!assets::derive_asset_ownership_message(proof, TESTNET, carrier, message, &error)) + throw std::runtime_error(error); + rct::ctkey input_secret; + input_secret.dest = spend_secret; + input_secret.mask = input_mask; + proof.signature = rct::proveRctCLSAGSimple(message, public_ring, input_secret, + pseudo_mask, proof.pseudo_input, real, hw::get_device("default")); + std::memcpy(&proof.key_image, &proof.signature.I, sizeof(proof.key_image)); + return proof; +} + +assets::asset_transaction_payload make_db_asset_issuance( + const crypto::hash& carrier) +{ + const assets::transaction_extension extension = make_asset_extension(carrier); + assets::asset_transaction_payload payload; + payload.network = TESTNET; + payload.carrier_prefix_hash = carrier; + payload.issuance = extension.issuance; + crypto::hash id{}; + if (!assets::derive_asset_id(extension.issuance.descriptor, id)) + throw std::runtime_error("failed to derive test asset id"); + const uint64_t supply = extension.issuance.descriptor.atomic_supply; + assets::confidential_asset_balance balance; + balance.asset_id = id; + balance.pseudo_inputs.push_back({id, rct::commit(supply, rct::zero())}); + balance.outputs.push_back(rct::commit(supply, rct::zero())); + balance.range_proofs.push_back(rct::bulletproof_plus_PROVE(supply, rct::zero())); + payload.balances.push_back(balance); + rct::key secret{}, destination{}; + rct::skpkGen(secret, destination); + payload.output_destinations.push_back({destination}); + return payload; +} + const std::vector<std::string> t_blocks = { "0100d5adc49a053b8818b2b6023cd2d532c6774e164a8fcacd603651cb3ea0cb7f9340b28ec016b4bc4ca301aa0101ff6e08acbb2702eab03067870349139bee7eab2ca2e030a6bb73d4f68ab6a3b6ca937214054cdac0843d028bbe23b57ea9bae53f12da93bb57bf8a2e40598d9fccd10c2921576e987d93cd80b4891302468738e391f07c4f2b356f7957160968e0bfef6e907c3cee2d8c23cbf04b089680c6868f01025a0f41f063e195a966051e3a29e17130a9ce97d48f55285b9bb04bdd55a09ae78088aca3cf0202d0f26169290450fe17e08974789c3458910b4db18361cdc564f8f2d0bdd2cf568090cad2c60e02d6f3483ec45505cc3be841046c7a12bf953ac973939bc7b727e54258e1881d4d80e08d84ddcb0102dae6dfb16d3e28aaaf43e00170b90606b36f35f38f8a3dceb5ee18199dd8f17c80c0caf384a30202385d7e57a4daba4cdd9e550a92dcc188838386e7581f13f09de796cbed4716a42101c052492a077abf41996b50c1b2e67fd7288bcd8c55cdc657b4e22d0804371f6901beb76a82ea17400cd6d7f595f70e1667d2018ed8f5a78d1ce07484222618c3cd" @@ -317,6 +384,151 @@ TYPED_TEST(BlockchainDBTest, AssetRecordsPersistAndRollbackAtomically) ASSERT_FALSE(this->m_db->get_asset_record(later, height, payload)); } +TYPED_TEST(BlockchainDBTest, AssetOutputsAndKeyImagesPersistAndRollbackAtomically) +{ + const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + const std::string dir_path = temp_path.string(); + this->set_prefix(dir_path); + ASSERT_NO_THROW(this->m_db->open(dir_path)); + this->get_filenames(); + + crypto::hash first_id{}, later_id{}, aborted_id{}, asset_id{}; + first_id.data[0] = 1; + later_id.data[0] = 2; + aborted_id.data[0] = 3; + asset_id.data[0] = 0xa7; + rct::key first_secret{}, first_key{}, later_key{}, aborted_key{}; + rct::skpkGen(first_secret, first_key); + rct::skpkGen(first_secret, later_key); + rct::skpkGen(first_secret, aborted_key); + + asset_output_data_t first{asset_id, first_key, rct::commit(11, rct::zero()), 30}; + asset_output_data_t later{asset_id, later_key, rct::commit(12, rct::zero()), 31}; + asset_output_data_t aborted{asset_id, aborted_key, rct::commit(13, rct::zero()), 32}; + crypto::key_image spent{}, aborted_spent{}; + reinterpret_cast<unsigned char*>(&spent)[0] = 0x41; + reinterpret_cast<unsigned char*>(&aborted_spent)[0] = 0x42; + + ASSERT_NO_THROW(this->m_db->add_asset_output(first_id, first)); + ASSERT_THROW(this->m_db->add_asset_output(first_id, first), DB_ERROR); + ASSERT_NO_THROW(this->m_db->add_asset_output(later_id, later)); + ASSERT_NO_THROW(this->m_db->add_asset_key_image(spent, 31)); + ASSERT_THROW(this->m_db->add_asset_key_image(spent, 31), KEY_IMAGE_EXISTS); + + this->m_db->block_wtxn_start(); + ASSERT_NO_THROW(this->m_db->add_asset_output(aborted_id, aborted)); + ASSERT_NO_THROW(this->m_db->add_asset_key_image(aborted_spent, 32)); + this->m_db->block_wtxn_abort(); + + asset_output_data_t restored{}; + ASSERT_FALSE(this->m_db->get_asset_output(aborted_id, restored)); + ASSERT_FALSE(this->m_db->has_asset_key_image(aborted_spent)); + ASSERT_NO_THROW(this->m_db->close()); + ASSERT_NO_THROW(this->m_db->open(dir_path)); + + ASSERT_TRUE(this->m_db->get_asset_output(first_id, restored)); + ASSERT_EQ(first.asset_id, restored.asset_id); + ASSERT_EQ(first.destination, restored.destination); + ASSERT_EQ(first.commitment, restored.commitment); + ASSERT_EQ(first.height, restored.height); + ASSERT_TRUE(this->m_db->has_asset_key_image(spent)); + + ASSERT_NO_THROW(this->m_db->remove_asset_outputs_from_height(31)); + ASSERT_NO_THROW(this->m_db->remove_asset_key_images_from_height(31)); + ASSERT_TRUE(this->m_db->get_asset_output(first_id, restored)); + ASSERT_FALSE(this->m_db->get_asset_output(later_id, restored)); + ASSERT_FALSE(this->m_db->has_asset_key_image(spent)); +} + +TYPED_TEST(BlockchainDBTest, AssetOwnershipResolvesAuthoritativeRingAndSpentState) +{ + const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + const std::string dir_path = temp_path.string(); + this->set_prefix(dir_path); + ASSERT_NO_THROW(this->m_db->open(dir_path)); + this->get_filenames(); + + crypto::hash id{}, carrier{}; + id.data[0] = 0x91; + carrier.data[0] = 0x92; + const auto proof = make_db_ownership_proof(id, carrier); + std::string error; + EXPECT_FALSE(assets::verify_asset_ownership_against_db( + *this->m_db, proof, TESTNET, carrier, &error)); + + for (const auto& member : proof.ring) + { + asset_output_data_t output{member.asset_id, member.public_output.dest, + member.public_output.mask, 40}; + ASSERT_NO_THROW(this->m_db->add_asset_output(member.output_id, output)); + } + ASSERT_TRUE(assets::verify_asset_ownership_against_db( + *this->m_db, proof, TESTNET, carrier, &error)) << error; + + ASSERT_NO_THROW(this->m_db->add_asset_key_image(proof.key_image, 41)); + EXPECT_FALSE(assets::verify_asset_ownership_against_db( + *this->m_db, proof, TESTNET, carrier, &error)); + ASSERT_NO_THROW(this->m_db->remove_asset_key_images_from_height(41)); + ASSERT_TRUE(assets::verify_asset_ownership_against_db( + *this->m_db, proof, TESTNET, carrier, &error)) << error; + + ASSERT_NO_THROW(this->m_db->remove_asset_outputs_from_height(40)); + EXPECT_FALSE(assets::verify_asset_ownership_against_db( + *this->m_db, proof, TESTNET, carrier, &error)); +} + +TYPED_TEST(BlockchainDBTest, AssetTransactionStateAppliesAtomicallyAndRejectsReplay) +{ + const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + const std::string dir_path = temp_path.string(); + this->set_prefix(dir_path); + ASSERT_NO_THROW(this->m_db->open(dir_path)); + this->get_filenames(); + + crypto::hash carrier{}; + carrier.data[0] = 0xd1; + const auto payload = make_db_asset_issuance(carrier); + std::vector<crypto::hash> output_ids; + std::string error; + this->m_db->block_wtxn_start(); + ASSERT_TRUE(assets::apply_asset_transaction_to_db(*this->m_db, payload, + TESTNET, carrier, 50, output_ids, &error)) << error; + this->m_db->block_wtxn_stop(); + ASSERT_EQ(1u, output_ids.size()); + + crypto::hash asset_id{}; + ASSERT_TRUE(assets::derive_asset_id(payload.issuance->descriptor, asset_id)); + uint64_t issuance_height = 0; + blobdata issuance_bytes; + ASSERT_TRUE(this->m_db->get_asset_record(asset_id, issuance_height, issuance_bytes)); + ASSERT_EQ(50u, issuance_height); + asset_output_data_t stored{}; + ASSERT_TRUE(this->m_db->get_asset_output(output_ids.front(), stored)); + ASSERT_EQ(asset_id, stored.asset_id); + ASSERT_EQ(50u, stored.height); + + std::vector<crypto::hash> replay_outputs; + this->m_db->block_wtxn_start(); + EXPECT_FALSE(assets::apply_asset_transaction_to_db(*this->m_db, payload, + TESTNET, carrier, 51, replay_outputs, &error)); + this->m_db->block_wtxn_abort(); + ASSERT_TRUE(replay_outputs.empty()); + + auto invalid = make_db_asset_issuance(crypto::hash{}); + crypto::hash invalid_carrier{}; + invalid_carrier.data[0] = 0xd2; + invalid.carrier_prefix_hash = invalid_carrier; + invalid.balances.front().outputs.front() = rct::commit( + invalid.issuance->descriptor.atomic_supply + 1, rct::zero()); + this->m_db->block_wtxn_start(); + EXPECT_FALSE(assets::apply_asset_transaction_to_db(*this->m_db, invalid, + TESTNET, invalid_carrier, 52, replay_outputs, &error)); + this->m_db->block_wtxn_abort(); + crypto::hash invalid_id{}; + ASSERT_TRUE(assets::derive_asset_id(invalid.issuance->descriptor, invalid_id)); + ASSERT_FALSE(this->m_db->get_asset_record(invalid_id, issuance_height, issuance_bytes)); +} + TYPED_TEST(BlockchainDBTest, AssetBlockExtensionsRebuildFromPersistentState) { const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); @@ -370,8 +582,15 @@ TYPED_TEST(BlockchainDBTest, PopBlockRemovesAssetStateAtDetachedHeight) this->init_hard_fork(); crypto::hash id{}; + crypto::hash output_id{}; + crypto::key_image spent{}; id.data[0] = 0xa5; + output_id.data[0] = 0xa6; + reinterpret_cast<unsigned char*>(&spent)[0] = 0xa7; const blobdata payload("detached asset"); + rct::key output_key{}, output_secret{}; + rct::skpkGen(output_secret, output_key); + const asset_output_data_t asset_output{id, output_key, rct::commit(50, rct::zero()), 1}; block popped; std::vector<transaction> transactions; { @@ -379,11 +598,16 @@ TYPED_TEST(BlockchainDBTest, PopBlockRemovesAssetStateAtDetachedHeight) ASSERT_NO_THROW(this->m_db->add_block(this->m_blocks[0], t_sizes[0], t_sizes[0], t_diffs[0], t_coins[0], this->m_txs[0])); ASSERT_NO_THROW(this->m_db->add_block(this->m_blocks[1], t_sizes[1], t_sizes[1], t_diffs[1], t_coins[1], this->m_txs[1])); ASSERT_NO_THROW(this->m_db->add_asset_record(id, 1, blobdata_ref(payload))); + ASSERT_NO_THROW(this->m_db->add_asset_output(output_id, asset_output)); + ASSERT_NO_THROW(this->m_db->add_asset_key_image(spent, 1)); } ASSERT_NO_THROW(this->m_db->pop_block(popped, transactions)); uint64_t height = 0; blobdata restored; + asset_output_data_t restored_output{}; ASSERT_FALSE(this->m_db->get_asset_record(id, height, restored)); + ASSERT_FALSE(this->m_db->get_asset_output(output_id, restored_output)); + ASSERT_FALSE(this->m_db->has_asset_key_image(spent)); ASSERT_EQ(1u, this->m_db->height()); } |
