aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcenobite pinhead <pinhead@pinhead>2026-08-15 23:39:44 +0100
committercenobite pinhead <pinhead@pinhead>2026-08-15 23:39:44 +0100
commite962463c011188acdb4373f38aa892e995f27c9f (patch)
tree05f9de23f75c23d6070255891585a707f4213279 /src
parent01cd9b0c3b7719ecd6ce45a32192dd1152ebd9ad (diff)
downloadmonzero-core-main.tar.gz
monzero-core-main.tar.xz
monzero-core-main.zip
define canonical confidential asset payloadHEADmain
Diffstat (limited to 'src')
-rw-r--r--src/blockchain_db/asset_db.cpp67
-rw-r--r--src/blockchain_db/asset_db.h14
-rw-r--r--src/cryptonote_basic/CMakeLists.txt1
-rw-r--r--src/cryptonote_basic/asset_wire.cpp355
-rw-r--r--src/cryptonote_basic/asset_wire.h68
5 files changed, 505 insertions, 0 deletions
diff --git a/src/blockchain_db/asset_db.cpp b/src/blockchain_db/asset_db.cpp
index bb2389d24..800ea78fb 100644
--- a/src/blockchain_db/asset_db.cpp
+++ b/src/blockchain_db/asset_db.cpp
@@ -117,5 +117,72 @@ bool verify_asset_ownership_against_db(const BlockchainDB& db,
return verify_asset_ownership_proof(
proof, expected_network, carrier_prefix_hash, error);
}
+
+bool apply_asset_transaction_to_db(BlockchainDB& db,
+ const asset_transaction_payload& payload, network_type expected_network,
+ const crypto::hash& expected_carrier_prefix_hash, uint64_t height,
+ std::vector<crypto::hash>& output_ids, std::string* error)
+{
+ asset_registry registry;
+ if (!load_registry_from_db(db, expected_network, registry, error)
+ || !verify_asset_transaction_payload(payload, registry.known_assets(),
+ expected_network, expected_carrier_prefix_hash, error))
+ return false;
+ for (const asset_ownership_proof& proof : payload.ownership_proofs)
+ if (!verify_asset_ownership_against_db(db, proof, expected_network,
+ expected_carrier_prefix_hash, error))
+ return false;
+
+ crypto::hash issued_id{};
+ std::vector<uint8_t> encoded_issuance;
+ if (payload.issuance)
+ {
+ if (!registry.apply_issuance(*payload.issuance, height, issued_id, error)
+ || !encode_issuance_payload(*payload.issuance, encoded_issuance, error))
+ return false;
+ }
+
+ std::vector<crypto::hash> candidate_ids;
+ uint32_t global_output_index = 0;
+ for (size_t group = 0; group < payload.balances.size(); ++group)
+ {
+ const confidential_asset_balance& balance = payload.balances[group];
+ for (size_t index = 0; index < balance.outputs.size(); ++index)
+ {
+ confidential_asset_output output{
+ payload.output_destinations[group][index], balance.outputs[index]};
+ crypto::hash output_id{};
+ if (!derive_asset_output_id(expected_network, expected_carrier_prefix_hash,
+ balance.asset_id, global_output_index++, output, output_id, error))
+ return false;
+ asset_output_data_t existing{};
+ if (db.get_asset_output(output_id, existing))
+ return fail(error, "asset output identity already exists");
+ candidate_ids.push_back(output_id);
+ }
+ }
+
+ if (payload.issuance)
+ {
+ const blobdata_ref bytes{reinterpret_cast<const char*>(encoded_issuance.data()),
+ encoded_issuance.size()};
+ db.add_asset_record(issued_id, height, bytes);
+ }
+ for (const asset_ownership_proof& proof : payload.ownership_proofs)
+ db.add_asset_key_image(proof.key_image, height);
+ size_t candidate_index = 0;
+ for (size_t group = 0; group < payload.balances.size(); ++group)
+ {
+ const confidential_asset_balance& balance = payload.balances[group];
+ for (size_t index = 0; index < balance.outputs.size(); ++index)
+ {
+ const asset_output_data_t output{balance.asset_id,
+ payload.output_destinations[group][index], balance.outputs[index], height};
+ db.add_asset_output(candidate_ids[candidate_index++], output);
+ }
+ }
+ output_ids = std::move(candidate_ids);
+ return true;
+}
}
}
diff --git a/src/blockchain_db/asset_db.h b/src/blockchain_db/asset_db.h
index b5a763b75..a43d86585 100644
--- a/src/blockchain_db/asset_db.h
+++ b/src/blockchain_db/asset_db.h
@@ -6,6 +6,7 @@
#include "blockchain_db.h"
#include "cryptonote_basic/asset_confidential.h"
#include "cryptonote_basic/asset_types.h"
+#include "cryptonote_basic/asset_wire.h"
namespace cryptonote
{
@@ -38,5 +39,18 @@ namespace assets
network_type expected_network,
const crypto::hash& carrier_prefix_hash,
std::string* error = nullptr);
+
+ // The caller owns the outer blockchain write transaction. Every semantic,
+ // ownership, collision, and collection-authority check completes before the
+ // first write so an accepted payload changes registry, output, and spent-key
+ // state atomically.
+ bool apply_asset_transaction_to_db(
+ BlockchainDB& db,
+ const asset_transaction_payload& payload,
+ network_type expected_network,
+ const crypto::hash& expected_carrier_prefix_hash,
+ uint64_t height,
+ std::vector<crypto::hash>& output_ids,
+ std::string* error = nullptr);
}
}
diff --git a/src/cryptonote_basic/CMakeLists.txt b/src/cryptonote_basic/CMakeLists.txt
index 84e86803c..c8b41a03e 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
asset_confidential.cpp
+ asset_wire.cpp
account.cpp
asset_types.cpp
connection_context.cpp
diff --git a/src/cryptonote_basic/asset_wire.cpp b/src/cryptonote_basic/asset_wire.cpp
new file mode 100644
index 000000000..a0d77bf33
--- /dev/null
+++ b/src/cryptonote_basic/asset_wire.cpp
@@ -0,0 +1,355 @@
+#include "asset_wire.h"
+
+#include <cstring>
+#include <limits>
+
+#include "ringct/rctOps.h"
+
+namespace cryptonote
+{
+namespace assets
+{
+namespace
+{
+ constexpr char OUTPUT_ID_DOMAIN[] = "MonzeroAssetOutputIdV1";
+
+ bool fail(std::string* error, const std::string& message)
+ {
+ if (error)
+ *error = message;
+ return false;
+ }
+
+ class writer
+ {
+ public:
+ template<typename T> void pod(const T& value)
+ {
+ const auto* begin = reinterpret_cast<const uint8_t*>(&value);
+ bytes.insert(bytes.end(), begin, begin + sizeof(value));
+ }
+ void count(size_t value) { pod(static_cast<uint8_t>(value)); }
+ void u16(uint16_t value)
+ {
+ bytes.push_back(static_cast<uint8_t>(value));
+ bytes.push_back(static_cast<uint8_t>(value >> 8));
+ }
+ void u32(uint32_t value)
+ {
+ for (unsigned shift = 0; shift < 32; shift += 8)
+ bytes.push_back(static_cast<uint8_t>(value >> shift));
+ }
+ std::vector<uint8_t> bytes;
+ };
+
+ class reader
+ {
+ public:
+ explicit reader(const std::vector<uint8_t>& source) : source_(source) {}
+ template<typename T> bool pod(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 count(size_t limit, size_t& value)
+ {
+ uint8_t encoded = 0;
+ if (!pod(encoded) || encoded > limit)
+ return false;
+ value = encoded;
+ return true;
+ }
+ bool u16(uint16_t& value)
+ {
+ uint8_t low = 0, high = 0;
+ if (!pod(low) || !pod(high))
+ return false;
+ value = static_cast<uint16_t>(low) | (static_cast<uint16_t>(high) << 8);
+ return true;
+ }
+ bool done() const { return offset_ == source_.size(); }
+ private:
+ const std::vector<uint8_t>& source_;
+ size_t offset_ = 0;
+ };
+
+ void write_keys(writer& out, const rct::keyV& keys)
+ {
+ out.count(keys.size());
+ for (const rct::key& key : keys)
+ out.pod(key);
+ }
+
+ bool read_keys(reader& in, size_t limit, rct::keyV& keys)
+ {
+ size_t count = 0;
+ if (!in.count(limit, count))
+ return false;
+ keys.resize(count);
+ for (rct::key& key : keys)
+ if (!in.pod(key))
+ return false;
+ return true;
+ }
+
+ void write_range_proof(writer& out, const rct::BulletproofPlus& proof)
+ {
+ write_keys(out, proof.V);
+ out.pod(proof.A); out.pod(proof.A1); out.pod(proof.B);
+ out.pod(proof.r1); out.pod(proof.s1); out.pod(proof.d1);
+ write_keys(out, proof.L);
+ write_keys(out, proof.R);
+ }
+
+ bool read_range_proof(reader& in, rct::BulletproofPlus& proof)
+ {
+ return read_keys(in, MAX_CONFIDENTIAL_ASSET_OUTPUTS, proof.V)
+ && in.pod(proof.A) && in.pod(proof.A1) && in.pod(proof.B)
+ && in.pod(proof.r1) && in.pod(proof.s1) && in.pod(proof.d1)
+ && read_keys(in, 16, proof.L) && read_keys(in, 16, proof.R)
+ && !proof.L.empty() && proof.L.size() == proof.R.size();
+ }
+}
+
+bool validate_asset_transaction_payload_shape(const asset_transaction_payload& payload,
+ std::string* error)
+{
+ if (payload.version != ASSET_TRANSACTION_WIRE_VERSION)
+ return fail(error, "unsupported asset transaction wire version");
+ if (payload.network != MAINNET && payload.network != TESTNET && payload.network != STAGENET)
+ return fail(error, "asset transaction requires a public network");
+ if (payload.carrier_prefix_hash == crypto::null_hash)
+ return fail(error, "asset transaction has a zero carrier hash");
+ if (payload.balances.empty() || payload.balances.size() > MAX_ASSET_BALANCE_GROUPS)
+ return fail(error, "asset transaction has an invalid balance-group count");
+ if (payload.output_destinations.size() != payload.balances.size())
+ return fail(error, "asset transaction output destinations do not match balance groups");
+ if (payload.ownership_proofs.size() > MAX_ASSET_OWNERSHIP_PROOFS)
+ return fail(error, "asset transaction has too many ownership proofs");
+ size_t total_inputs = 0, total_destinations = 0;
+ for (size_t group = 0; group < payload.balances.size(); ++group)
+ {
+ const confidential_asset_balance& balance = payload.balances[group];
+ if (balance.pseudo_inputs.empty()
+ || balance.pseudo_inputs.size() > MAX_CONFIDENTIAL_ASSET_INPUTS
+ || balance.outputs.size() + balance.burns.size() > MAX_CONFIDENTIAL_ASSET_OUTPUTS
+ || balance.range_proofs.empty()
+ || balance.range_proofs.size() > MAX_ASSET_RANGE_PROOFS)
+ return fail(error, "asset balance group exceeds canonical limits");
+ if (payload.output_destinations[group].size() != balance.outputs.size())
+ return fail(error, "asset output destination count does not match commitments");
+ total_inputs += balance.pseudo_inputs.size();
+ total_destinations += balance.outputs.size() + balance.burns.size();
+ if (total_inputs > MAX_ASSET_TOTAL_INPUTS
+ || total_destinations > MAX_ASSET_TOTAL_DESTINATIONS)
+ return fail(error, "asset transaction exceeds aggregate input or destination limits");
+ }
+ for (const asset_ownership_proof& proof : payload.ownership_proofs)
+ if (proof.ring.size() != CONFIDENTIAL_ASSET_RING_SIZE
+ || proof.signature.s.size() != CONFIDENTIAL_ASSET_RING_SIZE)
+ return fail(error, "asset ownership proof has a noncanonical ring size");
+ return true;
+}
+
+bool encode_asset_transaction_payload(const asset_transaction_payload& payload,
+ std::vector<uint8_t>& encoded, std::string* error)
+{
+ if (!validate_asset_transaction_payload_shape(payload, error))
+ return false;
+ writer out;
+ out.pod(payload.version);
+ out.pod(static_cast<uint8_t>(payload.network));
+ out.pod(payload.carrier_prefix_hash);
+ out.pod(static_cast<uint8_t>(payload.issuance ? 1 : 0));
+ if (payload.issuance)
+ {
+ std::vector<uint8_t> issuance;
+ if (!encode_issuance_payload(*payload.issuance, issuance, error))
+ return false;
+ if (issuance.size() > std::numeric_limits<uint16_t>::max())
+ return fail(error, "asset issuance payload is too large");
+ out.u16(static_cast<uint16_t>(issuance.size()));
+ out.bytes.insert(out.bytes.end(), issuance.begin(), issuance.end());
+ }
+ out.count(payload.balances.size());
+ for (size_t group = 0; group < payload.balances.size(); ++group)
+ {
+ const confidential_asset_balance& balance = payload.balances[group];
+ out.pod(balance.asset_id);
+ out.count(balance.pseudo_inputs.size());
+ for (const confidential_pseudo_input& input : balance.pseudo_inputs)
+ {
+ out.pod(input.source_asset_id);
+ out.pod(input.commitment);
+ }
+ out.count(balance.outputs.size());
+ for (size_t index = 0; index < balance.outputs.size(); ++index)
+ {
+ out.pod(payload.output_destinations[group][index]);
+ out.pod(balance.outputs[index]);
+ }
+ write_keys(out, balance.burns);
+ out.count(balance.range_proofs.size());
+ for (const rct::BulletproofPlus& proof : balance.range_proofs)
+ write_range_proof(out, proof);
+ }
+ out.count(payload.ownership_proofs.size());
+ for (const asset_ownership_proof& proof : payload.ownership_proofs)
+ {
+ out.pod(proof.asset_id);
+ out.pod(proof.pseudo_input);
+ out.pod(proof.key_image);
+ for (const asset_ring_member& member : proof.ring)
+ {
+ out.pod(member.asset_id); out.pod(member.output_id);
+ out.pod(member.public_output.dest); out.pod(member.public_output.mask);
+ }
+ out.pod(proof.signature.c1);
+ out.pod(proof.signature.D);
+ for (const rct::key& response : proof.signature.s)
+ out.pod(response);
+ }
+ if (out.bytes.size() > MAX_ASSET_WIRE_BYTES)
+ return fail(error, "asset transaction payload exceeds maximum size");
+ encoded = std::move(out.bytes);
+ return true;
+}
+
+bool decode_asset_transaction_payload(const std::vector<uint8_t>& encoded,
+ asset_transaction_payload& payload, std::string* error)
+{
+ if (encoded.empty() || encoded.size() > MAX_ASSET_WIRE_BYTES)
+ return fail(error, "asset transaction payload has an invalid size");
+ reader in(encoded);
+ asset_transaction_payload candidate;
+ uint8_t network = 0, has_issuance = 0;
+ if (!in.pod(candidate.version) || !in.pod(network)
+ || !in.pod(candidate.carrier_prefix_hash) || !in.pod(has_issuance)
+ || has_issuance > 1)
+ return fail(error, "truncated asset transaction header");
+ candidate.network = static_cast<network_type>(network);
+ if (has_issuance)
+ {
+ uint16_t size = 0;
+ if (!in.u16(size))
+ return fail(error, "truncated asset issuance length");
+ std::vector<uint8_t> issuance(size);
+ for (uint8_t& byte : issuance)
+ if (!in.pod(byte))
+ return fail(error, "truncated asset issuance payload");
+ issuance_payload decoded;
+ if (!decode_issuance_payload(issuance, decoded, error))
+ return false;
+ candidate.issuance = decoded;
+ }
+ size_t groups = 0;
+ if (!in.count(MAX_ASSET_BALANCE_GROUPS, groups) || groups == 0)
+ return fail(error, "invalid asset balance-group count");
+ candidate.balances.resize(groups);
+ candidate.output_destinations.resize(groups);
+ for (size_t group = 0; group < groups; ++group)
+ {
+ confidential_asset_balance& balance = candidate.balances[group];
+ size_t inputs = 0, outputs = 0, proofs = 0;
+ if (!in.pod(balance.asset_id) || !in.count(MAX_CONFIDENTIAL_ASSET_INPUTS, inputs) || inputs == 0)
+ return fail(error, "truncated or invalid asset inputs");
+ balance.pseudo_inputs.resize(inputs);
+ for (confidential_pseudo_input& input : balance.pseudo_inputs)
+ if (!in.pod(input.source_asset_id) || !in.pod(input.commitment))
+ return fail(error, "truncated asset pseudo input");
+ if (!in.count(MAX_CONFIDENTIAL_ASSET_OUTPUTS, outputs))
+ return fail(error, "invalid asset output count");
+ balance.outputs.resize(outputs);
+ candidate.output_destinations[group].resize(outputs);
+ for (size_t index = 0; index < outputs; ++index)
+ if (!in.pod(candidate.output_destinations[group][index]) || !in.pod(balance.outputs[index]))
+ return fail(error, "truncated asset output");
+ if (!read_keys(in, MAX_CONFIDENTIAL_ASSET_OUTPUTS - outputs, balance.burns)
+ || !in.count(MAX_ASSET_RANGE_PROOFS, proofs) || proofs == 0)
+ return fail(error, "invalid asset burn or range-proof count");
+ balance.range_proofs.resize(proofs);
+ for (rct::BulletproofPlus& proof : balance.range_proofs)
+ if (!read_range_proof(in, proof))
+ return fail(error, "truncated or noncanonical asset range proof");
+ }
+ size_t ownership_count = 0;
+ if (!in.count(MAX_ASSET_OWNERSHIP_PROOFS, ownership_count))
+ return fail(error, "invalid asset ownership-proof count");
+ candidate.ownership_proofs.resize(ownership_count);
+ for (asset_ownership_proof& proof : candidate.ownership_proofs)
+ {
+ if (!in.pod(proof.asset_id) || !in.pod(proof.pseudo_input) || !in.pod(proof.key_image))
+ return fail(error, "truncated asset ownership proof");
+ proof.ring.resize(CONFIDENTIAL_ASSET_RING_SIZE);
+ for (asset_ring_member& member : proof.ring)
+ if (!in.pod(member.asset_id) || !in.pod(member.output_id)
+ || !in.pod(member.public_output.dest) || !in.pod(member.public_output.mask))
+ return fail(error, "truncated asset ownership ring");
+ if (!in.pod(proof.signature.c1) || !in.pod(proof.signature.D))
+ return fail(error, "truncated asset CLSAG header");
+ proof.signature.s.resize(CONFIDENTIAL_ASSET_RING_SIZE);
+ for (rct::key& response : proof.signature.s)
+ if (!in.pod(response))
+ return fail(error, "truncated asset CLSAG responses");
+ std::memcpy(&proof.signature.I, &proof.key_image, sizeof(proof.signature.I));
+ }
+ if (!in.done() || !validate_asset_transaction_payload_shape(candidate, error))
+ return fail(error, in.done() ? "invalid asset transaction payload" : "trailing asset transaction bytes");
+ payload = std::move(candidate);
+ return true;
+}
+
+bool verify_asset_transaction_payload(const asset_transaction_payload& payload,
+ const std::set<crypto::hash>& known_assets, network_type expected_network,
+ const crypto::hash& expected_carrier_prefix_hash, std::string* error)
+{
+ if (!validate_asset_transaction_payload_shape(payload, error))
+ return false;
+ if (payload.network != expected_network)
+ return fail(error, "asset transaction belongs to a different network");
+ if (payload.carrier_prefix_hash != expected_carrier_prefix_hash)
+ return fail(error, "asset transaction carrier hash mismatch");
+ boost::optional<issuance_descriptor> descriptor;
+ if (payload.issuance)
+ {
+ if (!verify_issuance_authorization(payload.issuance->descriptor,
+ payload.issuance->issuer_signature, error))
+ return false;
+ descriptor = payload.issuance->descriptor;
+ }
+ for (const std::vector<rct::key>& destinations : payload.output_destinations)
+ for (const rct::key& destination : destinations)
+ if (!rct::isInMainSubgroup(destination))
+ return fail(error, "asset transaction contains an invalid destination key");
+ return verify_confidential_asset_transaction_with_ownership(
+ payload.balances, payload.ownership_proofs, known_assets, descriptor,
+ expected_network, expected_carrier_prefix_hash, error);
+}
+
+bool derive_asset_output_id(network_type network,
+ const crypto::hash& carrier_prefix_hash,
+ const crypto::hash& asset_id, uint32_t output_index,
+ const confidential_asset_output& output, crypto::hash& output_id,
+ std::string* error)
+{
+ if (network != MAINNET && network != TESTNET && network != STAGENET)
+ return fail(error, "asset output identity requires a public network");
+ if (carrier_prefix_hash == crypto::null_hash || asset_id == crypto::null_hash)
+ return fail(error, "asset output identity has a zero carrier or asset id");
+ if (!rct::isInMainSubgroup(output.destination)
+ || !rct::isInMainSubgroup(output.commitment))
+ return fail(error, "asset output identity contains an invalid point");
+ writer bytes;
+ bytes.bytes.insert(bytes.bytes.end(), OUTPUT_ID_DOMAIN,
+ OUTPUT_ID_DOMAIN + sizeof(OUTPUT_ID_DOMAIN) - 1);
+ bytes.pod(get_config(network).NETWORK_ID);
+ bytes.pod(carrier_prefix_hash); bytes.pod(asset_id);
+ bytes.u32(output_index); bytes.pod(output.destination); bytes.pod(output.commitment);
+ output_id = crypto::cn_fast_hash(bytes.bytes.data(), bytes.bytes.size());
+ return true;
+}
+}
+}
diff --git a/src/cryptonote_basic/asset_wire.h b/src/cryptonote_basic/asset_wire.h
new file mode 100644
index 000000000..e5f63a01b
--- /dev/null
+++ b/src/cryptonote_basic/asset_wire.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <boost/optional.hpp>
+
+#include "asset_confidential.h"
+
+namespace cryptonote
+{
+namespace assets
+{
+ constexpr uint8_t ASSET_TRANSACTION_WIRE_VERSION = 1;
+ constexpr size_t MAX_ASSET_BALANCE_GROUPS = 8;
+ constexpr size_t MAX_ASSET_TOTAL_INPUTS = 64;
+ constexpr size_t MAX_ASSET_TOTAL_DESTINATIONS = 64;
+ constexpr size_t MAX_ASSET_OWNERSHIP_PROOFS = 64;
+ constexpr size_t MAX_ASSET_RANGE_PROOFS = 16;
+ constexpr size_t MAX_ASSET_WIRE_BYTES = 256 * 1024;
+
+ struct confidential_asset_output
+ {
+ rct::key destination{};
+ rct::key commitment{};
+ };
+
+ // Inactive canonical payload prototype. The carrier hash is computed from
+ // the native transaction prefix with this envelope omitted; activation code
+ // must enforce that procedure to avoid a self-referential hash.
+ struct asset_transaction_payload
+ {
+ uint8_t version = ASSET_TRANSACTION_WIRE_VERSION;
+ network_type network = UNDEFINED;
+ crypto::hash carrier_prefix_hash{};
+ boost::optional<issuance_payload> issuance;
+ std::vector<confidential_asset_balance> balances;
+ std::vector<std::vector<rct::key>> output_destinations;
+ std::vector<asset_ownership_proof> ownership_proofs;
+ };
+
+ bool validate_asset_transaction_payload_shape(
+ const asset_transaction_payload& payload,
+ std::string* error = nullptr);
+ bool encode_asset_transaction_payload(
+ const asset_transaction_payload& payload,
+ std::vector<uint8_t>& encoded,
+ std::string* error = nullptr);
+ bool decode_asset_transaction_payload(
+ const std::vector<uint8_t>& encoded,
+ asset_transaction_payload& payload,
+ std::string* error = nullptr);
+ bool verify_asset_transaction_payload(
+ const asset_transaction_payload& payload,
+ const std::set<crypto::hash>& known_assets,
+ network_type expected_network,
+ const crypto::hash& expected_carrier_prefix_hash,
+ std::string* error = nullptr);
+ bool derive_asset_output_id(
+ network_type network,
+ const crypto::hash& carrier_prefix_hash,
+ const crypto::hash& asset_id,
+ uint32_t output_index,
+ const confidential_asset_output& output,
+ crypto::hash& output_id,
+ std::string* error = nullptr);
+}
+}