aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMonzero Build System <builds@monzero.org>2026-08-15 21:55:20 +0100
committerMonzero Build System <builds@monzero.org>2026-08-15 21:55:20 +0100
commite933bd36cee44b42d631c1c64e9f3df027be7ab0 (patch)
tree0a35af5b088d2226a693cfee82dd12b5a813b31f
parentfb59a510e4c61e9b971c5197856144fda7b6ec7a (diff)
downloadmonzero-core-e933bd36cee44b42d631c1c64e9f3df027be7ab0.tar.gz
monzero-core-e933bd36cee44b42d631c1c64e9f3df027be7ab0.tar.xz
monzero-core-e933bd36cee44b42d631c1c64e9f3df027be7ab0.zip
Add strict canonical asset descriptor decoding
-rw-r--r--docs/MONZERO_ASSETS_V1_SPEC.md7
-rw-r--r--docs/MONZERO_PHASE0_STABILIZATION.md1
-rw-r--r--src/cryptonote_basic/asset_types.cpp89
-rw-r--r--src/cryptonote_basic/asset_types.h1
-rw-r--r--tests/unit_tests/asset_types.cpp62
5 files changed, 160 insertions, 0 deletions
diff --git a/docs/MONZERO_ASSETS_V1_SPEC.md b/docs/MONZERO_ASSETS_V1_SPEC.md
index 0da86ae83..d41dc9b7c 100644
--- a/docs/MONZERO_ASSETS_V1_SPEC.md
+++ b/docs/MONZERO_ASSETS_V1_SPEC.md
@@ -128,6 +128,13 @@ on-chain issuance, whether it can rotate, and how it is permanently closed.
Until that complete path exists, wallets must not display a collection as
verified.
+The prototype now includes a strict decoder for this canonical descriptor. It
+rejects truncated inputs at every byte boundary, unknown network UUIDs,
+unsupported versions and classes, oversized references, embedded NUL bytes,
+trailing bytes, mismatched lengths, and any encoding that does not reproduce
+the canonical byte sequence exactly. This parser is still not connected to
+transaction or block deserialization.
+
### 4.2 Inactive registry and reorganisation model
The prototype includes an in-memory reference registry, disconnected from the
diff --git a/docs/MONZERO_PHASE0_STABILIZATION.md b/docs/MONZERO_PHASE0_STABILIZATION.md
index 2a42c8ca6..3ebf09d99 100644
--- a/docs/MONZERO_PHASE0_STABILIZATION.md
+++ b/docs/MONZERO_PHASE0_STABILIZATION.md
@@ -127,6 +127,7 @@ isolated disposable development network may be used after cryptographic review.
- [x] Domain-separated issuer authorization signatures.
- [x] Domain-separated collection-membership authorization signatures.
- [x] Inactive authenticated issuance registry with deterministic reorg rollback.
+- [x] Strict canonical issuance-descriptor decoder with byte-boundary truncation tests.
- [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 faac669d0..927650b5a 100644
--- a/src/cryptonote_basic/asset_types.cpp
+++ b/src/cryptonote_basic/asset_types.cpp
@@ -5,6 +5,8 @@
#include <limits>
#include <map>
+#include <boost/uuid/uuid.hpp>
+
#include "crypto/hash.h"
namespace cryptonote
@@ -42,6 +44,36 @@ namespace assets
for (unsigned shift = 0; shift < 64; 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)
+ {
+ 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 read_u16_le(const std::vector<uint8_t>& source, size_t& offset, uint16_t& value)
+ {
+ if (offset > source.size() || 2 > source.size() - offset)
+ return false;
+ value = static_cast<uint16_t>(source[offset])
+ | static_cast<uint16_t>(source[offset + 1]) << 8;
+ offset += 2;
+ return true;
+ }
+
+ bool read_u64_le(const std::vector<uint8_t>& source, size_t& offset, uint64_t& value)
+ {
+ if (offset > source.size() || 8 > source.size() - offset)
+ return false;
+ value = 0;
+ for (unsigned shift = 0; shift < 64; shift += 8)
+ value |= static_cast<uint64_t>(source[offset++]) << shift;
+ return true;
+ }
}
bool validate_issuance_descriptor(const issuance_descriptor& descriptor, std::string* error)
@@ -102,6 +134,63 @@ namespace assets
return true;
}
+ bool decode_issuance_descriptor(const std::vector<uint8_t>& encoded, issuance_descriptor& descriptor, std::string* error)
+ {
+ constexpr size_t domain_size = sizeof(DOMAIN) - 1;
+ constexpr size_t fixed_size = domain_size + 1 + 16 + 1 + 32 + 32 + 8 + 1 + 32 + 32 + 2;
+ if (encoded.size() < fixed_size)
+ return fail(error, "truncated issuance descriptor");
+ if (!std::equal(DOMAIN, DOMAIN + domain_size, encoded.begin()))
+ return fail(error, "invalid issuance descriptor domain");
+
+ issuance_descriptor parsed;
+ size_t offset = domain_size;
+ parsed.version = encoded[offset++];
+
+ boost::uuids::uuid network_id{};
+ if (!read_pod(encoded, offset, network_id))
+ return fail(error, "truncated issuance descriptor network");
+ parsed.network = UNDEFINED;
+ for (const network_type candidate : {MAINNET, TESTNET, STAGENET})
+ {
+ if (network_id == get_config(candidate).NETWORK_ID)
+ {
+ parsed.network = candidate;
+ break;
+ }
+ }
+
+ parsed.type = static_cast<asset_class>(encoded[offset++]);
+ if (!read_pod(encoded, offset, parsed.issuer_key)
+ || !read_pod(encoded, offset, parsed.issuance_nonce)
+ || !read_u64_le(encoded, offset, parsed.atomic_supply))
+ return fail(error, "truncated issuance descriptor identity");
+ parsed.display_decimals = encoded[offset++];
+ if (!read_pod(encoded, offset, parsed.metadata_hash)
+ || !read_pod(encoded, offset, parsed.collection_id))
+ return fail(error, "truncated issuance descriptor metadata");
+
+ uint16_t reference_size = 0;
+ if (!read_u16_le(encoded, offset, reference_size))
+ return fail(error, "truncated issuance descriptor metadata length");
+ if (reference_size > MAX_METADATA_REFERENCE_BYTES)
+ return fail(error, "metadata reference is too long");
+ if (offset > encoded.size() || reference_size != encoded.size() - offset)
+ return fail(error, "issuance descriptor length is not canonical");
+ parsed.metadata_reference.assign(
+ reinterpret_cast<const char*>(encoded.data() + offset), reference_size);
+
+ if (!validate_issuance_descriptor(parsed, error))
+ return false;
+
+ std::vector<uint8_t> canonical;
+ if (!encode_issuance_descriptor(parsed, canonical, error) || canonical != encoded)
+ return fail(error, "issuance descriptor encoding is not canonical");
+
+ descriptor = std::move(parsed);
+ return true;
+ }
+
bool derive_asset_id(const issuance_descriptor& descriptor, crypto::hash& asset_id, std::string* error)
{
std::vector<uint8_t> encoded;
diff --git a/src/cryptonote_basic/asset_types.h b/src/cryptonote_basic/asset_types.h
index e24f61576..7f2fe0c99 100644
--- a/src/cryptonote_basic/asset_types.h
+++ b/src/cryptonote_basic/asset_types.h
@@ -48,6 +48,7 @@ namespace assets
bool validate_issuance_descriptor(const issuance_descriptor& descriptor, std::string* error = nullptr);
bool encode_issuance_descriptor(const issuance_descriptor& descriptor, std::vector<uint8_t>& encoded, std::string* error = nullptr);
+ bool decode_issuance_descriptor(const std::vector<uint8_t>& encoded, issuance_descriptor& descriptor, std::string* error = nullptr);
bool derive_asset_id(const issuance_descriptor& descriptor, crypto::hash& asset_id, std::string* error = nullptr);
bool derive_issuance_authorization_hash(const issuance_descriptor& descriptor, crypto::hash& message, std::string* error = nullptr);
bool verify_issuance_authorization(const issuance_descriptor& descriptor, const crypto::signature& signature, std::string* error = nullptr);
diff --git a/tests/unit_tests/asset_types.cpp b/tests/unit_tests/asset_types.cpp
index b2f250fc5..79f26cd29 100644
--- a/tests/unit_tests/asset_types.cpp
+++ b/tests/unit_tests/asset_types.cpp
@@ -55,6 +55,68 @@ TEST(asset_types, deterministic_canonical_identity)
ASSERT_GT(encoded.size(), descriptor.metadata_reference.size());
}
+TEST(asset_types, canonical_descriptor_round_trip)
+{
+ for (const auto network : {cryptonote::MAINNET, cryptonote::TESTNET, cryptonote::STAGENET})
+ {
+ const auto original = make_descriptor(network);
+ std::vector<uint8_t> encoded;
+ ASSERT_TRUE(cryptonote::assets::encode_issuance_descriptor(original, encoded));
+
+ cryptonote::assets::issuance_descriptor decoded;
+ ASSERT_TRUE(cryptonote::assets::decode_issuance_descriptor(encoded, decoded));
+ EXPECT_EQ(original.version, decoded.version);
+ EXPECT_EQ(original.network, decoded.network);
+ EXPECT_EQ(original.type, decoded.type);
+ EXPECT_EQ(original.issuer_key, decoded.issuer_key);
+ EXPECT_EQ(original.issuance_nonce, decoded.issuance_nonce);
+ EXPECT_EQ(original.atomic_supply, decoded.atomic_supply);
+ EXPECT_EQ(original.display_decimals, decoded.display_decimals);
+ EXPECT_EQ(original.metadata_hash, decoded.metadata_hash);
+ EXPECT_EQ(original.collection_id, decoded.collection_id);
+ EXPECT_EQ(original.metadata_reference, decoded.metadata_reference);
+
+ std::vector<uint8_t> reencoded;
+ ASSERT_TRUE(cryptonote::assets::encode_issuance_descriptor(decoded, reencoded));
+ EXPECT_EQ(encoded, reencoded);
+ }
+}
+
+TEST(asset_types, descriptor_decoder_rejects_truncation_and_noncanonical_lengths)
+{
+ std::vector<uint8_t> encoded;
+ ASSERT_TRUE(cryptonote::assets::encode_issuance_descriptor(
+ make_descriptor(cryptonote::TESTNET), encoded));
+
+ cryptonote::assets::issuance_descriptor decoded;
+ std::string error;
+ for (size_t size = 0; size < encoded.size(); ++size)
+ {
+ const std::vector<uint8_t> truncated(encoded.begin(), encoded.begin() + size);
+ EXPECT_FALSE(cryptonote::assets::decode_issuance_descriptor(truncated, decoded, &error))
+ << "accepted truncated size " << size;
+ }
+
+ auto trailing = encoded;
+ trailing.push_back(0);
+ EXPECT_FALSE(cryptonote::assets::decode_issuance_descriptor(trailing, decoded, &error));
+
+ auto bad_domain = encoded;
+ bad_domain[0] ^= 1;
+ EXPECT_FALSE(cryptonote::assets::decode_issuance_descriptor(bad_domain, decoded, &error));
+
+ auto bad_version = encoded;
+ bad_version[sizeof("MonzeroAssetIssuanceV2") - 1]++;
+ EXPECT_FALSE(cryptonote::assets::decode_issuance_descriptor(bad_version, decoded, &error));
+
+ // The final two bytes before the reference are its little-endian length.
+ const size_t length_offset = encoded.size() - make_descriptor(cryptonote::TESTNET).metadata_reference.size() - 2;
+ auto oversized = encoded;
+ oversized[length_offset] = 1;
+ oversized[length_offset + 1] = 1;
+ EXPECT_FALSE(cryptonote::assets::decode_issuance_descriptor(oversized, decoded, &error));
+}
+
TEST(asset_types, network_domain_separation)
{
auto mainnet = make_descriptor(cryptonote::MAINNET);