aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMonzero Build System <builds@monzero.org>2026-08-15 22:07:17 +0100
committerMonzero Build System <builds@monzero.org>2026-08-15 22:07:17 +0100
commitf4b7631ee2c13c936ca44b658423612ecba16f3c (patch)
treefdb95ff45e029bf401fdb701f0f57b5a1bbba298
parent24d3d77a21fe6be7db9bde7232af48e8136ec001 (diff)
downloadmonzero-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.md9
-rw-r--r--docs/MONZERO_PHASE0_STABILIZATION.md1
-rw-r--r--src/cryptonote_basic/asset_types.cpp30
-rw-r--r--src/cryptonote_basic/asset_types.h6
-rw-r--r--tests/unit_tests/asset_types.cpp88
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{};