aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/MONZERO_ASSETS_V1_SPEC.md11
-rw-r--r--docs/MONZERO_PHASE0_STABILIZATION.md1
-rw-r--r--src/cryptonote_basic/asset_types.cpp91
-rw-r--r--src/cryptonote_basic/asset_types.h22
-rw-r--r--tests/unit_tests/asset_types.cpp68
5 files changed, 193 insertions, 0 deletions
diff --git a/docs/MONZERO_ASSETS_V1_SPEC.md b/docs/MONZERO_ASSETS_V1_SPEC.md
index 95ae0bea7..66bb37606 100644
--- a/docs/MONZERO_ASSETS_V1_SPEC.md
+++ b/docs/MONZERO_ASSETS_V1_SPEC.md
@@ -144,6 +144,17 @@ signatures from being silently omitted or attached to unrelated issuance.
Collection-controller verification still requires the future persistent
registry lookup and therefore remains part of consensus integration work.
+The next inactive layer is a detached, versioned transaction extension. It is
+bound to one public network and a non-zero commitment to the carrier native
+transaction prefix, then contains exactly one bounded operation payload.
+Version 1 recognizes issuance only. Its canonical parser rejects unknown
+operations, network disagreement, a missing carrier commitment, malformed
+lengths, truncated prefixes, invalid nested signatures, and trailing data.
+Changing the carrier commitment changes the extension ID. The extension is not
+registered as a `tx_extra` variant and therefore does not change what active
+mainnet nodes accept; a reviewed hard-fork integration must define the exact
+non-circular carrier-prefix hashing procedure first.
+
### 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 d7cba0206..aa2d73c39 100644
--- a/docs/MONZERO_PHASE0_STABILIZATION.md
+++ b/docs/MONZERO_PHASE0_STABILIZATION.md
@@ -131,6 +131,7 @@ isolated disposable development network may be used after cryptographic review.
- [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] Detached versioned transaction extension bound to a native prefix commitment.
- [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 7ac078415..aa17b359f 100644
--- a/src/cryptonote_basic/asset_types.cpp
+++ b/src/cryptonote_basic/asset_types.cpp
@@ -20,6 +20,7 @@ namespace assets
constexpr char COLLECTION_MEMBERSHIP_DOMAIN[] = "MonzeroCollectionMembershipV1";
constexpr char ISSUANCE_PAYLOAD_DOMAIN[] = "MonzeroAssetIssuancePayloadV1";
constexpr char REGISTRY_SNAPSHOT_DOMAIN[] = "MonzeroAssetRegistrySnapshotV1";
+ constexpr char TRANSACTION_EXTENSION_DOMAIN[] = "MonzeroAssetTransactionExtensionV1";
constexpr uint8_t REGISTRY_SNAPSHOT_VERSION = 1;
constexpr uint32_t MAX_REGISTRY_SNAPSHOT_RECORDS = 100000;
@@ -365,6 +366,96 @@ namespace assets
return true;
}
+ bool encode_transaction_extension(const transaction_extension& extension, std::vector<uint8_t>& encoded, std::string* error)
+ {
+ if (extension.version != TRANSACTION_EXTENSION_VERSION)
+ return fail(error, "unsupported asset transaction extension version");
+ if (extension.network != MAINNET && extension.network != TESTNET && extension.network != STAGENET)
+ return fail(error, "asset transaction extension requires an explicit public network");
+ if (extension.operation != transaction_operation::issuance)
+ return fail(error, "unsupported asset transaction operation");
+ if (extension.carrier_prefix_hash == crypto::null_hash)
+ return fail(error, "asset transaction extension requires a carrier prefix commitment");
+ if (extension.issuance.descriptor.network != extension.network)
+ return fail(error, "asset transaction extension network does not match its issuance");
+
+ std::vector<uint8_t> operation;
+ if (!encode_issuance_payload(extension.issuance, operation, error))
+ return false;
+ if (operation.size() > std::numeric_limits<uint16_t>::max())
+ return fail(error, "asset transaction operation is too large");
+
+ encoded.clear();
+ encoded.reserve(sizeof(TRANSACTION_EXTENSION_DOMAIN) - 1 + 1 + 16 + 1
+ + sizeof(extension.carrier_prefix_hash) + 2 + operation.size());
+ encoded.insert(encoded.end(), TRANSACTION_EXTENSION_DOMAIN,
+ TRANSACTION_EXTENSION_DOMAIN + sizeof(TRANSACTION_EXTENSION_DOMAIN) - 1);
+ encoded.push_back(extension.version);
+ append_pod(encoded, get_config(extension.network).NETWORK_ID);
+ encoded.push_back(static_cast<uint8_t>(extension.operation));
+ append_pod(encoded, extension.carrier_prefix_hash);
+ append_u16_le(encoded, static_cast<uint16_t>(operation.size()));
+ encoded.insert(encoded.end(), operation.begin(), operation.end());
+ return true;
+ }
+
+ bool decode_transaction_extension(const std::vector<uint8_t>& encoded, transaction_extension& extension, std::string* error)
+ {
+ constexpr size_t domain_size = sizeof(TRANSACTION_EXTENSION_DOMAIN) - 1;
+ constexpr size_t header_size = domain_size + 1 + 16 + 1 + sizeof(crypto::hash) + 2;
+ if (encoded.size() < header_size)
+ return fail(error, "truncated asset transaction extension");
+ if (!std::equal(TRANSACTION_EXTENSION_DOMAIN,
+ TRANSACTION_EXTENSION_DOMAIN + domain_size, encoded.begin()))
+ return fail(error, "invalid asset transaction extension domain");
+
+ transaction_extension parsed;
+ size_t offset = domain_size;
+ parsed.version = encoded[offset++];
+ if (parsed.version != TRANSACTION_EXTENSION_VERSION)
+ return fail(error, "unsupported asset transaction extension version");
+ boost::uuids::uuid network_id{};
+ if (!read_pod(encoded, offset, network_id))
+ return fail(error, "truncated asset transaction extension 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.operation = static_cast<transaction_operation>(encoded[offset++]);
+ if (!read_pod(encoded, offset, parsed.carrier_prefix_hash))
+ return fail(error, "truncated asset transaction carrier commitment");
+ uint16_t operation_size = 0;
+ if (!read_u16_le(encoded, offset, operation_size)
+ || offset > encoded.size() || operation_size != encoded.size() - offset)
+ return fail(error, "asset transaction operation length is not canonical");
+ const std::vector<uint8_t> operation(
+ encoded.begin() + offset, encoded.begin() + offset + operation_size);
+ if (parsed.operation != transaction_operation::issuance)
+ return fail(error, "unsupported asset transaction operation");
+ if (!decode_issuance_payload(operation, parsed.issuance, error))
+ return false;
+
+ std::vector<uint8_t> canonical;
+ if (!encode_transaction_extension(parsed, canonical, error) || canonical != encoded)
+ return fail(error, "asset transaction extension encoding is not canonical");
+ extension = std::move(parsed);
+ return true;
+ }
+
+ bool derive_transaction_extension_id(const transaction_extension& extension, crypto::hash& extension_id, std::string* error)
+ {
+ std::vector<uint8_t> encoded;
+ if (!encode_transaction_extension(extension, encoded, error))
+ return false;
+ extension_id = crypto::cn_fast_hash(encoded.data(), encoded.size());
+ return true;
+ }
+
bool asset_registry::apply_issuance(
const issuance_descriptor& descriptor,
const crypto::signature& issuer_signature,
diff --git a/src/cryptonote_basic/asset_types.h b/src/cryptonote_basic/asset_types.h
index 2b0e01890..5b2bf6006 100644
--- a/src/cryptonote_basic/asset_types.h
+++ b/src/cryptonote_basic/asset_types.h
@@ -75,6 +75,28 @@ namespace assets
bool encode_issuance_payload(const issuance_payload& payload, std::vector<uint8_t>& encoded, std::string* error = nullptr);
bool decode_issuance_payload(const std::vector<uint8_t>& encoded, issuance_payload& payload, std::string* error = nullptr);
+ constexpr uint8_t TRANSACTION_EXTENSION_VERSION = 1;
+ enum class transaction_operation : uint8_t
+ {
+ issuance = 1
+ };
+
+ // Detached transaction-extension prototype. The carrier commitment binds
+ // the operation to a native transaction prefix without changing active
+ // transaction serialization or validity.
+ struct transaction_extension
+ {
+ uint8_t version = TRANSACTION_EXTENSION_VERSION;
+ network_type network = UNDEFINED;
+ transaction_operation operation = transaction_operation::issuance;
+ crypto::hash carrier_prefix_hash{};
+ issuance_payload issuance;
+ };
+
+ bool encode_transaction_extension(const transaction_extension& extension, std::vector<uint8_t>& encoded, std::string* error = nullptr);
+ bool decode_transaction_extension(const std::vector<uint8_t>& encoded, transaction_extension& extension, std::string* error = nullptr);
+ bool derive_transaction_extension_id(const transaction_extension& extension, crypto::hash& extension_id, std::string* error = nullptr);
+
struct asset_record
{
issuance_descriptor descriptor;
diff --git a/tests/unit_tests/asset_types.cpp b/tests/unit_tests/asset_types.cpp
index 270f8100e..5f196301d 100644
--- a/tests/unit_tests/asset_types.cpp
+++ b/tests/unit_tests/asset_types.cpp
@@ -184,6 +184,74 @@ TEST(asset_types, issuance_payload_rejects_tampering_and_signature_shape_errors)
EXPECT_FALSE(cryptonote::assets::encode_issuance_payload(payload, encoded, &error));
}
+TEST(asset_types, detached_transaction_extension_is_canonical_and_network_bound)
+{
+ crypto::public_key issuer_public{};
+ crypto::secret_key issuer_secret{};
+ crypto::generate_keys(issuer_public, issuer_secret);
+
+ cryptonote::assets::transaction_extension original;
+ original.network = cryptonote::TESTNET;
+ original.carrier_prefix_hash.data[0] = 0x7a;
+ original.issuance.descriptor = make_descriptor(cryptonote::TESTNET);
+ original.issuance.descriptor.issuer_key = issuer_public;
+ original.issuance.issuer_signature = authorize(
+ original.issuance.descriptor, issuer_public, issuer_secret);
+
+ std::vector<uint8_t> encoded;
+ ASSERT_TRUE(cryptonote::assets::encode_transaction_extension(original, encoded));
+ cryptonote::assets::transaction_extension decoded;
+ ASSERT_TRUE(cryptonote::assets::decode_transaction_extension(encoded, decoded));
+ EXPECT_EQ(original.network, decoded.network);
+ EXPECT_EQ(original.operation, decoded.operation);
+ EXPECT_EQ(original.carrier_prefix_hash, decoded.carrier_prefix_hash);
+
+ crypto::hash first{};
+ crypto::hash second{};
+ ASSERT_TRUE(cryptonote::assets::derive_transaction_extension_id(original, first));
+ ASSERT_TRUE(cryptonote::assets::derive_transaction_extension_id(decoded, second));
+ EXPECT_EQ(first, second);
+
+ decoded.carrier_prefix_hash.data[0] ^= 1;
+ ASSERT_TRUE(cryptonote::assets::derive_transaction_extension_id(decoded, second));
+ EXPECT_NE(first, second);
+}
+
+TEST(asset_types, detached_transaction_extension_rejects_malformed_or_unbound_data)
+{
+ crypto::public_key issuer_public{};
+ crypto::secret_key issuer_secret{};
+ crypto::generate_keys(issuer_public, issuer_secret);
+
+ cryptonote::assets::transaction_extension extension;
+ extension.network = cryptonote::STAGENET;
+ extension.carrier_prefix_hash.data[0] = 1;
+ extension.issuance.descriptor = make_descriptor(cryptonote::STAGENET);
+ extension.issuance.descriptor.issuer_key = issuer_public;
+ extension.issuance.issuer_signature = authorize(
+ extension.issuance.descriptor, issuer_public, issuer_secret);
+
+ std::vector<uint8_t> encoded;
+ ASSERT_TRUE(cryptonote::assets::encode_transaction_extension(extension, encoded));
+ cryptonote::assets::transaction_extension 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_transaction_extension(truncated, decoded, &error))
+ << "accepted truncated extension size " << size;
+ }
+
+ extension.carrier_prefix_hash = crypto::null_hash;
+ EXPECT_FALSE(cryptonote::assets::encode_transaction_extension(extension, encoded, &error));
+ extension.carrier_prefix_hash.data[0] = 1;
+ extension.network = cryptonote::TESTNET;
+ EXPECT_FALSE(cryptonote::assets::encode_transaction_extension(extension, encoded, &error));
+ extension.network = cryptonote::STAGENET;
+ extension.operation = static_cast<cryptonote::assets::transaction_operation>(2);
+ EXPECT_FALSE(cryptonote::assets::encode_transaction_extension(extension, encoded, &error));
+}
+
TEST(asset_types, network_domain_separation)
{
auto mainnet = make_descriptor(cryptonote::MAINNET);