diff options
| -rw-r--r-- | docs/MONZERO_ASSETS_V1_SPEC.md | 23 | ||||
| -rw-r--r-- | docs/MONZERO_PHASE0_STABILIZATION.md | 2 | ||||
| -rw-r--r-- | src/cryptonote_basic/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/cryptonote_basic/asset_confidential.cpp | 131 | ||||
| -rw-r--r-- | src/cryptonote_basic/asset_confidential.h | 42 | ||||
| -rw-r--r-- | tests/unit_tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | tests/unit_tests/asset_confidential.cpp | 96 |
7 files changed, 296 insertions, 0 deletions
diff --git a/docs/MONZERO_ASSETS_V1_SPEC.md b/docs/MONZERO_ASSETS_V1_SPEC.md index 73bd27790..55d1391ef 100644 --- a/docs/MONZERO_ASSETS_V1_SPEC.md +++ b/docs/MONZERO_ASSETS_V1_SPEC.md @@ -252,6 +252,29 @@ properties remain prerequisites for an isolated prototype network. The transparent statement must never be mistaken for a production transaction design. +### 6.2 Inactive confidential conservation prototype + +The source tree now contains an inactive per-asset Pedersen-commitment and +Bulletproof+ verifier. It reuses the inherited RingCT curve and range-proof +implementation, requires exact proof coverage of every output and explicit +burn commitment, and checks that pseudo inputs equal outputs plus burns inside +each public asset-ID domain. Fixed-supply issuance uses a zero-mask commitment +to the descriptor's public lifetime supply. Limits currently cap each balance +group at 16 pseudo inputs and 16 destinations. + +Adversarial tests reject inflation, substituted commitments, malformed range +proofs, duplicate asset groups, unknown assets, cross-asset pseudo inputs, and +issuance commitments that disagree with the declared supply. Malformed curve +proofs are converted to deterministic validation failure rather than escaping +as exceptions. + +This layer still does **not** authorize ownership or prevent double spends. +Production integration requires ring-member references resolved against stored +asset outputs, asset key images, a domain-separated CLSAG transcript, canonical +wire serialization, and database indexes for unspent output lookup. Until that +layer exists and is reviewed, these proofs cannot make an asset transaction +valid on any Monzero network. + ## 7. Metadata Consensus stores only bounded identity and commitment fields. Descriptions, diff --git a/docs/MONZERO_PHASE0_STABILIZATION.md b/docs/MONZERO_PHASE0_STABILIZATION.md index 13ce9ef20..4b9a37e72 100644 --- a/docs/MONZERO_PHASE0_STABILIZATION.md +++ b/docs/MONZERO_PHASE0_STABILIZATION.md @@ -133,6 +133,8 @@ isolated disposable development network may be used after cryptographic review. - [x] Atomic ordered block-issuance adapter, detach behavior, and deterministic state commitments. - [x] Detached versioned transaction extension bound to a native prefix commitment. - [x] Atomic block-extension validation against independently supplied native carrier commitments. +- [x] Persistent LMDB issuance registry with restart reconstruction and native reorg rollback. +- [x] Inactive per-asset Pedersen/Bulletproof+ conservation and burn verifier. - [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/CMakeLists.txt b/src/cryptonote_basic/CMakeLists.txt index 91612168c..84e86803c 100644 --- a/src/cryptonote_basic/CMakeLists.txt +++ b/src/cryptonote_basic/CMakeLists.txt @@ -45,6 +45,7 @@ target_link_libraries(cryptonote_format_utils_basic ) set(cryptonote_basic_sources + asset_confidential.cpp account.cpp asset_types.cpp connection_context.cpp diff --git a/src/cryptonote_basic/asset_confidential.cpp b/src/cryptonote_basic/asset_confidential.cpp new file mode 100644 index 000000000..d2473bea1 --- /dev/null +++ b/src/cryptonote_basic/asset_confidential.cpp @@ -0,0 +1,131 @@ +#include "asset_confidential.h" + +#include <cstring> + +#include "ringct/bulletproofs_plus.h" +#include "ringct/rctOps.h" + +namespace cryptonote +{ +namespace assets +{ +namespace +{ + bool fail(std::string* error, const std::string& message) + { + if (error) + *error = message; + return false; + } + + bool valid_points(const rct::keyV& points) + { + for (const rct::key& point : points) + if (!rct::isInMainSubgroup(point)) + return false; + return true; + } +} + +bool verify_confidential_asset_balance(const confidential_asset_balance& balance, std::string* error) +{ + if (balance.asset_id == crypto::null_hash) + return fail(error, "confidential balance has a zero asset id"); + if (balance.pseudo_inputs.empty()) + return fail(error, "confidential balance has no pseudo inputs"); + if (balance.outputs.empty() && balance.burns.empty()) + return fail(error, "confidential balance has no outputs or burns"); + if (balance.pseudo_inputs.size() > MAX_CONFIDENTIAL_ASSET_INPUTS + || balance.outputs.size() + balance.burns.size() > MAX_CONFIDENTIAL_ASSET_OUTPUTS) + return fail(error, "confidential balance exceeds input or output limits"); + rct::keyV input_commitments; + input_commitments.reserve(balance.pseudo_inputs.size()); + for (const confidential_pseudo_input& input : balance.pseudo_inputs) + { + if (input.source_asset_id != balance.asset_id) + return fail(error, "pseudo input crosses asset balance domains"); + input_commitments.push_back(input.commitment); + } + if (!valid_points(input_commitments) || !valid_points(balance.outputs) || !valid_points(balance.burns)) + return fail(error, "confidential balance contains an invalid curve point"); + + const size_t commitment_count = balance.outputs.size() + balance.burns.size(); + size_t proof_commitments = 0; + rct::keyV proven_commitments; + proven_commitments.reserve(commitment_count); + for (const rct::BulletproofPlus& proof : balance.range_proofs) + { + if (proof.V.empty() || proof_commitments > commitment_count - proof.V.size()) + return fail(error, "range proofs do not cover the declared commitments exactly"); + try + { + if (!rct::bulletproof_plus_VERIFY(proof)) + return fail(error, "invalid asset Bulletproof+"); + } + catch (const std::exception&) + { + return fail(error, "malformed asset Bulletproof+"); + } + for (const rct::key& scaled : proof.V) + { + proven_commitments.push_back(rct::scalarmult8(scaled)); + } + proof_commitments += proof.V.size(); + } + if (proof_commitments != commitment_count) + return fail(error, "range proofs do not cover every output and burn"); + for (size_t index = 0; index < balance.outputs.size(); ++index) + if (!rct::equalKeys(proven_commitments[index], balance.outputs[index])) + return fail(error, "range proof output commitment mismatch"); + for (size_t index = 0; index < balance.burns.size(); ++index) + if (!rct::equalKeys(proven_commitments[balance.outputs.size() + index], balance.burns[index])) + return fail(error, "range proof burn commitment mismatch"); + + const rct::key input_sum = rct::addKeys(input_commitments); + rct::keyV destinations = balance.outputs; + destinations.insert(destinations.end(), balance.burns.begin(), balance.burns.end()); + const rct::key output_sum = rct::addKeys(destinations); + if (!rct::equalKeys(input_sum, output_sum)) + return fail(error, "asset commitments do not conserve value"); + return true; +} + +bool verify_confidential_asset_transaction( + const std::vector<confidential_asset_balance>& balances, + const std::set<crypto::hash>& known_assets, + const boost::optional<issuance_descriptor>& issuance, + std::string* error) +{ + if (balances.empty()) + return fail(error, "confidential asset transaction is empty"); + std::set<crypto::hash> seen; + crypto::hash issued_id{}; + if (issuance && !derive_asset_id(*issuance, issued_id, error)) + return false; + bool found_issuance = false; + for (const confidential_asset_balance& balance : balances) + { + if (!seen.insert(balance.asset_id).second) + return fail(error, "asset appears in more than one balance group"); + const bool is_issuance = issuance && balance.asset_id == issued_id; + if (!is_issuance && known_assets.count(balance.asset_id) == 0) + return fail(error, "confidential balance references an unknown asset"); + if (is_issuance) + { + if (known_assets.count(balance.asset_id) != 0) + return fail(error, "confidential issuance duplicates an existing asset"); + if (balance.pseudo_inputs.size() != 1 + || !rct::equalKeys(balance.pseudo_inputs.front().commitment, rct::commit(issuance->atomic_supply, rct::zero())) + || balance.pseudo_inputs.front().source_asset_id != balance.asset_id) + return fail(error, "issuance pseudo input does not commit to the fixed supply"); + found_issuance = true; + } + if (!verify_confidential_asset_balance(balance, error)) + return false; + } + if (issuance && !found_issuance) + return fail(error, "issuance has no matching confidential balance group"); + return true; +} +} +} diff --git a/src/cryptonote_basic/asset_confidential.h b/src/cryptonote_basic/asset_confidential.h new file mode 100644 index 000000000..d79efb56f --- /dev/null +++ b/src/cryptonote_basic/asset_confidential.h @@ -0,0 +1,42 @@ +#pragma once + +#include <set> +#include <string> +#include <vector> + +#include "asset_types.h" +#include "ringct/rctTypes.h" + +namespace cryptonote +{ +namespace assets +{ + constexpr size_t MAX_CONFIDENTIAL_ASSET_INPUTS = 16; + constexpr size_t MAX_CONFIDENTIAL_ASSET_OUTPUTS = 16; + + struct confidential_pseudo_input + { + crypto::hash source_asset_id{}; + rct::key commitment{}; + }; + + struct confidential_asset_balance + { + crypto::hash asset_id{}; + std::vector<confidential_pseudo_input> pseudo_inputs; + rct::keyV outputs; + rct::keyV burns; + std::vector<rct::BulletproofPlus> range_proofs; + }; + + bool verify_confidential_asset_balance( + const confidential_asset_balance& balance, + std::string* error = nullptr); + + bool verify_confidential_asset_transaction( + const std::vector<confidential_asset_balance>& balances, + const std::set<crypto::hash>& known_assets, + const boost::optional<issuance_descriptor>& issuance, + std::string* error = nullptr); +} +} diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 16e498ae9..b3328fe74 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -30,6 +30,7 @@ set(unit_tests_sources account.cpp apply_permutation.cpp address_from_url.cpp + asset_confidential.cpp asset_types.cpp base58.cpp blockchain_db.cpp diff --git a/tests/unit_tests/asset_confidential.cpp b/tests/unit_tests/asset_confidential.cpp new file mode 100644 index 000000000..2b3980758 --- /dev/null +++ b/tests/unit_tests/asset_confidential.cpp @@ -0,0 +1,96 @@ +#include "gtest/gtest.h" + +#include "cryptonote_basic/asset_confidential.h" +#include "ringct/bulletproofs_plus.h" +#include "ringct/rctOps.h" + +namespace +{ + crypto::hash asset_id(unsigned char value) + { + crypto::hash id{}; + id.data[0] = value; + return id; + } + + cryptonote::assets::confidential_asset_balance make_balance( + const crypto::hash& id, uint64_t input_amount, + const std::vector<uint64_t>& outputs, const std::vector<uint64_t>& burns) + { + rct::keyV masks = rct::skvGen(outputs.size() + burns.size()); + rct::key input_mask = rct::zero(); + for (const rct::key& mask : masks) + sc_add(input_mask.bytes, input_mask.bytes, mask.bytes); + cryptonote::assets::confidential_asset_balance balance; + balance.asset_id = id; + balance.pseudo_inputs.push_back({id, rct::commit(input_amount, input_mask)}); + size_t index = 0; + for (const uint64_t amount : outputs) + balance.outputs.push_back(rct::commit(amount, masks[index++])); + for (const uint64_t amount : burns) + balance.burns.push_back(rct::commit(amount, masks[index++])); + std::vector<uint64_t> amounts = outputs; + amounts.insert(amounts.end(), burns.begin(), burns.end()); + balance.range_proofs.push_back(rct::bulletproof_plus_PROVE(amounts, masks)); + return balance; + } +} + +TEST(asset_confidential, verifies_private_transfer_and_explicit_burn) +{ + const crypto::hash id = asset_id(1); + const auto balance = make_balance(id, 10, {7}, {3}); + std::string error; + ASSERT_TRUE(cryptonote::assets::verify_confidential_asset_balance(balance, &error)) << error; + ASSERT_TRUE(cryptonote::assets::verify_confidential_asset_transaction({balance}, {id}, boost::none, &error)) << error; +} + +TEST(asset_confidential, rejects_inflation_and_commitment_substitution) +{ + const crypto::hash id = asset_id(2); + std::string error; + auto inflated = make_balance(id, 10, {11}, {}); + EXPECT_FALSE(cryptonote::assets::verify_confidential_asset_balance(inflated, &error)); + auto substituted = make_balance(id, 10, {10}, {}); + substituted.outputs.front() = rct::commit(10, rct::skGen()); + EXPECT_FALSE(cryptonote::assets::verify_confidential_asset_balance(substituted, &error)); + auto malformed = make_balance(id, 10, {10}, {}); + malformed.range_proofs.front().A.bytes[0] ^= 1; + EXPECT_FALSE(cryptonote::assets::verify_confidential_asset_balance(malformed, &error)); +} + +TEST(asset_confidential, rejects_cross_asset_and_duplicate_balance_domains) +{ + const crypto::hash first = asset_id(3); + const crypto::hash second = asset_id(4); + std::string error; + auto crossed = make_balance(first, 9, {9}, {}); + crossed.pseudo_inputs.front().source_asset_id = second; + EXPECT_FALSE(cryptonote::assets::verify_confidential_asset_balance(crossed, &error)); + const auto valid = make_balance(first, 9, {9}, {}); + EXPECT_FALSE(cryptonote::assets::verify_confidential_asset_transaction({valid, valid}, {first}, boost::none, &error)); + EXPECT_FALSE(cryptonote::assets::verify_confidential_asset_transaction({valid}, {second}, boost::none, &error)); +} + +TEST(asset_confidential, validates_fixed_supply_issuance_commitment) +{ + crypto::public_key issuer{}; + crypto::secret_key secret{}; + crypto::generate_keys(issuer, secret); + cryptonote::assets::issuance_descriptor descriptor; + descriptor.network = cryptonote::TESTNET; + descriptor.issuer_key = issuer; + descriptor.atomic_supply = 10; + descriptor.issuance_nonce.data[0] = 9; + crypto::hash id{}; + ASSERT_TRUE(cryptonote::assets::derive_asset_id(descriptor, id)); + cryptonote::assets::confidential_asset_balance balance; + balance.asset_id = id; + balance.pseudo_inputs.push_back({id, rct::commit(10, rct::zero())}); + balance.outputs.push_back(rct::commit(10, rct::zero())); + balance.range_proofs.push_back(rct::bulletproof_plus_PROVE(10, rct::zero())); + std::string error; + ASSERT_TRUE(cryptonote::assets::verify_confidential_asset_transaction({balance}, {}, descriptor, &error)) << error; + balance.pseudo_inputs.front().commitment = rct::commit(11, rct::zero()); + EXPECT_FALSE(cryptonote::assets::verify_confidential_asset_transaction({balance}, {}, descriptor, &error)); +} |
