aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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.cpp143
-rw-r--r--src/cryptonote_basic/asset_types.h4
-rw-r--r--tests/unit_tests/asset_types.cpp79
5 files changed, 235 insertions, 1 deletions
diff --git a/docs/MONZERO_ASSETS_V1_SPEC.md b/docs/MONZERO_ASSETS_V1_SPEC.md
index a49764fe2..826536ea2 100644
--- a/docs/MONZERO_ASSETS_V1_SPEC.md
+++ b/docs/MONZERO_ASSETS_V1_SPEC.md
@@ -158,6 +158,15 @@ issued at or above a detached height. Tests demonstrate removal and subsequent
valid reissuance after a simulated reorganisation. This is an executable state
model, not the final database schema or consensus integration.
+The inactive registry also has a deterministic, network-separated snapshot
+format. Records retain issuer and optional collection signatures, are ordered
+by height with collections before same-height members, and are fully
+revalidated into temporary state before replacing the active registry.
+Corruption, cross-network input, noncanonical order, duplicate issuance,
+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.
+
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 ec45be62b..267020435 100644
--- a/docs/MONZERO_PHASE0_STABILIZATION.md
+++ b/docs/MONZERO_PHASE0_STABILIZATION.md
@@ -129,6 +129,7 @@ isolated disposable development network may be used after cryptographic review.
- [x] Inactive authenticated issuance registry with deterministic reorg rollback.
- [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] 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 05a8614b4..a4e9d79b3 100644
--- a/src/cryptonote_basic/asset_types.cpp
+++ b/src/cryptonote_basic/asset_types.cpp
@@ -19,6 +19,9 @@ namespace assets
constexpr char ISSUANCE_AUTHORIZATION_DOMAIN[] = "MonzeroAssetIssuanceAuthorizationV1";
constexpr char COLLECTION_MEMBERSHIP_DOMAIN[] = "MonzeroCollectionMembershipV1";
constexpr char ISSUANCE_PAYLOAD_DOMAIN[] = "MonzeroAssetIssuancePayloadV1";
+ constexpr char REGISTRY_SNAPSHOT_DOMAIN[] = "MonzeroAssetRegistrySnapshotV1";
+ constexpr uint8_t REGISTRY_SNAPSHOT_VERSION = 1;
+ constexpr uint32_t MAX_REGISTRY_SNAPSHOT_RECORDS = 100000;
bool fail(std::string* error, const char* message)
{
@@ -46,6 +49,12 @@ namespace assets
target.push_back(static_cast<uint8_t>(value >> shift));
}
+ void append_u32_le(std::vector<uint8_t>& target, uint32_t value)
+ {
+ for (unsigned shift = 0; shift < 32; shift += 8)
+ target.push_back(static_cast<uint8_t>(value >> shift));
+ }
+
template<typename T>
bool read_pod(const std::vector<uint8_t>& source, size_t& offset, T& value)
{
@@ -75,6 +84,21 @@ namespace assets
value |= static_cast<uint64_t>(source[offset++]) << shift;
return true;
}
+
+ bool read_u32_le(const std::vector<uint8_t>& source, size_t& offset, uint32_t& value)
+ {
+ if (offset > source.size() || 4 > source.size() - offset)
+ return false;
+ value = 0;
+ for (unsigned shift = 0; shift < 32; shift += 8)
+ value |= static_cast<uint32_t>(source[offset++]) << shift;
+ return true;
+ }
+
+ unsigned snapshot_class_priority(asset_class type)
+ {
+ return type == asset_class::collection ? 0 : 1;
+ }
}
bool validate_issuance_descriptor(const issuance_descriptor& descriptor, std::string* error)
@@ -379,7 +403,7 @@ namespace assets
else if (collection_signature)
return fail(error, "unexpected collection signature for an uncollected asset");
- records_.emplace(asset_id, asset_record{descriptor, height});
+ records_.emplace(asset_id, asset_record{descriptor, height, issuer_signature, collection_signature});
return true;
}
@@ -428,6 +452,123 @@ namespace assets
return result;
}
+ bool asset_registry::encode_snapshot(network_type network, std::vector<uint8_t>& encoded, std::string* error) const
+ {
+ if (network != MAINNET && network != TESTNET && network != STAGENET)
+ return fail(error, "asset registry snapshot requires an explicit public network");
+ if (records_.size() > MAX_REGISTRY_SNAPSHOT_RECORDS)
+ return fail(error, "asset registry snapshot has too many records");
+
+ using ordered_record = std::pair<crypto::hash, const asset_record*>;
+ std::vector<ordered_record> ordered;
+ ordered.reserve(records_.size());
+ for (const auto& entry : records_)
+ {
+ if (entry.second.descriptor.network != network)
+ return fail(error, "asset registry contains a record from another network");
+ ordered.emplace_back(entry.first, &entry.second);
+ }
+ std::sort(ordered.begin(), ordered.end(), [](const ordered_record& left, const ordered_record& right) {
+ if (left.second->issuance_height != right.second->issuance_height)
+ return left.second->issuance_height < right.second->issuance_height;
+ const unsigned left_priority = snapshot_class_priority(left.second->descriptor.type);
+ const unsigned right_priority = snapshot_class_priority(right.second->descriptor.type);
+ if (left_priority != right_priority)
+ return left_priority < right_priority;
+ return left.first < right.first;
+ });
+
+ encoded.clear();
+ encoded.insert(encoded.end(), REGISTRY_SNAPSHOT_DOMAIN,
+ REGISTRY_SNAPSHOT_DOMAIN + sizeof(REGISTRY_SNAPSHOT_DOMAIN) - 1);
+ encoded.push_back(REGISTRY_SNAPSHOT_VERSION);
+ append_pod(encoded, get_config(network).NETWORK_ID);
+ append_u32_le(encoded, static_cast<uint32_t>(ordered.size()));
+ for (const ordered_record& entry : ordered)
+ {
+ issuance_payload payload;
+ payload.descriptor = entry.second->descriptor;
+ payload.issuer_signature = entry.second->issuer_signature;
+ payload.collection_signature = entry.second->collection_signature;
+ std::vector<uint8_t> payload_bytes;
+ if (!encode_issuance_payload(payload, payload_bytes, error))
+ return false;
+ if (payload_bytes.size() > std::numeric_limits<uint16_t>::max())
+ return fail(error, "issuance payload is too large for a registry snapshot");
+ append_u64_le(encoded, entry.second->issuance_height);
+ append_u16_le(encoded, static_cast<uint16_t>(payload_bytes.size()));
+ encoded.insert(encoded.end(), payload_bytes.begin(), payload_bytes.end());
+ }
+ return true;
+ }
+
+ bool asset_registry::decode_snapshot(const std::vector<uint8_t>& encoded, network_type expected_network, std::string* error)
+ {
+ constexpr size_t domain_size = sizeof(REGISTRY_SNAPSHOT_DOMAIN) - 1;
+ constexpr size_t header_size = domain_size + 1 + 16 + 4;
+ if (expected_network != MAINNET && expected_network != TESTNET && expected_network != STAGENET)
+ return fail(error, "asset registry snapshot requires an explicit expected network");
+ if (encoded.size() < header_size)
+ return fail(error, "truncated asset registry snapshot");
+ if (!std::equal(REGISTRY_SNAPSHOT_DOMAIN,
+ REGISTRY_SNAPSHOT_DOMAIN + domain_size, encoded.begin()))
+ return fail(error, "invalid asset registry snapshot domain");
+
+ size_t offset = domain_size;
+ if (encoded[offset++] != REGISTRY_SNAPSHOT_VERSION)
+ return fail(error, "unsupported asset registry snapshot version");
+ boost::uuids::uuid network_id{};
+ if (!read_pod(encoded, offset, network_id)
+ || network_id != get_config(expected_network).NETWORK_ID)
+ return fail(error, "asset registry snapshot network mismatch");
+ uint32_t count = 0;
+ if (!read_u32_le(encoded, offset, count) || count > MAX_REGISTRY_SNAPSHOT_RECORDS)
+ return fail(error, "invalid asset registry snapshot record count");
+
+ asset_registry restored;
+ bool have_previous = false;
+ uint64_t previous_height = 0;
+ unsigned previous_priority = 0;
+ crypto::hash previous_id{};
+ for (uint32_t index = 0; index < count; ++index)
+ {
+ uint64_t height = 0;
+ uint16_t payload_size = 0;
+ if (!read_u64_le(encoded, offset, height)
+ || !read_u16_le(encoded, offset, payload_size)
+ || offset > encoded.size() || payload_size > encoded.size() - offset)
+ return fail(error, "truncated asset registry snapshot record");
+ const std::vector<uint8_t> payload_bytes(
+ encoded.begin() + offset, encoded.begin() + offset + payload_size);
+ offset += payload_size;
+
+ issuance_payload payload;
+ if (!decode_issuance_payload(payload_bytes, payload, error))
+ return false;
+ if (payload.descriptor.network != expected_network)
+ return fail(error, "asset registry record network mismatch");
+ crypto::hash asset_id{};
+ if (!derive_asset_id(payload.descriptor, asset_id, error))
+ return false;
+ const unsigned priority = snapshot_class_priority(payload.descriptor.type);
+ if (have_previous && (height < previous_height
+ || (height == previous_height && (priority < previous_priority
+ || (priority == previous_priority && !(previous_id < asset_id))))))
+ return fail(error, "asset registry snapshot records are not in canonical order");
+ if (!restored.apply_issuance(payload, height, asset_id, error))
+ return false;
+ have_previous = true;
+ previous_height = height;
+ previous_priority = priority;
+ previous_id = asset_id;
+ }
+ if (offset != encoded.size())
+ return fail(error, "asset registry snapshot has trailing bytes");
+
+ records_ = std::move(restored.records_);
+ 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 7265ffdf2..70d1d39bd 100644
--- a/src/cryptonote_basic/asset_types.h
+++ b/src/cryptonote_basic/asset_types.h
@@ -79,6 +79,8 @@ namespace assets
{
issuance_descriptor descriptor;
uint64_t issuance_height = 0;
+ crypto::signature issuer_signature{};
+ boost::optional<crypto::signature> collection_signature;
};
// Inactive in-memory reference model for authenticated issuance state and
@@ -100,6 +102,8 @@ namespace assets
const asset_record* find(const crypto::hash& asset_id) const;
size_t size() const { return records_.size(); }
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);
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 700f29ede..494959fcd 100644
--- a/tests/unit_tests/asset_types.cpp
+++ b/tests/unit_tests/asset_types.cpp
@@ -393,6 +393,85 @@ TEST(asset_types, registry_authenticates_collection_membership_and_reorgs)
EXPECT_EQ(0u, registry.size());
}
+TEST(asset_types, registry_snapshot_round_trip_preserves_authenticated_state)
+{
+ 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::asset_registry original;
+ auto collection = make_descriptor(cryptonote::TESTNET);
+ collection.type = cryptonote::assets::asset_class::collection;
+ collection.atomic_supply = 1;
+ collection.display_decimals = 0;
+ collection.issuer_key = collection_public;
+ crypto::hash collection_id{};
+ ASSERT_TRUE(original.apply_issuance(
+ collection, authorize(collection, collection_public, collection_secret), boost::none, 7, collection_id));
+
+ auto member = make_descriptor(cryptonote::TESTNET);
+ member.type = cryptonote::assets::asset_class::non_fungible;
+ member.atomic_supply = 1;
+ member.display_decimals = 0;
+ member.issuer_key = member_public;
+ member.collection_id = collection_id;
+ crypto::hash member_id{};
+ ASSERT_TRUE(cryptonote::assets::derive_asset_id(member, 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);
+ ASSERT_TRUE(original.apply_issuance(
+ member, authorize(member, member_public, member_secret), membership_signature, 7, member_id));
+
+ std::vector<uint8_t> snapshot;
+ ASSERT_TRUE(original.encode_snapshot(cryptonote::TESTNET, snapshot));
+ cryptonote::assets::asset_registry restored;
+ ASSERT_TRUE(restored.decode_snapshot(snapshot, cryptonote::TESTNET));
+ EXPECT_EQ(original.known_assets(), restored.known_assets());
+ ASSERT_NE(nullptr, restored.find(collection_id));
+ ASSERT_NE(nullptr, restored.find(member_id));
+ EXPECT_EQ(7u, restored.find(member_id)->issuance_height);
+ EXPECT_TRUE(restored.find(member_id)->collection_signature);
+
+ std::vector<uint8_t> second;
+ ASSERT_TRUE(restored.encode_snapshot(cryptonote::TESTNET, second));
+ EXPECT_EQ(snapshot, second);
+}
+
+TEST(asset_types, registry_snapshot_rejects_corruption_and_preserves_existing_state)
+{
+ 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 registry;
+ crypto::hash asset_id{};
+ ASSERT_TRUE(registry.apply_issuance(
+ descriptor, authorize(descriptor, issuer_public, issuer_secret), boost::none, 3, asset_id));
+ std::vector<uint8_t> snapshot;
+ ASSERT_TRUE(registry.encode_snapshot(cryptonote::STAGENET, snapshot));
+
+ auto corrupted = snapshot;
+ corrupted.back() ^= 1;
+ EXPECT_FALSE(registry.decode_snapshot(corrupted, cryptonote::STAGENET));
+ EXPECT_TRUE(registry.contains(asset_id));
+ EXPECT_EQ(1u, registry.size());
+ EXPECT_FALSE(registry.decode_snapshot(snapshot, cryptonote::TESTNET));
+ EXPECT_TRUE(registry.contains(asset_id));
+
+ auto trailing = snapshot;
+ trailing.push_back(0);
+ EXPECT_FALSE(registry.decode_snapshot(trailing, cryptonote::STAGENET));
+ EXPECT_TRUE(registry.contains(asset_id));
+}
+
TEST(asset_types, registry_rejects_fake_or_missing_collection_authority)
{
crypto::public_key issuer_public{};