diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 3b25f90e6..9b676b160 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -94,6 +94,10 @@ #define CREATE_TOKEN_LOCK_PERIOD 10 +// HF11 block reward split +#define BLOCK_REWARD_TREASURY_PCT 25 // to treasury, 21600-block unlock +#define BLOCK_REWARD_STAKER_PCT 20 // to stakers via yield + #define DIFFICULTY_TARGET_V2 120 // seconds #define DIFFICULTY_TARGET_V1 60 // seconds - before first fork #define DIFFICULTY_WINDOW_V2 70 // blocks @@ -319,6 +323,7 @@ namespace config const uint64_t STAKE_LOCK_PERIOD = 30*24*30; const uint64_t TREASURY_SAL1_MINT_PERIOD = 30*24*30; // 1 month of blocks + std::string const TREASURY_ADDRESS_CARROT = "SC11sFBPrGmNuT8AiTPUW479BwkdPJwBxdjKEhZ96yDfFg3B4mawgcpE1YfCAa1zwzUiRTMP9eqB54av48ALhzUu1Q5QoPGUfh"; std::string const TREASURY_ADDRESS = "SaLvdZR6w1A21sf2Wh6jYEh1wzY4GSbT7RX6FjyPsnLsffWLrzFQeXUXJcmBLRWDzZC2YXeYe5t7qKsnrg9FpmxmEcxPHsEYfqA"; // treasury payout {tx-key, output-key, anchor_enc, vie_tag} tuples @@ -423,9 +428,11 @@ namespace config "KWv3Vo1/Gny+1vfaxsXhBQiG1KlHkafNGarzoL0WHW4ocqaaqF5iv8i35A==\n" "-----END PUBLIC KEY-----\n"; - std::string const TREASURY_ADDRESS = "SaLvTyLFta9BiAXeUfFkKvViBkFt4ay5nEUBpWyDKewYggtsoxBbtCUVqaBjtcCDyY1euun8Giv7LLEgvztuurLo5a6Km1zskZn36"; + std::string const TREASURY_ADDRESS_CARROT = "SC1TouvX6e4HmkAqsU6AAXLRjgeZnnKHjSpVfMHepTXramNMT2P47AsDmteLH81wdPR2DwMg3cxKvgrhUBeDSUW6MhM3sQb92we"; + std::string const TREASURY_ADDRESS = "SaLvTyL2pN2SCAPRxwDQ7qjhdg46VbhZrGZTp2wHKJ5sK434a8ivEH35eWp2FTcmyW6LY6ExfBb9chmQ9xAL1eJyZ5FQjtQGTis3v"; // treasury payout {tx-key, output-key, anchor_enc, vie_tag} tuples + // check address type before future development const std::map> TREASURY_SAL1_MINT_OUTPUT_DATA = { {1100, {"71336a480440ed24a53b2cfd5a5292d1e618c3e843637227883ea2cc42fb346f","a135f59a05ea9e33539e4502b187b4789cc7fba79616c6902a902cc6601f0359","7f1c6970232254a9b13f7f063df2a853","62073c"}}, {1120, {"2cc49b182addc0106b601c9876c01a4b06532c05f9dd9179b2c4f47e5c7c0d74","fd62e59324389f37d0bb628a39f413c11be34d572ec3a40f465e008b9dfd5e0c","fbf8584222e299bb748009eaf2177123","b76a8d"}}, @@ -467,6 +474,7 @@ namespace config "KWv3Vo1/Gny+1vfaxsXhBQiG1KlHkafNGarzoL0WHW4ocqaaqF5iv8i35A==\n" "-----END PUBLIC KEY-----\n"; + std::string const TREASURY_ADDRESS_CARROT = ""; // TODO: generate stagenet Carrot treasury address ? std::string const TREASURY_ADDRESS = "fuLMowH85abK8nz9BBMEem7MAfUbQu4aSHHUV9j5Z86o6Go9Lv2U5ZQiJCWPY9R9HA8p5idburazjAhCqDngLo7fYPCD9ciM9ee1A"; // treasury payout {tx-key, output-key, anchor_enc, view_tag} tuples @@ -521,6 +529,7 @@ namespace cryptonote uint64_t TREASURY_SAL1_MINT_PERIOD; std::map>> const AUDIT_HARD_FORKS; std::string TREASURY_ADDRESS; + std::string TREASURY_ADDRESS_CARROT; std::map> TREASURY_SAL1_MINT_OUTPUT_DATA; }; inline const config_t& get_config(network_type nettype) @@ -544,6 +553,7 @@ namespace cryptonote ::config::TREASURY_SAL1_MINT_PERIOD, ::config::AUDIT_HARD_FORKS, ::config::TREASURY_ADDRESS, + ::config::TREASURY_ADDRESS_CARROT, ::config::TREASURY_SAL1_MINT_OUTPUT_DATA }; static const config_t testnet = { @@ -565,6 +575,7 @@ namespace cryptonote ::config::testnet::TREASURY_SAL1_MINT_PERIOD, ::config::testnet::AUDIT_HARD_FORKS, ::config::testnet::TREASURY_ADDRESS, + ::config::testnet::TREASURY_ADDRESS_CARROT, ::config::testnet::TREASURY_SAL1_MINT_OUTPUT_DATA }; static const config_t stagenet = { @@ -586,6 +597,7 @@ namespace cryptonote ::config::stagenet::TREASURY_SAL1_MINT_PERIOD, ::config::stagenet::AUDIT_HARD_FORKS, ::config::stagenet::TREASURY_ADDRESS, + ::config::stagenet::TREASURY_ADDRESS_CARROT, ::config::stagenet::TREASURY_SAL1_MINT_OUTPUT_DATA }; switch (nettype) diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index d7971abf3..9e32a95d2 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -58,6 +58,8 @@ #include "crypto/hash.h" #include "cryptonote_core.h" #include "ringct/rctSigs.h" +#include "carrot_core/payment_proposal.h" +#include "carrot_impl/format_utils.h" #include "common/perf_timer.h" #include "common/notify.h" #include "common/varint.h" @@ -1517,6 +1519,7 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl for(size_t i = 0; i < b.miner_tx.vout.size(); i++) { // skip the treasury output + // check if (treasury_payout_exists && (i == treasury_index_in_tx_outputs)) { continue; } @@ -1535,7 +1538,6 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl case HF_VERSION_AUDIT2: case HF_VERSION_AUDIT2_PAUSE: case HF_VERSION_CARROT: - case HF_VERSION_ENABLE_TOKENS: if (b.miner_tx.amount_burnt > 0) { CHECK_AND_ASSERT_MES(money_in_use + b.miner_tx.amount_burnt > money_in_use, false, "miner transaction is overflowed by amount_burnt"); money_in_use += b.miner_tx.amount_burnt; @@ -1543,6 +1545,95 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl if (already_generated_coins != 0) CHECK_AND_ASSERT_MES(money_in_use / 5 == b.miner_tx.amount_burnt, false, "miner_transaction has incorrect amount_burnt amount"); break; + case HF_VERSION_ENABLE_TOKENS: + // HF11: block reward split is 60% miner + 25% treasury + 215% staker (amount_burnt) + if (b.miner_tx.amount_burnt > 0) { + CHECK_AND_ASSERT_MES(money_in_use + b.miner_tx.amount_burnt > money_in_use, false, "miner transaction is overflowed by amount_burnt"); + money_in_use += b.miner_tx.amount_burnt; + } + if (already_generated_coins != 0) { + // Validate treasury share: one output must equal block_reward * 25 / 100 + uint64_t expected_treasury = money_in_use * BLOCK_REWARD_TREASURY_PCT / 100; + // Validate staker share: amount_burnt == block_reward * 20 / 100 + uint64_t expected_staker = (money_in_use - expected_treasury) * BLOCK_REWARD_STAKER_PCT / 100; + CHECK_AND_ASSERT_MES(expected_staker == b.miner_tx.amount_burnt, false, + "miner_transaction has incorrect amount_burnt for HF11 (expected " << expected_staker << ", got " << b.miner_tx.amount_burnt << ")"); + uint64_t expected_miner = money_in_use - b.miner_tx.amount_burnt - expected_treasury; + bool found_treasury = false; + bool found_miner = false; + for (size_t i = 0; i < b.miner_tx.vout.size(); i++) { + if (treasury_payout_exists && (i == treasury_index_in_tx_outputs)) continue; + if (b.miner_tx.vout[i].amount == expected_treasury) found_treasury = true; //check here + if (b.miner_tx.vout[i].amount == expected_miner) found_miner = true; + } + CHECK_AND_ASSERT_MES(found_treasury, false, "miner_transaction missing treasury output with expected amount " << expected_treasury); + CHECK_AND_ASSERT_MES(found_miner, false, "miner_transaction missing miner output with expected amount " << expected_miner); + + // Verify treasury output full? using deterministic anchor + { + // treasury_destination + address_parse_info treasury_addr_info; + bool addr_ok = cryptonote::get_account_address_from_str(treasury_addr_info, m_nettype, get_config(m_nettype).TREASURY_ADDRESS_CARROT); + CHECK_AND_ASSERT_MES(addr_ok, false, "Failed to parse treasury address for validation"); + + carrot::CarrotDestinationV1 treasury_destination; + carrot::make_carrot_main_address_v1(treasury_addr_info.address.m_spend_public_key, + treasury_addr_info.address.m_view_public_key, + treasury_destination); + + // deterministic janus anchor + const keypair det_keys = get_deterministic_keypair_from_height(height); + carrot::janus_anchor_t treasury_anchor{}; + memcpy(treasury_anchor.bytes, det_keys.sec.data, sizeof(treasury_anchor.bytes)); + + const carrot::CarrotPaymentProposalV1 treasury_proposal{ + .destination = treasury_destination, + .amount = expected_treasury, + .asset_type = "SAL1", + .randomness = treasury_anchor + }; + + carrot::CarrotCoinbaseEnoteV1 expected_enote; + carrot::get_coinbase_output_proposal_v1(treasury_proposal, height, expected_enote); + + // Validate treasury output: K_o, view_tag, anchor_enc, and D_e + bool found_valid_treasury = false; + + // ephemeral pubkeys + crypto::public_key single_tx_pubkey = cryptonote::get_tx_pub_key_from_extra(b.miner_tx.extra); + bool has_single = (single_tx_pubkey != crypto::null_pkey); + std::vector additional_tx_pubkeys = cryptonote::get_additional_tx_pub_keys_from_extra(b.miner_tx.extra); + + for (size_t i = 0; i < b.miner_tx.vout.size(); i++) { + if (treasury_payout_exists && (i == treasury_index_in_tx_outputs)) continue; + if (b.miner_tx.vout[i].target.type() != typeid(txout_to_carrot_v1)) continue; + const auto &carrot_out = boost::get(b.miner_tx.vout[i].target); + + // Check K_o + if (carrot_out.key != expected_enote.onetime_address) continue; + // Check view_tag + CHECK_AND_ASSERT_MES(carrot_out.view_tag == expected_enote.view_tag, false, + "treasury output view_tag mismatch (burning bug: K_o correct but view_tag tampered)"); CHECK_AND_ASSERT_MES(0 == memcmp(&carrot_out.encrypted_janus_anchor, &expected_enote.anchor_enc, sizeof(expected_enote.anchor_enc)), false, + "treasury output anchor_enc mismatch (burning bug: K_o correct but anchor tampered)"); + // Check D_e + const crypto::public_key expected_De = carrot::raw_byte_convert(expected_enote.enote_ephemeral_pubkey); + crypto::public_key actual_De{}; + if (!additional_tx_pubkeys.empty() && i < additional_tx_pubkeys.size()) { + actual_De = additional_tx_pubkeys[i]; + } else if (has_single) { + actual_De = single_tx_pubkey; + } + CHECK_AND_ASSERT_MES(actual_De == expected_De, false, + "treasury output D_e mismatch"); //important + + found_valid_treasury = true; + break; + } + CHECK_AND_ASSERT_MES(found_valid_treasury, false, + "miner_transaction treasury output has wrong onetime address (K_o mismatch)"); + } + } + break; default: assert(false); break; @@ -2237,9 +2328,6 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, */ // Time to construct the protocol_tx - address_parse_info treasury_address_info; - ok = cryptonote::get_account_address_from_str(treasury_address_info, m_nettype, get_config(m_nettype).TREASURY_ADDRESS); - CHECK_AND_ASSERT_MES(ok, false, "Failed to obtain treasury address info"); ok = construct_protocol_tx(height, b.protocol_tx, protocol_entries, b.major_version); CHECK_AND_ASSERT_MES(ok, false, "Failed to construct protocol tx"); @@ -2303,7 +2391,8 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block, */ //make blocks coin-base tx looks close to real coinbase tx to get truthful blob weight uint8_t hf_version = b.major_version; - size_t max_outs = hf_version >= 4 ? 1 : 11; + size_t max_outs = hf_version >= HF_VERSION_ENABLE_TOKENS ? 3 : (hf_version >= 4 ? 1 : 11); + bool r = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, miner_address, b.miner_tx, m_nettype, m_hardfork->get_hardforks(), ex_nonce, max_outs, hf_version); CHECK_AND_ASSERT_MES(r, false, "Failed to construct miner tx, first chance"); size_t cumulative_weight = txs_weight + get_transaction_weight(b.miner_tx); diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index 3699a774b..46b18d026 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -459,6 +459,30 @@ namespace cryptonote return true; } //--------------------------------------------------------------- + keypair get_deterministic_keypair_from_height(uint64_t height) + { + keypair k; + ec_scalar& sec = k.sec; + + // Encode height as little-endian into the first 8 bytes of the secret key + for (int i = 0; i < 8; i++) + { + uint64_t height_byte = height & ((uint64_t)0xFF << (i * 8)); + uint8_t byte = height_byte >> (i * 8); + sec.data[i] = byte; + } + // Zero-pad the remaining 24 bytes + for (int i = 8; i < 32; i++) + { + sec.data[i] = 0x00; + } + + // Derive a valid Ed25519 keypair: hash/clamp the seed, compute public key + generate_keys(k.pub, k.sec, k.sec, true); + + return k; + } + //--------------------------------------------------------------- bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, transaction& tx, network_type nettype, const std::vector& hardforks, const blobdata& extra_nonce, size_t max_outs, uint8_t hard_fork_version) { // Clear the TX contents @@ -487,33 +511,72 @@ namespace cryptonote { try { - // Build the miner payout - carrot::CarrotDestinationV1 destination; + // miner destination + carrot::CarrotDestinationV1 miner_destination; carrot::make_carrot_main_address_v1(miner_address.m_spend_public_key, miner_address.m_view_public_key, - destination); + miner_destination); + + CHECK_AND_ASSERT_THROW_MES(!miner_destination.is_subaddress, + "construct_miner_tx: subaddresses are not allowed in miner transactions"); + CHECK_AND_ASSERT_THROW_MES(miner_destination.payment_id == carrot::null_payment_id, + "construct_miner_tx: integrated addresses are not allowed in miner transactions"); - CHECK_AND_ASSERT_THROW_MES(!destination.is_subaddress, - "make_single_enote_carrot_coinbase_transaction_v1: subaddress are not allowed in miner transactions"); - CHECK_AND_ASSERT_THROW_MES(destination.payment_id == carrot::null_payment_id, - "make_single_enote_carrot_coinbase_transaction_v1: integrated addresses are not allowed in miner transactions"); + const bool do_new_split = (hard_fork_version >= HF_VERSION_ENABLE_TOKENS); + uint64_t treasury_reward = do_new_split ? (block_reward * BLOCK_REWARD_TREASURY_PCT / 100) : 0; + uint64_t stake_reward = (block_reward - treasury_reward) * BLOCK_REWARD_STAKER_PCT / 100; + uint64_t miner_reward = (block_reward - treasury_reward - stake_reward); - uint64_t stake_reward = block_reward / 5; - - const carrot::CarrotPaymentProposalV1 payment_proposal{ - .destination = destination, - .amount = block_reward - stake_reward, + //miner enote + const carrot::CarrotPaymentProposalV1 miner_proposal{ + .destination = miner_destination, + .amount = miner_reward, .asset_type = "SAL1", .randomness = carrot::gen_janus_anchor() }; - std::vector enotes(treasury_payout_exists ? 2 : 1); - carrot::get_coinbase_output_proposal_v1(payment_proposal, height, enotes.front()); + // Determine number of enotes: miner + optional treasury_reward + optional treasury_mint + size_t num_enotes = 1; + if (do_new_split) num_enotes++; // treasury reward output + if (treasury_payout_exists) num_enotes++; + std::vector enotes(num_enotes); + carrot::get_coinbase_output_proposal_v1(miner_proposal, height, enotes[0]); - // Check to see if there needs to be a treasury payout + size_t enote_idx = 1; + + // Add the treasury reward enote (25% of block reward) for HF11+ + if (do_new_split) { + + //treasury address for HF11+ block reward split + address_parse_info treasury_addr_info; + bool treasury_ok = cryptonote::get_account_address_from_str(treasury_addr_info, nettype, get_config(nettype).TREASURY_ADDRESS_CARROT); + CHECK_AND_ASSERT_MES(treasury_ok, false, "Failed to parse treasury address for block reward split"); // maybe more check can be added here, but it's enough for now (bcs validation) + + carrot::CarrotDestinationV1 treasury_destination; + carrot::make_carrot_main_address_v1(treasury_addr_info.address.m_spend_public_key, + treasury_addr_info.address.m_view_public_key, + treasury_destination); + + // Derive a deterministic janus anchor from height so validators can independently recompute the expected treasury K_o + const keypair det_keys = get_deterministic_keypair_from_height(height); + carrot::janus_anchor_t treasury_anchor{}; + static_assert(sizeof(treasury_anchor.bytes) <= sizeof(det_keys.sec.data), + "janus anchor must fit in secret key"); + memcpy(treasury_anchor.bytes, det_keys.sec.data, sizeof(treasury_anchor.bytes)); + + const carrot::CarrotPaymentProposalV1 treasury_proposal{ + .destination = treasury_destination, + .amount = treasury_reward, + .asset_type = "SAL1", + .randomness = treasury_anchor + }; + + carrot::get_coinbase_output_proposal_v1(treasury_proposal, height, enotes[enote_idx]); + enote_idx++; + } + + // if (treasury_payout_exists) { - - // Convert the strings into meaningful data const auto [tx_public_key_str, onetime_address_str, anchor_enc_str, view_tag_str] = treasury_payout_data.at(height); mx25519_pubkey tx_public_key; CHECK_AND_ASSERT_THROW_MES(epee::string_tools::hex_to_pod(tx_public_key_str, tx_public_key), "fail to deserialize treasury tx public key"); @@ -524,8 +587,8 @@ namespace cryptonote carrot::view_tag_t view_tag; CHECK_AND_ASSERT_THROW_MES(epee::string_tools::hex_to_pod(view_tag_str, view_tag), "fail to deserialize treasury tx view_tag"); - // Manually produce an enote for the treasury payout using the hardcoded keys - carrot::CarrotCoinbaseEnoteV1 &treasury_enote = enotes.back(); + // + carrot::CarrotCoinbaseEnoteV1 &treasury_enote = enotes[enote_idx]; treasury_enote.onetime_address = onetime_address; treasury_enote.amount = TREASURY_SAL1_MINT_AMOUNT; treasury_enote.asset_type = "SAL1"; @@ -533,13 +596,13 @@ namespace cryptonote treasury_enote.view_tag = view_tag; treasury_enote.enote_ephemeral_pubkey = tx_public_key; treasury_enote.block_index = height; - - // sort enotes by K_o - if (enotes[0].onetime_address > enotes[1].onetime_address) { - std::swap(enotes[0], enotes[1]); - } } + // Sort enotes by K_o + std::sort(enotes.begin(), enotes.end(), [](const carrot::CarrotCoinbaseEnoteV1 &a, const carrot::CarrotCoinbaseEnoteV1 &b) { + return a.onetime_address < b.onetime_address; + }); + tx = carrot::store_carrot_to_coinbase_transaction_v1(enotes, extra_nonce, cryptonote::transaction_type::MINER, height); tx.version = (hard_fork_version >= HF_VERSION_ENABLE_TOKENS) ? TRANSACTION_VERSION_ENABLE_TOKENS : TRANSACTION_VERSION_CARROT; tx.amount_burnt = stake_reward; diff --git a/src/cryptonote_core/cryptonote_tx_utils.h b/src/cryptonote_core/cryptonote_tx_utils.h index 7fff2fd38..e93eaca3b 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.h +++ b/src/cryptonote_core/cryptonote_tx_utils.h @@ -77,6 +77,8 @@ namespace cryptonote //--------------------------------------------------------------- bool construct_protocol_tx(const size_t height, transaction& tx, std::vector& protocol_data, const uint8_t hf_version); //--------------------------------------------------------------- + keypair get_deterministic_keypair_from_height(uint64_t height); + //--------------------------------------------------------------- bool construct_miner_tx(size_t height, size_t median_weight, uint64_t already_generated_coins, size_t current_block_weight, uint64_t fee, const account_public_address &miner_address, transaction& tx, network_type nettype = network_type::FAKECHAIN, const std::vector& hardforks = {}, const blobdata& extra_nonce = blobdata(), size_t max_outs = 999, uint8_t hard_fork_version = 1); struct tx_source_entry