From 7c17e803151ef3d568a28e7a3c4533ec6c549cf4 Mon Sep 17 00:00:00 2001 From: cenobite pinhead Date: Sat, 15 Aug 2026 22:34:45 +0100 Subject: apply asset extensions to persistent chain state --- src/blockchain_db/CMakeLists.txt | 1 + src/blockchain_db/asset_db.cpp | 96 +++++++++++++++++++++++++++++++++++++ src/blockchain_db/asset_db.h | 31 ++++++++++++ src/blockchain_db/blockchain_db.cpp | 4 ++ tests/unit_tests/blockchain_db.cpp | 92 +++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+) create mode 100644 src/blockchain_db/asset_db.cpp create mode 100644 src/blockchain_db/asset_db.h diff --git a/src/blockchain_db/CMakeLists.txt b/src/blockchain_db/CMakeLists.txt index 14ed76295..7547b54e6 100644 --- a/src/blockchain_db/CMakeLists.txt +++ b/src/blockchain_db/CMakeLists.txt @@ -27,6 +27,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. set(blockchain_db_sources + asset_db.cpp blockchain_db.cpp lmdb/db_lmdb.cpp ) diff --git a/src/blockchain_db/asset_db.cpp b/src/blockchain_db/asset_db.cpp new file mode 100644 index 000000000..d80b61028 --- /dev/null +++ b/src/blockchain_db/asset_db.cpp @@ -0,0 +1,96 @@ +#include "asset_db.h" + +#include +#include + +namespace cryptonote +{ +namespace assets +{ +namespace +{ + bool fail(std::string* error, const std::string& message) + { + if (error) + *error = message; + return false; + } + + struct stored_issuance + { + crypto::hash id{}; + uint64_t height = 0; + std::vector payload; + }; +} + +bool load_registry_from_db(const BlockchainDB& db, network_type expected_network, + asset_registry& registry, std::string* error) +{ + std::vector records; + if (!db.for_all_asset_records([&records](const crypto::hash& id, uint64_t height, const blobdata_ref& payload) { + stored_issuance record; + record.id = id; + record.height = height; + record.payload.assign(payload.begin(), payload.end()); + records.push_back(std::move(record)); + return true; + })) + return fail(error, "asset database enumeration was interrupted"); + + std::sort(records.begin(), records.end(), [](const stored_issuance& left, const stored_issuance& right) { + if (left.height != right.height) + return left.height < right.height; + return std::memcmp(&left.id, &right.id, sizeof(left.id)) < 0; + }); + + asset_registry candidate; + for (const stored_issuance& stored : records) + { + issuance_payload payload; + if (!decode_issuance_payload(stored.payload, payload, error)) + return false; + if (payload.descriptor.network != expected_network) + return fail(error, "stored asset belongs to a different network"); + crypto::hash derived{}; + if (!candidate.apply_issuance(payload, stored.height, derived, error)) + return false; + if (derived != stored.id) + return fail(error, "stored asset id does not match its authenticated payload"); + } + registry = std::move(candidate); + return true; +} + +bool apply_block_extensions_to_db(BlockchainDB& db, + const std::vector& extensions, + const std::vector& carrier_prefix_hashes, + network_type expected_network, uint64_t height, + std::vector& asset_ids, std::string* error) +{ + asset_registry registry; + if (!load_registry_from_db(db, expected_network, registry, error)) + return false; + std::vector candidate_ids; + if (!registry.apply_block_extensions(extensions, carrier_prefix_hashes, + expected_network, height, candidate_ids, error)) + return false; + + std::vector> encoded; + encoded.reserve(extensions.size()); + for (const transaction_extension& extension : extensions) + { + encoded.emplace_back(); + if (!encode_issuance_payload(extension.issuance, encoded.back(), error)) + return false; + } + for (size_t index = 0; index < encoded.size(); ++index) + { + const blobdata_ref bytes{reinterpret_cast(encoded[index].data()), encoded[index].size()}; + db.add_asset_record(candidate_ids[index], height, bytes); + } + asset_ids = std::move(candidate_ids); + return true; +} +} +} diff --git a/src/blockchain_db/asset_db.h b/src/blockchain_db/asset_db.h new file mode 100644 index 000000000..0df5721e0 --- /dev/null +++ b/src/blockchain_db/asset_db.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "blockchain_db.h" +#include "cryptonote_basic/asset_types.h" + +namespace cryptonote +{ +namespace assets +{ + bool load_registry_from_db( + const BlockchainDB& db, + network_type expected_network, + asset_registry& registry, + std::string* error = nullptr); + + // The caller must hold the blockchain write transaction. Semantic + // validation finishes before the first database write, and any exception + // raised by storage must cause that outer transaction to be aborted. + bool apply_block_extensions_to_db( + BlockchainDB& db, + const std::vector& extensions, + const std::vector& carrier_prefix_hashes, + network_type expected_network, + uint64_t height, + std::vector& asset_ids, + std::string* error = nullptr); +} +} diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp index ab73e255c..b1c4a4136 100644 --- a/src/blockchain_db/blockchain_db.cpp +++ b/src/blockchain_db/blockchain_db.cpp @@ -316,6 +316,10 @@ void BlockchainDB::pop_block(block& blk, std::vector& txs) { blk = get_top_block(); + // 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); + remove_block(); for (const auto& h : boost::adaptors::reverse(blk.tx_hashes)) diff --git a/tests/unit_tests/blockchain_db.cpp b/tests/unit_tests/blockchain_db.cpp index 423106522..f5149a41e 100644 --- a/tests/unit_tests/blockchain_db.cpp +++ b/tests/unit_tests/blockchain_db.cpp @@ -37,6 +37,7 @@ #include "string_tools.h" #include "blockchain_db/blockchain_db.h" +#include "blockchain_db/asset_db.h" #include "blockchain_db/lmdb/db_lmdb.h" #include "cryptonote_basic/cryptonote_format_utils.h" @@ -47,6 +48,27 @@ using epee::string_tools::pod_to_hex; namespace { // anonymous namespace +assets::transaction_extension make_asset_extension(const crypto::hash& carrier) +{ + crypto::public_key public_key{}; + crypto::secret_key secret_key{}; + crypto::generate_keys(public_key, secret_key); + assets::transaction_extension extension; + extension.network = TESTNET; + extension.carrier_prefix_hash = carrier; + extension.issuance.descriptor.network = TESTNET; + extension.issuance.descriptor.issuer_key = public_key; + extension.issuance.descriptor.atomic_supply = 1000000; + extension.issuance.descriptor.display_decimals = 2; + extension.issuance.descriptor.metadata_reference = "ipfs://monzero-db-test"; + extension.issuance.descriptor.issuance_nonce.data[0] = carrier.data[0]; + crypto::hash message{}; + if (!assets::derive_issuance_authorization_hash(extension.issuance.descriptor, message)) + throw std::runtime_error("failed to derive issuance authorization"); + crypto::generate_signature(message, public_key, secret_key, extension.issuance.issuer_signature); + return extension; +} + const std::vector t_blocks = { "0100d5adc49a053b8818b2b6023cd2d532c6774e164a8fcacd603651cb3ea0cb7f9340b28ec016b4bc4ca301aa0101ff6e08acbb2702eab03067870349139bee7eab2ca2e030a6bb73d4f68ab6a3b6ca937214054cdac0843d028bbe23b57ea9bae53f12da93bb57bf8a2e40598d9fccd10c2921576e987d93cd80b4891302468738e391f07c4f2b356f7957160968e0bfef6e907c3cee2d8c23cbf04b089680c6868f01025a0f41f063e195a966051e3a29e17130a9ce97d48f55285b9bb04bdd55a09ae78088aca3cf0202d0f26169290450fe17e08974789c3458910b4db18361cdc564f8f2d0bdd2cf568090cad2c60e02d6f3483ec45505cc3be841046c7a12bf953ac973939bc7b727e54258e1881d4d80e08d84ddcb0102dae6dfb16d3e28aaaf43e00170b90606b36f35f38f8a3dceb5ee18199dd8f17c80c0caf384a30202385d7e57a4daba4cdd9e550a92dcc188838386e7581f13f09de796cbed4716a42101c052492a077abf41996b50c1b2e67fd7288bcd8c55cdc657b4e22d0804371f6901beb76a82ea17400cd6d7f595f70e1667d2018ed8f5a78d1ce07484222618c3cd" @@ -295,6 +317,76 @@ TYPED_TEST(BlockchainDBTest, AssetRecordsPersistAndRollbackAtomically) ASSERT_FALSE(this->m_db->get_asset_record(later, height, payload)); } +TYPED_TEST(BlockchainDBTest, AssetBlockExtensionsRebuildFromPersistentState) +{ + 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_carrier{}, second_carrier{}; + first_carrier.data[0] = 0x31; + second_carrier.data[0] = 0x32; + const auto first = make_asset_extension(first_carrier); + const auto second = make_asset_extension(second_carrier); + std::vector ids; + std::string error; + + this->m_db->block_wtxn_start(); + ASSERT_TRUE(assets::apply_block_extensions_to_db(*this->m_db, {first}, {first_carrier}, TESTNET, 20, ids, &error)) << error; + this->m_db->block_wtxn_stop(); + ASSERT_EQ(1u, ids.size()); + + this->m_db->block_wtxn_start(); + ASSERT_TRUE(assets::apply_block_extensions_to_db(*this->m_db, {second}, {second_carrier}, TESTNET, 21, ids, &error)) << error; + this->m_db->block_wtxn_stop(); + + assets::asset_registry registry; + ASSERT_TRUE(assets::load_registry_from_db(*this->m_db, TESTNET, registry, &error)) << error; + ASSERT_EQ(2u, registry.size()); + ASSERT_FALSE(assets::load_registry_from_db(*this->m_db, MAINNET, registry, &error)); + + ASSERT_NO_THROW(this->m_db->remove_asset_records_from_height(21)); + ASSERT_TRUE(assets::load_registry_from_db(*this->m_db, TESTNET, registry, &error)) << error; + ASSERT_EQ(1u, registry.size()); + + auto invalid = second; + invalid.carrier_prefix_hash.data[1] ^= 1; + this->m_db->block_wtxn_start(); + ASSERT_FALSE(assets::apply_block_extensions_to_db(*this->m_db, {invalid}, {second_carrier}, TESTNET, 22, ids, &error)); + this->m_db->block_wtxn_abort(); + ASSERT_TRUE(assets::load_registry_from_db(*this->m_db, TESTNET, registry, &error)); + ASSERT_EQ(1u, registry.size()); +} + +TYPED_TEST(BlockchainDBTest, PopBlockRemovesAssetStateAtDetachedHeight) +{ + 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(); + this->init_hard_fork(); + + crypto::hash id{}; + id.data[0] = 0xa5; + const blobdata payload("detached asset"); + block popped; + std::vector transactions; + { + db_wtxn_guard guard(this->m_db); + 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->pop_block(popped, transactions)); + uint64_t height = 0; + blobdata restored; + ASSERT_FALSE(this->m_db->get_asset_record(id, height, restored)); + ASSERT_EQ(1u, this->m_db->height()); +} + TYPED_TEST(BlockchainDBTest, AddBlock) { -- cgit v1.2.3