aboutsummaryrefslogtreecommitdiff
path: root/src/cryptonote_basic
diff options
context:
space:
mode:
Diffstat (limited to 'src/cryptonote_basic')
-rw-r--r--src/cryptonote_basic/CMakeLists.txt1
-rw-r--r--src/cryptonote_basic/asset_types.cpp314
-rw-r--r--src/cryptonote_basic/asset_types.h116
-rw-r--r--src/cryptonote_basic/cryptonote_format_utils.cpp16
4 files changed, 445 insertions, 2 deletions
diff --git a/src/cryptonote_basic/CMakeLists.txt b/src/cryptonote_basic/CMakeLists.txt
index 1414be1b2..91612168c 100644
--- a/src/cryptonote_basic/CMakeLists.txt
+++ b/src/cryptonote_basic/CMakeLists.txt
@@ -46,6 +46,7 @@ target_link_libraries(cryptonote_format_utils_basic
set(cryptonote_basic_sources
account.cpp
+ asset_types.cpp
connection_context.cpp
cryptonote_basic_impl.cpp
cryptonote_format_utils.cpp
diff --git a/src/cryptonote_basic/asset_types.cpp b/src/cryptonote_basic/asset_types.cpp
new file mode 100644
index 000000000..faac669d0
--- /dev/null
+++ b/src/cryptonote_basic/asset_types.cpp
@@ -0,0 +1,314 @@
+#include "asset_types.h"
+
+#include <algorithm>
+#include <cstring>
+#include <limits>
+#include <map>
+
+#include "crypto/hash.h"
+
+namespace cryptonote
+{
+namespace assets
+{
+ namespace
+ {
+ constexpr char DOMAIN[] = "MonzeroAssetIssuanceV2";
+ constexpr char ISSUANCE_AUTHORIZATION_DOMAIN[] = "MonzeroAssetIssuanceAuthorizationV1";
+ constexpr char COLLECTION_MEMBERSHIP_DOMAIN[] = "MonzeroCollectionMembershipV1";
+
+ bool fail(std::string* error, const char* message)
+ {
+ if (error)
+ *error = message;
+ return false;
+ }
+
+ template<typename T>
+ void append_pod(std::vector<uint8_t>& target, const T& value)
+ {
+ const auto* begin = reinterpret_cast<const uint8_t*>(&value);
+ target.insert(target.end(), begin, begin + sizeof(value));
+ }
+
+ void append_u16_le(std::vector<uint8_t>& target, uint16_t value)
+ {
+ target.push_back(static_cast<uint8_t>(value));
+ target.push_back(static_cast<uint8_t>(value >> 8));
+ }
+
+ void append_u64_le(std::vector<uint8_t>& target, uint64_t value)
+ {
+ for (unsigned shift = 0; shift < 64; shift += 8)
+ target.push_back(static_cast<uint8_t>(value >> shift));
+ }
+ }
+
+ bool validate_issuance_descriptor(const issuance_descriptor& descriptor, std::string* error)
+ {
+ if (descriptor.version != ISSUANCE_DESCRIPTOR_VERSION)
+ return fail(error, "unsupported issuance descriptor version");
+ if (descriptor.network != MAINNET && descriptor.network != TESTNET && descriptor.network != STAGENET)
+ return fail(error, "issuance descriptor requires an explicit public network");
+ if (descriptor.type != asset_class::fungible
+ && descriptor.type != asset_class::non_fungible
+ && descriptor.type != asset_class::collection
+ && descriptor.type != asset_class::edition)
+ return fail(error, "unsupported asset class");
+ if (!crypto::check_key(descriptor.issuer_key))
+ return fail(error, "invalid issuer public key");
+ if (descriptor.atomic_supply == 0)
+ return fail(error, "asset supply must be greater than zero");
+ if (descriptor.display_decimals > MAX_DISPLAY_DECIMALS)
+ return fail(error, "asset display precision exceeds XMZ precision");
+ if ((descriptor.type == asset_class::non_fungible || descriptor.type == asset_class::collection)
+ && (descriptor.atomic_supply != 1 || descriptor.display_decimals != 0))
+ return fail(error, "NFT and collection supply must be exactly one indivisible unit");
+ if (descriptor.type == asset_class::edition && descriptor.display_decimals != 0)
+ return fail(error, "edition supply must be indivisible");
+ if ((descriptor.type == asset_class::fungible || descriptor.type == asset_class::collection)
+ && descriptor.collection_id != crypto::null_hash)
+ return fail(error, "this asset class cannot reference a collection");
+ if ((descriptor.type == asset_class::non_fungible || descriptor.type == asset_class::edition)
+ && descriptor.metadata_hash == crypto::null_hash)
+ return fail(error, "NFT and edition metadata must have a content hash");
+ if (descriptor.metadata_reference.size() > MAX_METADATA_REFERENCE_BYTES)
+ return fail(error, "metadata reference is too long");
+ if (std::find(descriptor.metadata_reference.begin(), descriptor.metadata_reference.end(), '\0') != descriptor.metadata_reference.end())
+ return fail(error, "metadata reference contains a NUL byte");
+ return true;
+ }
+
+ bool encode_issuance_descriptor(const issuance_descriptor& descriptor, std::vector<uint8_t>& encoded, std::string* error)
+ {
+ if (!validate_issuance_descriptor(descriptor, error))
+ return false;
+
+ encoded.clear();
+ encoded.reserve(sizeof(DOMAIN) - 1 + 1 + 16 + 1 + 32 + 32 + 8 + 1 + 32 + 32 + 2 + descriptor.metadata_reference.size());
+ encoded.insert(encoded.end(), DOMAIN, DOMAIN + sizeof(DOMAIN) - 1);
+ encoded.push_back(descriptor.version);
+ const config_t& network = get_config(descriptor.network);
+ append_pod(encoded, network.NETWORK_ID);
+ encoded.push_back(static_cast<uint8_t>(descriptor.type));
+ append_pod(encoded, descriptor.issuer_key);
+ append_pod(encoded, descriptor.issuance_nonce);
+ append_u64_le(encoded, descriptor.atomic_supply);
+ encoded.push_back(descriptor.display_decimals);
+ append_pod(encoded, descriptor.metadata_hash);
+ append_pod(encoded, descriptor.collection_id);
+ append_u16_le(encoded, static_cast<uint16_t>(descriptor.metadata_reference.size()));
+ encoded.insert(encoded.end(), descriptor.metadata_reference.begin(), descriptor.metadata_reference.end());
+ return true;
+ }
+
+ bool derive_asset_id(const issuance_descriptor& descriptor, crypto::hash& asset_id, std::string* error)
+ {
+ std::vector<uint8_t> encoded;
+ if (!encode_issuance_descriptor(descriptor, encoded, error))
+ return false;
+ asset_id = crypto::cn_fast_hash(encoded.data(), encoded.size());
+ return true;
+ }
+
+ bool derive_issuance_authorization_hash(const issuance_descriptor& descriptor, crypto::hash& message, std::string* error)
+ {
+ std::vector<uint8_t> encoded;
+ if (!encode_issuance_descriptor(descriptor, encoded, error))
+ return false;
+ std::vector<uint8_t> authorization;
+ authorization.reserve(sizeof(ISSUANCE_AUTHORIZATION_DOMAIN) - 1 + encoded.size());
+ authorization.insert(authorization.end(), ISSUANCE_AUTHORIZATION_DOMAIN,
+ ISSUANCE_AUTHORIZATION_DOMAIN + sizeof(ISSUANCE_AUTHORIZATION_DOMAIN) - 1);
+ authorization.insert(authorization.end(), encoded.begin(), encoded.end());
+ message = crypto::cn_fast_hash(authorization.data(), authorization.size());
+ return true;
+ }
+
+ bool verify_issuance_authorization(const issuance_descriptor& descriptor, const crypto::signature& signature, std::string* error)
+ {
+ crypto::hash message{};
+ if (!derive_issuance_authorization_hash(descriptor, message, error))
+ return false;
+ if (!crypto::check_signature(message, descriptor.issuer_key, signature))
+ return fail(error, "invalid asset issuer authorization signature");
+ return true;
+ }
+
+ bool derive_collection_membership_hash(const crypto::hash& collection_id, const crypto::hash& member_asset_id, crypto::hash& message, std::string* error)
+ {
+ if (collection_id == crypto::null_hash || member_asset_id == crypto::null_hash)
+ return fail(error, "collection and member asset IDs must be non-zero");
+ std::vector<uint8_t> encoded;
+ encoded.reserve(sizeof(COLLECTION_MEMBERSHIP_DOMAIN) - 1 + sizeof(collection_id) + sizeof(member_asset_id));
+ encoded.insert(encoded.end(), COLLECTION_MEMBERSHIP_DOMAIN,
+ COLLECTION_MEMBERSHIP_DOMAIN + sizeof(COLLECTION_MEMBERSHIP_DOMAIN) - 1);
+ append_pod(encoded, collection_id);
+ append_pod(encoded, member_asset_id);
+ message = crypto::cn_fast_hash(encoded.data(), encoded.size());
+ return true;
+ }
+
+ bool verify_collection_membership(
+ const crypto::hash& collection_id,
+ const crypto::hash& member_asset_id,
+ const crypto::public_key& collection_controller,
+ const crypto::signature& signature,
+ std::string* error)
+ {
+ if (!crypto::check_key(collection_controller))
+ return fail(error, "invalid collection controller public key");
+ crypto::hash message{};
+ if (!derive_collection_membership_hash(collection_id, member_asset_id, message, error))
+ return false;
+ if (!crypto::check_signature(message, collection_controller, signature))
+ return fail(error, "invalid collection membership signature");
+ return true;
+ }
+
+ bool asset_registry::apply_issuance(
+ const issuance_descriptor& descriptor,
+ const crypto::signature& issuer_signature,
+ const boost::optional<crypto::signature>& collection_signature,
+ uint64_t height,
+ crypto::hash& asset_id,
+ std::string* error)
+ {
+ if (!derive_asset_id(descriptor, asset_id, error))
+ return false;
+ if (contains(asset_id))
+ return fail(error, "asset ID is already registered");
+ if (!verify_issuance_authorization(descriptor, issuer_signature, error))
+ return false;
+
+ const bool claims_collection = descriptor.collection_id != crypto::null_hash;
+ if (claims_collection)
+ {
+ const asset_record* collection = find(descriptor.collection_id);
+ if (!collection || collection->descriptor.type != asset_class::collection)
+ return fail(error, "referenced collection is unknown or not a collection");
+ if (collection->descriptor.network != descriptor.network)
+ return fail(error, "collection and member must use the same network");
+ if (collection->issuance_height > height)
+ return fail(error, "collection membership cannot precede collection issuance");
+ if (!collection_signature)
+ return fail(error, "collection membership requires controller authorization");
+ if (!verify_collection_membership(
+ descriptor.collection_id,
+ asset_id,
+ collection->descriptor.issuer_key,
+ *collection_signature,
+ error))
+ return false;
+ }
+ else if (collection_signature)
+ return fail(error, "unexpected collection signature for an uncollected asset");
+
+ records_.emplace(asset_id, asset_record{descriptor, height});
+ return true;
+ }
+
+ void asset_registry::detach(uint64_t height)
+ {
+ for (auto it = records_.begin(); it != records_.end();)
+ {
+ if (it->second.issuance_height >= height)
+ it = records_.erase(it);
+ else
+ ++it;
+ }
+ }
+
+ bool asset_registry::contains(const crypto::hash& asset_id) const
+ {
+ return records_.count(asset_id) != 0;
+ }
+
+ const asset_record* asset_registry::find(const crypto::hash& asset_id) const
+ {
+ const auto found = records_.find(asset_id);
+ return found == records_.end() ? nullptr : &found->second;
+ }
+
+ std::set<crypto::hash> asset_registry::known_assets() const
+ {
+ std::set<crypto::hash> result;
+ for (const auto& record : records_)
+ result.insert(record.first);
+ return result;
+ }
+
+ bool validate_transparent_balance_statement(
+ const transparent_balance_statement& statement,
+ const std::set<crypto::hash>& known_assets,
+ std::string* error)
+ {
+ if (statement.xmz_outputs > std::numeric_limits<uint64_t>::max() - statement.xmz_fee
+ || statement.xmz_inputs != statement.xmz_outputs + statement.xmz_fee)
+ return fail(error, "XMZ inputs must equal XMZ outputs plus the XMZ fee");
+
+ using balances = std::map<crypto::hash, uint64_t>;
+ balances inputs;
+ balances outputs;
+ balances burns;
+
+ const auto aggregate = [error](const std::vector<transparent_amount>& entries, balances& totals) {
+ for (const transparent_amount& entry : entries)
+ {
+ if (entry.asset_id == crypto::null_hash)
+ return fail(error, "the zero asset ID is reserved and invalid");
+ if (entry.amount == 0)
+ return fail(error, "zero-valued asset entries are not canonical");
+ uint64_t& total = totals[entry.asset_id];
+ if (entry.amount > std::numeric_limits<uint64_t>::max() - total)
+ return fail(error, "asset amount aggregation overflow");
+ total += entry.amount;
+ }
+ return true;
+ };
+
+ if (!aggregate(statement.asset_inputs, inputs)
+ || !aggregate(statement.asset_outputs, outputs)
+ || !aggregate(statement.asset_burns, burns))
+ return false;
+
+ boost::optional<crypto::hash> issued_asset;
+ if (statement.issuance)
+ {
+ crypto::hash asset_id{};
+ if (!derive_asset_id(*statement.issuance, asset_id, error))
+ return false;
+ if (known_assets.count(asset_id) != 0)
+ return fail(error, "asset ID is already issued");
+ if (inputs.count(asset_id) != 0 || burns.count(asset_id) != 0)
+ return fail(error, "newly issued supply cannot have inputs or burns");
+ const auto issued_outputs = outputs.find(asset_id);
+ if (issued_outputs == outputs.end() || issued_outputs->second != statement.issuance->atomic_supply)
+ return fail(error, "issuance outputs must equal the complete fixed supply");
+ issued_asset = asset_id;
+ }
+
+ std::set<crypto::hash> touched;
+ for (const auto& entry : inputs) touched.insert(entry.first);
+ for (const auto& entry : outputs) touched.insert(entry.first);
+ for (const auto& entry : burns) touched.insert(entry.first);
+ if (touched.empty())
+ return fail(error, "asset statement contains no issuance, transfer, or burn");
+
+ for (const crypto::hash& asset_id : touched)
+ {
+ if (issued_asset && asset_id == *issued_asset)
+ continue;
+ if (known_assets.count(asset_id) == 0)
+ return fail(error, "statement references an unknown asset ID");
+
+ const uint64_t input = inputs.count(asset_id) ? inputs[asset_id] : 0;
+ const uint64_t output = outputs.count(asset_id) ? outputs[asset_id] : 0;
+ const uint64_t burn = burns.count(asset_id) ? burns[asset_id] : 0;
+ if (output > std::numeric_limits<uint64_t>::max() - burn || input != output + burn)
+ return fail(error, "asset inputs must equal outputs plus explicit burns");
+ }
+ return true;
+ }
+}
+}
diff --git a/src/cryptonote_basic/asset_types.h b/src/cryptonote_basic/asset_types.h
new file mode 100644
index 000000000..e24f61576
--- /dev/null
+++ b/src/cryptonote_basic/asset_types.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <boost/optional.hpp>
+
+#include "crypto/crypto.h"
+#include "cryptonote_config.h"
+
+namespace cryptonote
+{
+namespace assets
+{
+ constexpr uint8_t ISSUANCE_DESCRIPTOR_VERSION = 2;
+ constexpr uint8_t MAX_DISPLAY_DECIMALS = CRYPTONOTE_DISPLAY_DECIMAL_POINT;
+ constexpr size_t MAX_METADATA_REFERENCE_BYTES = 256;
+
+ enum class asset_class : uint8_t
+ {
+ fungible = 1,
+ non_fungible = 2,
+ collection = 3,
+ edition = 4
+ };
+
+ // Research-only fixed-supply issuance identity. This type is deliberately
+ // not part of transaction serialization and is not accepted by consensus.
+ struct issuance_descriptor
+ {
+ uint8_t version = ISSUANCE_DESCRIPTOR_VERSION;
+ network_type network = UNDEFINED;
+ asset_class type = asset_class::fungible;
+ crypto::public_key issuer_key{};
+ crypto::hash issuance_nonce{};
+ uint64_t atomic_supply = 0;
+ uint8_t display_decimals = 0;
+ crypto::hash metadata_hash{};
+ // Required for NFTs/editions that claim collection membership. The zero
+ // hash means no collection; fungible and collection descriptors must use
+ // zero so the field cannot acquire ambiguous meaning.
+ crypto::hash collection_id{};
+ std::string metadata_reference;
+ };
+
+ 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 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);
+ bool derive_collection_membership_hash(const crypto::hash& collection_id, const crypto::hash& member_asset_id, crypto::hash& message, std::string* error = nullptr);
+ bool verify_collection_membership(
+ const crypto::hash& collection_id,
+ const crypto::hash& member_asset_id,
+ const crypto::public_key& collection_controller,
+ const crypto::signature& signature,
+ std::string* error = nullptr);
+
+ struct asset_record
+ {
+ issuance_descriptor descriptor;
+ uint64_t issuance_height = 0;
+ };
+
+ // Inactive in-memory reference model for authenticated issuance state and
+ // deterministic reorg rollback. Production consensus/database code must not
+ // use this class without a separately reviewed activation change.
+ class asset_registry
+ {
+ public:
+ bool apply_issuance(
+ const issuance_descriptor& descriptor,
+ const crypto::signature& issuer_signature,
+ const boost::optional<crypto::signature>& collection_signature,
+ uint64_t height,
+ crypto::hash& asset_id,
+ 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;
+ size_t size() const { return records_.size(); }
+ std::set<crypto::hash> known_assets() const;
+
+ private:
+ std::map<crypto::hash, asset_record> records_;
+ };
+
+ struct transparent_amount
+ {
+ crypto::hash asset_id{};
+ uint64_t amount = 0;
+ };
+
+ // Test-only semantic statement used to validate conservation rules before a
+ // confidential commitment/proof construction is selected. It is not a wire
+ // transaction and must never be serialized into the public protocol.
+ struct transparent_balance_statement
+ {
+ uint64_t xmz_inputs = 0;
+ uint64_t xmz_outputs = 0;
+ uint64_t xmz_fee = 0;
+ boost::optional<issuance_descriptor> issuance;
+ std::vector<transparent_amount> asset_inputs;
+ std::vector<transparent_amount> asset_outputs;
+ std::vector<transparent_amount> asset_burns;
+ };
+
+ bool validate_transparent_balance_statement(
+ const transparent_balance_statement& statement,
+ const std::set<crypto::hash>& known_assets,
+ std::string* error = nullptr);
+}
+}
diff --git a/src/cryptonote_basic/cryptonote_format_utils.cpp b/src/cryptonote_basic/cryptonote_format_utils.cpp
index 923ad64af..2d8f6e2af 100644
--- a/src/cryptonote_basic/cryptonote_format_utils.cpp
+++ b/src/cryptonote_basic/cryptonote_format_utils.cpp
@@ -1131,6 +1131,10 @@ namespace cryptonote
{
switch (decimal_point)
{
+ case 11:
+ case 8:
+ case 5:
+ case 2:
case 12:
case 9:
case 6:
@@ -1154,6 +1158,16 @@ namespace cryptonote
decimal_point = default_decimal_point;
switch (decimal_point)
{
+ case 11:
+ return "XMZ";
+ case 8:
+ return "mXMZ";
+ case 5:
+ return "uXMZ";
+ case 2:
+ return "nXMZ";
+ case 0:
+ return "atomic XMZ";
case 12:
return "monero";
case 9:
@@ -1162,8 +1176,6 @@ namespace cryptonote
return "micronero";
case 3:
return "nanonero";
- case 0:
- return "piconero";
default:
ASSERT_MES_AND_THROW("Invalid decimal point specification: " << decimal_point);
}