aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/MONZERO_ASSETS_V1_SPEC.md51
-rw-r--r--src/blockchain_db/asset_db.cpp92
-rw-r--r--src/blockchain_db/asset_db.h25
-rw-r--r--src/blockchain_db/blockchain_db.cpp5
-rw-r--r--src/blockchain_db/blockchain_db.h14
-rw-r--r--src/blockchain_db/lmdb/db_lmdb.cpp156
-rw-r--r--src/blockchain_db/lmdb/db_lmdb.h11
-rw-r--r--src/blockchain_db/testdb.h6
-rw-r--r--src/cryptonote_basic/CMakeLists.txt1
-rw-r--r--src/cryptonote_basic/asset_wire.cpp355
-rw-r--r--src/cryptonote_basic/asset_wire.h68
-rw-r--r--tests/unit_tests/CMakeLists.txt1
-rw-r--r--tests/unit_tests/asset_wire.cpp169
-rw-r--r--tests/unit_tests/blockchain_db.cpp224
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());
}