diff options
| author | Monzero Build System <builds@monzero.org> | 2026-08-15 22:07:17 +0100 |
|---|---|---|
| committer | Monzero Build System <builds@monzero.org> | 2026-08-15 22:07:17 +0100 |
| commit | f4b7631ee2c13c936ca44b658423612ecba16f3c (patch) | |
| tree | fdb95ff45e029bf401fdb701f0f57b5a1bbba298 | |
| parent | 24d3d77a21fe6be7db9bde7232af48e8136ec001 (diff) | |
| download | monzero-core-f4b7631ee2c13c936ca44b658423612ecba16f3c.tar.gz monzero-core-f4b7631ee2c13c936ca44b658423612ecba16f3c.tar.xz monzero-core-f4b7631ee2c13c936ca44b658423612ecba16f3c.zip | |
Make block asset issuance atomic and reorg-safe
| -rw-r--r-- | docs/MONZERO_ASSETS_V1_SPEC.md | 9 | ||||
| -rw-r--r-- | docs/MONZERO_PHASE0_STABILIZATION.md | 1 | ||||
| -rw-r--r-- | src/cryptonote_basic/asset_types.cpp | 30 | ||||
| -rw-r--r-- | src/cryptonote_basic/asset_types.h | 6 | ||||
| -rw-r--r-- | tests/unit_tests/asset_types.cpp | 88 |
5 files changed, 134 insertions, 0 deletions
diff --git a/docs/MONZERO_ASSETS_V1_SPEC.md b/docs/MONZERO_ASSETS_V1_SPEC.md index 826536ea2..95ae0bea7 100644 --- a/docs/MONZERO_ASSETS_V1_SPEC.md +++ b/docs/MONZERO_ASSETS_V1_SPEC.md @@ -167,6 +167,15 @@ trailing data, and invalid signatures fail without modifying existing state. This provides a recovery and migration model; it is not yet an LMDB table or a substitute for replaying authenticated transactions from the chain. +The registry's inactive block adapter applies issuance payloads in transaction +order against temporary state and commits only if every issuance succeeds. +This makes same-block collection membership order explicit and prevents a +partially valid block from leaving partial asset state. Detaching at the block +height removes all of that block's issuances. A domain-separated hash of the +canonical snapshot provides a deterministic integrity commitment for testing +and future database migration checks; it is not currently committed in block +headers. + This encoding is a stable prototype vector for review, not an activation decision. A cryptographic review may replace the hash construction or fields; doing so must deliberately update the golden vector before any testnet fork. diff --git a/docs/MONZERO_PHASE0_STABILIZATION.md b/docs/MONZERO_PHASE0_STABILIZATION.md index 267020435..d7cba0206 100644 --- a/docs/MONZERO_PHASE0_STABILIZATION.md +++ b/docs/MONZERO_PHASE0_STABILIZATION.md @@ -130,6 +130,7 @@ isolated disposable development network may be used after cryptographic review. - [x] Strict canonical issuance-descriptor decoder with byte-boundary truncation tests. - [x] Bounded authenticated issuance-payload envelope with strict signature shape rules. - [x] Deterministic authenticated registry snapshots with atomic restore failure behavior. +- [x] Atomic ordered block-issuance adapter, detach behavior, and deterministic state commitments. - [x] External cryptographic-review brief and mandatory threat cases. - [ ] Reviewed confidential per-asset commitment and range-proof construction. - [ ] Versioned asset transaction serialization with strict parser limits. diff --git a/src/cryptonote_basic/asset_types.cpp b/src/cryptonote_basic/asset_types.cpp index a4e9d79b3..7ac078415 100644 --- a/src/cryptonote_basic/asset_types.cpp +++ b/src/cryptonote_basic/asset_types.cpp @@ -422,6 +422,27 @@ namespace assets error); } + bool asset_registry::apply_block_issuances( + const std::vector<issuance_payload>& payloads, + uint64_t height, + std::vector<crypto::hash>& asset_ids, + std::string* error) + { + asset_registry candidate = *this; + std::vector<crypto::hash> candidate_ids; + candidate_ids.reserve(payloads.size()); + for (const issuance_payload& payload : payloads) + { + crypto::hash asset_id{}; + if (!candidate.apply_issuance(payload, height, asset_id, error)) + return false; + candidate_ids.push_back(asset_id); + } + records_ = std::move(candidate.records_); + asset_ids = std::move(candidate_ids); + return true; + } + void asset_registry::detach(uint64_t height) { for (auto it = records_.begin(); it != records_.end();) @@ -569,6 +590,15 @@ namespace assets return true; } + bool asset_registry::derive_snapshot_hash(network_type network, crypto::hash& snapshot_hash, std::string* error) const + { + std::vector<uint8_t> snapshot; + if (!encode_snapshot(network, snapshot, error)) + return false; + snapshot_hash = crypto::cn_fast_hash(snapshot.data(), snapshot.size()); + return true; + } + bool validate_transparent_balance_statement( const transparent_balance_statement& statement, const std::set<crypto::hash>& known_assets, diff --git a/src/cryptonote_basic/asset_types.h b/src/cryptonote_basic/asset_types.h index 70d1d39bd..2b0e01890 100644 --- a/src/cryptonote_basic/asset_types.h +++ b/src/cryptonote_basic/asset_types.h @@ -97,6 +97,11 @@ namespace assets crypto::hash& asset_id, std::string* error = nullptr); bool apply_issuance(const issuance_payload& payload, uint64_t height, crypto::hash& asset_id, std::string* error = nullptr); + bool apply_block_issuances( + const std::vector<issuance_payload>& payloads, + uint64_t height, + std::vector<crypto::hash>& asset_ids, + std::string* error = nullptr); void detach(uint64_t height); bool contains(const crypto::hash& asset_id) const; const asset_record* find(const crypto::hash& asset_id) const; @@ -104,6 +109,7 @@ namespace assets std::set<crypto::hash> known_assets() const; bool encode_snapshot(network_type network, std::vector<uint8_t>& encoded, std::string* error = nullptr) const; bool decode_snapshot(const std::vector<uint8_t>& encoded, network_type expected_network, std::string* error = nullptr); + bool derive_snapshot_hash(network_type network, crypto::hash& snapshot_hash, std::string* error = nullptr) const; private: std::map<crypto::hash, asset_record> records_; diff --git a/tests/unit_tests/asset_types.cpp b/tests/unit_tests/asset_types.cpp index 494959fcd..270f8100e 100644 --- a/tests/unit_tests/asset_types.cpp +++ b/tests/unit_tests/asset_types.cpp @@ -472,6 +472,94 @@ TEST(asset_types, registry_snapshot_rejects_corruption_and_preserves_existing_st EXPECT_TRUE(registry.contains(asset_id)); } +TEST(asset_types, block_issuance_application_is_atomic_and_ordered) +{ + crypto::public_key collection_public{}; + crypto::secret_key collection_secret{}; + crypto::generate_keys(collection_public, collection_secret); + crypto::public_key member_public{}; + crypto::secret_key member_secret{}; + crypto::generate_keys(member_public, member_secret); + + cryptonote::assets::issuance_payload collection_payload; + collection_payload.descriptor = make_descriptor(cryptonote::TESTNET); + collection_payload.descriptor.type = cryptonote::assets::asset_class::collection; + collection_payload.descriptor.atomic_supply = 1; + collection_payload.descriptor.display_decimals = 0; + collection_payload.descriptor.issuer_key = collection_public; + collection_payload.issuer_signature = authorize( + collection_payload.descriptor, collection_public, collection_secret); + crypto::hash collection_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(collection_payload.descriptor, collection_id)); + + cryptonote::assets::issuance_payload member_payload; + member_payload.descriptor = make_descriptor(cryptonote::TESTNET); + member_payload.descriptor.type = cryptonote::assets::asset_class::non_fungible; + member_payload.descriptor.atomic_supply = 1; + member_payload.descriptor.display_decimals = 0; + member_payload.descriptor.issuer_key = member_public; + member_payload.descriptor.collection_id = collection_id; + member_payload.issuer_signature = authorize( + member_payload.descriptor, member_public, member_secret); + crypto::hash member_id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(member_payload.descriptor, member_id)); + crypto::hash membership_message{}; + ASSERT_TRUE(cryptonote::assets::derive_collection_membership_hash( + collection_id, member_id, membership_message)); + crypto::signature membership_signature{}; + crypto::generate_signature( + membership_message, collection_public, collection_secret, membership_signature); + member_payload.collection_signature = membership_signature; + + cryptonote::assets::asset_registry registry; + std::vector<crypto::hash> ids; + ASSERT_TRUE(registry.apply_block_issuances( + {collection_payload, member_payload}, 20, ids)); + ASSERT_EQ(2u, ids.size()); + EXPECT_EQ(collection_id, ids[0]); + EXPECT_EQ(member_id, ids[1]); + EXPECT_EQ(2u, registry.size()); + + registry.detach(20); + EXPECT_EQ(0u, registry.size()); + EXPECT_FALSE(registry.apply_block_issuances( + {member_payload, collection_payload}, 21, ids)); + EXPECT_EQ(0u, registry.size()); + + auto invalid_member = member_payload; + invalid_member.issuer_signature.c.data[0] ^= 1; + EXPECT_FALSE(registry.apply_block_issuances( + {collection_payload, invalid_member}, 22, ids)); + EXPECT_EQ(0u, registry.size()); +} + +TEST(asset_types, registry_snapshot_commitment_is_deterministic_and_state_sensitive) +{ + crypto::public_key issuer_public{}; + crypto::secret_key issuer_secret{}; + crypto::generate_keys(issuer_public, issuer_secret); + auto descriptor = make_descriptor(cryptonote::STAGENET); + descriptor.issuer_key = issuer_public; + + cryptonote::assets::asset_registry first; + cryptonote::assets::asset_registry second; + crypto::hash asset_id{}; + const crypto::signature signature = authorize(descriptor, issuer_public, issuer_secret); + ASSERT_TRUE(first.apply_issuance(descriptor, signature, boost::none, 5, asset_id)); + ASSERT_TRUE(second.apply_issuance(descriptor, signature, boost::none, 5, asset_id)); + + crypto::hash first_hash{}; + crypto::hash second_hash{}; + ASSERT_TRUE(first.derive_snapshot_hash(cryptonote::STAGENET, first_hash)); + ASSERT_TRUE(second.derive_snapshot_hash(cryptonote::STAGENET, second_hash)); + EXPECT_EQ(first_hash, second_hash); + + second.detach(5); + ASSERT_TRUE(second.derive_snapshot_hash(cryptonote::STAGENET, second_hash)); + EXPECT_NE(first_hash, second_hash); + EXPECT_FALSE(first.derive_snapshot_hash(cryptonote::TESTNET, second_hash)); +} + TEST(asset_types, registry_rejects_fake_or_missing_collection_authority) { crypto::public_key issuer_public{}; |
