From 866691d9d89ea54a7604e59f1d96a6233fd664df Mon Sep 17 00:00:00 2001 From: Sean Darcy Date: Fri, 29 Apr 2022 09:51:14 +1000 Subject: [PATCH] Batching of service node rewards This updates the coinbase transactions to reward service nodes periodically rather than every block. If you recieve a service node reward this reward will be delayed x blocks, if you receive another reward to the same wallet before those blocks have been completed it will be added to your total and all will be paid out after those x blocks has passed. For example if our batching interval is 2 blocks: Block 1 - Address A receives reward of 10 oxen - added to batch Block 2 - Address A receives reward of 10 oxen - added to batch Block 3 - Address A is paid out 20 oxen. Batching accumulates a small reward for all nodes every block The batching of service node rewards allows us to drip feed rewards to service nodes. Rather than accruing each service node 16.5 oxen every time they are pulse block leader we now reward every node the 16.5 / num_service_nodes every block and pay each wallet the full amount that has been accrued after a period of time (Likely 3.5 days). To spread each payment evenly we now pay the rewards based on the address of the recipient. This modulus of their address determines which block the address should be paid and by setting the interval to our service_node_batching interval we can guarantee they will be paid out regularly and evenly distribute the payments for all wallets over this --- .drone.jsonnet | 7 +- CMakeLists.txt | 1 + external/SQLiteCpp | 2 +- src/blockchain_db/CMakeLists.txt | 3 + src/blockchain_db/blockchain_db.cpp | 2 + src/blockchain_db/lmdb/db_lmdb.cpp | 3 + src/blockchain_db/sqlite/db_sqlite.cpp | 636 +++++++++ src/blockchain_db/sqlite/db_sqlite.h | 113 ++ src/blockchain_utilities/CMakeLists.txt | 1 + .../blockchain_ancestry.cpp | 2 +- src/blockchain_utilities/blockchain_depth.cpp | 2 +- .../blockchain_export.cpp | 2 +- src/blockchain_utilities/blockchain_stats.cpp | 2 +- src/blockchain_utilities/blockchain_usage.cpp | 2 +- src/checkpoints/checkpoints.cpp | 3 +- src/checkpoints/checkpoints.h | 4 +- src/cryptonote_basic/cryptonote_basic.cpp | 35 +- src/cryptonote_basic/cryptonote_basic.h | 15 + .../cryptonote_basic_impl.cpp | 11 + src/cryptonote_basic/cryptonote_basic_impl.h | 34 +- .../cryptonote_boost_serialization.h | 5 + .../cryptonote_format_utils.cpp | 17 +- src/cryptonote_basic/hardfork.cpp | 6 +- src/cryptonote_config.h | 31 + src/cryptonote_core/CMakeLists.txt | 1 + src/cryptonote_core/blockchain.cpp | 306 +++-- src/cryptonote_core/blockchain.h | 6 +- src/cryptonote_core/cryptonote_core.cpp | 23 +- src/cryptonote_core/cryptonote_core.h | 18 +- src/cryptonote_core/cryptonote_tx_utils.cpp | 181 ++- src/cryptonote_core/cryptonote_tx_utils.h | 40 +- src/cryptonote_core/service_node_list.cpp | 102 +- src/cryptonote_core/service_node_list.h | 12 +- src/cryptonote_core/service_node_rules.h | 1 + src/cryptonote_core/tx_pool.cpp | 13 +- src/cryptonote_protocol/CMakeLists.txt | 1 + .../cryptonote_protocol_handler.inl | 2 +- src/daemon/CMakeLists.txt | 1 + src/daemon/rpc_command_executor.cpp | 48 +- src/device/CMakeLists.txt | 1 + src/p2p/CMakeLists.txt | 1 + src/rpc/CMakeLists.txt | 2 + src/rpc/core_rpc_server.cpp | 14 +- src/sqlitedb/database.hpp | 6 +- src/wallet/api/CMakeLists.txt | 2 + src/wallet/wallet2.cpp | 9 +- tests/block_weight/CMakeLists.txt | 1 + tests/block_weight/block_weight.cpp | 3 +- tests/core_proxy/CMakeLists.txt | 1 + tests/core_proxy/core_proxy.cpp | 9 +- tests/core_proxy/core_proxy.h | 7 +- tests/core_tests/CMakeLists.txt | 1 + tests/core_tests/block_reward.cpp | 3 +- tests/core_tests/block_validation.cpp | 2 +- tests/core_tests/chaingen.cpp | 109 +- tests/core_tests/chaingen.h | 25 +- tests/core_tests/chaingen_main.cpp | 5 + tests/core_tests/integer_overflow.cpp | 27 +- tests/core_tests/oxen_tests.cpp | 1212 +++++++++++------ tests/core_tests/oxen_tests.h | 8 +- tests/core_tests/transaction_tests.cpp | 17 +- tests/performance_tests/multi_tx_test_base.h | 6 +- tests/performance_tests/single_tx_test_base.h | 7 +- tests/unit_tests/CMakeLists.txt | 2 + tests/unit_tests/long_term_block_weight.cpp | 4 +- tests/unit_tests/memwipe.cpp | 2 + tests/unit_tests/node_server.cpp | 7 +- tests/unit_tests/output_distribution.cpp | 2 +- tests/unit_tests/sqlite.cpp | 143 ++ tests/unit_tests/test_tx_utils.cpp | 12 +- utils/local-devnet/.gitignore | 2 + utils/local-devnet/commands/getheight.py | 1 + utils/local-devnet/commands/gettimeblock.py | 41 + utils/local-devnet/commands/gettxpool.py | 25 + utils/local-devnet/commands/proofed.py | 35 + utils/local-devnet/daemons.py | 42 +- utils/local-devnet/service_node_network.py | 34 +- 77 files changed, 2748 insertions(+), 776 deletions(-) create mode 100644 src/blockchain_db/sqlite/db_sqlite.cpp create mode 100644 src/blockchain_db/sqlite/db_sqlite.h create mode 100644 tests/unit_tests/sqlite.cpp create mode 100755 utils/local-devnet/commands/gettimeblock.py create mode 100755 utils/local-devnet/commands/gettxpool.py create mode 100755 utils/local-devnet/commands/proofed.py diff --git a/.drone.jsonnet b/.drone.jsonnet index 6f9fbdd0c..725551163 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -134,9 +134,10 @@ local mac_builder(name, 'mkdir build', 'cd build', 'cmake .. -G Ninja -DCMAKE_CXX_FLAGS=-fcolor-diagnostics -DCMAKE_BUILD_TYPE=' + build_type + ' ' + - '-DLOCAL_MIRROR=https://builds.lokinet.dev/deps ' - + cmake_options({ USE_LTO: lto, WARNINGS_AS_ERRORS: werror, BUILD_TESTS: build_tests || run_tests }) - + cmake_extra, + '-DLOCAL_MIRROR=https://builds.lokinet.dev/deps -DUSE_LTO=' + (if lto then 'ON ' else 'OFF ') + + (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') + + (if build_tests || run_tests then '-DBUILD_TESTS=ON ' else '') + + cmake_extra, 'ninja -j' + jobs + ' -v', ] + ( if run_tests then [ diff --git a/CMakeLists.txt b/CMakeLists.txt index f206aee65..6d4988437 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,6 +288,7 @@ if(NOT MANUAL_SUBMODULES) check_submodule(external/trezor-common) check_submodule(external/randomx) check_submodule(external/loki-mq cppzmq) + check_submodule(external/SQLiteCpp) if(BUILD_TESTS) check_submodule(external/googletest) endif() diff --git a/external/SQLiteCpp b/external/SQLiteCpp index beb2b2964..ce6dd9b82 160000 --- a/external/SQLiteCpp +++ b/external/SQLiteCpp @@ -1 +1 @@ -Subproject commit beb2b2964036f7ec87394a0d7f32db170d4bcdfe +Subproject commit ce6dd9b82234e0427801380286d87f5bae417cca diff --git a/src/blockchain_db/CMakeLists.txt b/src/blockchain_db/CMakeLists.txt index ef3820d9b..da67b3895 100644 --- a/src/blockchain_db/CMakeLists.txt +++ b/src/blockchain_db/CMakeLists.txt @@ -30,9 +30,12 @@ add_library(blockchain_db blockchain_db.cpp lmdb/db_lmdb.cpp + sqlite/db_sqlite.cpp ) target_link_libraries(blockchain_db + PUBLIC + SQLiteCpp PRIVATE common ringct diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp index eedd8c9e2..a3af9fba2 100644 --- a/src/blockchain_db/blockchain_db.cpp +++ b/src/blockchain_db/blockchain_db.cpp @@ -291,7 +291,9 @@ bool BlockchainDB::get_pruned_tx(const crypto::hash& h, cryptonote::transaction if (!get_pruned_tx_blob(h, bd)) return false; if (!parse_and_validate_tx_base_from_blob(bd, tx)) + { throw DB_ERROR("Failed to parse transaction base from blob retrieved from the db"); + } return true; } diff --git a/src/blockchain_db/lmdb/db_lmdb.cpp b/src/blockchain_db/lmdb/db_lmdb.cpp index 3dd9f9766..717733ba0 100644 --- a/src/blockchain_db/lmdb/db_lmdb.cpp +++ b/src/blockchain_db/lmdb/db_lmdb.cpp @@ -2542,6 +2542,7 @@ T BlockchainLMDB::get_and_convert_block_blob_from_height(uint64_t height) const // header, not the full-block (of which a good chunk is thrown away because we // only want the header). LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); TXN_PREFIX_RDONLY(); @@ -3619,7 +3620,9 @@ bool BlockchainLMDB::for_all_transactions(std::function + +#include + +#include + +#include + +#include + +#include + +#include "cryptonote_core/blockchain.h" + +#include "cryptonote_core/service_node_list.h" + +#include "common/string_util.h" + +#include "cryptonote_basic/hardfork.h" + +#undef OXEN_DEFAULT_LOG_CATEGORY +#define OXEN_DEFAULT_LOG_CATEGORY "blockchain.db.sqlite" + +namespace cryptonote { + + BlockchainSQLite::BlockchainSQLite(cryptonote::network_type nettype, fs::path db_path): db::Database(db_path, ""), m_nettype(nettype), filename {db_path.u8string()} { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + height = 0; + + if (!db.tableExists("batched_payments_accrued") || !db.tableExists("batched_payments_raw") || !db.tableExists("batch_db_info")) { + create_schema(); + } + + SQLite::Statement st { + db, + "SELECT height FROM batch_db_info" + }; + while (st.executeStep()) { + height = st.getColumn(0).getInt64(); + } + } + + void BlockchainSQLite::create_schema() { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + + db.exec(R"( + CREATE TABLE batched_payments_accrued( + address VARCHAR NOT NULL, + amount BIGINT NOT NULL, + PRIMARY KEY(address), + CHECK(amount >= 0) + ); + + CREATE TRIGGER batch_payments_delete_empty AFTER UPDATE ON batched_payments_accrued FOR EACH ROW WHEN NEW.amount = 0 BEGIN DELETE FROM batched_payments_accrued WHERE address = NEW.address; END; + + CREATE TABLE batched_payments_raw( + address VARCHAR NOT NULL, + amount BIGINT NOT NULL, + height_paid BIGINT NOT NULL, + PRIMARY KEY(address, height_paid), + CHECK(amount >= 0) + ); + + CREATE INDEX batched_payments_raw_height_idx ON batched_payments_raw(height_paid); + + CREATE TABLE batch_db_info( + height BIGINT NOT NULL + ); + + INSERT INTO batch_db_info(height) VALUES(0); + + CREATE TRIGGER batch_payments_prune AFTER UPDATE ON batch_db_info FOR EACH ROW BEGIN DELETE FROM batched_payments_raw WHERE height_paid < (NEW.height - 10000); END; + + CREATE VIEW batched_payments_paid AS SELECT * FROM batched_payments_raw; + + CREATE TRIGGER make_payment INSTEAD OF INSERT ON batched_payments_paid FOR EACH ROW BEGIN UPDATE batched_payments_accrued SET amount = (amount - NEW.amount) WHERE address = NEW.address; SELECT RAISE(ABORT, 'Address not found') WHERE changes() = 0; INSERT INTO batched_payments_raw(address, amount, height_paid) VALUES(NEW.address, NEW.amount, NEW.height_paid); END; + + CREATE TRIGGER rollback_payment INSTEAD OF DELETE ON batched_payments_paid FOR EACH ROW BEGIN DELETE FROM batched_payments_raw WHERE address = OLD.address AND height_paid = OLD.height_paid; INSERT INTO batched_payments_accrued(address, amount) VALUES(OLD.address, OLD.amount) ON CONFLICT(address) DO UPDATE SET amount = (amount + excluded.amount); END; + )"); + + MDEBUG("Database setup complete"); + } + + void BlockchainSQLite::reset_database() { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + + db.exec(R"( + DROP TABLE IF EXISTS batched_payments_accrued; + + DROP VIEW IF EXISTS batched_payments_paid; + + DROP TABLE IF EXISTS batched_payments_raw; + + DROP TABLE IF EXISTS batch_db_info; + )"); + + create_schema(); + + MDEBUG("Database reset complete"); + } + + void BlockchainSQLite::update_height(uint64_t new_height) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__ << " Called with new height: " << new_height); + height = new_height; + SQLite::Statement update_height { + db, + "UPDATE batch_db_info SET height = ?" + }; + db::exec_query(update_height, static_cast(height)); + } + + void BlockchainSQLite::increment_height() { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__ << " Called with height: " << height + 1); + update_height(height + 1); + } + + void BlockchainSQLite::decrement_height() { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__ << " Called with height: " << height - 1); + update_height(height - 1); + } + + bool BlockchainSQLite::add_sn_payments(std::vector& payments) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + SQLite::Statement insert_payment { + db, + "INSERT INTO batched_payments_accrued (address, amount) VALUES (?, ?) ON CONFLICT (address) DO UPDATE SET amount = amount + excluded.amount" + }; + + for (auto& payment: payments) { + std::string address_str = cryptonote::get_account_address_as_str(m_nettype, 0, payment.address_info.address); + MTRACE("Adding record for SN reward contributor " << address_str << "to database with amount " << static_cast(payment.amount)); + + db::exec_query(insert_payment, address_str, static_cast(payment.amount * 1000)); + insert_payment.reset(); + }; + + return true; + } + + bool BlockchainSQLite::subtract_sn_payments(std::vector& payments) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + SQLite::Statement update_payment { + db, + "UPDATE batched_payments_accrued SET amount = (amount - ?) WHERE address = ?" + }; + + for (auto& payment: payments) { + std::string address_str = cryptonote::get_account_address_as_str(m_nettype, 0, payment.address_info.address); + auto result = db::exec_query(update_payment, static_cast(payment.amount * 1000), address_str); + if (!result) { + MERROR("tried to subtract payment from an address that doesnt exist: " << address_str); + return false; + } + update_payment.reset(); + }; + + return true; + } + + std::optional> BlockchainSQLite::get_sn_payments(uint64_t block_height) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + + if (block_height == 0) + return std::nullopt; + + const auto& conf = get_config(m_nettype); + + SQLite::Statement select_payments { + db, + "SELECT address, amount FROM batched_payments_accrued WHERE amount > ? ORDER BY address ASC" + }; + + select_payments.bind(1, static_cast(conf.MIN_BATCH_PAYMENT_AMOUNT * 1000)); + + std::vector payments; + + std::string address; + uint64_t amount; + while (select_payments.executeStep()) { + address = select_payments.getColumn(0).getString(); + amount = static_cast(select_payments.getColumn(1).getInt64() / 1000); + if (cryptonote::is_valid_address(address, m_nettype)) { + cryptonote::address_parse_info addr_info {}; + cryptonote::get_account_address_from_str(addr_info, m_nettype, address); + uint64_t next_payout_height = addr_info.address.next_payout_height(block_height - 1, conf.BATCHING_INTERVAL); + if (block_height == next_payout_height) { + payments.emplace_back(address, amount, m_nettype); + } + } else { + MERROR("Invalid address returned from batching database: " << address); + return std::nullopt; + } + } + + return payments; + } + + std::vector BlockchainSQLite::calculate_rewards(uint8_t hf_version, uint64_t distribution_amount, service_nodes::service_node_info sn_info) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + + // Find out how much is due for the operator + uint64_t operator_fee = 0; + { + // This calculates the operator fee using (operator_portion / max_operator_portion) * distribution_amount but using 128 bit integer math + uint64_t hi, lo, resulthi, resultlo; + lo = mul128(sn_info.portions_for_operator, distribution_amount, &hi); + div128_64(hi, lo, STAKING_PORTIONS, &resulthi, &resultlo); + if (resulthi > 0) + throw std::logic_error("overflow from calculating sn operator fee"); + operator_fee = resultlo; + } + std::vector payments; + // Pay the operator fee to the operator + if (operator_fee > 0) + payments.emplace_back(sn_info.operator_address, operator_fee, m_nettype); + + // Pay the balance to all the contributors (including the operator again) + uint64_t total_contributed_to_sn = std::accumulate(sn_info.contributors.begin(), sn_info.contributors.end(), uint64_t(0), [](auto + const a, auto + const b) { + return a + b.amount; + }); + + for (auto& contributor: sn_info.contributors) { + // This calculates (contributor.amount / total_contributed_to_winner_sn) * (distribution_amount - operator_fee) but using 128 bit integer math + uint64_t hi, lo, resulthi, resultlo; + lo = mul128(contributor.amount, distribution_amount - operator_fee, &hi); + div128_64(hi, lo, total_contributed_to_sn, &resulthi, &resultlo); + if (resulthi > 0) + throw std::logic_error("overflow from calculating sn contributor reward"); + if (resultlo > 0) + payments.emplace_back(contributor.address, resultlo, m_nettype); + } + + return payments; + } + + bool BlockchainSQLite::add_block(const cryptonote::block& block, + const service_nodes::service_node_list::state_t& service_nodes_state) { + auto block_height = get_block_height(block); + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__ << " called on height: " << block_height); + + auto hf_version = block.major_version; + if (hf_version < cryptonote::network_version_19) { + update_height(block_height); + print_database(); + return true; + } + + auto fork_height = cryptonote::get_hard_fork_heights(m_nettype, cryptonote::network_version_19); + if (block_height == fork_height.first.value_or(0)) { + MDEBUG("Batching of Service Node Rewards Begins"); + reset_database(); + update_height(block_height - 1); + } + + if (block_height != height + 1) { + MERROR("Block height out of sync with batching database. Block height: " << block_height << " batching db height: " << height); + return false; + } + + // We query our own database as a source of truth to verify the blocks payments against. The calculated_rewards + // variable contains a known good list of who should have been paid in this block + auto calculated_rewards = get_sn_payments(block_height); + + // We iterate through the block's coinbase payments and build a copy of our own list of the payments + // miner_tx_vouts this will be compared against calculated_rewards and if they match we know the block is + // paying the correct people only. + std::vector> miner_tx_vouts; + for (auto & vout: block.miner_tx.vout) + miner_tx_vouts.emplace_back(var::get(vout.target).key, vout.amount); + + try { + SQLite::Transaction transaction { + db, + SQLite::TransactionBehavior::IMMEDIATE + }; + + // Goes through the miner transactions vouts checks they are right and marks them as paid in the database + if (!validate_batch_payment(miner_tx_vouts, * calculated_rewards, block_height)) { + return false; + } + + // Step 1: Pay out the block producer their fees + uint64_t service_node_reward = cryptonote::service_node_reward_formula(0, block.major_version); + if (block.reward - service_node_reward > 0) { + if (block.service_node_winner_key // "service_node_winner_key" tracks the pulse winner; 0 if a mined block + && + crypto_core_ed25519_is_valid_point(reinterpret_cast < + const unsigned char * > (block.service_node_winner_key.data))) { + auto service_node_winner = service_nodes_state.service_nodes_infos.find(block.service_node_winner_key); + if (service_node_winner != service_nodes_state.service_nodes_infos.end()) { + std::vector block_producer_fee_payments = calculate_rewards(block.major_version, block.reward - service_node_reward, * service_node_winner -> second); + // Takes the block producer and adds its contributors to the batching database for the transaction fees + if (!add_sn_payments(block_producer_fee_payments)) + return false; + } + } + } + + // Step 2: Iterate over the whole service node list and pay each node 1/service_node_list fraction + const auto payable_service_nodes = service_nodes_state.payable_service_nodes_infos(block_height, m_nettype); + size_t total_service_nodes_payable = payable_service_nodes.size(); + for (const auto& [node_pubkey, node_info]: payable_service_nodes) { + std::vector node_contributors; + auto payable_service_node = service_nodes_state.service_nodes_infos.find(node_pubkey); + if (payable_service_node == service_nodes_state.service_nodes_infos.end()) + continue; + std::vector node_rewards = calculate_rewards(block.major_version, service_node_reward / total_service_nodes_payable, * payable_service_node -> second); + // Takes the node and adds its contributors to the batching database + if (!add_sn_payments(node_rewards)) + return false; + } + + // Step 3: Add Governance reward to the list + if (m_nettype != cryptonote::FAKECHAIN) { + std::vector governance_rewards; + cryptonote::address_parse_info governance_wallet_address; + cryptonote::get_account_address_from_str(governance_wallet_address, m_nettype, cryptonote::get_config(m_nettype).governance_wallet_address(hf_version)); + uint64_t foundation_reward = cryptonote::governance_reward_formula(hf_version); + governance_rewards.emplace_back(governance_wallet_address.address, foundation_reward, m_nettype); + if (!add_sn_payments(governance_rewards)) + return false; + } + increment_height(); + + transaction.commit(); + } catch (std::exception& e) { + MFATAL("Exception: " << e.what()); + return false; + } + print_database(); + return true; + } + + bool BlockchainSQLite::pop_block(const cryptonote::block& block, + const service_nodes::service_node_list::state_t& service_nodes_state) { + auto block_height = get_block_height(block); + + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__ << " called on height: " << block_height); + if (height < block_height) { + MDEBUG("Block above batching DB height skipping pop"); + return true; + } + if (block_height != height) { + MERROR("Block height out of sync with batching database"); + return false; + } + + const auto& conf = get_config(m_nettype); + auto hf_version = block.major_version; + if (hf_version < cryptonote::network_version_19) { + decrement_height(); + return true; + } + + try { + SQLite::Transaction transaction { + db, + SQLite::TransactionBehavior::IMMEDIATE + }; + + // Step 1: Remove the block producers txn fees + uint64_t service_node_reward = cryptonote::service_node_reward_formula(0, block.major_version); + if (block.reward - service_node_reward > 0) // If true then we have tx fees to roll back + { + if (block.service_node_winner_key // "service_node_winner_key" tracks the pulse winner; 0 if a mined block + && + crypto_core_ed25519_is_valid_point(reinterpret_cast < + const unsigned char * > (block.service_node_winner_key.data))) { + auto service_node_winner = service_nodes_state.service_nodes_infos.find(block.service_node_winner_key); + if (service_node_winner != service_nodes_state.service_nodes_infos.end()) { + std::vector block_producer_fee_payments = calculate_rewards(block.major_version, block.reward - service_node_reward, * service_node_winner -> second); + // Takes the block producer and adds its contributors to the batching database for the transaction fees + if (!subtract_sn_payments(block_producer_fee_payments)) + return false; + } + } + } + + // Step 2: Iterate over the whole service node list and subtract each node 1/service_node_list fraction + const auto payable_service_nodes = service_nodes_state.payable_service_nodes_infos(block_height, m_nettype); + size_t total_service_nodes_payable = payable_service_nodes.size(); + for (const auto& [node_pubkey, node_info]: payable_service_nodes) { + std::vector node_contributors; + auto payable_service_node = service_nodes_state.service_nodes_infos.find(node_pubkey); + if (payable_service_node == service_nodes_state.service_nodes_infos.end()) + continue; + std::vector node_rewards = calculate_rewards(block.major_version, service_node_reward / total_service_nodes_payable, * payable_service_node -> second); + // Takes the node and adds its contributors to the batching database + + if (!subtract_sn_payments(node_rewards)) + return false; + } + // Step 3: Remove Governance reward + if (m_nettype != cryptonote::FAKECHAIN) { + std::vector governance_rewards; + cryptonote::address_parse_info governance_wallet_address; + cryptonote::get_account_address_from_str(governance_wallet_address, m_nettype, cryptonote::get_config(m_nettype).governance_wallet_address(hf_version)); + uint64_t foundation_reward = cryptonote::governance_reward_formula(hf_version); + governance_rewards.emplace_back(governance_wallet_address.address, foundation_reward, m_nettype); + if (!subtract_sn_payments(governance_rewards)) + return false; + } + + // Add back to the database payments that had been made in this block + delete_block_payments(block_height); + + decrement_height(); + transaction.commit(); + } catch (std::exception& e) { + MFATAL("Exception: " << e.what()); + return false; + } + return true; + } + + bool BlockchainSQLite::validate_batch_payment(std::vector> miner_tx_vouts, std::vector calculated_payments_from_batching_db, uint64_t block_height) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + size_t length_miner_tx_vouts = miner_tx_vouts.size(); + size_t length_calculated_payments_from_batching_db = calculated_payments_from_batching_db.size(); + + if (length_miner_tx_vouts != length_calculated_payments_from_batching_db) { + MERROR("Length of batch paments does not match, block vouts: " << length_miner_tx_vouts << " batch size: " << length_calculated_payments_from_batching_db); + return false; + } + + int8_t vout_index = 0; + uint64_t total_oxen_payout_in_our_db = std::accumulate(calculated_payments_from_batching_db.begin(), calculated_payments_from_batching_db.end(), uint64_t(0), [](auto + const a, auto + const b) { + return a + b.amount; + }); + uint64_t total_oxen_payout_in_vouts = 0; + std::vector finalised_payments; + cryptonote::keypair + const deterministic_keypair = cryptonote::get_deterministic_keypair_from_height(block_height); + for (auto& vout: miner_tx_vouts) { + if (std::get<1>(vout) != calculated_payments_from_batching_db[vout_index].amount) { + MERROR("Service node reward amount incorrect. Should be " << cryptonote::print_money(calculated_payments_from_batching_db[vout_index].amount) << ", is: " << cryptonote::print_money(std::get<1>(vout))); + return false; + } + crypto::public_key out_eph_public_key {}; + if (!cryptonote::get_deterministic_output_key(calculated_payments_from_batching_db[vout_index].address_info.address, deterministic_keypair, vout_index, out_eph_public_key)) { + MERROR("Failed to generate output one-time public key"); + return false; + } + if (tools::view_guts(std::get<0>(vout)) != tools::view_guts(out_eph_public_key)) { + MERROR("Output ephemeral public key does not match"); + return false; + } + total_oxen_payout_in_vouts += std::get<1>(vout); + finalised_payments.emplace_back(calculated_payments_from_batching_db[vout_index].address, std::get<1>(vout), m_nettype); + vout_index++; + } + if (total_oxen_payout_in_vouts != total_oxen_payout_in_our_db) { + MERROR("Total service node reward amount incorrect. Should be " << cryptonote::print_money(total_oxen_payout_in_our_db) << ", is: " << cryptonote::print_money(total_oxen_payout_in_vouts)); + return false; + } + + return save_payments(block_height, finalised_payments); + } + + bool BlockchainSQLite::save_payments(uint64_t block_height, std::vectorpaid_amounts) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + + SQLite::Statement select_sum { + db, + "SELECT amount from batched_payments_accrued WHERE address = ?;" + }; + + SQLite::Statement update_paid { + db, + "INSERT INTO batched_payments_paid (address, amount, height_paid) VALUES (?,?,?);" + }; + + for (const auto& payment: paid_amounts) { + select_sum.bind(1, payment.address); + while (select_sum.executeStep()) { + uint64_t amount = static_cast(select_sum.getColumn(0).getInt64()); + if (amount != payment.amount * 1000) { + MERROR("Invalid amounts passed in to save payments for address: " << payment.address << " received " << payment.amount << " expected " << amount); + return false; + } + + db::exec_query(update_paid, payment.address, static_cast(amount), static_cast(block_height)); + update_paid.reset(); + } + select_sum.reset(); + }; + return true; + } + + std::vector BlockchainSQLite::get_block_payments(uint64_t block_height) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__ << " Called with height: " << block_height); + + std::vector payments_at_height; + SQLite::Statement st { + db, + "SELECT address, amount FROM batched_payments_paid WHERE height_paid = ? ORDER BY address" + }; + st.bind(1, static_cast(block_height)); + while (st.executeStep()) { + payments_at_height.emplace_back(st.getColumn(0).getString(), st.getColumn(1).getInt64(), m_nettype); + } + return payments_at_height; + } + + bool BlockchainSQLite::delete_block_payments(uint64_t block_height) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__ << " Called with height: " << block_height); + SQLite::Statement delete_payments { + db, + "DELETE FROM batched_payments_paid WHERE height_paid >= ?" + }; + db::exec_query(delete_payments, static_cast(block_height)); + return true; + } + + void BlockchainSQLite::print_database() + { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__ << " Called with height: " << height); + LOG_PRINT_L3("Print Database called with height: " << height); + SQLite::Statement st{db, "SELECT address, amount FROM batched_payments_accrued ORDER BY address ASC"}; + while (st.executeStep()) { + LOG_PRINT_L3(" Address: " << st.getColumn(0).getString() << " has amount: " << st.getColumn(1).getString() << " in the database"); + } + } + + fs::path check_if_copy_filename(std::string_view db_path) { + return (db_path != ":memory:") ? fs::path(std::string(db_path) + "-copy") : fs::path(std::string(db_path)); + } + + BlockchainSQLiteTest::BlockchainSQLiteTest(cryptonote::network_type nettype, fs::path db_path): BlockchainSQLite(nettype, db_path) {}; + + BlockchainSQLiteTest::BlockchainSQLiteTest(BlockchainSQLiteTest& other): BlockchainSQLiteTest(other.m_nettype, check_if_copy_filename(other.filename)) { + std::vector> all_payments_accrued; + SQLite::Statement st { + other.db, "SELECT address, amount FROM batched_payments_accrued" + }; + while (st.executeStep()) + all_payments_accrued.emplace_back(st.getColumn(0).getString(), st.getColumn(1).getInt64()); + std::vector> all_payments_paid; + SQLite::Statement st2 { + other.db, "SELECT address, amount, height_paid FROM batched_payments_raw" + }; + while (st2.executeStep()) + all_payments_paid.emplace_back(st2.getColumn(0).getString(), st2.getColumn(1).getInt64(), st2.getColumn(2).getInt64()); + + SQLite::Transaction transaction { + db, + SQLite::TransactionBehavior::IMMEDIATE + }; + + SQLite::Statement insert_payment_paid { + db, + "INSERT INTO batched_payments_raw (address, amount, height_paid) VALUES (?, ?, ?)" + }; + + for (auto& [address, amount, height_paid]: all_payments_paid) { + db::exec_query(insert_payment_paid, address, amount, height_paid); + insert_payment_paid.reset(); + }; + + SQLite::Statement insert_payment_accrued { + db, + "INSERT INTO batched_payments_accrued (address, amount) VALUES (?, ?)" + }; + + for (auto& [address, amount]: all_payments_accrued) { + db::exec_query(insert_payment_accrued, address, amount); + insert_payment_accrued.reset(); + }; + + transaction.commit(); + + update_height(other.height); + } + + uint64_t BlockchainSQLiteTest::batching_count() { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + SQLite::Statement st { + db, + "SELECT count(*) FROM batched_payments_accrued" + }; + uint64_t count = 0; + while (st.executeStep()) { + count = st.getColumn(0).getInt64(); + } + return count; + } + + std::optional BlockchainSQLiteTest::retrieve_amount_by_address(const std::string& address) { + LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); + SQLite::Statement st { + db, + "SELECT amount FROM batched_payments_accrued WHERE address = ?" + }; + st.bind(1, address); + std::optional amount = std::nullopt; + while (st.executeStep()) { + assert(!amount); + amount.emplace(st.getColumn(0).getInt64()); + } + return amount; + } + +} // namespace cryptonote diff --git a/src/blockchain_db/sqlite/db_sqlite.h b/src/blockchain_db/sqlite/db_sqlite.h new file mode 100644 index 000000000..3d949974a --- /dev/null +++ b/src/blockchain_db/sqlite/db_sqlite.h @@ -0,0 +1,113 @@ +// Copyright (c) 2021, The Oxen Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include "epee/misc_log_ex.h" +#include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_core/cryptonote_tx_utils.h" +#include "sqlitedb/database.hpp" +#include "common/fs.h" + +#include + +namespace cryptonote +{ + +fs::path check_if_copy_filename(std::string_view db_path); + +class BlockchainSQLite : public db::Database +{ +public: + explicit BlockchainSQLite(cryptonote::network_type nettype, fs::path db_path); + BlockchainSQLite(const BlockchainSQLite&) = delete; + + // Database management functions. Should be called on creation of BlockchainSQLite + void create_schema(); + void reset_database(); + + // The batching database maintains a height variable to know if it gets out of sync with the mainchain. Calling increment and decrement is the primary method of interacting with this height variable + void update_height(uint64_t new_height); + void increment_height(); + void decrement_height(); + + + // add_sn_payments/subtract_sn_payments -> passing an array of addresses and amounts. These will be added or subtracted to the database for each address specified. If the address does not exist it will be created. + bool add_sn_payments(std::vector& payments); + bool subtract_sn_payments(std::vector& payments); + + // get_payments -> passing a block height will return an array of payments that should be created in a coinbase transaction on that block given the current batching DB state. + std::optional> get_sn_payments(uint64_t block_height); + + // calculate_rewards -> takes the list of contributors from sn_info with their SN contribution amounts and will calculate how much of the block rewards should be the allocated to the contributors. The function will return a list suitable for passing to add_sn_payments + std::vector calculate_rewards(uint8_t hf_version, uint64_t distribution_amount, service_nodes::service_node_info sn_info); + + // add/pop_block -> takes a block that contains new block rewards to be batched and added to the database + // and/or batching payments that need to be subtracted from the database, in addition it takes a reference to + // the service node state which it will use to calculate the individual payouts. + // The function will then process this block add and subtracting to the batching DB appropriately. + // This is the primary entry point for the blockchain to add to the batching database. + // Each accepted block should call this passing in the SN list structure. + bool add_block(const cryptonote::block& block, const service_nodes::service_node_list::state_t& service_nodes_state); + bool pop_block(const cryptonote::block& block, const service_nodes::service_node_list::state_t& service_nodes_state); + + // validate_batch_payment -> used to make sure that list of miner_tx_vouts is correct. Compares the miner_tx_vouts with a list previously extracted payments to make sure that the correct persons are being paid. + bool validate_batch_payment(std::vector> miner_tx_vouts, std::vector calculated_payments_from_batching_db, uint64_t block_height); + + // these keep track of payments made to SN operators after then payment has been made. Allows for popping blocks back and knowing who got paid in those blocks. + // passing in a list of people to be marked as paid in the paid_amounts vector. Block height will be added to the batched_payments_paid database as height_paid. + bool save_payments(uint64_t block_height, std::vector paid_amounts); + std::vector get_block_payments(uint64_t block_height); + bool delete_block_payments(uint64_t block_height); + + // This is a debugging function. It prints the entire batching database to trace logs + void print_database(); + + uint64_t height; + +protected: + + cryptonote::network_type m_nettype; + std::string filename; + +}; + +class BlockchainSQLiteTest : public BlockchainSQLite +{ +public: + BlockchainSQLiteTest(cryptonote::network_type nettype, fs::path db_path); + BlockchainSQLiteTest(BlockchainSQLiteTest &other); + + // Helper functions, used in testing to assess the state of the database + uint64_t batching_count(); + std::optional retrieve_amount_by_address(const std::string& address); +}; + +} diff --git a/src/blockchain_utilities/CMakeLists.txt b/src/blockchain_utilities/CMakeLists.txt index 34068e108..dd796d716 100644 --- a/src/blockchain_utilities/CMakeLists.txt +++ b/src/blockchain_utilities/CMakeLists.txt @@ -34,6 +34,7 @@ target_link_libraries(blockchain_tools_common_libs INTERFACE version filesystem Boost::program_options + SQLiteCpp extra) diff --git a/src/blockchain_utilities/blockchain_ancestry.cpp b/src/blockchain_utilities/blockchain_ancestry.cpp index 0dd0e4d8c..94e4bdc23 100644 --- a/src/blockchain_utilities/blockchain_ancestry.cpp +++ b/src/blockchain_utilities/blockchain_ancestry.cpp @@ -458,7 +458,7 @@ int main(int argc, char* argv[]) LOG_PRINT_L0("Error opening database: " << e.what()); return 1; } - r = core_storage->init(db, nullptr /*ons_db*/, net_type); + r = core_storage->init(db, nullptr /*ons_db*/, nullptr, net_type); CHECK_AND_ASSERT_MES(r, 1, "Failed to initialize source blockchain storage"); LOG_PRINT_L0("Source blockchain storage initialized OK"); diff --git a/src/blockchain_utilities/blockchain_depth.cpp b/src/blockchain_utilities/blockchain_depth.cpp index 766e28a17..99d53077d 100644 --- a/src/blockchain_utilities/blockchain_depth.cpp +++ b/src/blockchain_utilities/blockchain_depth.cpp @@ -143,7 +143,7 @@ int main(int argc, char* argv[]) LOG_PRINT_L0("Error opening database: " << e.what()); return 1; } - r = core_storage->init(db, nullptr /*ons_db*/, net_type); + r = core_storage->init(db, nullptr /*ons_db*/, nullptr, net_type); CHECK_AND_ASSERT_MES(r, 1, "Failed to initialize source blockchain storage"); LOG_PRINT_L0("Source blockchain storage initialized OK"); diff --git a/src/blockchain_utilities/blockchain_export.cpp b/src/blockchain_utilities/blockchain_export.cpp index 15ae67143..2fe5b78ca 100644 --- a/src/blockchain_utilities/blockchain_export.cpp +++ b/src/blockchain_utilities/blockchain_export.cpp @@ -142,7 +142,7 @@ int main(int argc, char* argv[]) LOG_PRINT_L0("Error opening database: " << e.what()); return 1; } - r = core_storage->init(db, nullptr, opt_testnet ? cryptonote::TESTNET : opt_devnet ? cryptonote::DEVNET : cryptonote::MAINNET); + r = core_storage->init(db, nullptr, nullptr, opt_testnet ? cryptonote::TESTNET : opt_devnet ? cryptonote::DEVNET : cryptonote::MAINNET); if (core_storage->get_blockchain_pruning_seed() && !opt_blocks_dat) { diff --git a/src/blockchain_utilities/blockchain_stats.cpp b/src/blockchain_utilities/blockchain_stats.cpp index 55bb6bd1b..2d095b420 100644 --- a/src/blockchain_utilities/blockchain_stats.cpp +++ b/src/blockchain_utilities/blockchain_stats.cpp @@ -144,7 +144,7 @@ int main(int argc, char* argv[]) LOG_PRINT_L0("Error opening database: " << e.what()); return 1; } - r = core_storage->init(db, nullptr /*ons_db*/, net_type); + r = core_storage->init(db, nullptr /*ons_db*/, nullptr, net_type); CHECK_AND_ASSERT_MES(r, 1, "Failed to initialize source blockchain storage"); LOG_PRINT_L0("Source blockchain storage initialized OK"); diff --git a/src/blockchain_utilities/blockchain_usage.cpp b/src/blockchain_utilities/blockchain_usage.cpp index 76cdc96ea..c0e9f742b 100644 --- a/src/blockchain_utilities/blockchain_usage.cpp +++ b/src/blockchain_utilities/blockchain_usage.cpp @@ -172,7 +172,7 @@ int main(int argc, char* argv[]) return 1; } - r = core_storage->init(db, nullptr /*ons_db*/, net_type); + r = core_storage->init(db, nullptr /*ons_db*/, nullptr, net_type); CHECK_AND_ASSERT_MES(r, 1, "Failed to initialize source blockchain storage"); LOG_PRINT_L0("Source blockchain storage initialized OK"); diff --git a/src/checkpoints/checkpoints.cpp b/src/checkpoints/checkpoints.cpp index f49e23504..6fa52b0fc 100644 --- a/src/checkpoints/checkpoints.cpp +++ b/src/checkpoints/checkpoints.cpp @@ -313,7 +313,8 @@ namespace cryptonote for (size_t i = 0; i < oxen::array_count(HARDCODED_MAINNET_CHECKPOINTS); ++i) { height_to_hash const &checkpoint = HARDCODED_MAINNET_CHECKPOINTS[i]; - ADD_CHECKPOINT(checkpoint.height, checkpoint.hash); + bool added = add_checkpoint(checkpoint.height, checkpoint.hash); + CHECK_AND_ASSERT(added, false); } } diff --git a/src/checkpoints/checkpoints.h b/src/checkpoints/checkpoints.h index 0d557c0d0..01003e170 100644 --- a/src/checkpoints/checkpoints.h +++ b/src/checkpoints/checkpoints.h @@ -38,11 +38,11 @@ #include "cryptonote_basic/cryptonote_basic_impl.h" #include "common/fs.h" -#define ADD_CHECKPOINT(h, hash) CHECK_AND_ASSERT(add_checkpoint(h, hash), false); -#define JSON_HASH_FILE_NAME "checkpoints.json" namespace cryptonote { + constexpr std::string_view JSON_HASH_FILE_NAME = "checkpoints.json"sv; + enum struct checkpoint_type { hardcoded, diff --git a/src/cryptonote_basic/cryptonote_basic.cpp b/src/cryptonote_basic/cryptonote_basic.cpp index b8f8492ef..12a54f405 100644 --- a/src/cryptonote_basic/cryptonote_basic.cpp +++ b/src/cryptonote_basic/cryptonote_basic.cpp @@ -82,7 +82,10 @@ block::block(const block& b) : block_header(b), miner_tx{b.miner_tx}, tx_hashes{b.tx_hashes}, - signatures{b.signatures} + signatures{b.signatures}, + height{b.height}, + service_node_winner_key{b.service_node_winner_key}, + reward{b.reward} { copy_hash(b); } @@ -91,7 +94,10 @@ block::block(block&& b) : block_header(std::move(b)), miner_tx{std::move(b.miner_tx)}, tx_hashes{std::move(b.tx_hashes)}, - signatures{std::move(b.signatures)} + signatures{std::move(b.signatures)}, + height{std::move(b.height)}, + service_node_winner_key{std::move(b.service_node_winner_key)}, + reward{std::move(b.reward)} { copy_hash(b); } @@ -102,6 +108,9 @@ block& block::operator=(const block& b) miner_tx = b.miner_tx; tx_hashes = b.tx_hashes; signatures = b.signatures; + height = b.height; + service_node_winner_key = b.service_node_winner_key; + reward = b.reward; copy_hash(b); return *this; } @@ -111,6 +120,9 @@ block& block::operator=(block&& b) miner_tx = std::move(b.miner_tx); tx_hashes = std::move(b.tx_hashes); signatures = std::move(b.signatures); + height = std::move(b.height); + service_node_winner_key = std::move(b.service_node_winner_key); + reward = std::move(b.reward); copy_hash(b); return *this; } @@ -124,4 +136,23 @@ void block::set_hash_valid(bool v) const hash_valid.store(v,std::memory_order_release); } +// Convert the address to an integer and then performs (address % interval) +// it does this by taking the first 64 bits of the public_view_key and converting to an integer +// This is used to determine when an address gets paid their batching reward. +uint64_t account_public_address::modulus(uint64_t interval) const +{ + uint64_t address_as_integer = 0; + std::memcpy(&address_as_integer, m_view_public_key.data, sizeof(address_as_integer)); + boost::endian::native_to_little_inplace(address_as_integer); + return address_as_integer % interval; +} + +uint64_t account_public_address::next_payout_height(uint64_t current_height, uint64_t interval) const +{ + uint64_t next_payout_height = current_height + (modulus(interval) - current_height % interval); + if (next_payout_height <= current_height) + next_payout_height += interval; + return next_payout_height; +} + } diff --git a/src/cryptonote_basic/cryptonote_basic.h b/src/cryptonote_basic/cryptonote_basic.h index 3a04fca31..e94b13742 100644 --- a/src/cryptonote_basic/cryptonote_basic.h +++ b/src/cryptonote_basic/cryptonote_basic.h @@ -208,7 +208,9 @@ namespace cryptonote FIELD(vin) FIELD(vout) if (version >= txversion::v3_per_output_unlock_times && vout.size() != output_unlock_times.size()) + { throw std::invalid_argument{"v3 tx without correct unlock times"}; + } FIELD(extra) if (version >= txversion::v4_tx_types) ENUM_FIELD_N("type", type, type < txtype::_count); @@ -428,6 +430,9 @@ namespace cryptonote void set_hash_valid(bool v) const; transaction miner_tx; + size_t height; + crypto::public_key service_node_winner_key; + uint64_t reward = 0; std::vector tx_hashes; // hash cache @@ -445,6 +450,12 @@ namespace cryptonote throw std::invalid_argument{"too many txs in block"}; if (major_version >= cryptonote::network_version_16_pulse) FIELD(signatures) + if (major_version >= cryptonote::network_version_19) + { + VARINT_FIELD(height) + FIELD(service_node_winner_key) + FIELD(reward) + } END_SERIALIZE() }; @@ -477,6 +488,10 @@ namespace cryptonote { return !(*this == rhs); } + + uint64_t modulus(uint64_t interval) const; + uint64_t next_payout_height(uint64_t current_height, uint64_t interval) const; + }; inline constexpr account_public_address null_address{}; diff --git a/src/cryptonote_basic/cryptonote_basic_impl.cpp b/src/cryptonote_basic/cryptonote_basic_impl.cpp index c088521bf..d8e6ceba2 100644 --- a/src/cryptonote_basic/cryptonote_basic_impl.cpp +++ b/src/cryptonote_basic/cryptonote_basic_impl.cpp @@ -164,6 +164,17 @@ namespace cryptonote { return true; } //------------------------------------------------------------------------------------ + batch_sn_payment::batch_sn_payment(std::string addr, uint64_t amt, cryptonote::network_type nettype):address(addr),amount(amt){ + cryptonote::get_account_address_from_str(address_info, nettype, address); + }; + batch_sn_payment::batch_sn_payment(cryptonote::address_parse_info& addr_info, uint64_t amt, cryptonote::network_type nettype):address_info(addr_info),amount(amt){ + address = cryptonote::get_account_address_as_str(nettype, address_info.is_subaddress, address_info.address); + }; + batch_sn_payment::batch_sn_payment(const cryptonote::account_public_address& addr, uint64_t amt, cryptonote::network_type nettype):amount(amt){ + address_info = cryptonote::address_parse_info{addr,0}; + address = cryptonote::get_account_address_as_str(nettype, address_info.is_subaddress, address_info.address); + }; + //------------------------------------------------------------------------------------ uint8_t get_account_address_checksum(const public_address_outer_blob& bl) { const unsigned char* pbuf = reinterpret_cast(&bl); diff --git a/src/cryptonote_basic/cryptonote_basic_impl.h b/src/cryptonote_basic/cryptonote_basic_impl.h index 9f67e6e14..1951f724d 100644 --- a/src/cryptonote_basic/cryptonote_basic_impl.h +++ b/src/cryptonote_basic/cryptonote_basic_impl.h @@ -54,10 +54,32 @@ namespace cryptonote { virtual void init() = 0; }; + struct address_parse_info + { + account_public_address address; + bool is_subaddress; + bool has_payment_id; + crypto::hash8 payment_id; + + std::string as_str(network_type nettype) const; + + KV_MAP_SERIALIZABLE + }; + + struct batch_sn_payment { + std::string address; + cryptonote::address_parse_info address_info; + uint64_t amount; + batch_sn_payment() = default; + batch_sn_payment(std::string addr, uint64_t amt, cryptonote::network_type nettype); + batch_sn_payment(cryptonote::address_parse_info& addr_info, uint64_t amt, cryptonote::network_type nettype); + batch_sn_payment(const cryptonote::account_public_address& addr, uint64_t amt, cryptonote::network_type nettype); + }; + class ValidateMinerTxHook { public: - virtual bool validate_miner_tx(cryptonote::block const &block, struct block_reward_parts const &reward_parts) const = 0; + virtual bool validate_miner_tx(cryptonote::block const &block, struct block_reward_parts const &reward_parts, std::optional> const &batched_sn_payments) const = 0; }; class AltBlockAddedHook @@ -89,17 +111,7 @@ namespace cryptonote { return addresses[0]; } - struct address_parse_info - { - account_public_address address; - bool is_subaddress; - bool has_payment_id; - crypto::hash8 payment_id; - std::string as_str(network_type nettype) const; - - KV_MAP_SERIALIZABLE - }; /************************************************************************/ /* Cryptonote helper functions */ diff --git a/src/cryptonote_basic/cryptonote_boost_serialization.h b/src/cryptonote_basic/cryptonote_boost_serialization.h index 9523ef4b3..7f860a3e3 100644 --- a/src/cryptonote_basic/cryptonote_boost_serialization.h +++ b/src/cryptonote_basic/cryptonote_boost_serialization.h @@ -215,6 +215,11 @@ namespace boost //------------------ a & b.miner_tx; a & b.tx_hashes; + if (ver < 19) + return; + a & b.height; + a & b.service_node_winner_key; + a & b.reward; } template diff --git a/src/cryptonote_basic/cryptonote_format_utils.cpp b/src/cryptonote_basic/cryptonote_format_utils.cpp index 7e6940603..d4f3f57fe 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.cpp +++ b/src/cryptonote_basic/cryptonote_format_utils.cpp @@ -868,9 +868,19 @@ namespace cryptonote //--------------------------------------------------------------- uint64_t get_block_height(const block& b) { - CHECK_AND_ASSERT_MES(b.miner_tx.vin.size() == 1, 0, "wrong miner tx in block: " << get_block_hash(b) << ", b.miner_tx.vin.size() != 1 (size is: " << b.miner_tx.vin.size() << ")"); - CHECKED_GET_SPECIFIC_VARIANT(b.miner_tx.vin[0], txin_gen, coinbase_in, 0); - return coinbase_in.height; + cryptonote::block bl = b; + if (b.miner_tx.vout.size() > 0) + { + CHECK_AND_ASSERT_MES(b.miner_tx.vin.size() == 1, 0, "wrong miner tx in block: " << get_block_hash(b) << ", b.miner_tx.vin.size() != 1 (size is: " << b.miner_tx.vin.size() << ")"); + CHECKED_GET_SPECIFIC_VARIANT(b.miner_tx.vin[0], txin_gen, coinbase_in, 0); + if (b.major_version >= cryptonote::network_version_19) + { + CHECK_AND_ASSERT_MES(coinbase_in.height == b.height, 0, "wrong miner tx in block: " << get_block_hash(b)); + } + return coinbase_in.height; + } else { + return b.height; + } } //--------------------------------------------------------------- bool check_inputs_types_supported(const transaction& tx) @@ -880,7 +890,6 @@ namespace cryptonote CHECK_AND_ASSERT_MES(std::holds_alternative(in), false, "wrong variant type: " << tools::type_name(tools::variant_type(in)) << ", expected " << tools::type_name() << ", in transaction id=" << get_transaction_hash(tx)); - } return true; } diff --git a/src/cryptonote_basic/hardfork.cpp b/src/cryptonote_basic/hardfork.cpp index a85df9b05..b2213f70e 100644 --- a/src/cryptonote_basic/hardfork.cpp +++ b/src/cryptonote_basic/hardfork.cpp @@ -65,6 +65,7 @@ static constexpr std::array testnet_hard_forks = hard_fork{17, 0, 447275, 1608276840 }, // 2020-12-18 05:34 UTC hard_fork{18, 0, 501750, 1616631051 }, // 2021-03-25 12:10 UTC hard_fork{18, 1, 578637, 1624040400 }, // 2021-06-18 18:20 UTC + hard_fork{19, 0, 732355, 1650402545 }, }; static constexpr std::array devnet_hard_forks = @@ -74,7 +75,10 @@ static constexpr std::array devnet_hard_forks = hard_fork{ 12, 0, 3, 1599848400 }, hard_fork{ 13, 0, 4, 1599848400 }, hard_fork{ 15, 0, 5, 1599848400 }, - hard_fork{ 16, 0, 99, 1599848400 }, + hard_fork{ 16, 0, 100, 1599848400 }, + hard_fork{ 17, 0, 151, 1599848400 }, + hard_fork{ 18, 0, 152, 1599848400 }, + hard_fork{ 19, 0, 153, 1599848400 }, }; diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index c46e574ba..eb83d0a1c 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -257,6 +257,13 @@ namespace config inline constexpr std::string_view HASH_KEY_CLSAG_AGG_0 = "CLSAG_agg_0"sv; inline constexpr std::string_view HASH_KEY_CLSAG_AGG_1 = "CLSAG_agg_1"sv; + //Batching SN Rewards + inline constexpr uint64_t BATCHING_INTERVAL = 2520; + inline constexpr uint64_t MIN_BATCH_PAYMENT_AMOUNT = 1'000'000'000; // 1 OXEN (in atomic units) + inline constexpr uint64_t LIMIT_BATCH_OUTPUTS = 15; + // If a node has been online for this amount of blocks they will receive SN rewards + inline constexpr uint64_t SERVICE_NODE_PAYABLE_AFTER_BLOCKS = 720; + namespace testnet { inline constexpr uint64_t HEIGHT_ESTIMATE_HEIGHT = 339767; @@ -284,6 +291,8 @@ namespace config // Testnet uptime proofs are 6x faster than mainnet (devnet config also uses these) inline constexpr auto UPTIME_PROOF_FREQUENCY = 10min; inline constexpr auto UPTIME_PROOF_VALIDITY = 21min; + inline constexpr uint64_t BATCHING_INTERVAL = 20; + inline constexpr uint64_t SERVICE_NODE_PAYABLE_AFTER_BLOCKS = 4; } namespace devnet @@ -390,6 +399,12 @@ namespace cryptonote std::chrono::seconds UPTIME_PROOF_FREQUENCY; std::chrono::seconds UPTIME_PROOF_VALIDITY; + uint64_t BATCHING_INTERVAL; + uint64_t MIN_BATCH_PAYMENT_AMOUNT; + uint64_t LIMIT_BATCH_OUTPUTS; + uint64_t SERVICE_NODE_PAYABLE_AFTER_BLOCKS; + + inline constexpr std::string_view governance_wallet_address(int hard_fork_version) const { const auto wallet_switch = (NETWORK_TYPE == MAINNET || NETWORK_TYPE == FAKECHAIN) @@ -419,6 +434,10 @@ namespace cryptonote config::UPTIME_PROOF_CHECK_INTERVAL, config::UPTIME_PROOF_FREQUENCY, config::UPTIME_PROOF_VALIDITY, + config::BATCHING_INTERVAL, + config::MIN_BATCH_PAYMENT_AMOUNT, + config::LIMIT_BATCH_OUTPUTS, + config::SERVICE_NODE_PAYABLE_AFTER_BLOCKS, }; inline constexpr network_config testnet_config{ TESTNET, @@ -441,6 +460,10 @@ namespace cryptonote config::UPTIME_PROOF_CHECK_INTERVAL, config::testnet::UPTIME_PROOF_FREQUENCY, config::testnet::UPTIME_PROOF_VALIDITY, + ::config::testnet::BATCHING_INTERVAL, + config::MIN_BATCH_PAYMENT_AMOUNT, + config::LIMIT_BATCH_OUTPUTS, + ::config::testnet::SERVICE_NODE_PAYABLE_AFTER_BLOCKS, }; inline constexpr network_config devnet_config{ DEVNET, @@ -463,6 +486,10 @@ namespace cryptonote config::UPTIME_PROOF_CHECK_INTERVAL, config::testnet::UPTIME_PROOF_FREQUENCY, config::testnet::UPTIME_PROOF_VALIDITY, + ::config::testnet::BATCHING_INTERVAL, + config::MIN_BATCH_PAYMENT_AMOUNT, + config::LIMIT_BATCH_OUTPUTS, + ::config::testnet::SERVICE_NODE_PAYABLE_AFTER_BLOCKS, }; inline constexpr network_config fakenet_config{ FAKECHAIN, @@ -485,6 +512,10 @@ namespace cryptonote config::fakechain::UPTIME_PROOF_CHECK_INTERVAL, config::fakechain::UPTIME_PROOF_FREQUENCY, config::fakechain::UPTIME_PROOF_VALIDITY, + ::config::testnet::BATCHING_INTERVAL, + config::MIN_BATCH_PAYMENT_AMOUNT, + config::LIMIT_BATCH_OUTPUTS, + ::config::testnet::SERVICE_NODE_PAYABLE_AFTER_BLOCKS, }; inline constexpr const network_config& get_config(network_type nettype) diff --git a/src/cryptonote_core/CMakeLists.txt b/src/cryptonote_core/CMakeLists.txt index d7f2284d3..8d5f7dcb2 100644 --- a/src/cryptonote_core/CMakeLists.txt +++ b/src/cryptonote_core/CMakeLists.txt @@ -55,6 +55,7 @@ target_link_libraries(cryptonote_core SQLite::SQLite3 PRIVATE Boost::program_options + SQLiteCpp systemd extra) diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index c7ee99d86..50a28798d 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include "common/rules.h" #include "common/hex.h" @@ -300,22 +301,35 @@ uint64_t Blockchain::get_current_blockchain_height(bool lock) const //------------------------------------------------------------------ bool Blockchain::load_missing_blocks_into_oxen_subsystems() { - uint64_t const snl_height = std::max(hard_fork_begins(m_nettype, network_version_9_service_nodes).value_or(0), m_service_node_list.height() + 1); - uint64_t const ons_height = std::max(hard_fork_begins(m_nettype, network_version_15_ons).value_or(0), m_ons_db.height() + 1); - uint64_t const end_height = m_db->height(); - uint64_t const start_height = std::min(end_height, std::min(ons_height, snl_height)); + std::vector start_height_options; + uint64_t const snl_height = std::max(hard_fork_begins(m_nettype, network_version_9_service_nodes).value_or(0), m_service_node_list.height() + 1); + start_height_options.push_back(snl_height); + uint64_t const ons_height = std::max(hard_fork_begins(m_nettype, network_version_15_ons).value_or(0), m_ons_db.height() + 1); + start_height_options.push_back(ons_height); + uint64_t sqlite_height = 0; + if (m_sqlite_db) + { + sqlite_height = std::max(hard_fork_begins(m_nettype, network_version_19).value_or(0) - 1, m_sqlite_db->height + 1); + start_height_options.push_back(sqlite_height); + } else { + if (m_nettype != FAKECHAIN) + throw std::logic_error("Blockchain missing SQLite Database"); + } + uint64_t const end_height = m_db->height(); + start_height_options.push_back(end_height); + uint64_t const start_height = *std::min_element(start_height_options.begin(), start_height_options.end()); int64_t const total_blocks = static_cast(end_height) - static_cast(start_height); if (total_blocks <= 0) return true; if (total_blocks > 1) - MGINFO("Loading blocks into oxen subsystems, scanning blockchain from height: " << start_height << " to: " << end_height << " (snl: " << snl_height << ", ons: " << ons_height << ")"); + MGINFO("Loading blocks into oxen subsystems, scanning blockchain from height: " << start_height << " to: " << end_height << " (snl: " << snl_height << ", ons: " << ons_height << ", sqlite: " << sqlite_height << ")"); using clock = std::chrono::steady_clock; using work_time = std::chrono::duration; int64_t constexpr BLOCK_COUNT = 1000; auto work_start = clock::now(); auto scan_start = work_start; - work_time ons_duration{}, snl_duration{}, ons_iteration_duration{}, snl_iteration_duration{}; + work_time ons_duration{}, snl_duration{}, sqlite_duration{}, ons_iteration_duration{}, snl_iteration_duration{}, sqlite_iteration_duration{}; std::vector blocks; std::vector txs; @@ -330,7 +344,7 @@ bool Blockchain::load_missing_blocks_into_oxen_subsystems() { m_service_node_list.store(); auto duration = work_time{clock::now() - work_start}; - MGINFO("... scanning height " << start_height + (index * BLOCK_COUNT) << " (" << duration.count() << "s) (snl: " << snl_iteration_duration.count() << "s; ons: " << ons_iteration_duration.count() << "s)"); + MGINFO("... scanning height " << start_height + (index * BLOCK_COUNT) << " (" << duration.count() << "s) (snl: " << snl_iteration_duration.count() << "s; ons: " << ons_iteration_duration.count() << "s; sqlite: " << sqlite_iteration_duration.count() << "s)"); #ifdef ENABLE_SYSTEMD // Tell systemd that we're doing something so that it should let us continue starting up // (giving us 120s until we have to send the next notification): @@ -340,6 +354,7 @@ bool Blockchain::load_missing_blocks_into_oxen_subsystems() ons_duration += ons_iteration_duration; snl_duration += snl_iteration_duration; + sqlite_duration += sqlite_iteration_duration; ons_iteration_duration = snl_iteration_duration = {}; } @@ -390,13 +405,24 @@ bool Blockchain::load_missing_blocks_into_oxen_subsystems() } ons_iteration_duration += clock::now() - ons_start; } + + if (m_sqlite_db && (block_height >= sqlite_height)) + { + auto sqlite_start = clock::now(); + if (!m_service_node_list.process_batching_rewards(blk)) + { + MFATAL("Unable to process block for updating SQLite DB: " << cryptonote::get_block_hash(blk)); + return false; + } + sqlite_iteration_duration += clock::now() - sqlite_start; + } } } if (total_blocks > 1) { auto duration = work_time{clock::now() - scan_start}; - MGINFO("Done recalculating oxen subsystems (" << duration.count() << "s) (snl: " << snl_duration.count() << "s; ons: " << ons_duration.count() << "s)"); + MGINFO("Done recalculating oxen subsystems (" << duration.count() << "s) (snl: " << snl_duration.count() << "s; ons: " << ons_duration.count() << "s)" << "s; sqlite: " << sqlite_duration.count() << "s)"); } if (total_blocks > 0) @@ -407,7 +433,8 @@ bool Blockchain::load_missing_blocks_into_oxen_subsystems() //------------------------------------------------------------------ //FIXME: possibly move this into the constructor, to avoid accidentally // dereferencing a null BlockchainDB pointer -bool Blockchain::init(BlockchainDB* db, sqlite3 *ons_db, const network_type nettype, bool offline, const cryptonote::test_options *test_options, difficulty_type fixed_difficulty, const GetCheckpointsCallback& get_checkpoints/* = nullptr*/) +bool Blockchain::init(BlockchainDB* db, sqlite3 *ons_db, std::shared_ptr sqlite_db, const network_type nettype, bool offline, const cryptonote::test_options *test_options, difficulty_type fixed_difficulty, const GetCheckpointsCallback& get_checkpoints/* = nullptr*/) + { LOG_PRINT_L3("Blockchain::" << __func__); @@ -439,6 +466,13 @@ bool Blockchain::init(BlockchainDB* db, sqlite3 *ons_db, const network_type nett if (test_options) // Fakechain mode fakechain_hardforks = test_options->hard_forks; + if (sqlite_db) + { + m_sqlite_db = std::move(sqlite_db); + } else { + if (m_nettype != FAKECHAIN) + throw std::logic_error("Blockchain missing SQLite Database"); + } // if the blockchain is new, add the genesis block // this feels kinda kludgy to do it this way, but can be looked at later. @@ -511,6 +545,11 @@ bool Blockchain::init(BlockchainDB* db, sqlite3 *ons_db, const network_type nett try { m_db->pop_block(popped_block, popped_txs); + if (!m_service_node_list.pop_batching_rewards_block(popped_block)) + { + LOG_ERROR("Failed to pop to batch rewards DB. throwing"); + throw; + } } // anything that could cause this to throw is likely catastrophic, // so we re-throw @@ -550,6 +589,7 @@ bool Blockchain::init(BlockchainDB* db, sqlite3 *ons_db, const network_type nett return false; } + hook_block_added(m_checkpoints); hook_blockchain_detached(m_checkpoints); for (InitHook* hook : m_init_hooks) @@ -692,6 +732,7 @@ block Blockchain::pop_block_from_blockchain() CHECK_AND_ASSERT_THROW_MES(m_db->height() > 1, "Cannot pop the genesis block"); + try { m_db->pop_block(popped_block, popped_txs); @@ -708,6 +749,11 @@ block Blockchain::pop_block_from_blockchain() LOG_ERROR("Error popping block from blockchain, throwing!"); throw; } + if (!m_service_node_list.pop_batching_rewards_block(popped_block)) + { + LOG_ERROR("Failed to pop to batch rewards DB. throwing"); + throw; + } m_ons_db.block_detach(*this, m_db->height()); @@ -1219,46 +1265,49 @@ difficulty_type Blockchain::get_difficulty_for_alternative_chain(const std::list bool Blockchain::prevalidate_miner_transaction(const block& b, uint64_t height, uint8_t hf_version) { LOG_PRINT_L3("Blockchain::" << __func__); - CHECK_AND_ASSERT_MES(b.miner_tx.vin.size() == 1, false, "coinbase transaction in the block has no inputs"); - CHECK_AND_ASSERT_MES(std::holds_alternative(b.miner_tx.vin[0]), false, "coinbase transaction in the block has the wrong type"); - if (var::get(b.miner_tx.vin[0]).height != height) + if (b.miner_tx.vout.size() > 0) { - MWARNING("The miner transaction in block has invalid height: " << var::get(b.miner_tx.vin[0]).height << ", expected: " << height); - return false; - } - MDEBUG("Miner tx hash: " << get_transaction_hash(b.miner_tx)); - CHECK_AND_ASSERT_MES(b.miner_tx.unlock_time == height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW, false, "coinbase transaction transaction has the wrong unlock time=" << b.miner_tx.unlock_time << ", expected " << height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW); - - if (hf_version >= cryptonote::network_version_12_checkpointing) - { - if (b.miner_tx.type != txtype::standard) + CHECK_AND_ASSERT_MES(b.miner_tx.vin.size() == 1, false, "coinbase transaction in the block has no inputs"); + CHECK_AND_ASSERT_MES(std::holds_alternative(b.miner_tx.vin[0]), false, "coinbase transaction in the block has the wrong type"); + if (var::get(b.miner_tx.vin[0]).height != height) { - MERROR("Coinbase invalid transaction type for coinbase transaction."); + MWARNING("The miner transaction in block has invalid height: " << var::get(b.miner_tx.vin[0]).height << ", expected: " << height); return false; } + MDEBUG("Miner tx hash: " << get_transaction_hash(b.miner_tx)); + CHECK_AND_ASSERT_MES(b.miner_tx.unlock_time == height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW, false, "coinbase transaction transaction has the wrong unlock time=" << b.miner_tx.unlock_time << ", expected " << height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW); - txversion min_version = transaction::get_max_version_for_hf(hf_version); - txversion max_version = transaction::get_min_version_for_hf(hf_version); - if (b.miner_tx.version < min_version || b.miner_tx.version > max_version) + if (hf_version >= cryptonote::network_version_12_checkpointing) { - MERROR_VER("Coinbase invalid version: " << b.miner_tx.version << " for hardfork: " << hf_version << " min/max version: " << min_version << "/" << max_version); + if (b.miner_tx.type != txtype::standard) + { + MERROR("Coinbase invalid transaction type for coinbase transaction."); + return false; + } + + txversion min_version = transaction::get_max_version_for_hf(hf_version); + txversion max_version = transaction::get_min_version_for_hf(hf_version); + if (b.miner_tx.version < min_version || b.miner_tx.version > max_version) + { + MERROR_VER("Coinbase invalid version: " << b.miner_tx.version << " for hardfork: " << hf_version << " min/max version: " << min_version << "/" << max_version); + return false; + } + } + + if (hf_version >= HF_VERSION_REJECT_SIGS_IN_COINBASE) // Enforce empty rct signatures for miner transactions, + CHECK_AND_ASSERT_MES(b.miner_tx.rct_signatures.type == rct::RCTType::Null, false, "RingCT signatures not allowed in coinbase transactions"); + + //check outs overflow + //NOTE: not entirely sure this is necessary, given that this function is + // designed simply to make sure the total amount for a transaction + // does not overflow a uint64_t, and this transaction *is* a uint64_t... + if(!check_outs_overflow(b.miner_tx)) + { + MERROR("miner transaction has money overflow in block " << get_block_hash(b)); return false; } } - if (hf_version >= HF_VERSION_REJECT_SIGS_IN_COINBASE) // Enforce empty rct signatures for miner transactions, - CHECK_AND_ASSERT_MES(b.miner_tx.rct_signatures.type == rct::RCTType::Null, false, "RingCT signatures not allowed in coinbase transactions"); - - //check outs overflow - //NOTE: not entirely sure this is necessary, given that this function is - // designed simply to make sure the total amount for a transaction - // does not overflow a uint64_t, and this transaction *is* a uint64_t... - if(!check_outs_overflow(b.miner_tx)) - { - MERROR("miner transaction has money overflow in block " << get_block_hash(b)); - return false; - } - return true; } //------------------------------------------------------------------ @@ -1269,8 +1318,10 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl //validate reward uint64_t const money_in_use = get_outs_money_amount(b.miner_tx); if (b.miner_tx.vout.size() == 0) { - MERROR_VER("miner tx has no outputs"); - return false; + if (b.major_version < cryptonote::network_version_19) { + MERROR_VER("miner tx has no outputs"); + return false; + } } uint64_t median_weight; @@ -1296,21 +1347,20 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl return false; } - block_reward_parts reward_parts; + block_reward_parts reward_parts{0}; if (!get_oxen_block_reward(median_weight, cumulative_block_weight, already_generated_coins, version, reward_parts, block_reward_context)) - { - MERROR_VER("block weight " << cumulative_block_weight << " is bigger than allowed for this blockchain"); return false; - } + auto batched_sn_payments = m_sqlite_db->get_sn_payments(height); + cryptonote::block bl = b; for (ValidateMinerTxHook* hook : m_validate_miner_tx_hooks) { - if (!hook->validate_miner_tx(b, reward_parts)) + if (!hook->validate_miner_tx(b, reward_parts, batched_sn_payments)) return false; } - if (already_generated_coins != 0 && block_has_governance_output(nettype(), b)) + if (already_generated_coins != 0 && block_has_governance_output(nettype(), b) && version < cryptonote::network_version_19) { if (version >= network_version_10_bulletproofs && reward_parts.governance_paid == 0) { @@ -1324,6 +1374,7 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl return false; } + if (!validate_governance_reward_key( m_db->height(), cryptonote::get_config(m_nettype).governance_wallet_address(version), @@ -1338,8 +1389,16 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl // +1 here to allow a 1 atomic unit error in the calculation (which can happen because of floating point errors or rounding) // TODO(oxen): eliminate all floating point math in reward calculations. - uint64_t max_base_reward = reward_parts.base_miner + reward_parts.governance_paid + reward_parts.service_node_total + 1; + uint64_t max_base_reward = reward_parts.governance_paid + 1; + + if (version >= network_version_19 && batched_sn_payments.has_value()) { + max_base_reward += std::accumulate(batched_sn_payments->begin(), batched_sn_payments->end(), uint64_t{0}, [&](auto a, auto b){return a + b.amount;}); + } else { + max_base_reward += reward_parts.base_miner + reward_parts.service_node_total; + } + uint64_t max_money_in_use = max_base_reward + reward_parts.miner_fee; + if (money_in_use > max_money_in_use) { MERROR_VER("coinbase transaction spends too much money (" << print_money(money_in_use) << "). Maximum block reward is " @@ -1347,8 +1406,17 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl return false; } - CHECK_AND_ASSERT_MES(money_in_use >= reward_parts.miner_fee, false, "base reward calculation bug"); - base_reward = money_in_use - reward_parts.miner_fee; + if (version < network_version_19) { + CHECK_AND_ASSERT_MES(money_in_use >= reward_parts.miner_fee, false, "base reward calculation bug"); + base_reward = money_in_use - reward_parts.miner_fee; + } + + if (b.reward > reward_parts.base_miner + reward_parts.miner_fee + reward_parts.service_node_total) + { + MERROR_VER("block reward to be batched spends too much money (" << print_money(b.reward) << "). Maximum block reward is " + << print_money(max_money_in_use) << " (= " << print_money(max_base_reward) << " base + " << print_money(reward_parts.miner_fee) << " fees)"); + return false; + } return true; } @@ -1465,6 +1533,7 @@ bool Blockchain::create_block_template_internal(block& b, const crypto::hash *fr invalidate_block_template_cache(); } + // from_block is usually nullptr, used to build altchains if (from_block) { //build alternative subchain, front -> mainchain, back -> alternative head @@ -1524,6 +1593,7 @@ bool Blockchain::create_block_template_internal(block& b, const crypto::hash *fr } else { + // Creates the block template for next block on main chain height = m_db->height(); auto [maj, min] = get_ideal_block_version(m_nettype, height); b.major_version = maj; @@ -1543,8 +1613,11 @@ bool Blockchain::create_block_template_internal(block& b, const crypto::hash *fr CHECK_AND_ASSERT_MES(diffic, false, "difficulty overhead."); + uint8_t hf_version = b.major_version; size_t txs_weight; uint64_t fee; + + // Add transactions in mempool to block if (!m_tx_pool.fill_block_template(b, median_weight, already_generated_coins, txs_weight, fee, expected_reward, b.major_version, height)) { return false; @@ -1556,7 +1629,6 @@ bool Blockchain::create_block_template_internal(block& b, const crypto::hash *fr block weight, so first miner transaction generated with fake amount of money, and with phase we know think we know expected block weight */ //make blocks coin-base tx looks close to real coinbase tx to get truthful blob weight - uint8_t hf_version = b.major_version; auto miner_tx_context = info.is_miner ? oxen_miner_tx_context::miner_block(m_nettype, info.miner_address, m_service_node_list.get_block_leader()) @@ -1567,13 +1639,22 @@ bool Blockchain::create_block_template_internal(block& b, const crypto::hash *fr return false; } - bool r = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, b.miner_tx, miner_tx_context, ex_nonce, hf_version); + // This will check the batching database for who is due to be paid out in this block + std::optional> sn_rwds = std::nullopt; + if (hf_version >= cryptonote::network_version_19) + { + sn_rwds = m_sqlite_db->get_sn_payments(height); //Rewards to pay out + } + + uint64_t block_rewards = 0; + bool r; + std::tie(r, block_rewards) = construct_miner_tx(height, median_weight, already_generated_coins, txs_weight, fee, b.miner_tx, miner_tx_context, sn_rwds, ex_nonce, 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); for (size_t try_count = 0; try_count != 10; ++try_count) { - r = construct_miner_tx(height, median_weight, already_generated_coins, cumulative_weight, fee, b.miner_tx, miner_tx_context, ex_nonce, hf_version); + std::tie(r, block_rewards) = construct_miner_tx(height, median_weight, already_generated_coins, cumulative_weight, fee, b.miner_tx, miner_tx_context, sn_rwds, ex_nonce, hf_version); CHECK_AND_ASSERT_MES(r, false, "Failed to construct miner tx, second chance"); size_t coinbase_weight = get_transaction_weight(b.miner_tx); @@ -1606,6 +1687,14 @@ bool Blockchain::create_block_template_internal(block& b, const crypto::hash *fr if (!from_block) cache_block_template(b, info.miner_address, ex_nonce, diffic, height, expected_reward, pool_cookie); + + if (miner_tx_context.pulse) + b.service_node_winner_key = miner_tx_context.pulse_block_producer.key; + else + b.service_node_winner_key = {0}; + + b.reward = block_rewards; + b.height = height; return true; } LOG_ERROR("Failed to create_block_template with " << 10 << " tries"); @@ -2857,6 +2946,7 @@ size_t Blockchain::get_total_transactions() const // m_db functions which do not depend on one another (ie, no getheight + gethash(height-1), as // well as not accessing class members, even read only (ie, m_invalid_blocks). The caller must // lock if it is otherwise needed. + return m_db->get_tx_count(); } //------------------------------------------------------------------ @@ -3100,9 +3190,12 @@ bool Blockchain::have_tx_keyimges_as_spent(const transaction &tx) const LOG_PRINT_L3("Blockchain::" << __func__); for (const txin_v& in: tx.vin) { - CHECKED_GET_SPECIFIC_VARIANT(in, txin_to_key, in_to_key, true); - if(have_tx_keyimg_as_spent(in_to_key.k_image)) - return true; + if (!std::holds_alternative(in)) + { + CHECKED_GET_SPECIFIC_VARIANT(in, txin_to_key, in_to_key, true); + if(have_tx_keyimg_as_spent(in_to_key.k_image)) + return true; + } } return false; } @@ -3221,7 +3314,7 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc, if (tx.is_transfer()) { - if (tx.type != txtype::oxen_name_system && hf_version >= HF_VERSION_MIN_2_OUTPUTS && tx.vout.size() < 2) + if (tx.type != txtype::oxen_name_system && !std::holds_alternative(tx.vin[0]) && hf_version >= HF_VERSION_MIN_2_OUTPUTS && tx.vout.size() < 2) { MERROR_VER("Tx " << get_transaction_hash(tx) << " has fewer than two outputs, which is not allowed as of hardfork " << +HF_VERSION_MIN_2_OUTPUTS); tvc.m_too_few_outputs = true; @@ -3338,8 +3431,12 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc, { case rct::RCTType::Null: { // we only accept no signatures for coinbase txes - MERROR_VER("Null rct signature on non-coinbase tx"); - return false; + if (!std::holds_alternative(tx.vin[0])) + { + MERROR_VER("Null rct signature on non-coinbase tx"); + return false; + } + break; } case rct::RCTType::Simple: case rct::RCTType::Bulletproof: @@ -4224,6 +4321,7 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& TIME_MEASURE_START(aa); // XXX old code does not check whether tx exists + if (m_db->tx_exists(tx_id)) { MGINFO_RED("Block with id: " << id << " attempting to add transaction already in blockchain with id: " << tx_id); @@ -4407,6 +4505,18 @@ bool Blockchain::handle_block_to_main_chain(const block& bl, const crypto::hash& MGINFO_RED("Failed to add block to ONS DB."); bvc.m_verifivation_failed = true; return false; + } + + if (m_sqlite_db) { + if (!m_service_node_list.process_batching_rewards(bl)) + { + MERROR("Failed to add block to batch rewards DB."); + bvc.m_verifivation_failed = true; + return false; + } + } else { + if (m_nettype != FAKECHAIN) + throw std::logic_error("Blockchain missing SQLite Database"); } for (BlockAddedHook* hook : m_block_added_hooks) @@ -4938,6 +5048,13 @@ bool Blockchain::calc_batched_governance_reward(uint64_t height, uint64_t &rewar return true; } + // Constant reward every block at HF19 and batched through service node batching + if (hard_fork_version >= cryptonote::network_version_19) + { + reward = cryptonote::governance_reward_formula(hard_fork_version); + return true; + } + // Ignore governance reward and payout instead the last // GOVERNANCE_BLOCK_REWARD_INTERVAL number of blocks governance rewards. We // come back for this height's rewards in the next interval. The reward is @@ -4957,8 +5074,7 @@ bool Blockchain::calc_batched_governance_reward(uint64_t height, uint64_t &rewar } uint64_t start_height = height - num_blocks; - if (height < num_blocks) - { + if (height < num_blocks) { start_height = 0; num_blocks = height; } @@ -5193,14 +5309,18 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector(txin); - // check for duplicate - auto it = its->second.find(in_to_key.k_image); - if (it != its->second.end()) - SCAN_TABLE_QUIT("Duplicate key_image found from incoming blocks."); + if (!std::holds_alternative(txin)) + { + const auto& in_to_key = var::get(txin); - amounts.push_back(in_to_key.amount); + // check for duplicate + auto it = its->second.find(in_to_key.k_image); + if (it != its->second.end()) + SCAN_TABLE_QUIT("Duplicate key_image found from incoming blocks."); + + amounts.push_back(in_to_key.amount); + } } // sort and remove duplicate amounts from amounts list @@ -5221,11 +5341,14 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector(txin); - // no need to check for duplicate here. - auto absolute_offsets = relative_output_offsets_to_absolute(in_to_key.key_offsets); - for (const auto & offset : absolute_offsets) - offset_map[in_to_key.amount].push_back(offset); + if (!std::holds_alternative(txin)) + { + const auto& in_to_key = var::get(txin); + // no need to check for duplicate here. + auto absolute_offsets = relative_output_offsets_to_absolute(in_to_key.key_offsets); + for (const auto & offset : absolute_offsets) + offset_map[in_to_key.amount].push_back(offset); + } } } @@ -5287,33 +5410,36 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector(txin); - auto needed_offsets = relative_output_offsets_to_absolute(in_to_key.key_offsets); - - std::vector outputs; - for (const uint64_t & offset_needed : needed_offsets) + if (!std::holds_alternative(txin)) { - size_t pos = 0; - bool found = false; + const txin_to_key &in_to_key = var::get(txin); + auto needed_offsets = relative_output_offsets_to_absolute(in_to_key.key_offsets); - for (const uint64_t &offset_found : offset_map[in_to_key.amount]) + std::vector outputs; + for (const uint64_t & offset_needed : needed_offsets) { - if (offset_needed == offset_found) + size_t pos = 0; + bool found = false; + + for (const uint64_t &offset_found : offset_map[in_to_key.amount]) { - found = true; - break; + if (offset_needed == offset_found) + { + found = true; + break; + } + + ++pos; } - ++pos; + if (found && pos < tx_map[in_to_key.amount].size()) + outputs.push_back(tx_map[in_to_key.amount].at(pos)); + else + break; } - if (found && pos < tx_map[in_to_key.amount].size()) - outputs.push_back(tx_map[in_to_key.amount].at(pos)); - else - break; + its->second.emplace(in_to_key.k_image, outputs); } - - its->second.emplace(in_to_key.k_image, outputs); } } } diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index 703f5c7e1..492836fec 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -63,6 +63,7 @@ #include "crypto/hash.h" #include "checkpoints/checkpoints.h" #include "blockchain_db/blockchain_db.h" +#include "blockchain_db/sqlite/db_sqlite.h" #include "cryptonote_core/oxen_name_system.h" #include "pulse.h" @@ -149,7 +150,7 @@ namespace cryptonote * * @return true on success, false if any initialization steps fail */ - bool init(BlockchainDB* db, sqlite3 *ons_db, const network_type nettype = MAINNET, bool offline = false, const cryptonote::test_options *test_options = nullptr, difficulty_type fixed_difficulty = 0, const GetCheckpointsCallback& get_checkpoints = nullptr); + bool init(BlockchainDB* db, sqlite3 *ons_db, std::shared_ptr sqlite_db, const network_type nettype = MAINNET, bool offline = false, const cryptonote::test_options *test_options = nullptr, difficulty_type fixed_difficulty = 0, const GetCheckpointsCallback& get_checkpoints = nullptr); /** * @brief Uninitializes the blockchain state @@ -1009,6 +1010,8 @@ namespace cryptonote const ons::name_system_db &name_system_db() const { return m_ons_db; } + std::shared_ptr sqlite_db() { return m_sqlite_db; } + /** * @brief flush the invalid blocks set */ @@ -1056,6 +1059,7 @@ namespace cryptonote tx_memory_pool& m_tx_pool; service_nodes::service_node_list& m_service_node_list; ons::name_system_db m_ons_db; + std::shared_ptr m_sqlite_db; mutable std::recursive_mutex m_blockchain_lock; // TODO: add here reader/writer lock diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 66f52d0f9..4c762ec79 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -64,6 +64,7 @@ extern "C" { #include "checkpoints/checkpoints.h" #include "ringct/rctTypes.h" #include "blockchain_db/blockchain_db.h" +#include "blockchain_db/sqlite/db_sqlite.h" #include "ringct/rctSigs.h" #include "common/notify.h" #include "version.h" @@ -608,6 +609,12 @@ namespace cryptonote if(fs::exists(folder / "lns.db")) ons_db_file_path = folder / "lns.db"; + auto sqlite_db_file_path = folder / "sqlite.db"; + if (m_nettype == FAKECHAIN) + { + sqlite_db_file_path = ":memory:"; + } + auto sqliteDB = std::make_shared(m_nettype, sqlite_db_file_path); folder /= db->get_db_name(); MGINFO("Loading blockchain from folder " << folder << " ..."); @@ -779,10 +786,11 @@ namespace cryptonote sqlite3 *ons_db = ons::init_oxen_name_system(ons_db_file_path, db->is_read_only()); if (!ons_db) return false; + init_oxenmq(vm); const difficulty_type fixed_difficulty = command_line::get_arg(vm, arg_fixed_difficulty); - r = m_blockchain_storage.init(db.release(), ons_db, m_nettype, m_offline, regtest ? ®test_test_options : test_options, fixed_difficulty, get_checkpoints); + r = m_blockchain_storage.init(db.release(), ons_db, std::move(sqliteDB), m_nettype, m_offline, regtest ? ®test_test_options : test_options, fixed_difficulty, get_checkpoints); CHECK_AND_ASSERT_MES(r, false, "Failed to initialize blockchain storage"); r = m_mempool.init(max_txpool_weight); @@ -1253,11 +1261,11 @@ namespace cryptonote } } //----------------------------------------------------------------------------------------------- - std::vector core::parse_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) + std::vector core::parse_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) { // Caller needs to do this around both this *and* handle_parsed_txs //auto lock = incoming_tx_lock(); - std::vector tx_info(tx_blobs.size()); + std::vector tx_info(tx_blobs.size()); tools::threadpool& tpool = tools::threadpool::getInstance(); tools::threadpool::waiter waiter; @@ -1347,7 +1355,7 @@ namespace cryptonote return ok; } //----------------------------------------------------------------------------------------------- - std::vector core::handle_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) + std::vector core::handle_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) { auto lock = incoming_tx_lock(); auto parsed = parse_incoming_txs(tx_blobs, opts); @@ -2062,6 +2070,7 @@ namespace cryptonote MERROR("Block found, but failed to prepare to add"); return false; } + // add_new_block will verify block and set bvc.m_verification_failed accordingly add_new_block(b, bvc, nullptr /*checkpoint*/); cleanup_handle_incoming_blocks(true); m_miner.on_block_chain_update(); @@ -2250,8 +2259,10 @@ namespace cryptonote { std::vector const states = get_service_node_list_state({ m_service_keys.pub }); - // wait one block before starting uptime proofs. - if (!states.empty() && (states[0].info->registration_height + 1) < get_current_blockchain_height()) + // wait one block before starting uptime proofs (but not on testnet/devnet, where we sometimes + // have mass registrations/deregistrations where the waiting causes problems). + uint64_t delay_blocks = m_nettype == MAINNET ? 1 : 0; + if (!states.empty() && (states[0].info->registration_height + delay_blocks) < get_current_blockchain_height()) { m_check_uptime_proof_interval.do_call([this]() { // This timer is not perfectly precise and can leak seconds slightly, so send the uptime diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h index e66d5bc12..f9ebe0a8c 100644 --- a/src/cryptonote_core/cryptonote_core.h +++ b/src/cryptonote_core/cryptonote_core.h @@ -182,22 +182,6 @@ namespace cryptonote */ bool handle_incoming_tx(const blobdata& tx_blob, tx_verification_context& tvc, const tx_pool_options &opts); - /** - * Returned type of parse_incoming_txs() that provides details about which transactions failed - * and why. This is passed on to handle_parsed_txs() (potentially after modification such as - * setting `approved_blink`) to handle_parsed_txs() to actually insert the transactions. - */ - struct tx_verification_batch_info { - tx_verification_context tvc{}; // Verification information - bool parsed = false; // Will be true if we were able to at least parse the transaction - bool result = false; // Indicates that the transaction was parsed and passed some basic checks - bool already_have = false; // Indicates that the tx was found to already exist (in mempool or blockchain) - bool approved_blink = false; // Can be set between the parse and handle calls to make this a blink tx (that replaces conflicting non-blink txes) - const blobdata *blob = nullptr; // Will be set to a pointer to the incoming blobdata (i.e. string). caller must keep it alive! - crypto::hash tx_hash; // The transaction hash (only set if `parsed`) - transaction tx; // The parsed transaction (only set if `parsed`) - }; - /// Returns an RAII unique lock holding the incoming tx mutex. auto incoming_tx_lock() { return std::unique_lock{m_incoming_tx_lock}; } @@ -217,7 +201,7 @@ namespace cryptonote * * @return vector of tx_verification_batch_info structs for the given transactions. */ - std::vector parse_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts); + std::vector parse_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts); /** * @brief handles parsed incoming transactions diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index 4af9ae1af..636fd28b4 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -38,6 +38,7 @@ #include "blockchain.h" #include "cryptonote_basic/miner.h" #include "cryptonote_basic/tx_extra.h" +#include "cryptonote_basic/cryptonote_format_utils.h" #include "crypto/crypto.h" #include "crypto/hash.h" #include "ringct/rctSigs.h" @@ -121,6 +122,8 @@ namespace cryptonote cryptonote::get_account_address_from_str(governance_wallet_address, nettype, governance_wallet_address_str); crypto::public_key correct_key; + + if (!get_deterministic_output_key(governance_wallet_address.address, gov_key, output_index, correct_key)) { MERROR("Failed to generate deterministic output key for governance wallet output validation"); @@ -130,7 +133,7 @@ namespace cryptonote return correct_key == output_key; } - uint64_t governance_reward_formula(uint64_t base_reward, uint8_t hf_version) + uint64_t governance_reward_formula(uint8_t hf_version, uint64_t base_reward) { return hf_version >= network_version_17 ? FOUNDATION_REWARD_HF17 : hf_version >= network_version_16_pulse ? FOUNDATION_REWARD_HF15 + CHAINFLIP_LIQUIDITY_HF16 : @@ -149,9 +152,10 @@ namespace cryptonote if (height == 0) return false; - if (hard_fork_version <= network_version_9_service_nodes) + if (hard_fork_version <= network_version_9_service_nodes || hard_fork_version >= cryptonote::network_version_19) return true; + if (height % cryptonote::get_config(nettype).GOVERNANCE_REWARD_INTERVAL_IN_BLOCKS != 0) { return false; @@ -163,7 +167,7 @@ namespace cryptonote uint64_t derive_governance_from_block_reward(network_type nettype, const cryptonote::block &block, uint8_t hf_version) { if (hf_version >= 15) - return governance_reward_formula(0, hf_version); + return governance_reward_formula(hf_version); uint64_t result = 0; uint64_t snode_reward = 0; @@ -179,7 +183,7 @@ namespace cryptonote } uint64_t base_reward = snode_reward * 2; // pre-HF15, SN reward = half of base reward - uint64_t governance = governance_reward_formula(base_reward, hf_version); + uint64_t governance = governance_reward_formula(hf_version, base_reward); uint64_t block_reward = base_reward - governance; uint64_t actual_reward = 0; // sanity check @@ -240,22 +244,7 @@ namespace cryptonote return reward; } - enum struct reward_type - { - miner, - snode, - governance - }; - - struct reward_payout - { - reward_type type; - account_public_address address; - uint64_t amount; - bool operator==(service_nodes::payout_entry const &other) const { return address == other.address; } - }; - - bool construct_miner_tx( + std::pair construct_miner_tx( size_t height, size_t median_weight, uint64_t already_generated_coins, @@ -263,6 +252,7 @@ namespace cryptonote uint64_t fee, transaction& tx, const oxen_miner_tx_context &miner_tx_context, + const std::optional> sn_rwds, const blobdata& extra_nonce, uint8_t hard_fork_version) { @@ -276,22 +266,22 @@ namespace cryptonote keypair const txkey{hw::get_device("default")}; keypair const gov_key = get_deterministic_keypair_from_height(height); // NOTE: Always need since we use same key for service node + uint64_t block_rewards = 0; + // NOTE: TX Extra add_tx_extra(tx, txkey.pub); if(!extra_nonce.empty()) { if(!add_extra_nonce_to_tx_extra(tx.extra, extra_nonce)) - return false; + return std::make_pair(false, block_rewards); } // TODO(doyle): We don't need to do this. It's a deterministic key. if (already_generated_coins != 0) add_tx_extra(tx, gov_key.pub); - add_service_node_winner_to_tx_extra(tx.extra, miner_tx_context.block_leader.key); - oxen_block_reward_context block_reward_context = {}; block_reward_context.fee = fee; block_reward_context.height = height; @@ -302,33 +292,26 @@ namespace cryptonote if(!get_oxen_block_reward(median_weight, current_block_weight, already_generated_coins, hard_fork_version, reward_parts, block_reward_context)) { LOG_PRINT_L0("Failed to calculate block reward"); - return false; + return std::make_pair(false, block_rewards); } - // TODO(doyle): Batching awards + // NOTE: Batched Pulse Block Payment Details // - // NOTE: Summarise rewards to payout (up to 9 payout entries/outputs) - // - // Miner Block - // - 1 | Miner - // - Up To 4 | Block Leader (Queued node at the top of the Service Node List) - // - Up To 1 | Governance - // - // Pulse Block - // - Up to 4 | Block Producer (0-3 for Pooled Service Node) - // - Up To 4 | Block Leader (Queued node at the top of the Service Node List) - // - Up To 1 | Governance (When a block is at the Governance payout interval) - // - // NOTE: Pulse Block Payment Details + // Each block accrues a small reward to each service node this amount + // is essentially 16.5 (Coinbase reward for Service Nodes) divided by + // the size of the service node list. // + // The service node list is adjusted to only accrue for nodes + // that have been online for greater than 1 hour. + // // By default, when Pulse round is 0, the Block Producer is the Block - // Leader. Coinbase and transaction fees are given to the Block Leader. - // This is the common case, and in that instance we avoid generating - // duplicate outputs and payment occurs in 1 output. + // Leader. Transaction fees are given to the Block Leader. + // This is the common case, and the transaction fees incentivise the + // block producer to produce the block and not stall the network. // // On alternative rounds, transaction fees are given to the alternative // block producer (which is now different from the Block Leader). The - // original block producer still receives the coinbase reward. A Pulse + // original block producer still accrues their share of the coinbase. A Pulse // round's failure is determined by the non-participation of the members of // the quorum, so failing a round's onus is not always on the original block // producer (it could be the validators colluding) hence why they still @@ -345,19 +328,22 @@ namespace cryptonote // (multiple non-participation marks over the monitoring period will induce // a decommission) by members of the quorum. - size_t rewards_length = 0; - std::array rewards = {}; + std::vector rewards = {}; + std::vector batched_rewards = {}; + const network_type nettype = miner_tx_context.nettype; if (hard_fork_version >= cryptonote::network_version_9_service_nodes) - CHECK_AND_ASSERT_MES(miner_tx_context.block_leader.payouts.size(), false, "Constructing a block leader reward for block but no payout entries specified"); + CHECK_AND_ASSERT_MES(miner_tx_context.block_leader.payouts.size(), std::make_pair(false, block_rewards), "Constructing a block leader reward for block but no payout entries specified"); // NOTE: Add Block Producer Reward service_nodes::payout const &leader = miner_tx_context.block_leader; if (miner_tx_context.pulse) { - CHECK_AND_ASSERT_MES(miner_tx_context.pulse_block_producer.payouts.size(), false, "Constructing a reward for block produced by pulse but no payout entries specified"); - CHECK_AND_ASSERT_MES(miner_tx_context.pulse_block_producer.key, false, "Null Key given for Pulse Block Producer"); - CHECK_AND_ASSERT_MES(hard_fork_version >= cryptonote::network_version_16_pulse, false, "Pulse Block Producer is not valid until HF16, current HF" << hard_fork_version); + // PULSE BLOCKS + + CHECK_AND_ASSERT_MES(miner_tx_context.pulse_block_producer.payouts.size(), std::make_pair(false, block_rewards), "Constructing a reward for block produced by pulse but no payout entries specified"); + CHECK_AND_ASSERT_MES(miner_tx_context.pulse_block_producer.key, std::make_pair(false, block_rewards), "Null Key given for Pulse Block Producer"); + CHECK_AND_ASSERT_MES(hard_fork_version >= cryptonote::network_version_16_pulse, std::make_pair(false, block_rewards), "Pulse Block Producer is not valid until HF16, current HF" << hard_fork_version); uint64_t leader_reward = reward_parts.service_node_total; if (miner_tx_context.block_leader.key == miner_tx_context.pulse_block_producer.key) @@ -371,29 +357,53 @@ namespace cryptonote std::vector split_rewards = distribute_reward_by_portions(producer.payouts, reward_parts.miner_fee, true /*distribute_remainder*/); for (size_t i = 0; i < producer.payouts.size(); i++) - rewards[rewards_length++] = {reward_type::snode, producer.payouts[i].address, split_rewards[i]}; + if (hard_fork_version < cryptonote::network_version_19) + { + rewards.push_back({reward_type::snode, producer.payouts[i].address, split_rewards[i]}); + } else { + batched_rewards.emplace_back(producer.payouts[i].address, split_rewards[i], nettype); + } } std::vector split_rewards = distribute_reward_by_portions(leader.payouts, leader_reward, true /*distribute_remainder*/); for (size_t i = 0; i < leader.payouts.size(); i++) - rewards[rewards_length++] = {reward_type::snode, leader.payouts[i].address, split_rewards[i]}; + { + if (hard_fork_version < cryptonote::network_version_19) { + rewards.push_back({reward_type::snode, leader.payouts[i].address, split_rewards[i]}); + } else { + batched_rewards.emplace_back(leader.payouts[i].address, split_rewards[i], nettype); + } + + } } else { - - CHECK_AND_ASSERT_MES(miner_tx_context.pulse_block_producer.payouts.empty(), false, "Constructing a reward for block produced by miner but payout entries specified"); + // MINED BLOCKS + + CHECK_AND_ASSERT_MES(miner_tx_context.pulse_block_producer.payouts.empty(), std::make_pair(false, block_rewards), "Constructing a reward for block produced by miner but payout entries specified"); if (uint64_t miner_amount = reward_parts.base_miner + reward_parts.miner_fee; miner_amount) - rewards[rewards_length++] = {reward_type::miner, miner_tx_context.miner_block_producer, miner_amount}; - - if (hard_fork_version >= cryptonote::network_version_9_service_nodes) { + if (hard_fork_version < cryptonote::network_version_19) { + rewards.push_back({reward_type::miner, miner_tx_context.miner_block_producer, miner_amount}); + } else { + batched_rewards.emplace_back(miner_tx_context.miner_block_producer, miner_amount, nettype); + } + } + + if (hard_fork_version >= cryptonote::network_version_9_service_nodes) { std::vector split_rewards = distribute_reward_by_portions(leader.payouts, reward_parts.service_node_total, hard_fork_version >= cryptonote::network_version_16_pulse /*distribute_remainder*/); for (size_t i = 0; i < leader.payouts.size(); i++) - rewards[rewards_length++] = {reward_type::snode, leader.payouts[i].address, split_rewards[i]}; + { + if (hard_fork_version < cryptonote::network_version_19) { + rewards.push_back({reward_type::snode, leader.payouts[i].address, split_rewards[i]}); + } else { + batched_rewards.emplace_back(leader.payouts[i].address, split_rewards[i], nettype); + } + } } } @@ -402,24 +412,40 @@ namespace cryptonote { if (reward_parts.governance_paid == 0) { - CHECK_AND_ASSERT_MES(hard_fork_version >= network_version_10_bulletproofs, false, "Governance reward can NOT be 0 before hardfork 10, hard_fork_version: " << hard_fork_version); + CHECK_AND_ASSERT_MES(hard_fork_version >= network_version_10_bulletproofs, std::make_pair(false, block_rewards), "Governance reward can NOT be 0 before hardfork 10, hard_fork_version: " << hard_fork_version); } else { - const network_type nettype = miner_tx_context.nettype; cryptonote::address_parse_info governance_wallet_address; cryptonote::get_account_address_from_str(governance_wallet_address, nettype, cryptonote::get_config(nettype).governance_wallet_address(hard_fork_version)); - rewards[rewards_length++] = {reward_type::governance, governance_wallet_address.address, reward_parts.governance_paid}; + // Governance reward paid out through SN rewards batching after HF19 + if (hard_fork_version < cryptonote::network_version_19) { + rewards.push_back({reward_type::governance, governance_wallet_address.address, reward_parts.governance_paid}); + } } } - CHECK_AND_ASSERT_MES(rewards_length <= rewards.size(), false, "More rewards specified than supported, number of rewards: " << rewards_length << ", capacity: " << rewards.size()); - CHECK_AND_ASSERT_MES(rewards_length > 0, false, "Zero rewards are to be payed out, there should be at least 1"); + + uint64_t total_sn_rewards = 0; + // Add SN rewards to the block + if (sn_rwds) + { + for (const auto &reward : *sn_rwds) + { + rewards.emplace_back(reward_type::snode, reward.address_info.address, reward.amount); + total_sn_rewards += reward.amount; + } + } + + if (hard_fork_version < cryptonote::network_version_19) + { + CHECK_AND_ASSERT_MES(rewards.size() <= 9, std::make_pair(false, block_rewards), "More rewards specified than supported, number of rewards: " << rewards.size() << ", capacity: " << rewards.size()); + CHECK_AND_ASSERT_MES(rewards.size() > 0, std::make_pair(false, block_rewards), "Zero rewards are to be payed out, there should be at least 1"); + } // NOTE: Make TX Outputs uint64_t summary_amounts = 0; - for (size_t reward_index = 0; reward_index < rewards_length; reward_index++) - { - auto const &[type, address, amount] = rewards[reward_index]; + for (auto it = rewards.begin(); it != rewards.end(); ++it) { + auto const &[type, address, amount] = *it; assert(amount > 0); crypto::public_key out_eph_public_key{}; @@ -427,14 +453,14 @@ namespace cryptonote // TODO(doyle): I don't think txkey is necessary, just use the governance key? keypair const &derivation_pair = (type == reward_type::miner) ? txkey : gov_key; crypto::key_derivation derivation{}; + - if (!get_deterministic_output_key(address, derivation_pair, reward_index, out_eph_public_key)) + if (!get_deterministic_output_key(address, derivation_pair, it - rewards.begin(), out_eph_public_key)) { MERROR("Failed to generate output one-time public key"); - return false; + return std::make_pair(false, block_rewards); } - txout_to_key tk = {}; tk.key = out_eph_public_key; @@ -455,19 +481,25 @@ namespace cryptonote // division). This occurred prior to HF15, after that we redistribute dust // properly. expected_amount = reward_parts.base_miner + reward_parts.miner_fee + reward_parts.governance_paid; - for (size_t reward_index = 0; reward_index < rewards_length; reward_index++) + for (auto reward : rewards) { - [[maybe_unused]] auto const &[type, address, amount] = rewards[reward_index]; + [[maybe_unused]] auto const &[type, address, amount] = reward; if (type == reward_type::snode) expected_amount += amount; } } else { - expected_amount = reward_parts.base_miner + reward_parts.miner_fee + reward_parts.governance_paid + reward_parts.service_node_total; + expected_amount = 0; + if (hard_fork_version < cryptonote::network_version_19) + expected_amount = expected_amount + reward_parts.base_miner + reward_parts.miner_fee + reward_parts.service_node_total + reward_parts.governance_paid; + else + expected_amount = expected_amount + total_sn_rewards; } - CHECK_AND_ASSERT_MES(summary_amounts == expected_amount, false, "Failed to construct miner tx, summary_amounts = " << summary_amounts << " not equal total block_reward = " << expected_amount); - CHECK_AND_ASSERT_MES(tx.vout.size() == rewards_length, false, "TX output mis-match with rewards expected: " << rewards_length << ", tx outputs: " << tx.vout.size()); + CHECK_AND_ASSERT_MES(summary_amounts == expected_amount, std::make_pair(false, block_rewards), "Failed to construct miner tx, summary_amounts = " << summary_amounts << " not equal total block_reward = " << expected_amount); + CHECK_AND_ASSERT_MES(tx.vout.size() == rewards.size(), std::make_pair(false, block_rewards), "TX output mis-match with rewards expected: " << rewards.size() << ", tx outputs: " << tx.vout.size()); + + block_rewards = std::accumulate(batched_rewards.begin(), batched_rewards.end(), uint64_t(0), [](uint64_t const x, cryptonote::batch_sn_payment const y) { return x + y.amount; }); //lock tx.unlock_time = height + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; @@ -476,7 +508,8 @@ namespace cryptonote //LOG_PRINT("MINER_TX generated ok, block_reward=" << print_money(block_reward) << "(" << print_money(block_reward - fee) << "+" << print_money(fee) // << "), current_block_size=" << current_block_size << ", already_generated_coins=" << already_generated_coins << ", tx_id=" << get_transaction_hash(tx), LOG_LEVEL_2); - return true; + + return std::make_pair(true, block_rewards); } bool get_oxen_block_reward(size_t median_weight, size_t current_block_weight, uint64_t already_generated_coins, int hard_fork_version, block_reward_parts &result, const oxen_block_reward_context &oxen_context) @@ -509,7 +542,7 @@ namespace cryptonote // There is a goverance fee due every block. Beginning in hardfork 10 this is still subtracted // from the block reward as if it was paid, but the actual payments get batched into rare, large // accumulated payments. (Before hardfork 10 they are included in every block, unbatched). - result.governance_due = governance_reward_formula(result.original_base_reward, hard_fork_version); + result.governance_due = governance_reward_formula(hard_fork_version, result.original_base_reward); result.governance_paid = hard_fork_version >= network_version_10_bulletproofs ? oxen_context.batched_governance : result.governance_due; diff --git a/src/cryptonote_core/cryptonote_tx_utils.h b/src/cryptonote_core/cryptonote_tx_utils.h index a34ac237d..27e328216 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.h +++ b/src/cryptonote_core/cryptonote_tx_utils.h @@ -34,6 +34,7 @@ #include #include "ringct/rctOps.h" #include "cryptonote_core/service_node_list.h" +#include "cryptonote_basic/verification_context.h" namespace cryptonote { @@ -42,7 +43,7 @@ namespace cryptonote bool get_deterministic_output_key (const account_public_address& address, const keypair& tx_key, size_t output_index, crypto::public_key& output_key); bool validate_governance_reward_key (uint64_t height, std::string_view governance_wallet_address_str, size_t output_index, const crypto::public_key& output_key, const cryptonote::network_type nettype); - uint64_t governance_reward_formula (uint64_t base_reward, uint8_t hf_version); + uint64_t governance_reward_formula (uint8_t hf_version, uint64_t base_reward = 0); bool block_has_governance_output (network_type nettype, cryptonote::block const &block); bool height_has_governance_output (network_type nettype, uint8_t hard_fork_version, uint64_t height); uint64_t derive_governance_from_block_reward (network_type nettype, const cryptonote::block &block, uint8_t hf_version); @@ -51,6 +52,22 @@ namespace cryptonote uint64_t get_portion_of_reward (uint64_t portions, uint64_t total_service_node_reward); uint64_t service_node_reward_formula (uint64_t base_reward, uint8_t hard_fork_version); + /** + * Returned type of parse_incoming_txs() that provides details about which transactions failed + * and why. This is passed on to handle_parsed_txs() (potentially after modification such as + * setting `approved_blink`) to handle_parsed_txs() to actually insert the transactions. + */ + struct tx_verification_batch_info { + cryptonote::tx_verification_context tvc{}; // Verification information + bool parsed = false; // Will be true if we were able to at least parse the transaction + bool result = false; // Indicates that the transaction was parsed and passed some basic checks + bool already_have = false; // Indicates that the tx was found to already exist (in mempool or blockchain) + bool approved_blink = false; // Can be set between the parse and handle calls to make this a blink tx (that replaces conflicting non-blink txes) + const blobdata *blob = nullptr; // Will be set to a pointer to the incoming blobdata (i.e. string). caller must keep it alive! + crypto::hash tx_hash; // The transaction hash (only set if `parsed`) + transaction tx; // The parsed transaction (only set if `parsed`) + }; + struct oxen_miner_tx_context { static oxen_miner_tx_context miner_block(network_type nettype, @@ -86,7 +103,25 @@ namespace cryptonote uint64_t batched_governance; // NOTE: 0 until hardfork v10, then use blockchain::calc_batched_governance_reward }; - bool construct_miner_tx( + enum struct reward_type + { + miner, + snode, + governance + }; + + struct reward_payout + { + reward_type type; + account_public_address address; + uint64_t amount; + bool operator==(service_nodes::payout_entry const &other) const { return address == other.address; } + + reward_payout() = default; + reward_payout(reward_type t, account_public_address addr, uint64_t amt): type{t}, address{std::move(addr)}, amount{amt} {} + }; + + std::pair construct_miner_tx( size_t height, size_t median_weight, uint64_t already_generated_coins, @@ -94,6 +129,7 @@ namespace cryptonote uint64_t fee, transaction& tx, const oxen_miner_tx_context &miner_context, + const std::optional> sn_rwds, const blobdata& extra_nonce = blobdata(), uint8_t hard_fork_version = 1); diff --git a/src/cryptonote_core/service_node_list.cpp b/src/cryptonote_core/service_node_list.cpp index 09b97884d..3988f9494 100644 --- a/src/cryptonote_core/service_node_list.cpp +++ b/src/cryptonote_core/service_node_list.cpp @@ -133,6 +133,10 @@ namespace service_nodes return sort_and_filter(service_nodes_infos, [](const service_node_info &info) { return info.is_decommissioned() && info.is_fully_funded(); }, /*reserve=*/ false); } + std::vector service_node_list::state_t::payable_service_nodes_infos(uint64_t height, cryptonote::network_type nettype) const { + return sort_and_filter(service_nodes_infos, [height, nettype](const service_node_info &info) { return info.is_payable(height, nettype); }, /*reserve=*/ true); + } + std::shared_ptr service_node_list::get_quorum(quorum_type type, uint64_t height, bool include_old, std::vector> *alt_quorums) const { height = offset_testing_quorum_height(type, height); @@ -1284,6 +1288,7 @@ namespace service_nodes if (miner_block) { + if (cryptonote::block_has_pulse_components(block)) { if (log_errors) MGINFO("Pulse " << block_type << "received but only miner blocks are permitted\n" << dump_pulse_block_data(block, pulse_quorum.get())); @@ -1624,6 +1629,15 @@ namespace service_nodes return result; } + bool service_node_list::process_batching_rewards(const cryptonote::block& block) + { + return m_blockchain.sqlite_db()->add_block(block, m_state); + } + bool service_node_list::pop_batching_rewards_block(const cryptonote::block& block) + { + return m_blockchain.sqlite_db()->pop_block(block, m_state); + } + static std::mt19937_64 quorum_rng(uint8_t hf_version, crypto::hash const &hash, quorum_type type) { std::mt19937_64 result; @@ -2368,7 +2382,7 @@ namespace service_nodes return true; } - bool service_node_list::validate_miner_tx(cryptonote::block const &block, cryptonote::block_reward_parts const &reward_parts) const + bool service_node_list::validate_miner_tx(cryptonote::block const &block, cryptonote::block_reward_parts const &reward_parts, std::optional> const &batched_sn_payments) const { uint8_t const hf_version = block.major_version; if (hf_version < cryptonote::network_version_9_service_nodes) @@ -2398,6 +2412,7 @@ namespace service_nodes miner, pulse_block_leader_is_producer, pulse_different_block_producer, + batched_sn_rewards, }; verify_mode mode = verify_mode::miner; @@ -2443,7 +2458,8 @@ namespace service_nodes // std::shared_ptr block_producer = nullptr; - size_t expected_vouts_size = 0; + size_t expected_vouts_size = 0; + if (mode == verify_mode::pulse_block_leader_is_producer || mode == verify_mode::pulse_different_block_producer) { auto info_it = m_state.service_nodes_infos.find(block_producer_key); @@ -2454,17 +2470,39 @@ namespace service_nodes } block_producer = info_it->second; - if (mode == verify_mode::pulse_different_block_producer && reward_parts.miner_fee > 0) + if (mode == verify_mode::pulse_different_block_producer && reward_parts.miner_fee > 0 && block.major_version < cryptonote::network_version_19) + { expected_vouts_size += block_producer->contributors.size(); - } - else - { - if ((reward_parts.base_miner + reward_parts.miner_fee) > 0) // (HF >= 16) this can be zero, no miner coinbase. - expected_vouts_size += 1; /*miner*/ + } + } + + if (block.major_version >= cryptonote::network_version_19) + { + mode = verify_mode::batched_sn_rewards; + MDEBUG("Batched miner reward"); + } + + + if (mode == verify_mode::miner) + { + if ((reward_parts.base_miner + reward_parts.miner_fee) > 0) // (HF >= 16) this can be zero, no miner coinbase. + { + expected_vouts_size += 1; /*miner*/ + } + } + + if (mode == verify_mode::batched_sn_rewards) + { + if (batched_sn_payments.has_value()) + expected_vouts_size += batched_sn_payments->size(); + } else { + expected_vouts_size += block_leader.payouts.size(); + bool has_governance_output = cryptonote::height_has_governance_output(m_blockchain.nettype(), hf_version, height); + if (has_governance_output) { + expected_vouts_size++; + } } - expected_vouts_size += block_leader.payouts.size(); - expected_vouts_size += static_cast(cryptonote::height_has_governance_output(m_blockchain.nettype(), hf_version, height)); if (miner_tx.vout.size() != expected_vouts_size) { @@ -2564,6 +2602,48 @@ namespace service_nodes } } break; + + case verify_mode::batched_sn_rewards: + { + size_t vout_index = 0; + uint64_t total_payout_in_our_db = std::accumulate(batched_sn_payments->begin(),batched_sn_payments->end(), uint64_t{0}, [](auto const a, auto const b){return a + b.amount;}); + uint64_t total_payout_in_vouts = 0; + cryptonote::keypair const deterministic_keypair = cryptonote::get_deterministic_keypair_from_height(height); + for (auto & vout : block.miner_tx.vout) + { + if (!std::holds_alternative(vout.target)) + { + MGINFO_RED("Service node output target type should be txout_to_key"); + return false; + } + + if (vout.amount != (*batched_sn_payments)[vout_index].amount) + { + MERROR("Service node reward amount incorrect. Should be " << cryptonote::print_money((*batched_sn_payments)[vout_index].amount) << ", is: " << cryptonote::print_money(vout.amount)); + return false; + } + crypto::public_key out_eph_public_key{}; + if (!cryptonote::get_deterministic_output_key((*batched_sn_payments)[vout_index].address_info.address, deterministic_keypair, vout_index, out_eph_public_key)) + { + MERROR("Failed to generate output one-time public key"); + return false; + } + const auto& out_to_key = var::get(vout.target); + if (tools::view_guts(out_to_key) != tools::view_guts(out_eph_public_key)) + { + MERROR("Output Ephermeral Public Key does not match"); + return false; + } + total_payout_in_vouts += vout.amount; + vout_index++; + } + if (total_payout_in_vouts != total_payout_in_our_db) + { + MERROR("Total service node reward amount incorrect. Should be " << cryptonote::print_money(total_payout_in_our_db) << ", is: " << cryptonote::print_money(total_payout_in_vouts)); + return false; + } + } + break; } return true; @@ -3013,7 +3093,7 @@ namespace service_nodes REJECT_PROOF("timestamp is too far from now"); for (auto const &min : MIN_UPTIME_PROOF_VERSIONS) { - if (vers >= min.hardfork_revision) { + if (vers >= min.hardfork_revision && m_blockchain.nettype() != cryptonote::DEVNET) { if (proof->version < min.oxend) REJECT_PROOF("v" << tools::join(".", min.oxend) << "+ oxend version is required for v" << +vers.first << "." << +vers.second << "+ network proofs"); if (proof->lokinet_version < min.lokinet) diff --git a/src/cryptonote_core/service_node_list.h b/src/cryptonote_core/service_node_list.h index 4f3217059..66aebac8c 100644 --- a/src/cryptonote_core/service_node_list.h +++ b/src/cryptonote_core/service_node_list.h @@ -311,6 +311,10 @@ namespace service_nodes bool is_fully_funded() const { return total_contributed >= staking_requirement; } bool is_decommissioned() const { return active_since_height < 0; } bool is_active() const { return is_fully_funded() && !is_decommissioned(); } + bool is_payable(uint64_t at_height, cryptonote::network_type nettype) const { + auto& netconf = get_config(nettype); + return is_active() && at_height >= active_since_height + netconf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS; + } bool can_transition_to_state(uint8_t hf_version, uint64_t block_height, new_state proposed_state) const; bool can_be_voted_on (uint64_t block_height) const; @@ -454,9 +458,11 @@ namespace service_nodes service_node_list &operator=(const service_node_list &) = delete; bool block_added(const cryptonote::block& block, const std::vector& txs, cryptonote::checkpoint_t const *checkpoint) override; + bool process_batching_rewards(const cryptonote::block& block); + bool pop_batching_rewards_block(const cryptonote::block& block); void blockchain_detached(uint64_t height, bool by_pop_blocks) override; void init() override; - bool validate_miner_tx(cryptonote::block const &block, cryptonote::block_reward_parts const &base_reward) const override; + bool validate_miner_tx(cryptonote::block const &block, cryptonote::block_reward_parts const &base_reward, std::optional> const &batched_sn_payments) const override; bool alt_block_added(const cryptonote::block& block, const std::vector& txs, cryptonote::checkpoint_t const *checkpoint) override; payout get_block_leader() const { std::lock_guard lock{m_sn_mutex}; return m_state.get_block_leader(); } bool is_service_node(const crypto::public_key& pubkey, bool require_active = true) const; @@ -690,6 +696,8 @@ namespace service_nodes std::vector active_service_nodes_infos() const; std::vector decommissioned_service_nodes_infos() const; // return: All nodes that are fully funded *and* decommissioned. + std::vector payable_service_nodes_infos(uint64_t height, cryptonote::network_type nettype) const; // return: All nodes that are active and have been online for a period greater than SERVICE_NODE_PAYABLE_AFTER_BLOCKS + std::vector get_expired_nodes(cryptonote::BlockchainDB const &db, cryptonote::network_type nettype, uint8_t hf_version, uint64_t block_height) const; void update_from_block( cryptonote::BlockchainDB const &db, @@ -792,7 +800,7 @@ namespace service_nodes }; bool is_registration_tx (cryptonote::network_type nettype, uint8_t hf_version, const cryptonote::transaction& tx, uint64_t block_timestamp, uint64_t block_height, uint32_t index, crypto::public_key& key, service_node_info& info); - bool reg_tx_extract_fields(const cryptonote::transaction& tx, contributor_args_t &contributor_args, uint64_t& expiration_timestamp, crypto::public_key& service_node_key, crypto::signature& signature, crypto::public_key& tx_pub_key); + bool reg_tx_extract_fields(const cryptonote::transaction& tx, contributor_args_t &contributor_args, uint64_t& expiration_timestamp, crypto::public_key& service_node_key, crypto::signature& signature); uint64_t offset_testing_quorum_height(quorum_type type, uint64_t height); contributor_args_t convert_registration_args(cryptonote::network_type nettype, diff --git a/src/cryptonote_core/service_node_rules.h b/src/cryptonote_core/service_node_rules.h index 2608cb6b3..631fcaf2f 100644 --- a/src/cryptonote_core/service_node_rules.h +++ b/src/cryptonote_core/service_node_rules.h @@ -240,6 +240,7 @@ namespace service_nodes { //If the below percentage of service nodes are out of sync we will consider our clock out of sync constexpr uint8_t MAXIMUM_EXTERNAL_OUT_OF_SYNC = 80; + static_assert(STAKING_PORTIONS != UINT64_MAX, "UINT64_MAX is used as the invalid value for failing to calculate the min_node_contribution"); // return: UINT64_MAX if (num_contributions > the max number of contributions), otherwise the amount in oxen atomic units uint64_t get_min_node_contribution (uint8_t version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions); diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp index 1f52d5e54..fc8e1b327 100644 --- a/src/cryptonote_core/tx_pool.cpp +++ b/src/cryptonote_core/tx_pool.cpp @@ -260,7 +260,7 @@ namespace cryptonote return false; } - if(!check_inputs_types_supported(tx)) + if (!check_inputs_types_supported(tx)) { tvc.m_verifivation_failed = true; tvc.m_invalid_input = true; @@ -279,11 +279,14 @@ namespace cryptonote return false; } - if (!opts.kept_by_block && tx.is_transfer() && !m_blockchain.check_fee(tx_weight, tx.vout.size(), fee, burned, opts)) + if (hf_version < cryptonote::network_version_19) { - tvc.m_verifivation_failed = true; - tvc.m_fee_too_low = true; - return false; + if (!opts.kept_by_block && tx.is_transfer() && !m_blockchain.check_fee(tx_weight, tx.vout.size(), fee, burned, opts)) + { + tvc.m_verifivation_failed = true; + tvc.m_fee_too_low = true; + return false; + } } size_t tx_weight_limit = get_transaction_weight_limit(hf_version); diff --git a/src/cryptonote_protocol/CMakeLists.txt b/src/cryptonote_protocol/CMakeLists.txt index 68bdbbc32..3551da3e8 100644 --- a/src/cryptonote_protocol/CMakeLists.txt +++ b/src/cryptonote_protocol/CMakeLists.txt @@ -41,4 +41,5 @@ target_link_libraries(cryptonote_protocol p2p PRIVATE easylogging + SQLiteCpp extra) diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index 59e3b374e..eb0cc3d1a 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -557,6 +557,7 @@ namespace cryptonote int t_cryptonote_protocol_handler::handle_notify_new_fluffy_block(int command, NOTIFY_NEW_FLUFFY_BLOCK::request& arg, cryptonote_connection_context& context) { MLOGIF_P2P_MESSAGE(crypto::hash hash; cryptonote::block b; bool ret = cryptonote::parse_and_validate_block_from_blob(arg.b.block, b, &hash);, ret, "Received NOTIFY_NEW_FLUFFY_BLOCK " << hash << " (height " << arg.current_blockchain_height << ", " << arg.b.txs.size() << " txes)"); + if(context.m_state != cryptonote_connection_context::state_normal) return 1; if(!is_synchronized() || m_no_sync) // can happen if a peer connection goes to normal but another thread still hasn't finished adding queued blocks @@ -568,7 +569,6 @@ namespace cryptonote m_core.pause_mine(); block new_block; - transaction miner_tx; if(parse_and_validate_block_from_blob(arg.b.block, new_block)) { // This is a second notification, we must have asked for some missing tx diff --git a/src/daemon/CMakeLists.txt b/src/daemon/CMakeLists.txt index 9fc76d1ef..e3dfd6c0f 100644 --- a/src/daemon/CMakeLists.txt +++ b/src/daemon/CMakeLists.txt @@ -47,5 +47,6 @@ target_link_libraries(daemon version filesystem Boost::program_options + SQLiteCpp systemd extra) diff --git a/src/daemon/rpc_command_executor.cpp b/src/daemon/rpc_command_executor.cpp index 21ac5ebf6..0f7c2ba63 100644 --- a/src/daemon/rpc_command_executor.cpp +++ b/src/daemon/rpc_command_executor.cpp @@ -1929,28 +1929,6 @@ bool rpc_command_executor::prepare_registration(bool force_registration) !invoke({}, kres, "Failed to retrieve service node keys")) return false; - if (!res.service_node) - { - tools::fail_msg_writer() << "Unable to prepare registration: this daemon is not running in --service-node mode"; - return false; - } - else if (auto last_lokinet_ping = static_cast(res.last_lokinet_ping.value_or(0)); - last_lokinet_ping < (time(nullptr) - 60) && !force_registration) - { - tools::fail_msg_writer() << "Unable to prepare registration: this daemon has not received a ping from lokinet " - << (res.last_lokinet_ping == 0 ? "yet" : "since " + get_human_time_ago(last_lokinet_ping, std::time(nullptr))); - return false; - } - else if (auto last_storage_server_ping = static_cast(res.last_storage_server_ping.value_or(0)); - last_storage_server_ping < (time(nullptr) - 60) && !force_registration) - { - tools::fail_msg_writer() << "Unable to prepare registration: this daemon has not received a ping from the storage server " - << (res.last_storage_server_ping == 0 ? "yet" : "since " + get_human_time_ago(last_storage_server_ping, std::time(nullptr))); - return false; - } - - uint64_t block_height = std::max(res.height, res.target_height); - uint8_t hf_version = hf_res.version; cryptonote::network_type const nettype = res.mainnet ? cryptonote::MAINNET : res.devnet ? cryptonote::DEVNET : @@ -1958,6 +1936,32 @@ bool rpc_command_executor::prepare_registration(bool force_registration) res.nettype == "fakechain" ? cryptonote::FAKECHAIN : cryptonote::UNDEFINED; + if (!res.service_node) + { + tools::fail_msg_writer() << "Unable to prepare registration: this daemon is not running in --service-node mode"; + return false; + } + if (nettype != cryptonote::DEVNET) + { + if (auto last_lokinet_ping = static_cast(res.last_lokinet_ping.value_or(0)); + last_lokinet_ping < (time(nullptr) - 60) && !force_registration && nettype != cryptonote::DEVNET) + { + tools::fail_msg_writer() << "Unable to prepare registration: this daemon has not received a ping from lokinet " + << (res.last_lokinet_ping == 0 ? "yet" : "since " + get_human_time_ago(last_lokinet_ping, std::time(nullptr))); + return false; + } + if (auto last_storage_server_ping = static_cast(res.last_storage_server_ping.value_or(0)); + last_storage_server_ping < (time(nullptr) - 60) && !force_registration) + { + tools::fail_msg_writer() << "Unable to prepare registration: this daemon has not received a ping from the storage server " + << (res.last_storage_server_ping == 0 ? "yet" : "since " + get_human_time_ago(last_storage_server_ping, std::time(nullptr))); + return false; + } + } + + uint64_t block_height = std::max(res.height, res.target_height); + uint8_t hf_version = hf_res.version; + // Query the latest block we've synced and check that the timestamp is sensible, issue a warning if not { GET_LAST_BLOCK_HEADER::response res{}; diff --git a/src/device/CMakeLists.txt b/src/device/CMakeLists.txt index b47a74ddf..a6d5ce613 100644 --- a/src/device/CMakeLists.txt +++ b/src/device/CMakeLists.txt @@ -40,6 +40,7 @@ target_link_libraries(device ringct_basic Boost::serialization PRIVATE + SQLiteCpp version extra) diff --git a/src/p2p/CMakeLists.txt b/src/p2p/CMakeLists.txt index a971a2e39..6aa6e77f3 100644 --- a/src/p2p/CMakeLists.txt +++ b/src/p2p/CMakeLists.txt @@ -42,4 +42,5 @@ target_link_libraries(p2p Boost::program_options filesystem Boost::serialization + SQLiteCpp extra) diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index e0b64b833..3b7d685d2 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -75,6 +75,7 @@ target_link_libraries(rpc cryptonote_protocol Boost::thread Boost::program_options + SQLiteCpp extra) target_link_libraries(daemon_rpc_server @@ -82,6 +83,7 @@ target_link_libraries(daemon_rpc_server rpc_server_base rpc Boost::thread + SQLiteCpp extra) target_link_libraries(rpc_http_client diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index 049964680..2f92d3e88 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -1826,15 +1826,20 @@ namespace cryptonote { namespace rpc { response.difficulty = m_core.get_blockchain_storage().block_difficulty(height); response.cumulative_difficulty = m_core.get_blockchain_storage().get_db().get_block_cumulative_difficulty(height); response.block_weight = m_core.get_blockchain_storage().get_db().get_block_weight(height); - response.reward = get_block_reward(blk); - response.miner_reward = blk.miner_tx.vout[0].amount; + response.reward = (blk.reward > 0) ? blk.reward : get_block_reward(blk); response.block_size = response.block_weight = m_core.get_blockchain_storage().get_db().get_block_weight(height); response.num_txes = blk.tx_hashes.size(); if (fill_pow_hash) response.pow_hash = tools::type_to_hex(get_block_longhash_w_blockchain(m_core.get_nettype(), &(m_core.get_blockchain_storage()), blk, height, 0)); response.long_term_weight = m_core.get_blockchain_storage().get_db().get_block_long_term_weight(height); - response.miner_tx_hash = tools::type_to_hex(cryptonote::get_transaction_hash(blk.miner_tx)); - response.service_node_winner = tools::type_to_hex(cryptonote::get_service_node_winner_from_tx_extra(blk.miner_tx.extra)); + response.service_node_winner = (tools::type_to_hex(blk.service_node_winner_key) == "") ? tools::type_to_hex(cryptonote::get_service_node_winner_from_tx_extra(blk.miner_tx.extra)) : tools::type_to_hex(blk.service_node_winner_key); + if (blk.miner_tx.vout.size() > 0) + { + response.miner_reward = blk.miner_tx.vout[0].amount; + response.miner_tx_hash = tools::type_to_hex(cryptonote::get_transaction_hash(blk.miner_tx)); + } else { + response.miner_reward = get_block_reward(blk); + } if (get_tx_hashes) { response.tx_hashes.reserve(blk.tx_hashes.size()); @@ -2068,6 +2073,7 @@ namespace cryptonote { namespace rpc { block_hash = get_block_hash(blk); block_height = req.height; } + fill_block_header_response(blk, orphan, block_height, block_hash, res.block_header, req.fill_pow_hash && context.admin, false /*tx hashes*/); res.tx_hashes.reserve(blk.tx_hashes.size()); for (const auto& tx_hash : blk.tx_hashes) diff --git a/src/sqlitedb/database.hpp b/src/sqlitedb/database.hpp index 8b35e001d..55c186ca6 100644 --- a/src/sqlitedb/database.hpp +++ b/src/sqlitedb/database.hpp @@ -26,9 +26,9 @@ namespace db template constexpr bool is_cstr = true; template <> - constexpr bool is_cstr = true; + inline constexpr bool is_cstr = true; template <> - constexpr bool is_cstr = true; + inline constexpr bool is_cstr = true; // Simple wrapper class that can be used to bind a blob through the templated binding code below. // E.g. `exec_query(st, 100, 42, blob_binder{data})` binds the third parameter using no-copy blob @@ -286,7 +286,7 @@ namespace db } explicit Database(const fs::path& db_path, const std::string_view db_password) - : db{db_path.u8path(), SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE | SQLite::OPEN_FULLMUTEX, 5000/*ms*/} + : db{db_path.u8string(), SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE | SQLite::OPEN_FULLMUTEX, 5000/*ms*/} { // Don't fail on these because we can still work even if they fail if (int rc = db.tryExec("PRAGMA journal_mode = WAL"); rc != SQLITE_OK) diff --git a/src/wallet/api/CMakeLists.txt b/src/wallet/api/CMakeLists.txt index f4d58bf08..5fde757e5 100644 --- a/src/wallet/api/CMakeLists.txt +++ b/src/wallet/api/CMakeLists.txt @@ -126,6 +126,7 @@ if (STATIC AND BUILD_STATIC_DEPS) Boost::program_options Boost::serialization Boost::system Boost::thread zlib SQLite::SQLite3 + SQLiteCpp ${merged_protobuf} sodium libzmq @@ -136,6 +137,7 @@ if (STATIC AND BUILD_STATIC_DEPS) randomx uSockets cpr + fmt ) if(IOS) diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index b3fcc4641..504d7b5e8 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -100,7 +100,6 @@ namespace string_tools = epee::string_tools; #undef OXEN_DEFAULT_LOG_CATEGORY #define OXEN_DEFAULT_LOG_CATEGORY "wallet.wallet2" - namespace { constexpr std::string_view UNSIGNED_TX_PREFIX = "Loki unsigned tx set\004"sv; @@ -2828,15 +2827,17 @@ void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector 0) { THROW_WALLET_EXCEPTION_IF(txidx >= tx_cache_data.size(), error::wallet_internal_error, "txidx out of range"); const size_t n_vouts = m_refresh_type == RefreshType::RefreshOptimizeCoinbase ? 1 : parsed_blocks[i].block.miner_tx.vout.size(); @@ -2847,7 +2848,7 @@ void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector= tx_cache_data.size(), error::wallet_internal_error, "txidx out of range"); tpool.submit(&waiter, [&, i, j, txidx](){ geniod(parsed_blocks[i].txes[j], parsed_blocks[i].txes[j].vout.size(), txidx); }, true); - ++txidx; + ++txidx; } } THROW_WALLET_EXCEPTION_IF(txidx != tx_cache_data.size(), error::wallet_internal_error, "txidx did not reach expected value"); diff --git a/tests/block_weight/CMakeLists.txt b/tests/block_weight/CMakeLists.txt index 7826a13fd..198c4fa41 100644 --- a/tests/block_weight/CMakeLists.txt +++ b/tests/block_weight/CMakeLists.txt @@ -32,6 +32,7 @@ target_link_libraries(block_weight PRIVATE cryptonote_core blockchain_db + SQLiteCpp extra) add_test( diff --git a/tests/block_weight/block_weight.cpp b/tests/block_weight/block_weight.cpp index 33c5a9b38..c426f8be8 100644 --- a/tests/block_weight/block_weight.cpp +++ b/tests/block_weight/block_weight.cpp @@ -129,11 +129,12 @@ static void test(test_t t, uint64_t blocks) const cryptonote::test_options test_options{hard_forks, 5000}; auto& bc = bc_objects.m_blockchain; - if (!bc.init(new TestDB(), nullptr, cryptonote ::FAKECHAIN, true, &test_options, 0)) { + if (!bc.init(new TestDB(), nullptr /*ons_db*/, nullptr /*sqlite_db*/, cryptonote ::FAKECHAIN, true, &test_options, 0, NULL)) { fprintf(stderr, "Failed to init blockchain\n"); exit(1); }; + for (uint64_t h = 0; h < LONG_TERM_BLOCK_WEIGHT_WINDOW; ++h) { cryptonote::block b; diff --git a/tests/core_proxy/CMakeLists.txt b/tests/core_proxy/CMakeLists.txt index 86c80b313..5601a92ad 100644 --- a/tests/core_proxy/CMakeLists.txt +++ b/tests/core_proxy/CMakeLists.txt @@ -37,6 +37,7 @@ target_link_libraries(core_proxy version epee Boost::program_options + SQLiteCpp extra) set_property(TARGET core_proxy PROPERTY diff --git a/tests/core_proxy/core_proxy.cpp b/tests/core_proxy/core_proxy.cpp index 0dfa18e4f..5e4d15eea 100644 --- a/tests/core_proxy/core_proxy.cpp +++ b/tests/core_proxy/core_proxy.cpp @@ -45,6 +45,7 @@ //#include "cryptonote_core/cryptonote_core.h" #include "cryptonote_protocol/cryptonote_protocol_handler.h" #include "cryptonote_protocol/cryptonote_protocol_handler.inl" +#include "cryptonote_core/cryptonote_tx_utils.h" #include "core_proxy.h" #include "version.h" @@ -158,9 +159,9 @@ string tx2str(const cryptonote::transaction& tx, const cryptonote::hash256& tx_h return ss.str(); }*/ -std::vector tests::proxy_core::parse_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) { +std::vector tests::proxy_core::parse_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) { - std::vector tx_info(tx_blobs.size()); + std::vector tx_info(tx_blobs.size()); for (size_t i = 0; i < tx_blobs.size(); i++) { auto &txi = tx_info[i]; @@ -186,7 +187,7 @@ std::vector tests::proxy_core::par return tx_info; } -bool tests::proxy_core::handle_parsed_txs(std::vector &parsed_txs, const tx_pool_options &opts, uint64_t *blink_rollback_height) { +bool tests::proxy_core::handle_parsed_txs(std::vector &parsed_txs, const tx_pool_options &opts, uint64_t *blink_rollback_height) { if (blink_rollback_height) *blink_rollback_height = 0; @@ -197,7 +198,7 @@ bool tests::proxy_core::handle_parsed_txs(std::vector tests::proxy_core::handle_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) +std::vector tests::proxy_core::handle_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) { auto parsed = parse_incoming_txs(tx_blobs, opts); handle_parsed_txs(parsed, opts); diff --git a/tests/core_proxy/core_proxy.h b/tests/core_proxy/core_proxy.h index 90e8f3c5a..4f5b0225c 100644 --- a/tests/core_proxy/core_proxy.h +++ b/tests/core_proxy/core_proxy.h @@ -36,6 +36,7 @@ #include "cryptonote_basic/verification_context.h" #include "cryptonote_core/service_node_voting.h" #include "cryptonote_core/cryptonote_core.h" +#include "cryptonote_core/cryptonote_tx_utils.h" #include "cryptonote_core/tx_blink.h" #include @@ -78,9 +79,9 @@ namespace tests bool have_block(const crypto::hash& id); void get_blockchain_top(uint64_t& height, crypto::hash& top_id); bool handle_incoming_tx(const cryptonote::blobdata& tx_blob, cryptonote::tx_verification_context& tvc, const cryptonote::tx_pool_options &opts); - std::vector parse_incoming_txs(const std::vector& tx_blobs, const cryptonote::tx_pool_options &opts); - bool handle_parsed_txs(std::vector &parsed_txs, const cryptonote::tx_pool_options &opts, uint64_t *blink_rollback_height = nullptr); - std::vector handle_incoming_txs(const std::vector& tx_blobs, const cryptonote::tx_pool_options &opts); + std::vector parse_incoming_txs(const std::vector& tx_blobs, const cryptonote::tx_pool_options &opts); + bool handle_parsed_txs(std::vector &parsed_txs, const cryptonote::tx_pool_options &opts, uint64_t *blink_rollback_height = nullptr); + std::vector handle_incoming_txs(const std::vector& tx_blobs, const cryptonote::tx_pool_options &opts); std::pair>, std::unordered_set> parse_incoming_blinks(const std::vector &blinks); int add_blinks(const std::vector> &blinks) { return 0; } bool handle_incoming_block(const cryptonote::blobdata& block_blob, const cryptonote::block *block, cryptonote::block_verification_context& bvc, cryptonote::checkpoint_t *checkpoint, bool update_miner_blocktemplate = true); diff --git a/tests/core_tests/CMakeLists.txt b/tests/core_tests/CMakeLists.txt index e62f94666..99bc880e1 100644 --- a/tests/core_tests/CMakeLists.txt +++ b/tests/core_tests/CMakeLists.txt @@ -57,6 +57,7 @@ target_link_libraries(core_tests device wallet Boost::program_options + SQLiteCpp extra) set_property(TARGET core_tests PROPERTY diff --git a/tests/core_tests/block_reward.cpp b/tests/core_tests/block_reward.cpp index 6abac7f41..8a2b3daf3 100644 --- a/tests/core_tests/block_reward.cpp +++ b/tests/core_tests/block_reward.cpp @@ -43,7 +43,8 @@ namespace const account_public_address& miner_address, std::vector& block_weights, size_t target_tx_weight, size_t target_block_weight, uint64_t fee = 0) { - if (!construct_miner_tx(height, tools::median(block_weights.begin(), block_weights.end()), already_generated_coins, target_block_weight, fee, miner_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, mminer_address))) + std::optional> sn_rwds; + if (!construct_miner_tx(height, tools::median(block_weights.begin(), block_weights.end()), already_generated_coins, target_block_weight, fee, miner_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, mminer_address), sn_rwds)) return false; size_t current_weight = get_transaction_weight(miner_tx); diff --git a/tests/core_tests/block_validation.cpp b/tests/core_tests/block_validation.cpp index ac08128bd..3ceb0baa5 100644 --- a/tests/core_tests/block_validation.cpp +++ b/tests/core_tests/block_validation.cpp @@ -448,7 +448,7 @@ static bool construct_miner_tx_with_extra_output(cryptonote::transaction& tx, uint64_t governance_reward = 0; if (already_generated_coins != 0) { - governance_reward = governance_reward_formula(block_reward, hard_fork_version); + governance_reward = governance_reward_formula(hard_fork_version, block_reward); block_reward -= governance_reward; } diff --git a/tests/core_tests/chaingen.cpp b/tests/core_tests/chaingen.cpp index af4e90dfe..97534eaf5 100644 --- a/tests/core_tests/chaingen.cpp +++ b/tests/core_tests/chaingen.cpp @@ -40,6 +40,7 @@ #include #include "common/string_util.h" +#include "common/hex.h" #include "common/varint.h" #include "common/median.h" #include "epee/console_handler.h" @@ -164,14 +165,22 @@ std::vector oxen_chain_generator_db::get_blocks_range(const u return result; } -oxen_chain_generator::oxen_chain_generator(std::vector &events, const std::vector& hard_forks) +oxen_chain_generator::oxen_chain_generator(std::vector& events, const std::vector& hard_forks, std::string first_miner_seed) : events_(events) , hard_forks_(hard_forks) +, sqlite_db_(std::make_unique(cryptonote::FAKECHAIN, ":memory:")) { bool init = ons_db_->init(nullptr, cryptonote::FAKECHAIN, ons::init_oxen_name_system("", false /*read_only*/)); assert(init); - first_miner_.generate(); + if (first_miner_seed == "") { + first_miner_.generate(); + } else { + crypto::secret_key seeded_secret_key; + tools::hex_to_type(first_miner_seed, seeded_secret_key); + first_miner_.generate(seeded_secret_key, true); + } + oxen_blockchain_entry genesis = oxen_chain_generator::create_genesis_block(first_miner_, 1338224400); events_.push_back(genesis.block); db_.blocks.push_back(genesis); @@ -241,6 +250,8 @@ oxen_blockchain_entry &oxen_chain_generator::add_block(oxen_blockchain_entry con ons_db_->add_block(entry.block, entry.txs); } + sqlite_db_->add_block(entry.block, entry.service_node_state); + // TODO(oxen): State history culling and alt states state_history_.emplace_hint(state_history_.end(), result.service_node_state); @@ -257,6 +268,9 @@ oxen_blockchain_entry &oxen_chain_generator::add_block(oxen_blockchain_entry con events_.push_back(oxen_blockchain_addable(result.block, can_be_added_to_blockchain, fail_msg)); } + cryptonote::block sopthing = entry.block; + + return result; } @@ -825,16 +839,20 @@ oxen_blockchain_entry oxen_chain_generator::create_genesis_block(const cryptonot // TODO(doyle): Does this evaluate to 0? If so we can simplify this a lot more size_t target_block_weight = get_transaction_weight(blk.miner_tx); + std::optional> sn_rwds; + uint64_t block_rewards = 0; while (true) { - bool constructed = construct_miner_tx(height, + bool constructed = false; + std::tie(constructed, block_rewards) = construct_miner_tx(height, 0 /*median_weight*/, 0 /*already_generated_coins*/, target_block_weight, 0 /*total_fee*/, blk.miner_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner.get_keys().m_account_address), + sn_rwds, cryptonote::blobdata(), hf_version_); assert(constructed); @@ -893,6 +911,7 @@ bool oxen_chain_generator::block_begin(oxen_blockchain_entry &entry, oxen_create cryptonote::block &blk = entry.block; blk.major_version = params.hf_version; blk.minor_version = params.hf_version; + blk.height = height; blk.timestamp = params.timestamp; blk.prev_id = get_block_hash(params.prev.block); @@ -947,10 +966,12 @@ bool oxen_chain_generator::block_begin(oxen_blockchain_entry &entry, oxen_create } miner_tx_context = cryptonote::oxen_miner_tx_context::pulse_block(cryptonote::FAKECHAIN, block_producer, params.block_leader); + blk.service_node_winner_key = miner_tx_context.pulse_block_producer.key; } else { miner_tx_context = cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, params.miner_acc.get_keys().m_account_address, params.block_leader); + blk.service_node_winner_key = miner_tx_context.block_leader.key; } if (blk.major_version >= cryptonote::network_version_10_bulletproofs && @@ -959,13 +980,11 @@ bool oxen_chain_generator::block_begin(oxen_blockchain_entry &entry, oxen_create constexpr uint64_t num_blocks = cryptonote::get_config(cryptonote::FAKECHAIN).GOVERNANCE_REWARD_INTERVAL_IN_BLOCKS; uint64_t start_height = height - num_blocks; - static_assert(cryptonote::network_version_count == cryptonote::network_version_19 + 1, - "The code below needs to be updated to support higher hard fork versions"); if (blk.major_version == cryptonote::network_version_15_ons) miner_tx_context.batched_governance = FOUNDATION_REWARD_HF15 * num_blocks; else if (blk.major_version == cryptonote::network_version_16_pulse) miner_tx_context.batched_governance = (FOUNDATION_REWARD_HF15 + CHAINFLIP_LIQUIDITY_HF16) * num_blocks; - else if (blk.major_version >= cryptonote::network_version_17 && blk.major_version <= cryptonote::network_version_19) + else if (blk.major_version >= cryptonote::network_version_17) miner_tx_context.batched_governance = FOUNDATION_REWARD_HF17 * num_blocks; else { @@ -981,18 +1000,27 @@ bool oxen_chain_generator::block_begin(oxen_blockchain_entry &entry, oxen_create } size_t target_block_weight = txs_weight + get_transaction_weight(blk.miner_tx); + std::optional> sn_rwds; + if (hf_version_ >= cryptonote::network_version_19) + { + sn_rwds = sqlite_db_->get_sn_payments(height); //Rewards to pay out + } + uint64_t block_rewards = 0; + bool r; while (true) { - if (!construct_miner_tx(height, - tools::median(params.block_weights.begin(), params.block_weights.end()), - params.prev.already_generated_coins, - target_block_weight, - total_fee, - blk.miner_tx, - miner_tx_context, - cryptonote::blobdata(), - blk.major_version - )) + std::tie(r, block_rewards) = construct_miner_tx(height, + tools::median(params.block_weights.begin(), params.block_weights.end()), + params.prev.already_generated_coins, + target_block_weight, + total_fee, + blk.miner_tx, + miner_tx_context, + sn_rwds, + cryptonote::blobdata(), + blk.major_version + ); + if (!r) return false; entry.block_weight = txs_weight + get_transaction_weight(blk.miner_tx); @@ -1033,6 +1061,7 @@ bool oxen_chain_generator::block_begin(oxen_blockchain_entry &entry, oxen_create } } + blk.reward = block_rewards; entry.txs = tx_list; uint64_t block_reward, block_reward_unpenalized; cryptonote::get_base_block_reward(tools::median(params.block_weights.begin(), params.block_weights.end()), entry.block_weight, params.prev.already_generated_coins, block_reward, block_reward_unpenalized, params.hf_version, height); @@ -1066,6 +1095,21 @@ void oxen_chain_generator::block_end(oxen_blockchain_entry &entry, oxen_create_b entry.service_node_state.update_from_block(db_, cryptonote::FAKECHAIN, state_history_, {} /*state_archive*/, {} /*alt_states*/, entry.block, entry.txs, nullptr); } +bool oxen_chain_generator::process_registration_tx(cryptonote::transaction& tx, uint64_t block_height, uint8_t hf_version) +{ + service_nodes::contributor_args_t contributor_args = {}; + crypto::public_key service_node_key; + uint64_t expiration_timestamp{0}; + crypto::signature signature; + + if (!service_nodes::reg_tx_extract_fields(tx, contributor_args, expiration_timestamp, service_node_key, signature)) + return false; + + uint64_t staking_requirement = service_nodes::get_staking_requirement(cryptonote::FAKECHAIN, block_height); + + return true; +} + bool oxen_chain_generator::create_block(oxen_blockchain_entry &entry, oxen_create_block_params ¶ms, const std::vector &tx_list) const @@ -1229,7 +1273,7 @@ static void manual_calc_batched_governance(const test_generator &generator, if (hard_fork_version >= cryptonote::network_version_15_ons) { - miner_tx_context.batched_governance = num_blocks * cryptonote::governance_reward_formula(0, hard_fork_version); + miner_tx_context.batched_governance = num_blocks * cryptonote::governance_reward_formula(hard_fork_version); return; } @@ -1296,17 +1340,22 @@ bool test_generator::construct_block(cryptonote::block &blk, size_t target_block_weight = txs_weight + get_transaction_weight(blk.miner_tx); manual_calc_batched_governance(*this, prev_id, miner_tx_context, m_hf_version, height); + uint64_t block_rewards = 0; + bool r; + std::optional> sn_rwds; while (true) { - if (!construct_miner_tx(height, - tools::median(block_weights.begin(), block_weights.end()), - already_generated_coins, - target_block_weight, - total_fee, - blk.miner_tx, - miner_tx_context, - cryptonote::blobdata(), - m_hf_version)) + std::tie(r, block_rewards) = construct_miner_tx(height, + tools::median(block_weights.begin(), block_weights.end()), + already_generated_coins, + target_block_weight, + total_fee, + blk.miner_tx, + miner_tx_context, + sn_rwds, + cryptonote::blobdata(), + m_hf_version); + if (!r) return false; size_t actual_block_weight = txs_weight + get_transaction_weight(blk.miner_tx); @@ -1402,6 +1451,7 @@ bool test_generator::construct_block_manually( blk.tx_hashes = actual_params & bf_tx_hashes ? tx_hashes : std::vector(); size_t height = get_block_height(prev_block) + 1; + blk.height = height; uint64_t already_generated_coins = get_already_generated_coins(prev_block); std::vector block_weights; get_last_n_block_weights(block_weights, get_block_hash(prev_block), CRYPTONOTE_REWARD_BLOCKS_WINDOW); @@ -1416,8 +1466,12 @@ bool test_generator::construct_block_manually( miner_tx_context.nettype = cryptonote::FAKECHAIN; manual_calc_batched_governance(*this, prev_id, miner_tx_context, m_hf_version, height); + std::optional> sn_rwds; size_t current_block_weight = txs_weight + get_transaction_weight(blk.miner_tx); - if (!construct_miner_tx(height, tools::median(block_weights.begin(), block_weights.end()), already_generated_coins, current_block_weight, miner_fee, blk.miner_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc.get_keys().m_account_address), cryptonote::blobdata(), m_hf_version)) + uint64_t block_rewards = 0; + bool r; + std::tie(r, block_rewards) = construct_miner_tx(height, tools::median(block_weights.begin(), block_weights.end()), already_generated_coins, current_block_weight, miner_fee, blk.miner_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc.get_keys().m_account_address), sn_rwds, cryptonote::blobdata(), m_hf_version); + if (!r) return false; } @@ -1781,6 +1835,7 @@ void fill_tx_sources_and_multi_destinations(const std::vector& bool always_add_change_ouput, uint64_t *change_amount) { + sources.clear(); destinations.clear(); diff --git a/tests/core_tests/chaingen.h b/tests/core_tests/chaingen.h index a7e2b92b9..e5b296a65 100644 --- a/tests/core_tests/chaingen.h +++ b/tests/core_tests/chaingen.h @@ -1134,15 +1134,20 @@ inline bool do_replay_file(const std::string& filename) #define MAKE_MINER_TX_MANUALLY(TX, BLK) \ transaction TX; \ - if (!construct_miner_tx(get_block_height(BLK) + 1, \ + std::optional> sn_rwds; \ + uint64_t block_rewards = 0; \ + bool r; \ + std::tie(r, block_rewards) = construct_miner_tx(get_block_height(BLK) + 1, \ 0, \ generator.get_already_generated_coins(BLK), \ 0, \ 0, \ TX, \ cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_account.get_keys().m_account_address), \ - {}, \ - 7)) \ + sn_rwds, \ + {} \ + ); \ + if (!r) \ return false; #define MAKE_TX_LIST_START_RCT(VEC_EVENTS, SET_NAME, FROM, TO, AMOUNT, NMIX, HEAD) \ @@ -1380,6 +1385,7 @@ struct oxen_blockchain_entry cryptonote::checkpoint_t checkpoint; }; + struct oxen_chain_generator_db : public cryptonote::BaseTestDB { std::vector blocks; @@ -1431,13 +1437,17 @@ struct oxen_chain_generator service_nodes::service_node_list::state_set state_history_; uint64_t last_cull_height_ = 0; std::shared_ptr ons_db_ = std::make_shared(); + std::unique_ptr sqlite_db_; oxen_chain_generator_db db_; uint8_t hf_version_ = cryptonote::network_version_7; std::vector& events_; const std::vector hard_forks_; cryptonote::account_base first_miner_; - oxen_chain_generator(std::vector &events, const std::vector& hard_forks); + oxen_chain_generator(std::vector& events, const std::vector& hard_forks, std::string first_miner_seed = ""); + oxen_chain_generator(const oxen_chain_generator &other) + :tx_table_(other.tx_table_), service_node_keys_(other.service_node_keys_), state_history_(other.state_history_), last_cull_height_(other.last_cull_height_), sqlite_db_(std::make_unique(*other.sqlite_db_)), + ons_db_(other.ons_db_ ), db_(other.db_), hf_version_(other.hf_version_), events_(other.events_), hard_forks_(other.hard_forks_), first_miner_(other.first_miner_) {}; uint64_t height() const { return cryptonote::get_block_height(db_.blocks.back().block); } uint64_t chain_height() const { return height() + 1; } @@ -1505,8 +1515,15 @@ struct oxen_chain_generator bool block_begin(oxen_blockchain_entry &entry, oxen_create_block_params ¶ms, const std::vector &tx_list) const; void block_fill_pulse_data(oxen_blockchain_entry &entry, oxen_create_block_params const ¶ms, uint8_t round) const; void block_end(oxen_blockchain_entry &entry, oxen_create_block_params const ¶ms) const; + bool process_registration_tx(cryptonote::transaction& tx, uint64_t block_height, uint8_t hf_version); uint8_t get_hf_version_at(uint64_t height) const; std::vector last_n_block_weights(uint64_t height, uint64_t num) const; const cryptonote::account_base& first_miner() const { return first_miner_; } + + oxen_chain_generator& operator=(const oxen_chain_generator& other) + { + new(this) oxen_chain_generator(other); + return *this; + } }; diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 2295a4cc7..0957c4c32 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -147,6 +147,11 @@ int main(int argc, char* argv[]) GENERATE_AND_PLAY(oxen_pulse_fallback_to_pow_and_back); GENERATE_AND_PLAY(oxen_pulse_chain_split); GENERATE_AND_PLAY(oxen_pulse_chain_split_with_no_checkpoints); + GENERATE_AND_PLAY(oxen_batch_sn_rewards); + GENERATE_AND_PLAY(oxen_batch_sn_rewards_bad_amount); + GENERATE_AND_PLAY(oxen_batch_sn_rewards_bad_address); + GENERATE_AND_PLAY(oxen_batch_sn_rewards_pop_blocks); + GENERATE_AND_PLAY(oxen_batch_sn_rewards_pop_blocks_after_big_cycle); // NOTE: Monero Tests GENERATE_AND_PLAY(gen_simple_chain_001); diff --git a/tests/core_tests/integer_overflow.cpp b/tests/core_tests/integer_overflow.cpp index 26021906e..90a16ee1a 100644 --- a/tests/core_tests/integer_overflow.cpp +++ b/tests/core_tests/integer_overflow.cpp @@ -111,9 +111,12 @@ bool gen_uint_overflow_1::generate(std::vector& events) const // Problem 1. Miner tx outputs overflow { oxen_blockchain_entry entry = gen.create_next_block(); - cryptonote::transaction &miner_tx = entry.block.miner_tx; - split_miner_tx_outs(miner_tx, MONEY_SUPPLY); - gen.add_block(entry, false /*can_be_added_to_blockchain*/, "We purposely overflow miner tx by MONEY_SUPPLY in the miner tx"); + if ( entry.block.major_version < cryptonote::network_version_19) + { + cryptonote::transaction &miner_tx = entry.block.miner_tx; + split_miner_tx_outs(miner_tx, MONEY_SUPPLY); + gen.add_block(entry, false /*can_be_added_to_blockchain*/, "We purposely overflow miner tx by MONEY_SUPPLY in the miner tx"); + } } // Problem 2. block_reward overflow @@ -124,9 +127,12 @@ bool gen_uint_overflow_1::generate(std::vector& events) const txs.push_back(gen.create_and_add_tx(gen.first_miner_, alice.get_keys().m_account_address, MK_COINS(1), MK_COINS(100) /*fee*/, false /*kept_by_block*/)); oxen_blockchain_entry entry = gen.create_next_block(txs); - cryptonote::transaction &miner_tx = entry.block.miner_tx; - miner_tx.vout[0].amount = 0; // Take partial block reward, fee > block_reward so ordinarly it would overflow. This should be disallowed - gen.add_block(entry, false /*can_be_added_to_blockchain*/, "We should not be able to add TX because the fee is greater than the base miner reward"); + if ( entry.block.major_version < cryptonote::network_version_19) + { + cryptonote::transaction &miner_tx = entry.block.miner_tx; + miner_tx.vout[0].amount = 0; // Take partial block reward, fee > block_reward so ordinarly it would overflow. This should be disallowed + gen.add_block(entry, false /*can_be_added_to_blockchain*/, "We should not be able to add TX because the fee is greater than the base miner reward"); + } } { @@ -135,9 +141,12 @@ bool gen_uint_overflow_1::generate(std::vector& events) const txs.push_back(gen.create_and_add_tx(gen.first_miner_, alice.get_keys().m_account_address, MK_COINS(1), MK_COINS(100) /*fee*/, true /*kept_by_block*/)); oxen_blockchain_entry entry = gen.create_next_block(txs); - cryptonote::transaction &miner_tx = entry.block.miner_tx; - miner_tx.vout[0].amount = 0; // Take partial block reward, fee > block_reward so ordinarly it would overflow. This should be disallowed - gen.add_block(entry, false /*can_be_added_to_blockchain*/, "We should not be able to add TX because the fee is greater than the base miner reward even if kept_by_block is true"); + if ( entry.block.major_version < cryptonote::network_version_19) + { + cryptonote::transaction &miner_tx = entry.block.miner_tx; + miner_tx.vout[0].amount = 0; // Take partial block reward, fee > block_reward so ordinarly it would overflow. This should be disallowed + gen.add_block(entry, false /*can_be_added_to_blockchain*/, "We should not be able to add TX because the fee is greater than the base miner reward even if kept_by_block is true"); + } } } return true; diff --git a/tests/core_tests/oxen_tests.cpp b/tests/core_tests/oxen_tests.cpp index 1031330f0..15810194d 100644 --- a/tests/core_tests/oxen_tests.cpp +++ b/tests/core_tests/oxen_tests.cpp @@ -44,11 +44,15 @@ extern "C" #include }; -static void add_service_nodes(oxen_chain_generator &gen, size_t count) +static void add_service_nodes(oxen_chain_generator &gen, size_t count, uint8_t hf_version) { std::vector registration_txs(count); + uint64_t curr_height = gen.height(); for (auto i = 0u; i < count; ++i) + { registration_txs[i] = gen.create_and_add_registration_tx(gen.first_miner()); + gen.process_registration_tx(registration_txs[i], curr_height + 1, hf_version); + } gen.create_and_add_next_block(registration_txs); } @@ -74,7 +78,7 @@ bool oxen_checkpointing_alt_chain_handle_alt_blocks_at_tip::generate(std::vector gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE); + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE, hard_forks.back().version); // NOTE: Create next block on checkpoint boundary and add checkpoiont @@ -83,23 +87,25 @@ bool oxen_checkpointing_alt_chain_handle_alt_blocks_at_tip::generate(std::vector fork.add_blocks_until_next_checkpointable_height(); fork.add_service_node_checkpoint(fork.height(), service_nodes::CHECKPOINT_MIN_VOTES); + // NOTE: Though we receive a checkpoint via votes, the alt block is still in // the alt db because we don't trigger a chain switch until we receive a 2nd // block that confirms the alt block. uint64_t curr_height = gen.height(); + crypto::hash curr_hash = get_block_hash(gen.top().block); oxen_register_callback(events, "check_alt_block_count", [curr_height, curr_hash](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_alt_block_count"); + { + DEFINE_TESTS_ERROR_CONTEXT("check_alt_block_count"); - uint64_t top_height; - crypto::hash top_hash; - c.get_blockchain_top(top_height, top_hash); - CHECK_EQ(top_height, curr_height); - CHECK_EQ(top_hash, curr_hash); - CHECK_TEST_CONDITION(c.get_blockchain_storage().get_alternative_blocks_count() > 0); - return true; - }); + uint64_t top_height; + crypto::hash top_hash; + c.get_blockchain_top(top_height, top_hash); + CHECK_EQ(top_height, curr_height); + CHECK_EQ(top_hash, curr_hash); + CHECK_TEST_CONDITION(c.get_blockchain_storage().get_alternative_blocks_count() > 0); + return true; + }); // NOTE: We add a new block ontop that causes the alt block code path to run // again, and calculate that this alt chain now has 2 blocks on it with @@ -111,17 +117,17 @@ bool oxen_checkpointing_alt_chain_handle_alt_blocks_at_tip::generate(std::vector gen.create_and_add_next_block(); fork.create_and_add_next_block(); - crypto::hash expected_top_hash = cryptonote::get_block_hash(fork.top().block); + crypto::hash expected_top_hash = cryptonote::get_block_hash(fork.top().block); oxen_register_callback(events, "check_chain_reorged", [expected_top_hash](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_chain_reorged"); - CHECK_EQ(c.get_blockchain_storage().get_alternative_blocks_count(), 0); - uint64_t top_height; - crypto::hash top_hash; - c.get_blockchain_top(top_height, top_hash); - CHECK_EQ(expected_top_hash, top_hash); - return true; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_chain_reorged"); + CHECK_EQ(c.get_blockchain_storage().get_alternative_blocks_count(), 0); + uint64_t top_height; + crypto::hash top_hash; + c.get_blockchain_top(top_height, top_hash); + CHECK_EQ(expected_top_hash, top_hash); + return true; + }); return true; } @@ -134,11 +140,7 @@ bool oxen_checkpointing_alt_chain_more_service_node_checkpoints_less_pow_overtak gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - int constexpr NUM_SERVICE_NODES = service_nodes::CHECKPOINT_QUORUM_SIZE; - std::vector registration_txs(NUM_SERVICE_NODES); - for (auto i = 0u; i < NUM_SERVICE_NODES; ++i) - registration_txs[i] = gen.create_and_add_registration_tx(gen.first_miner()); - gen.create_and_add_next_block(registration_txs); + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE, hard_forks.back().version); gen.add_blocks_until_next_checkpointable_height(); oxen_chain_generator fork_with_more_checkpoints = gen; @@ -150,15 +152,15 @@ bool oxen_checkpointing_alt_chain_more_service_node_checkpoints_less_pow_overtak crypto::hash const fork_top_hash = cryptonote::get_block_hash(fork_with_more_checkpoints.top().block); oxen_register_callback(events, "check_switched_to_alt_chain", [fork_top_hash, fork_top_height](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_switched_to_alt_chain"); - uint64_t top_height; - crypto::hash top_hash; - c.get_blockchain_top(top_height, top_hash); - CHECK_EQ(top_height, fork_top_height); - CHECK_EQ(top_hash, fork_top_hash); - return true; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_switched_to_alt_chain"); + uint64_t top_height; + crypto::hash top_hash; + c.get_blockchain_top(top_height, top_hash); + CHECK_EQ(top_height, fork_top_height); + CHECK_EQ(top_hash, fork_top_hash); + return true; + }); return true; } @@ -171,11 +173,7 @@ bool oxen_checkpointing_alt_chain_receive_checkpoint_votes_should_reorg_back::ge gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - int constexpr NUM_SERVICE_NODES = service_nodes::CHECKPOINT_QUORUM_SIZE; - std::vector registration_txs(NUM_SERVICE_NODES); - for (auto i = 0u; i < NUM_SERVICE_NODES; ++i) - registration_txs[i] = gen.create_and_add_registration_tx(gen.first_miner()); - gen.create_and_add_next_block(registration_txs); + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE, hard_forks.back().version); gen.add_event_msg("Add blocks until we get to the first height that has a checkpointing quorum AND there are service nodes in the quorum."); gen.add_blocks_until_next_checkpointable_height(); @@ -219,14 +217,14 @@ bool oxen_checkpointing_alt_chain_receive_checkpoint_votes_should_reorg_back::ge fork.create_and_add_next_block({}); crypto::hash const fork_top_hash = cryptonote::get_block_hash(fork.top().block); oxen_register_callback(events, "check_switched_to_alt_chain", [fork_top_hash](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_switched_to_alt_chain"); - uint64_t top_height; - crypto::hash top_hash; - c.get_blockchain_top(top_height, top_hash); - CHECK_EQ(fork_top_hash, top_hash); - return true; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_switched_to_alt_chain"); + uint64_t top_height; + crypto::hash top_hash; + c.get_blockchain_top(top_height, top_hash); + CHECK_EQ(fork_top_hash, top_hash); + return true; + }); return true; } @@ -237,11 +235,7 @@ bool oxen_checkpointing_alt_chain_too_old_should_be_dropped::generate(std::vecto gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - int constexpr NUM_SERVICE_NODES = service_nodes::CHECKPOINT_QUORUM_SIZE; - std::vector registration_txs(NUM_SERVICE_NODES); - for (auto i = 0u; i < NUM_SERVICE_NODES; ++i) - registration_txs[i] = gen.create_and_add_registration_tx(gen.first_miner()); - gen.create_and_add_next_block(registration_txs); + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE, hard_forks.back().version); oxen_chain_generator fork = gen; gen.add_blocks_until_next_checkpointable_height(); @@ -272,7 +266,7 @@ bool oxen_checkpointing_alt_chain_with_increasing_service_node_checkpoints::gene gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE); + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE, hard_forks.back().version); gen.add_blocks_until_next_checkpointable_height(); @@ -291,14 +285,14 @@ bool oxen_checkpointing_alt_chain_with_increasing_service_node_checkpoints::gene crypto::hash const gen_top_hash = cryptonote::get_block_hash(gen.top().block); oxen_register_callback(events, "check_still_on_main_chain", [gen_top_hash](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_still_on_main_chain"); - uint64_t top_height; - crypto::hash top_hash; - c.get_blockchain_top(top_height, top_hash); - CHECK_EQ(top_hash, gen_top_hash); - return true; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_still_on_main_chain"); + uint64_t top_height; + crypto::hash top_hash; + c.get_blockchain_top(top_height, top_hash); + CHECK_EQ(top_hash, gen_top_hash); + return true; + }); // Now create the following chain, the fork chain should be switched to due to now having more checkpoints // Main chain C B B B B | B B B B B @@ -307,19 +301,20 @@ bool oxen_checkpointing_alt_chain_with_increasing_service_node_checkpoints::gene gen.create_and_add_next_block(); fork.add_blocks_until_next_checkpointable_height(); - cryptonote::checkpoint_t fork_second_checkpoint = fork.create_service_node_checkpoint(fork.height(), service_nodes::CHECKPOINT_MIN_VOTES); - fork.create_and_add_next_block({}, &fork_second_checkpoint); + //cryptonote::checkpoint_t fork_second_checkpoint = fork.create_service_node_checkpoint(fork.height(), service_nodes::CHECKPOINT_MIN_VOTES); + //fork.create_and_add_next_block({}, &fork_second_checkpoint); + fork.add_service_node_checkpoint(fork.height(), service_nodes::CHECKPOINT_MIN_VOTES); crypto::hash const fork_top_hash = cryptonote::get_block_hash(fork.top().block); oxen_register_callback(events, "check_switched_to_alt_chain", [fork_top_hash](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_switched_to_alt_chain"); - uint64_t top_height; - crypto::hash top_hash; - c.get_blockchain_top(top_height, top_hash); - CHECK_EQ(fork_top_hash, top_hash); - return true; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_switched_to_alt_chain"); + uint64_t top_height; + crypto::hash top_hash; + c.get_blockchain_top(top_height, top_hash); + CHECK_EQ(fork_top_hash, top_hash); + return true; + }); return true; } @@ -332,7 +327,7 @@ bool oxen_checkpointing_service_node_checkpoint_from_votes::generate(std::vector gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE); + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE, hard_forks.back().version); // NOTE: Generate service node votes gen.add_blocks_until_next_checkpointable_height(); @@ -355,9 +350,9 @@ bool oxen_checkpointing_service_node_checkpoint_from_votes::generate(std::vector service_nodes::quorum_vote_t invalid_vote = service_nodes::make_checkpointing_vote(gen.top().block.major_version, checkpointed_hash, checkpointed_height, 0, invalid_keys); gen.events_.push_back(oxen_blockchain_addable( - invalid_vote, - false /*can_be_added_to_blockchain*/, - "Can not add a vote that uses a service node key not part of the quorum")); + invalid_vote, + false /*can_be_added_to_blockchain*/, + "Can not add a vote that uses a service node key not part of the quorum")); } // NOTE: Add insufficient service node votes and check that no checkpoint is generated yet @@ -365,24 +360,24 @@ bool oxen_checkpointing_service_node_checkpoint_from_votes::generate(std::vector gen.events_.push_back(oxen_blockchain_addable(checkpoint_votes[i])); oxen_register_callback(events, "check_service_node_checkpoint_rejected_insufficient_votes", [checkpointed_height](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_service_node_checkpoint_rejected_insufficient_votes"); - cryptonote::Blockchain const &blockchain = c.get_blockchain_storage(); - cryptonote::checkpoint_t real_checkpoint; - CHECK_TEST_CONDITION(blockchain.get_checkpoint(checkpointed_height, real_checkpoint) == false); - return true; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_service_node_checkpoint_rejected_insufficient_votes"); + cryptonote::Blockchain const &blockchain = c.get_blockchain_storage(); + cryptonote::checkpoint_t real_checkpoint; + CHECK_TEST_CONDITION(blockchain.get_checkpoint(checkpointed_height, real_checkpoint) == false); + return true; + }); // NOTE: Add last vote and check checkpoint has been generated gen.events_.push_back(checkpoint_votes.back()); oxen_register_callback(events, "check_service_node_checkpoint_accepted", [checkpointed_height](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_service_node_checkpoint_accepted"); - cryptonote::Blockchain const &blockchain = c.get_blockchain_storage(); - cryptonote::checkpoint_t real_checkpoint; - CHECK_TEST_CONDITION(blockchain.get_checkpoint(checkpointed_height, real_checkpoint)); - return true; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_service_node_checkpoint_accepted"); + cryptonote::Blockchain const &blockchain = c.get_blockchain_storage(); + cryptonote::checkpoint_t real_checkpoint; + CHECK_TEST_CONDITION(blockchain.get_checkpoint(checkpointed_height, real_checkpoint)); + return true; + }); return true; } @@ -396,7 +391,7 @@ bool oxen_checkpointing_service_node_checkpoints_check_reorg_windows::generate(s gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE); + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE, hard_forks.back().version); // NOTE: Add blocks until we get to the first height that has a checkpointing quorum AND there are service nodes in the quorum. int const MAX_TRIES = 16; @@ -459,26 +454,26 @@ bool oxen_core_block_reward_unpenalized_pre_pulse::generate(std::vector& events) { - auto hard_forks = oxen_generate_hard_fork_table(cryptonote::network_version_count -1, 150 /*Proof Of Stake Delay*/); + auto hard_forks = oxen_generate_hard_fork_table(cryptonote::network_version_19 -1, 150 /*Proof Of Stake Delay*/); oxen_chain_generator gen(events, hard_forks); uint8_t const newest_hf = hard_forks.back().version; @@ -500,28 +495,28 @@ bool oxen_core_block_reward_unpenalized_post_pulse::generate(std::vector 0 && rewards_from_fee < tx_fee, "Block producer should receive a penalised tx fee less than " << cryptonote::print_money(tx_fee) << "received, " << cryptonote::print_money(rewards_from_fee) << ""); - CHECK_TEST_CONDITION_MSG(top_block.miner_tx.vout[1].amount == unpenalized_reward, "Service Node should receive full reward " << unpenalized_reward); + uint64_t rewards_from_fee = top_block.miner_tx.vout[0].amount; + CHECK_TEST_CONDITION_MSG(top_block.miner_tx.vout.size() == 2, "1 for miner, 1 for service node"); + CHECK_TEST_CONDITION_MSG(rewards_from_fee > 0 && rewards_from_fee < tx_fee, "Block producer should receive a penalised tx fee less than " << cryptonote::print_money(tx_fee) << "received, " << cryptonote::print_money(rewards_from_fee) << ""); + CHECK_TEST_CONDITION_MSG(top_block.miner_tx.vout[1].amount == unpenalized_reward, "Service Node should receive full reward " << unpenalized_reward); - MGINFO("rewards_from_fee: " << cryptonote::print_money(rewards_from_fee)); - MGINFO("tx_fee: " << cryptonote::print_money(tx_fee)); - MGINFO("unpenalized_amount: " << cryptonote::print_money(unpenalized_reward)); - return true; - }); + MGINFO("rewards_from_fee: " << cryptonote::print_money(rewards_from_fee)); + MGINFO("tx_fee: " << cryptonote::print_money(tx_fee)); + MGINFO("unpenalized_amount: " << cryptonote::print_money(unpenalized_reward)); + return true; + }); return true; } @@ -536,13 +531,15 @@ bool oxen_core_fee_burning::generate(std::vector& events) gen.add_mined_money_unlock_blocks(); + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE, hard_forks.back().version); + using namespace cryptonote; account_base dummy = gen.add_account(); static constexpr std::array, 3> send_fee_burn{{ {MK_COINS(5), MK_COINS(3), MK_COINS(1)}, - {MK_COINS(10), MK_COINS(5), MK_COINS(2)}, - {MK_COINS(5), MK_COINS(2), MK_COINS(1)}, + {MK_COINS(10), MK_COINS(5), MK_COINS(2)}, + {MK_COINS(5), MK_COINS(2), MK_COINS(1)}, }}; auto add_burning_tx = [&events, &gen, &dummy, newest_hf](const std::array &send_fee_burn) { @@ -569,7 +566,7 @@ bool oxen_core_fee_burning::generate(std::vector& events) ctx.fee = send_fee_burn[0][1] + send_fee_burn[1][1] - send_fee_burn[0][2] - send_fee_burn[1][2]; block_reward_parts reward_parts; cryptonote::get_oxen_block_reward(0, 0, 1 /*already generated, needs to be >0 to avoid premine*/, newest_hf, reward_parts, ctx); - good_miner_reward = reward_parts.miner_fee + reward_parts.base_miner; + good_miner_reward = reward_parts.miner_fee + reward_parts.base_miner + reward_parts.service_node_total; } txs.clear(); @@ -587,23 +584,23 @@ bool oxen_core_fee_burning::generate(std::vector& events) } oxen_register_callback(events, "check_fee_burned", [good_hash, good_miner_reward](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_fee_burned"); - uint64_t top_height; - crypto::hash top_hash; - c.get_blockchain_top(top_height, top_hash); + { + DEFINE_TESTS_ERROR_CONTEXT("check_fee_burned"); + uint64_t top_height; + crypto::hash top_hash; + c.get_blockchain_top(top_height, top_hash); - bool orphan; - cryptonote::block top_block; - CHECK_TEST_CONDITION(c.get_block_by_hash(top_hash, top_block, &orphan)); - CHECK_TEST_CONDITION(orphan == false); + bool orphan; + cryptonote::block top_block; + CHECK_TEST_CONDITION(c.get_block_by_hash(top_hash, top_block, &orphan)); + CHECK_TEST_CONDITION(orphan == false); - CHECK_EQ(top_hash, good_hash); + CHECK_EQ(top_hash, good_hash); - CHECK_EQ(top_block.miner_tx.vout[0].amount, good_miner_reward); + CHECK_EQ(top_block.reward, good_miner_reward); - return true; - }); + return true; + }); return true; } @@ -636,9 +633,9 @@ bool oxen_core_governance_batched_reward::generate(std::vector // you don't atleast progress and generate blocks from hf8 you will run into // problems std::vector other_hard_forks = { - {7,0,0,0}, - {8,0,1,0}, - {9,0,hf10_height,0}}; + {7,0,0,0}, + {8,0,1,0}, + {9,0,hf10_height,0}}; std::vector unused_events; oxen_chain_generator no_batched_governance_generator(unused_events, other_hard_forks); @@ -659,25 +656,25 @@ bool oxen_core_governance_batched_reward::generate(std::vector } oxen_register_callback(events, "check_batched_governance_amount_matches", [hf10_height, expected_total_governance_paid](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_batched_governance_amount_matches"); + { + DEFINE_TESTS_ERROR_CONTEXT("check_batched_governance_amount_matches"); - uint64_t height = c.get_current_blockchain_height(); - std::vector blockchain; - if (!c.get_blocks((uint64_t)0, (size_t)height, blockchain)) + uint64_t height = c.get_current_blockchain_height(); + std::vector blockchain; + if (!c.get_blocks((uint64_t)0, (size_t)height, blockchain)) return false; - uint64_t governance = 0; - for (size_t block_height = hf10_height; block_height < blockchain.size(); ++block_height) - { + uint64_t governance = 0; + for (size_t block_height = hf10_height; block_height < blockchain.size(); ++block_height) + { const cryptonote::block &block = blockchain[block_height]; if (cryptonote::block_has_governance_output(cryptonote::FAKECHAIN, block)) - governance += block.miner_tx.vout.back().amount; - } + governance += block.miner_tx.vout.back().amount; + } - CHECK_EQ(governance, expected_total_governance_paid); - return true; - }); + CHECK_EQ(governance, expected_total_governance_paid); + return true; + }); return true; } @@ -704,64 +701,64 @@ bool oxen_core_block_rewards_lrc6::generate(std::vector& event } oxen_register_callback(events, "check_lrc6_7_block_rewards", [hf15_height, hf16_height, hf17_height, interval=network.GOVERNANCE_REWARD_INTERVAL_IN_BLOCKS](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_lrc6_7_block_rewards"); + { + DEFINE_TESTS_ERROR_CONTEXT("check_lrc6_7_block_rewards"); - uint64_t height = c.get_current_blockchain_height(); - std::vector blockchain; - if (!c.get_blocks((uint64_t)0, (size_t)height, blockchain)) + uint64_t height = c.get_current_blockchain_height(); + std::vector blockchain; + if (!c.get_blocks((uint64_t)0, (size_t)height, blockchain)) return false; - int hf15_gov = 0, hf16_gov = 0, hf17_gov = 0; - for (size_t block_height = hf15_height; block_height < hf16_height; ++block_height) - { + int hf15_gov = 0, hf16_gov = 0, hf17_gov = 0; + for (size_t block_height = hf15_height; block_height < hf16_height; ++block_height) + { const cryptonote::block &block = blockchain[block_height]; CHECK_EQ(block.miner_tx.vout.at(0).amount, MINER_REWARD_HF15); CHECK_EQ(block.miner_tx.vout.at(1).amount, SN_REWARD_HF15); if (cryptonote::block_has_governance_output(cryptonote::FAKECHAIN, block)) { - hf15_gov++; - CHECK_EQ(block.miner_tx.vout.at(2).amount, FOUNDATION_REWARD_HF15 * interval); - CHECK_EQ(block.miner_tx.vout.size(), 3); + hf15_gov++; + CHECK_EQ(block.miner_tx.vout.at(2).amount, FOUNDATION_REWARD_HF15 * interval); + CHECK_EQ(block.miner_tx.vout.size(), 3); } else CHECK_EQ(block.miner_tx.vout.size(), 2); - } + } - for (size_t block_height = hf16_height; block_height < hf17_height; ++block_height) - { - const cryptonote::block &block = blockchain[block_height]; - CHECK_EQ(block.miner_tx.vout.at(0).amount, SN_REWARD_HF15); - if (cryptonote::block_has_governance_output(cryptonote::FAKECHAIN, block)) + for (size_t block_height = hf16_height; block_height < hf17_height; ++block_height) { - hf16_gov++; - CHECK_EQ(block.miner_tx.vout.at(1).amount, (FOUNDATION_REWARD_HF15 + CHAINFLIP_LIQUIDITY_HF16) * interval); - CHECK_EQ(block.miner_tx.vout.size(), 2); + const cryptonote::block &block = blockchain[block_height]; + CHECK_EQ(block.miner_tx.vout.at(0).amount, SN_REWARD_HF15); + if (cryptonote::block_has_governance_output(cryptonote::FAKECHAIN, block)) + { + hf16_gov++; + CHECK_EQ(block.miner_tx.vout.at(1).amount, (FOUNDATION_REWARD_HF15 + CHAINFLIP_LIQUIDITY_HF16) * interval); + CHECK_EQ(block.miner_tx.vout.size(), 2); + } + else + CHECK_EQ(block.miner_tx.vout.size(), 1); } - else - CHECK_EQ(block.miner_tx.vout.size(), 1); - } - for (size_t block_height = hf17_height; block_height < height; ++block_height) - { - const cryptonote::block &block = blockchain[block_height]; - CHECK_EQ(block.miner_tx.vout.at(0).amount, SN_REWARD_HF15); - if (cryptonote::block_has_governance_output(cryptonote::FAKECHAIN, block)) + for (size_t block_height = hf17_height; block_height < height; ++block_height) { - hf17_gov++; - CHECK_EQ(block.miner_tx.vout.at(1).amount, FOUNDATION_REWARD_HF17 * interval); - CHECK_EQ(block.miner_tx.vout.size(), 2); + const cryptonote::block &block = blockchain[block_height]; + CHECK_EQ(block.miner_tx.vout.at(0).amount, SN_REWARD_HF15); + if (cryptonote::block_has_governance_output(cryptonote::FAKECHAIN, block)) + { + hf17_gov++; + CHECK_EQ(block.miner_tx.vout.at(1).amount, FOUNDATION_REWARD_HF17 * interval); + CHECK_EQ(block.miner_tx.vout.size(), 2); + } + else + CHECK_EQ(block.miner_tx.vout.size(), 1); } - else - CHECK_EQ(block.miner_tx.vout.size(), 1); - } - CHECK_EQ(hf15_gov, 1); - CHECK_EQ(hf16_gov, 1); - CHECK_EQ(hf17_gov, 1); + CHECK_EQ(hf15_gov, 1); + CHECK_EQ(hf16_gov, 1); + CHECK_EQ(hf17_gov, 1); - return true; - }); + return true; + }); return true; } @@ -775,7 +772,7 @@ bool oxen_core_test_deregister_preferred::generate(std::vector gen.add_blocks_until_version(hard_forks.back().version); gen.add_n_blocks(10); /// give miner some outputs to spend and unlock them - add_service_nodes(gen, 12); + add_service_nodes(gen, 12, hard_forks.back().version); gen.add_mined_money_unlock_blocks(); /// generate high fee transactions with huge fees to fill up txpool entirely. This pushes all the @@ -795,34 +792,34 @@ bool oxen_core_test_deregister_preferred::generate(std::vector gen.create_and_add_state_change_tx(service_nodes::new_state::deregister, deregister_pub_key_2, 0, 0); oxen_register_callback(events, "check_prefer_deregisters", [&events, miner](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_prefer_deregisters"); - const auto tx_count = c.get_pool().get_transactions_count(); - cryptonote::block full_blk; - { + { + DEFINE_TESTS_ERROR_CONTEXT("check_prefer_deregisters"); + const auto tx_count = c.get_pool().get_transactions_count(); + cryptonote::block full_blk; + { cryptonote::difficulty_type diffic; uint64_t height; uint64_t expected_reward; cryptonote::blobdata extra_nonce; c.create_next_miner_block_template(full_blk, miner.get_keys().m_account_address, diffic, height, expected_reward, extra_nonce); - } + } - map_hash2tx_t mtx; - { + map_hash2tx_t mtx; + { std::vector chain; CHECK_TEST_CONDITION(find_block_chain(events, chain, mtx, get_block_hash(var::get(events[0])))); - } + } - const auto deregister_count = + const auto deregister_count = std::count_if(full_blk.tx_hashes.begin(), full_blk.tx_hashes.end(), [&mtx](const crypto::hash& tx_hash) { - return mtx[tx_hash]->type == cryptonote::txtype::state_change; - }); + return mtx[tx_hash]->type == cryptonote::txtype::state_change; + }); - CHECK_TEST_CONDITION(tx_count == 8); - CHECK_EQ(full_blk.tx_hashes.size(), 7); - CHECK_EQ(deregister_count, 2); - return true; - }); + CHECK_TEST_CONDITION(tx_count == 8); + CHECK_EQ(full_blk.tx_hashes.size(), 7); + CHECK_EQ(deregister_count, 2); + return true; + }); return true; } @@ -837,7 +834,7 @@ bool oxen_core_test_deregister_safety_buffer::generate(std::vector& /// generate some outputs and unlock them gen.add_n_blocks(20); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, 11); + add_service_nodes(gen, 11, hard_forks.back().version); gen.add_n_blocks(1); const auto pk = gen.top_quorum().obligations->workers[0]; @@ -898,9 +895,9 @@ bool oxen_core_test_deregister_too_old::generate(std::vector& /// tests we must add specify transactions manually), which should exercise the same validation code and reject the /// block gen.create_and_add_next_block({dereg_tx}, - nullptr /*checkpoint*/, - false /*can_be_added_to_blockchain*/, - "Trying to add a block with an old deregister sitting in the pool that was invalidated due to old age"); + nullptr /*checkpoint*/, + false /*can_be_added_to_blockchain*/, + "Trying to add a block with an old deregister sitting in the pool that was invalidated due to old age"); return true; } @@ -920,7 +917,7 @@ bool oxen_core_test_deregister_zero_fee::generate(std::vector gen.create_and_add_next_block(reg_txs); const auto deregister_pub_key = gen.top_quorum().obligations->workers[0]; cryptonote::transaction const invalid_deregister = - gen.create_state_change_tx(service_nodes::new_state::deregister, deregister_pub_key, 0, 0, -1 /*height*/, {} /*voters*/, MK_COINS(1) /*fee*/); + gen.create_state_change_tx(service_nodes::new_state::deregister, deregister_pub_key, 0, 0, -1 /*height*/, {} /*voters*/, MK_COINS(1) /*fee*/); gen.add_tx(invalid_deregister, false /*can_be_added_to_blockchain*/, "Deregister transactions with non-zero fee can NOT be added to the blockchain"); return true; } @@ -936,8 +933,8 @@ bool oxen_core_test_deregister_on_split::generate(std::vector gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - - add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE + 1); + + add_service_nodes(gen, service_nodes::CHECKPOINT_QUORUM_SIZE + 1, hard_forks.back().version); gen.create_and_add_next_block(); // Can't change service node state on the same height it was registered in auto fork = gen; @@ -970,21 +967,21 @@ bool oxen_core_test_deregister_on_split::generate(std::vector fork.add_service_node_checkpoint(fork.height(), service_nodes::CHECKPOINT_MIN_VOTES); oxen_register_callback(events, "test_on_split", [expected_tx_hash, expected_block_hash](cryptonote::core &c, size_t ev_index) - { - /// Check that the deregister transaction is the one from the alternative branch - DEFINE_TESTS_ERROR_CONTEXT("test_on_split"); + { + /// Check that the deregister transaction is the one from the alternative branch + DEFINE_TESTS_ERROR_CONTEXT("test_on_split"); - /// get the block with the deregister - bool orphan = false; - cryptonote::block blk; - CHECK_TEST_CONDITION(c.get_block_by_hash(expected_block_hash, blk, &orphan)); + /// get the block with the deregister + bool orphan = false; + cryptonote::block blk; + CHECK_TEST_CONDITION(c.get_block_by_hash(expected_block_hash, blk, &orphan)); - /// find the deregister tx: - const auto found_tx_hash = std::find(blk.tx_hashes.begin(), blk.tx_hashes.end(), expected_tx_hash); - CHECK_TEST_CONDITION(found_tx_hash != blk.tx_hashes.end()); - CHECK_EQ(*found_tx_hash, expected_tx_hash); /// check that it is the expected one - return true; - }); + /// find the deregister tx: + const auto found_tx_hash = std::find(blk.tx_hashes.begin(), blk.tx_hashes.end(), expected_tx_hash); + CHECK_TEST_CONDITION(found_tx_hash != blk.tx_hashes.end()); + CHECK_EQ(*found_tx_hash, expected_tx_hash); /// check that it is the expected one + return true; + }); return true; } @@ -997,7 +994,7 @@ bool oxen_core_test_state_change_ip_penalty_disallow_dupes::generate(std::vector gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, service_nodes::STATE_CHANGE_QUORUM_SIZE + 1); + add_service_nodes(gen, service_nodes::STATE_CHANGE_QUORUM_SIZE + 1, hard_forks.back().version); gen.create_and_add_next_block(); // Can't change service node state on the same height it was registered in const auto pub_key = gen.top_quorum().obligations->workers[0]; @@ -1024,15 +1021,15 @@ bool oxen_core_test_state_change_ip_penalty_disallow_dupes::generate(std::vector } static bool verify_ons_mapping_record(char const *perr_context, - ons::mapping_record const &record, - ons::mapping_type type, - std::string const &name, - ons::mapping_value const &value, - uint64_t update_height, - std::optional expiration_height, - crypto::hash const &txid, - ons::generic_owner const &owner, - ons::generic_owner const &backup_owner) + ons::mapping_record const &record, + ons::mapping_type type, + std::string const &name, + ons::mapping_value const &value, + uint64_t update_height, + std::optional expiration_height, + crypto::hash const &txid, + ons::generic_owner const &owner, + ons::generic_owner const &backup_owner) { CHECK_EQ(record.loaded, true); CHECK_EQ(record.type, type); @@ -1119,8 +1116,8 @@ bool oxen_name_system_expiration::generate(std::vector &events ons_keys_t miner_key = make_ons_keys(miner); for (auto mapping_type = ons::mapping_type::lokinet; - mapping_type <= ons::mapping_type::lokinet_10years; - mapping_type = static_cast(static_cast(mapping_type) + 1)) + mapping_type <= ons::mapping_type::lokinet_10years; + mapping_type = static_cast(static_cast(mapping_type) + 1)) { std::string const name = "mydomain.loki"; if (ons::mapping_type_allowed(gen.hardfork(), mapping_type)) @@ -1134,42 +1131,42 @@ bool oxen_name_system_expiration::generate(std::vector &events std::string name_hash = ons::name_to_base64_hash(name); oxen_register_callback(events, "check_ons_entries", [=](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_ons_entries"); - ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); - ons::owner_record owner = ons_db.get_owner_by_key(miner_key.owner); - CHECK_EQ(owner.loaded, true); - CHECK_EQ(owner.id, 1); - CHECK_TEST_CONDITION_MSG(miner_key.owner == owner.address, - miner_key.owner.to_string(cryptonote::FAKECHAIN) - << " == " << owner.address.to_string(cryptonote::FAKECHAIN)); + { + DEFINE_TESTS_ERROR_CONTEXT("check_ons_entries"); + ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); + ons::owner_record owner = ons_db.get_owner_by_key(miner_key.owner); + CHECK_EQ(owner.loaded, true); + CHECK_EQ(owner.id, 1); + CHECK_TEST_CONDITION_MSG(miner_key.owner == owner.address, + miner_key.owner.to_string(cryptonote::FAKECHAIN) + << " == " << owner.address.to_string(cryptonote::FAKECHAIN)); - ons::mapping_record record = ons_db.get_mapping(mapping_type, name_hash); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::lokinet, name, miner_key.lokinet_value, height_of_ons_entry, height_of_ons_entry + lokinet_expiry(mapping_type), tx_hash, miner_key.owner, {} /*backup_owner*/)); - return true; - }); + ons::mapping_record record = ons_db.get_mapping(mapping_type, name_hash); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::lokinet, name, miner_key.lokinet_value, height_of_ons_entry, height_of_ons_entry + lokinet_expiry(mapping_type), tx_hash, miner_key.owner, {} /*backup_owner*/)); + return true; + }); while (gen.height() <= expected_expiry_block) gen.create_and_add_next_block(); oxen_register_callback(events, "check_expired", [=, blockchain_height = gen.chain_height()](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_expired"); - ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); + { + DEFINE_TESTS_ERROR_CONTEXT("check_expired"); + ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); - // TODO(oxen): We should probably expire owners that no longer have any mappings remaining - ons::owner_record owner = ons_db.get_owner_by_key(miner_key.owner); - CHECK_EQ(owner.loaded, true); - CHECK_EQ(owner.id, 1); - CHECK_TEST_CONDITION_MSG(miner_key.owner == owner.address, - miner_key.owner.to_string(cryptonote::FAKECHAIN) - << " == " << owner.address.to_string(cryptonote::FAKECHAIN)); + // TODO(oxen): We should probably expire owners that no longer have any mappings remaining + ons::owner_record owner = ons_db.get_owner_by_key(miner_key.owner); + CHECK_EQ(owner.loaded, true); + CHECK_EQ(owner.id, 1); + CHECK_TEST_CONDITION_MSG(miner_key.owner == owner.address, + miner_key.owner.to_string(cryptonote::FAKECHAIN) + << " == " << owner.address.to_string(cryptonote::FAKECHAIN)); - ons::mapping_record record = ons_db.get_mapping(mapping_type, name_hash); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::lokinet, name, miner_key.lokinet_value, height_of_ons_entry, height_of_ons_entry + lokinet_expiry(mapping_type), tx_hash, miner_key.owner, {} /*backup_owner*/)); - CHECK_EQ(record.active(blockchain_height), false); - return true; - }); + ons::mapping_record record = ons_db.get_mapping(mapping_type, name_hash); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::lokinet, name, miner_key.lokinet_value, height_of_ons_entry, height_of_ons_entry + lokinet_expiry(mapping_type), tx_hash, miner_key.owner, {} /*backup_owner*/)); + CHECK_EQ(record.active(blockchain_height), false); + return true; + }); } else { @@ -1252,48 +1249,48 @@ bool oxen_name_system_get_mappings_by_owner::generate(std::vector records = ons_db.get_mappings_by_owner(bob_key.owner); + { + const char* perr_context = "check_ons_entries"; + ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); + std::vector records = ons_db.get_mappings_by_owner(bob_key.owner); - size_t expected_size = 0; - auto netv = get_network_version(c.get_nettype(), c.get_current_blockchain_height()); - if (ons::mapping_type_allowed(netv, ons::mapping_type::session)) expected_size += 2; - if (ons::mapping_type_allowed(netv, ons::mapping_type::wallet)) expected_size += 2; - if (ons::mapping_type_allowed(netv, ons::mapping_type::lokinet)) expected_size += 2; - CHECK_EQ(records.size(), expected_size); + size_t expected_size = 0; + auto netv = get_network_version(c.get_nettype(), c.get_current_blockchain_height()); + if (ons::mapping_type_allowed(netv, ons::mapping_type::session)) expected_size += 2; + if (ons::mapping_type_allowed(netv, ons::mapping_type::wallet)) expected_size += 2; + if (ons::mapping_type_allowed(netv, ons::mapping_type::lokinet)) expected_size += 2; + CHECK_EQ(records.size(), expected_size); - std::sort(records.begin(), records.end(), [](const auto& a, const auto& b) { - return std::make_tuple(a.update_height, a.name_hash) - < std::make_tuple(b.update_height, b.name_hash); - }); + std::sort(records.begin(), records.end(), [](const auto& a, const auto& b) { + return std::make_tuple(a.update_height, a.name_hash) + < std::make_tuple(b.update_height, b.name_hash); + }); - if (ons::mapping_type_allowed(netv, ons::mapping_type::session)) - { + if (ons::mapping_type_allowed(netv, ons::mapping_type::session)) + { CHECK_EQ(records[0].name_hash, session_name_hash1); CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[0], ons::mapping_type::session, session_name1, bob_key.session_value, session_height, std::nullopt, session_name1_txid, bob_key.owner, {} /*backup_owner*/)); CHECK_EQ(records[1].name_hash, session_name_hash2); CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[1], ons::mapping_type::session, session_name2, bob_key.session_value, session_height, std::nullopt, session_name2_txid, bob_key.owner, {} /*backup_owner*/)); - } + } - if (ons::mapping_type_allowed(netv, ons::mapping_type::lokinet)) - { - CHECK_EQ(records[2].name_hash, lokinet_name_hash1); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[2], ons::mapping_type::lokinet, lokinet_name1, bob_key.lokinet_value, lokinet_height, lokinet_height + lokinet_expiry(ons::mapping_type::lokinet), lokinet_name1_txid, bob_key.owner, {} /*backup_owner*/)); - CHECK_EQ(records[3].name_hash, lokinet_name_hash2); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[3], ons::mapping_type::lokinet, lokinet_name2, bob_key.lokinet_value, lokinet_height, lokinet_height + lokinet_expiry(ons::mapping_type::lokinet_5years), lokinet_name2_txid, bob_key.owner, {} /*backup_owner*/)); - } + if (ons::mapping_type_allowed(netv, ons::mapping_type::lokinet)) + { + CHECK_EQ(records[2].name_hash, lokinet_name_hash1); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[2], ons::mapping_type::lokinet, lokinet_name1, bob_key.lokinet_value, lokinet_height, lokinet_height + lokinet_expiry(ons::mapping_type::lokinet), lokinet_name1_txid, bob_key.owner, {} /*backup_owner*/)); + CHECK_EQ(records[3].name_hash, lokinet_name_hash2); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[3], ons::mapping_type::lokinet, lokinet_name2, bob_key.lokinet_value, lokinet_height, lokinet_height + lokinet_expiry(ons::mapping_type::lokinet_5years), lokinet_name2_txid, bob_key.owner, {} /*backup_owner*/)); + } - if (ons::mapping_type_allowed(netv, ons::mapping_type::wallet)) - { - CHECK_EQ(records[4].name_hash, wallet_name_hash1); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[4], ons::mapping_type::wallet, wallet_name1, bob_key.wallet_value, wallet_height, std::nullopt, wallet_name1_txid, bob_key.owner, {} /*backup_owner*/)); - CHECK_EQ(records[5].name_hash, wallet_name_hash2); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[5], ons::mapping_type::wallet, wallet_name2, bob_key.wallet_value, wallet_height, std::nullopt, wallet_name2_txid, bob_key.owner, {} /*backup_owner*/)); - } - return true; - }); + if (ons::mapping_type_allowed(netv, ons::mapping_type::wallet)) + { + CHECK_EQ(records[4].name_hash, wallet_name_hash1); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[4], ons::mapping_type::wallet, wallet_name1, bob_key.wallet_value, wallet_height, std::nullopt, wallet_name1_txid, bob_key.owner, {} /*backup_owner*/)); + CHECK_EQ(records[5].name_hash, wallet_name_hash2); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[5], ons::mapping_type::wallet, wallet_name2, bob_key.wallet_value, wallet_height, std::nullopt, wallet_name2_txid, bob_key.owner, {} /*backup_owner*/)); + } + return true; + }); return true; } @@ -1349,21 +1346,21 @@ bool oxen_name_system_get_mappings_by_owners::generate(std::vector records = ons_db.get_mappings_by_owners({bob_key.owner, miner_key.owner}); - CHECK_EQ(records.size(), 3); - std::sort(records.begin(), records.end(), [](ons::mapping_record const &lhs, ons::mapping_record const &rhs) { - return lhs.update_height < rhs.update_height; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_ons_entries"); + ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); + std::vector records = ons_db.get_mappings_by_owners({bob_key.owner, miner_key.owner}); + CHECK_EQ(records.size(), 3); + std::sort(records.begin(), records.end(), [](ons::mapping_record const &lhs, ons::mapping_record const &rhs) { + return lhs.update_height < rhs.update_height; + }); - int index = 0; - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[index++], ons::mapping_type::session, session_name1, bob_key.session_value, session_height1, std::nullopt, session_tx_hash1, bob_key.owner, {})); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[index++], ons::mapping_type::session, session_name2, bob_key.session_value, session_height2, std::nullopt, session_tx_hash2, bob_key.owner, {})); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[index++], ons::mapping_type::session, session_name3, miner_key.session_value, session_height3, std::nullopt, session_tx_hash3, miner_key.owner, {})); - return true; - }); + int index = 0; + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[index++], ons::mapping_type::session, session_name1, bob_key.session_value, session_height1, std::nullopt, session_tx_hash1, bob_key.owner, {})); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[index++], ons::mapping_type::session, session_name2, bob_key.session_value, session_height2, std::nullopt, session_tx_hash2, bob_key.owner, {})); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[index++], ons::mapping_type::session, session_name3, miner_key.session_value, session_height3, std::nullopt, session_tx_hash3, miner_key.owner, {})); + return true; + }); return true; } @@ -1397,15 +1394,15 @@ bool oxen_name_system_get_mappings::generate(std::vector &even uint64_t session_height = gen.height(); oxen_register_callback(events, "check_ons_entries", [bob_key, session_height, session_name1, session_tx_hash](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_ons_entries"); - ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); - std::string session_name_hash = ons::name_to_base64_hash(tools::lowercase_ascii_string(session_name1)); - std::vector records = ons_db.get_mappings({ons::mapping_type::session}, session_name_hash); - CHECK_EQ(records.size(), 1); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[0], ons::mapping_type::session, session_name1, bob_key.session_value, session_height, std::nullopt, session_tx_hash, bob_key.owner, {} /*backup_owner*/)); - return true; - }); + { + DEFINE_TESTS_ERROR_CONTEXT("check_ons_entries"); + ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); + std::string session_name_hash = ons::name_to_base64_hash(tools::lowercase_ascii_string(session_name1)); + std::vector records = ons_db.get_mappings({ons::mapping_type::session}, session_name_hash); + CHECK_EQ(records.size(), 1); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[0], ons::mapping_type::session, session_name1, bob_key.session_value, session_height, std::nullopt, session_tx_hash, bob_key.owner, {} /*backup_owner*/)); + return true; + }); return true; } @@ -1456,35 +1453,35 @@ bool oxen_name_system_handles_duplicate_in_ons_db::generate(std::vector &events, - cryptonote::account_base const &src, - cryptonote::tx_extra_oxen_name_system &data, - bool valid, - char const *reason) -> void { + std::vector &events, + cryptonote::account_base const &src, + cryptonote::tx_extra_oxen_name_system &data, + bool valid, + char const *reason) -> void { uint64_t new_height = cryptonote::get_block_height(gen.top().block) + 1; uint8_t new_hf_version = gen.get_hf_version_at(new_height); uint64_t burn_requirement = ons::burn_needed(new_hf_version, static_cast(data.type)); @@ -1550,10 +1547,10 @@ bool oxen_name_system_invalid_tx_extra_params::generate(std::vector &event first_ons_height = gen.height(); oxen_register_callback(events, "check_first_ons_entries", [=](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_first_ons_entries"); - ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); - std::vector records = ons_db.get_mappings_by_owner(miner_key.owner); - CHECK_EQ(ons_db.height(), first_ons_height); + { + DEFINE_TESTS_ERROR_CONTEXT("check_first_ons_entries"); + ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); + std::vector records = ons_db.get_mappings_by_owner(miner_key.owner); + CHECK_EQ(ons_db.height(), first_ons_height); - size_t expected_size = 1; - auto netv = get_network_version(c.get_nettype(), c.get_current_blockchain_height()); - if (ons::mapping_type_allowed(netv, ons::mapping_type::wallet)) expected_size += 1; - if (ons::mapping_type_allowed(netv, ons::mapping_type::lokinet)) expected_size += 1; - CHECK_EQ(records.size(), expected_size); + size_t expected_size = 1; + auto netv = get_network_version(c.get_nettype(), c.get_current_blockchain_height()); + if (ons::mapping_type_allowed(netv, ons::mapping_type::wallet)) expected_size += 1; + if (ons::mapping_type_allowed(netv, ons::mapping_type::lokinet)) expected_size += 1; + CHECK_EQ(records.size(), expected_size); - for (ons::mapping_record const &record : records) - { + for (ons::mapping_record const &record : records) + { if (record.type == ons::mapping_type::session) - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::session, session_name1, miner_key.session_value, first_ons_height, std::nullopt, session_tx_hash1, miner_key.owner, {} /*backup_owner*/)); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::session, session_name1, miner_key.session_value, first_ons_height, std::nullopt, session_tx_hash1, miner_key.owner, {} /*backup_owner*/)); else if (record.type == ons::mapping_type::lokinet) - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::lokinet, lokinet_name1, miner_key.lokinet_value, first_ons_height, first_ons_height + lokinet_expiry(ons::mapping_type::lokinet_10years), lokinet_tx_hash1, miner_key.owner, {} /*backup_owner*/)); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::lokinet, lokinet_name1, miner_key.lokinet_value, first_ons_height, first_ons_height + lokinet_expiry(ons::mapping_type::lokinet_10years), lokinet_tx_hash1, miner_key.owner, {} /*backup_owner*/)); else if (record.type == ons::mapping_type::wallet) - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::wallet, wallet_name1, miner_key.wallet_value, first_ons_height, std::nullopt, wallet_tx_hash1, miner_key.owner, {} /*backup_owner*/)); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::wallet, wallet_name1, miner_key.wallet_value, first_ons_height, std::nullopt, wallet_tx_hash1, miner_key.owner, {} /*backup_owner*/)); else { assert(false); } - } - return true; - }); + } + return true; + }); } // NOTE: Generate and add the second round of (transactions + block) to the blockchain, renew lokinet and add bob's session, update miner's session value to other's session value @@ -1758,49 +1755,49 @@ bool oxen_name_system_large_reorg::generate(std::vector &event second_ons_height = gen.height(); oxen_register_callback(events, "check_second_ons_entries", [=](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("check_second_ons_entries"); - ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); - CHECK_EQ(ons_db.height(), second_ons_height); + { + DEFINE_TESTS_ERROR_CONTEXT("check_second_ons_entries"); + ons::name_system_db &ons_db = c.get_blockchain_storage().name_system_db(); + CHECK_EQ(ons_db.height(), second_ons_height); - // NOTE: Check miner's record - { + // NOTE: Check miner's record + { std::vector records = ons_db.get_mappings_by_owner(miner_key.owner); for (ons::mapping_record const &record : records) { - if (record.type == ons::mapping_type::session) - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::session, session_name1, other_key.session_value, second_ons_height, std::nullopt, session_tx_hash3, miner_key.owner, {} /*backup_owner*/)); - else if (record.type == ons::mapping_type::lokinet) - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::lokinet, lokinet_name1, miner_key.lokinet_value, second_ons_height, first_ons_height + lokinet_expiry(ons::mapping_type::lokinet_5years) + lokinet_expiry(ons::mapping_type::lokinet_10years), lokinet_tx_hash2, miner_key.owner, {} /*backup_owner*/)); - else if (record.type == ons::mapping_type::wallet) - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::wallet, wallet_name1, miner_key.wallet_value, first_ons_height, std::nullopt, wallet_tx_hash1, miner_key.owner, {} /*backup_owner*/)); - else - { - assert(false); - } + if (record.type == ons::mapping_type::session) + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::session, session_name1, other_key.session_value, second_ons_height, std::nullopt, session_tx_hash3, miner_key.owner, {} /*backup_owner*/)); + else if (record.type == ons::mapping_type::lokinet) + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::lokinet, lokinet_name1, miner_key.lokinet_value, second_ons_height, first_ons_height + lokinet_expiry(ons::mapping_type::lokinet_5years) + lokinet_expiry(ons::mapping_type::lokinet_10years), lokinet_tx_hash2, miner_key.owner, {} /*backup_owner*/)); + else if (record.type == ons::mapping_type::wallet) + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, record, ons::mapping_type::wallet, wallet_name1, miner_key.wallet_value, first_ons_height, std::nullopt, wallet_tx_hash1, miner_key.owner, {} /*backup_owner*/)); + else + { + assert(false); + } + } } - } - // NOTE: Check bob's records - { - std::vector records = ons_db.get_mappings_by_owner(bob_key.owner); - CHECK_EQ(records.size(), 1); - CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[0], ons::mapping_type::session, bob_session_name1, bob_key.session_value, second_ons_height, std::nullopt, session_tx_hash2, bob_key.owner, {} /*backup_owner*/)); - } + // NOTE: Check bob's records + { + std::vector records = ons_db.get_mappings_by_owner(bob_key.owner); + CHECK_EQ(records.size(), 1); + CHECK_TEST_CONDITION(verify_ons_mapping_record(perr_context, records[0], ons::mapping_type::session, bob_session_name1, bob_key.session_value, second_ons_height, std::nullopt, session_tx_hash2, bob_key.owner, {} /*backup_owner*/)); + } - return true; - }); + return true; + }); } oxen_register_callback(events, "trigger_blockchain_detach", [=](cryptonote::core &c, size_t ev_index) - { - DEFINE_TESTS_ERROR_CONTEXT("trigger_blockchain_detach"); - cryptonote::Blockchain &blockchain = c.get_blockchain_storage(); + { + DEFINE_TESTS_ERROR_CONTEXT("trigger_blockchain_detach"); + cryptonote::Blockchain &blockchain = c.get_blockchain_storage(); - // NOTE: Reorg to just before the 2nd round of ONS entries - uint64_t curr_height = blockchain.get_current_blockchain_height(); - uint64_t blocks_to_pop = curr_height - second_ons_height; - blockchain.pop_blocks(blocks_to_pop); + // NOTE: Reorg to just before the 2nd round of ONS entries + uint64_t curr_height = blockchain.get_current_blockchain_height(); + uint64_t blocks_to_pop = curr_height - second_ons_height; + blockchain.pop_blocks(blocks_to_pop); ons::name_system_db &ons_db = blockchain.name_system_db(); CHECK_EQ(ons_db.height(), blockchain.get_current_blockchain_height() - 1); @@ -2610,7 +2607,7 @@ bool oxen_service_nodes_alt_quorums::generate(std::vector& eve gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, service_nodes::STATE_CHANGE_QUORUM_SIZE + 3); + add_service_nodes(gen, service_nodes::STATE_CHANGE_QUORUM_SIZE + 3, hard_forks.back().version); oxen_chain_generator fork = gen; gen.create_and_add_next_block(); @@ -2658,7 +2655,7 @@ bool oxen_service_nodes_checkpoint_quorum_size::generate(std::vector& e auto hard_forks = oxen_generate_hard_fork_table(); oxen_chain_generator gen(events, hard_forks); gen.add_blocks_until_version(hard_forks.back().version); - add_service_nodes(gen, 11); + add_service_nodes(gen, 11, hard_forks.back().version); gen.add_n_blocks(5); /// create a few blocks with active service nodes auto fork = gen; /// chain split here @@ -2859,7 +2856,7 @@ bool oxen_service_nodes_test_swarms_basic::generate(std::vector &eve result.add_blocks_until_version(hard_forks.back().version); result.add_mined_money_unlock_blocks(); + uint64_t curr_height = result.height(); std::vector registration_txs(service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN)); - for (auto i = 0u; i < service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN); ++i) + for (auto i = 0u; i < service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN); ++i) { registration_txs[i] = result.create_and_add_registration_tx(result.first_miner()); + result.process_registration_tx(registration_txs[i], curr_height + 1, hard_forks.back().version); + } // NOTE: Generate Valid Blocks result.create_and_add_next_block({registration_txs}); @@ -3187,7 +3187,7 @@ bool oxen_pulse_generate_blocks::generate(std::vector &events) gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN)); + add_service_nodes(gen, service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN), hard_forks.back().version); gen.add_n_blocks(40); // Chain genereator will generate blocks via Pulse quorums oxen_register_callback(events, "check_pulse_blocks", [](cryptonote::core &c, size_t ev_index) @@ -3211,7 +3211,7 @@ bool oxen_pulse_fallback_to_pow_and_back::generate(std::vector gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN)); + add_service_nodes(gen, service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN), hard_forks.back().version); gen.create_and_add_next_block(); gen.add_event_msg("Deregister 1 node, we now have insufficient nodes for Pulse"); @@ -3258,7 +3258,7 @@ bool oxen_pulse_chain_split::generate(std::vector &events) gen.add_blocks_until_version(hard_forks.back().version); gen.add_mined_money_unlock_blocks(); - add_service_nodes(gen, std::max(service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN), service_nodes::CHECKPOINT_QUORUM_SIZE)); + add_service_nodes(gen, std::max(service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN), service_nodes::CHECKPOINT_QUORUM_SIZE), hard_forks.back().version); gen.create_and_add_next_block(); @@ -3303,7 +3303,7 @@ bool oxen_pulse_chain_split_with_no_checkpoints::generate(std::vector &events) +{ + constexpr auto& conf = cryptonote::get_config(cryptonote::FAKECHAIN); + auto hard_forks = oxen_generate_hard_fork_table(); + oxen_chain_generator gen(events, hard_forks); + const auto miner = gen.first_miner(); + const auto alice = gen.add_account(); + size_t alice_account_base_event_index = gen.event_index(); + auto min_service_nodes = service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN); + + gen.add_blocks_until_version(hard_forks.back().version); + gen.add_n_blocks(10); + gen.add_mined_money_unlock_blocks(); + + for (auto i = 0u; i < min_service_nodes; ++i) { + const auto tx0 = gen.create_and_add_tx(miner, alice.get_keys().m_account_address, MK_COINS(101)); + gen.create_and_add_next_block({tx0}); + } + gen.add_transfer_unlock_blocks(); + + std::vector registration_txs(service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN)); + for (auto i = 0u; i < min_service_nodes; ++i) { + registration_txs[i] = gen.create_and_add_registration_tx(alice); + gen.process_registration_tx(registration_txs[i], 12+i, hard_forks.back().version); + } + gen.create_and_add_next_block({registration_txs}); + + uint64_t next_payout = alice.get_keys().m_account_address.next_payout_height(gen.height(), conf.BATCHING_INTERVAL); + uint64_t more_blocks = next_payout - gen.height(); + // There is an edge case where we get paid out before the node has been online long enough + // if this is the case just cycle for another batching interval + if (more_blocks <= conf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS) + more_blocks += conf.BATCHING_INTERVAL; + + oxen_register_callback(events, "check_registered", [&events, alice, min_service_nodes](cryptonote::core &c, size_t ev_index) + { + DEFINE_TESTS_ERROR_CONTEXT("gen_service_nodes::check_registered"); + std::vector blocks; + bool r = c.get_blocks((uint64_t)0, (uint64_t)-1, blocks); + CHECK_TEST_CONDITION(r); + std::vector chain; + map_hash2tx_t mtx; + r = find_block_chain(events, chain, mtx, cryptonote::get_block_hash(blocks.back())); + CHECK_TEST_CONDITION(r); + + const uint64_t unlocked_balance = get_balance(alice, blocks, mtx); + CHECK_EQ((MK_COINS(101) - TESTS_DEFAULT_FEE)*min_service_nodes, unlocked_balance); + + /// check that alice is registered + const auto info_v = c.get_service_node_list_state({}); + CHECK_EQ(info_v.size(), min_service_nodes); + return true; + }); + + + // Add blocks up to just before the batching payout block + for (auto i = 0u; i < more_blocks - 1; ++i) + gen.create_and_add_next_block(); + + oxen_register_callback(events, "check_no_rewards_before_batch", [&events, alice, min_service_nodes](cryptonote::core &c, size_t ev_index) + { + DEFINE_TESTS_ERROR_CONTEXT("check_no_rewards_before_batch"); + const auto stake_lock_time = service_nodes::staking_num_lock_blocks(cryptonote::FAKECHAIN); + + + std::vector blocks; + bool r = c.get_blocks((uint64_t)0, (uint64_t)-1, blocks); + CHECK_TEST_CONDITION(r); + std::vector chain; + map_hash2tx_t mtx; + r = find_block_chain(events, chain, mtx, cryptonote::get_block_hash(blocks.back())); + CHECK_TEST_CONDITION(r); + + // Expect no change in balance after blocks < batched block count constant + // 101 (balance) - 0.2 (test fee) + const uint64_t balance = get_balance(alice, blocks, mtx); + CHECK_EQ((MK_COINS(101) - TESTS_DEFAULT_FEE)*min_service_nodes, balance); + + return true; + }); + + // Add block that will contain the batching reward + gen.create_and_add_next_block(); + //void oxen_chain_generator::add_mined_money_unlock_blocks() + + oxen_register_callback(events, "check_rewards_received_after_batch", [&events, alice, min_service_nodes, more_blocks](cryptonote::core &c, size_t ev_index) + { + DEFINE_TESTS_ERROR_CONTEXT("check_rewards_received_after_batch"); + const auto stake_lock_time = service_nodes::staking_num_lock_blocks(cryptonote::FAKECHAIN); + + std::vector blocks; + bool r = c.get_blocks((uint64_t)0, (uint64_t)-1, blocks); + CHECK_TEST_CONDITION(r); + std::vector chain; + map_hash2tx_t mtx; + r = find_block_chain(events, chain, mtx, cryptonote::get_block_hash(blocks.back())); + CHECK_TEST_CONDITION(r); + + // Expect increase in balance after blocks < batched constant + // 201 (balance) - 100 (stake) - 0.2 (test fee) + 16.5*Batching_Interval (Batched reward) + const uint64_t balance = get_balance(alice, blocks, mtx); + const uint64_t staking_requirement = MK_COINS(100); + const uint64_t batched_rewards_earned = MK_COINS(1) * 16.5 * (more_blocks - conf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS); + + CHECK_EQ((MK_COINS(201) - TESTS_DEFAULT_FEE - staking_requirement)*min_service_nodes + batched_rewards_earned, balance); + return true; + }); + return true; +} + +bool oxen_batch_sn_rewards_bad_amount::generate(std::vector &events) +{ + + constexpr auto& conf = cryptonote::get_config(cryptonote::FAKECHAIN); + auto hard_forks = oxen_generate_hard_fork_table(); + oxen_chain_generator gen(events, hard_forks); + const auto miner = gen.first_miner(); + const auto alice = gen.add_account(); + size_t alice_account_base_event_index = gen.event_index(); + auto min_service_nodes = service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN); + + gen.add_blocks_until_version(hard_forks.back().version); + gen.add_n_blocks(10); + gen.add_mined_money_unlock_blocks(); + + for (auto i = 0u; i < min_service_nodes; ++i) { + const auto tx0 = gen.create_and_add_tx(miner, alice.get_keys().m_account_address, MK_COINS(101)); + gen.create_and_add_next_block({tx0}); + } + gen.add_transfer_unlock_blocks(); + + std::vector registration_txs(service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN)); + for (auto i = 0u; i < min_service_nodes; ++i) { + registration_txs[i] = gen.create_and_add_registration_tx(alice); + gen.process_registration_tx(registration_txs[i], 12+i, hard_forks.back().version); + } + gen.create_and_add_next_block({registration_txs}); + + uint64_t next_payout = alice.get_keys().m_account_address.next_payout_height(gen.height(), conf.BATCHING_INTERVAL); + uint64_t more_blocks = next_payout - gen.height(); + // There is an edge case where we get paid out before the node has been online long enough + // if this is the case just cycle for another batching interval + if (more_blocks <= conf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS) + more_blocks += conf.BATCHING_INTERVAL; + + for (auto i = 0u; i < more_blocks - 1; ++i) + gen.create_and_add_next_block(); + + //THIS BLOCK WILL CONTAIN THE BATCH TRANSACTION + oxen_blockchain_entry entry = gen.create_next_block(); + //Modify batch reward tx amount + entry.block.miner_tx.vout[0].amount = entry.block.miner_tx.vout[0].amount + 1; + oxen_blockchain_entry &result = gen.add_block(entry, false, "Block with modified amount in batched reward succeeded when it should have failed"); + + return true; +} + +bool oxen_batch_sn_rewards_bad_address::generate(std::vector &events) +{ + cryptonote::keypair const txkey{hw::get_device("default")}; + constexpr auto& conf = cryptonote::get_config(cryptonote::FAKECHAIN); + auto hard_forks = oxen_generate_hard_fork_table(); + oxen_chain_generator gen(events, hard_forks); + const auto miner = gen.first_miner(); + const auto alice = gen.add_account(); + size_t alice_account_base_event_index = gen.event_index(); + auto min_service_nodes = service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN); + + gen.add_blocks_until_version(hard_forks.back().version); + gen.add_n_blocks(10); + gen.add_mined_money_unlock_blocks(); + + for (auto i = 0u; i < min_service_nodes; ++i) { + const auto tx0 = gen.create_and_add_tx(miner, alice.get_keys().m_account_address, MK_COINS(101)); + gen.create_and_add_next_block({tx0}); + } + gen.add_transfer_unlock_blocks(); + + std::vector registration_txs(service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN)); + for (auto i = 0u; i < min_service_nodes; ++i) { + registration_txs[i] = gen.create_and_add_registration_tx(alice); + gen.process_registration_tx(registration_txs[i], 12+i, hard_forks.back().version); + } + gen.create_and_add_next_block({registration_txs}); + + uint64_t next_payout = alice.get_keys().m_account_address.next_payout_height(gen.height(), conf.BATCHING_INTERVAL); + uint64_t more_blocks = next_payout - gen.height(); + // There is an edge case where we get paid out before the node has been online long enough + // if this is the case just cycle for another batching interval + if (more_blocks <= conf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS) + more_blocks += conf.BATCHING_INTERVAL; + + for (auto i = 0u; i < more_blocks - 1; ++i) + gen.create_and_add_next_block(); + + //THIS BLOCK WILL CONTAIN THE BATCH TRANSACTION + oxen_blockchain_entry entry = gen.create_next_block(); + //Modify batch reward address + const auto bob = gen.add_account(); + auto bob_address = bob.get_keys().m_account_address; + crypto::public_key bob_deterministic_output_key{}; + if (!cryptonote::get_deterministic_output_key(bob_address, txkey, 0, bob_deterministic_output_key)) + { + MERROR("Failed to generate output one-time public key"); + return false; + } + // Switch Alice as recipient of payment to Bob + entry.block.miner_tx.vout[0].target = cryptonote::txout_to_key(bob_deterministic_output_key); + oxen_blockchain_entry &result = gen.add_block(entry, false, "Block with modified address in batched reward succeeded when it should have failed"); + + return true; +} + +bool oxen_batch_sn_rewards_pop_blocks::generate(std::vector &events) +{ + constexpr auto& conf = cryptonote::get_config(cryptonote::FAKECHAIN); + auto hard_forks = oxen_generate_hard_fork_table(); + oxen_chain_generator gen(events, hard_forks); + const auto miner = gen.first_miner(); + const auto alice = gen.add_account(); + size_t alice_account_base_event_index = gen.event_index(); + auto min_service_nodes = service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN); + + gen.add_blocks_until_version(hard_forks.back().version); + gen.add_n_blocks(10); + gen.add_mined_money_unlock_blocks(); + + for (auto i = 0u; i < min_service_nodes; ++i) { + const auto tx0 = gen.create_and_add_tx(miner, alice.get_keys().m_account_address, MK_COINS(101)); + gen.create_and_add_next_block({tx0}); + } + gen.add_transfer_unlock_blocks(); + + std::vector registration_txs(service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN)); + for (auto i = 0u; i < min_service_nodes; ++i) { + registration_txs[i] = gen.create_and_add_registration_tx(alice); + gen.process_registration_tx(registration_txs[i], 12+i, hard_forks.back().version); + } + gen.create_and_add_next_block({registration_txs}); + + uint64_t next_payout = alice.get_keys().m_account_address.next_payout_height(gen.height(), conf.BATCHING_INTERVAL); + uint64_t more_blocks = next_payout - gen.height(); + // There is an edge case where we get paid out before the node has been online long enough + // if this is the case just cycle for another batching interval + if (more_blocks <= conf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS) + more_blocks += conf.BATCHING_INTERVAL; + + // Generate blocks up to the block before the batched rewards are paid out. + for (auto i = 0u; i < more_blocks - 1; ++i) + gen.create_and_add_next_block(); + + oxen_register_callback(events, "trigger_blockchain_detach", [=](cryptonote::core &c, size_t ev_index) + { + DEFINE_TESTS_ERROR_CONTEXT("trigger_blockchain_detach"); + cryptonote::Blockchain& blockchain = c.get_blockchain_storage(); + uint64_t curr_height = blockchain.get_current_blockchain_height(); + auto sqliteDB = blockchain.sqlite_db(); + CHECK_EQ((*sqliteDB).height, curr_height - 1); + std::optional> records; + // curr_height = the block that would contain the batched service node payment + records = (*sqliteDB).get_sn_payments(curr_height); + CHECK_EQ(records.has_value(), true); + CHECK_EQ((*records).size(), 1); + // Check that the database has a full batch amount that includes the soon to be popped block + uint64_t batched_rewards_earned = MK_COINS(1) * 16.5 * (more_blocks - conf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS); + + // NOTE: Reorg to remove one block + blockchain.pop_blocks(1); + CHECK_EQ((*sqliteDB).height, blockchain.get_current_blockchain_height() - 1); + CHECK_EQ((*sqliteDB).height, curr_height - 2); + + records = (*sqliteDB).get_sn_payments(curr_height); + CHECK_EQ(records.has_value(), true); + if (batched_rewards_earned != MK_COINS(1) * 16.5) + { + CHECK_EQ((*records).size(), 1); + // Check that the database has a lower amount that does not include the popped block + batched_rewards_earned = MK_COINS(1) * 16.5 * (more_blocks - conf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS - 1); + CHECK_EQ((*records)[0].amount, batched_rewards_earned); + CHECK_EQ(tools::view_guts((*records)[0].address_info.address), tools::view_guts(alice.get_keys().m_account_address)); + } + else + CHECK_EQ((*records).size(), 0); + + // Pop the rest of the blocks and check that it goes to zero + blockchain.pop_blocks(more_blocks - 1); + CHECK_EQ((*sqliteDB).height, blockchain.get_current_blockchain_height() - 1); + CHECK_EQ((*sqliteDB).height, curr_height - more_blocks - 1); + + records = (*sqliteDB).get_sn_payments(curr_height + 1); + CHECK_EQ((*records).size(), 0); + + return true; + }); + return true; +} + +bool oxen_batch_sn_rewards_pop_blocks_after_big_cycle::generate(std::vector &events) +{ + + constexpr auto& conf = cryptonote::get_config(cryptonote::FAKECHAIN); + auto hard_forks = oxen_generate_hard_fork_table(); + oxen_chain_generator gen(events, hard_forks); + const auto miner = gen.first_miner(); + const cryptonote::account_base alice = gen.add_account(); + size_t alice_account_base_event_index = gen.event_index(); + auto min_service_nodes = service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN); + + gen.add_blocks_until_version(hard_forks.back().version); + gen.add_n_blocks(10); + gen.add_mined_money_unlock_blocks(); + + for (auto i = 0u; i < min_service_nodes; ++i) { + const auto tx0 = gen.create_and_add_tx(miner, alice.get_keys().m_account_address, MK_COINS(101)); + gen.create_and_add_next_block({tx0}); + } + gen.add_transfer_unlock_blocks(); + + std::vector registration_txs(service_nodes::pulse_min_service_nodes(cryptonote::FAKECHAIN)); + for (auto i = 0u; i < min_service_nodes; ++i) { + registration_txs[i] = gen.create_and_add_registration_tx(alice); + gen.process_registration_tx(registration_txs[i], 12+i, hard_forks.back().version); + } + gen.create_and_add_next_block({registration_txs}); + + uint64_t next_payout = alice.get_keys().m_account_address.next_payout_height(gen.height(), conf.BATCHING_INTERVAL); + uint64_t more_blocks = next_payout - gen.height(); + + // There is an edge case where we get paid out before the node has been online long enough + // if this is the case just cycle for another batching interval + if (more_blocks <= conf.SERVICE_NODE_PAYABLE_AFTER_BLOCKS) + more_blocks += conf.BATCHING_INTERVAL; + + for (auto i = 0u; i < more_blocks - 1; ++i) + gen.create_and_add_next_block(); + + // THIS BLOCK WILL CONTAIN THE BATCH TRANSACTION + // get the amount that was to be paid here. Then when we + // pop back we want the same amount + oxen_blockchain_entry entry = gen.create_next_block(); + uint64_t amount = entry.block.miner_tx.vout[0].amount; + oxen_blockchain_entry &result = gen.add_block(entry); + + // Generate blocks up through a few payment cycles and check that we can get back safely. + for (auto i = 0u; i < conf.BATCHING_INTERVAL * 3; ++i) + gen.create_and_add_next_block(); + + oxen_register_callback(events, "pop_3_cycles", [amount, alice](cryptonote::core &c, size_t ev_index) + { + DEFINE_TESTS_ERROR_CONTEXT("pop_3_cycles"); + cryptonote::Blockchain& blockchain = c.get_blockchain_storage(); + uint64_t curr_height = blockchain.get_current_blockchain_height(); + auto sqliteDB = blockchain.sqlite_db(); + CHECK_EQ((*sqliteDB).height, curr_height - 1); + + blockchain.pop_blocks(conf.BATCHING_INTERVAL * 3 + 1); + + + CHECK_EQ((*sqliteDB).height + 1, blockchain.get_current_blockchain_height()); + CHECK_EQ((*sqliteDB).height + 1, curr_height - conf.BATCHING_INTERVAL * 3 - 1); + + curr_height = blockchain.get_current_blockchain_height(); + + auto records = (*sqliteDB).get_sn_payments(curr_height); + CHECK_EQ((*records).size(), 1); + CHECK_EQ((*records)[0].amount, amount); + CHECK_EQ(tools::view_guts((*records)[0].address_info.address), tools::view_guts(alice.get_keys().m_account_address)); + + return true; + }); + + return true; +} diff --git a/tests/core_tests/oxen_tests.h b/tests/core_tests/oxen_tests.h index 1d95a2cdc..6d80a1e6b 100644 --- a/tests/core_tests/oxen_tests.h +++ b/tests/core_tests/oxen_tests.h @@ -88,6 +88,10 @@ struct oxen_pulse_out_of_order_voters struct oxen_pulse_reject_miner_block : public test_chain_unit_base { bool generate(std::vector& events); }; struct oxen_pulse_generate_blocks : public test_chain_unit_base { bool generate(std::vector& events); }; struct oxen_pulse_fallback_to_pow_and_back : public test_chain_unit_base { bool generate(std::vector& events); }; -struct oxen_pulse_chain_split : public test_chain_unit_base { bool generate(std::vector& events); }; -struct oxen_pulse_chain_split_with_no_checkpoints : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_pulse_chain_split : public test_chain_unit_base { bool generate(std::vector& events); }; struct oxen_pulse_chain_split_with_no_checkpoints : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_batch_sn_rewards : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_batch_sn_rewards_bad_amount : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_batch_sn_rewards_bad_address : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_batch_sn_rewards_pop_blocks : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_batch_sn_rewards_pop_blocks_after_big_cycle : public test_chain_unit_base { bool generate(std::vector& events); }; diff --git a/tests/core_tests/transaction_tests.cpp b/tests/core_tests/transaction_tests.cpp index bfecb35cc..b5273bc89 100644 --- a/tests/core_tests/transaction_tests.cpp +++ b/tests/core_tests/transaction_tests.cpp @@ -58,17 +58,18 @@ bool test_transaction_generation_and_ring_signature() account_base rv_acc2; rv_acc2.generate(); transaction tx_mine_1; - construct_miner_tx(0, 0, 0, 10, 0, tx_mine_1, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc1.get_keys().m_account_address)); + std::optional> sn_rwds; + construct_miner_tx(0, 0, 0, 10, 0, tx_mine_1, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc1.get_keys().m_account_address),sn_rwds); transaction tx_mine_2; - construct_miner_tx(0, 0, 0, 0, 0, tx_mine_2, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc2.get_keys().m_account_address)); + construct_miner_tx(0, 0, 0, 0, 0, tx_mine_2, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc2.get_keys().m_account_address),sn_rwds); transaction tx_mine_3; - construct_miner_tx(0, 0, 0, 0, 0, tx_mine_3, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc3.get_keys().m_account_address)); + construct_miner_tx(0, 0, 0, 0, 0, tx_mine_3, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc3.get_keys().m_account_address),sn_rwds); transaction tx_mine_4; - construct_miner_tx(0, 0, 0, 0, 0, tx_mine_4, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc4.get_keys().m_account_address)); + construct_miner_tx(0, 0, 0, 0, 0, tx_mine_4, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc4.get_keys().m_account_address),sn_rwds); transaction tx_mine_5; - construct_miner_tx(0, 0, 0, 0, 0, tx_mine_5, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc5.get_keys().m_account_address)); + construct_miner_tx(0, 0, 0, 0, 0, tx_mine_5, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc5.get_keys().m_account_address),sn_rwds); transaction tx_mine_6; - construct_miner_tx(0, 0, 0, 0, 0, tx_mine_6, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc6.get_keys().m_account_address)); + construct_miner_tx(0, 0, 0, 0, 0, tx_mine_6, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, miner_acc6.get_keys().m_account_address),sn_rwds); //fill inputs entry typedef tx_source_entry::output_entry tx_output_entry; @@ -139,7 +140,9 @@ bool test_block_creation() bool r = get_account_address_from_str(info, MAINNET, "0099be99c70ef10fd534c43c88e9d13d1c8853213df7e362afbec0e4ee6fec4948d0c190b58f4b356cd7feaf8d9d0a76e7c7e5a9a0a497a6b1faf7a765882dd08ac2"); CHECK_AND_ASSERT_MES(r, false, "failed to import"); block b; - r = construct_miner_tx(90, tools::median(std::move(szs)), 3553616528562147, 33094, 10000000, b.miner_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, info.address), blobdata()); + std::optional> sn_rwds; + uint64_t block_rewards = 0; + std::tie(r, block_rewards) = construct_miner_tx(90, tools::median(std::move(szs)), 3553616528562147, 33094, 10000000, b.miner_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, info.address), sn_rwds, blobdata()); return r; } diff --git a/tests/performance_tests/multi_tx_test_base.h b/tests/performance_tests/multi_tx_test_base.h index b6893ec78..3a159c4c6 100644 --- a/tests/performance_tests/multi_tx_test_base.h +++ b/tests/performance_tests/multi_tx_test_base.h @@ -51,11 +51,15 @@ public: using namespace cryptonote; std::vector output_entries; + std::optional> sn_rwds; for (size_t i = 0; i < ring_size; ++i) { m_miners[i].generate(); - if (!construct_miner_tx(0, 0, 0, 2, 0, m_miner_txs[1], cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, m_miners[i].get_keys().m_account_address))) + uint64_t block_rewards = 0; + bool r; + std::tie(r, block_rewards) = construct_miner_tx(0, 0, 0, 2, 0, m_miner_txs[1], cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, m_miners[i].get_keys().m_account_address), sn_rwds); + if (!r) return false; txout_to_key tx_out = var::get(m_miner_txs[i].vout[0].target); diff --git a/tests/performance_tests/single_tx_test_base.h b/tests/performance_tests/single_tx_test_base.h index 8ee79931f..543cb55f6 100644 --- a/tests/performance_tests/single_tx_test_base.h +++ b/tests/performance_tests/single_tx_test_base.h @@ -44,8 +44,13 @@ public: m_bob.generate(); oxen_miner_tx_context miner_tx_context = {}; + std::optional> sn_rwds; miner_tx_context.miner_block_producer = m_bob.get_keys().m_account_address; - if (!construct_miner_tx(0, 0, 0, 2, 0, m_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, m_bob.get_keys().m_account_address))) + + uint64_t block_rewards = 0; + bool r; + std::tie(r, block_rewards) = construct_miner_tx(0, 0, 0, 2, 0, m_tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, m_bob.get_keys().m_account_address), sn_rwds); + if (!r) return false; m_tx_pub_key = get_tx_pub_key_from_extra(m_tx); diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index def4e54ae..4f4dac570 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -73,6 +73,7 @@ add_executable(unit_tests service_nodes.cpp service_nodes_swarm.cpp sha256.cpp + sqlite.cpp string_util.cpp subaddress.cpp test_tx_utils.cpp @@ -101,6 +102,7 @@ target_link_libraries(unit_tests wallet p2p version + SQLiteCpp Boost::thread gtest extra) diff --git a/tests/unit_tests/long_term_block_weight.cpp b/tests/unit_tests/long_term_block_weight.cpp index 8df05557d..45e00dcea 100644 --- a/tests/unit_tests/long_term_block_weight.cpp +++ b/tests/unit_tests/long_term_block_weight.cpp @@ -34,6 +34,7 @@ #include "cryptonote_core/cryptonote_core.h" #include "cryptonote_core/uptime_proof.h" #include "blockchain_utilities/blockchain_objects.h" +#include "blockchain_db/sqlite/db_sqlite.h" #include "blockchain_db/testdb.h" #define TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW 5000 @@ -117,7 +118,8 @@ static uint32_t lcg() window, \ }; \ cryptonote::Blockchain *bc = &bc_objects.m_blockchain; \ - bool r = bc->init(new ::TestDB(), nullptr /*ons_db*/, cryptonote::FAKECHAIN, true, &test_options, 0); \ + auto sqliteDB = std::make_shared(cryptonote::FAKECHAIN, ":memory:"); \ + bool r = bc->init(new ::TestDB(), nullptr /*ons_db*/, sqliteDB /*sqlite_db*/, cryptonote::FAKECHAIN, true, &test_options, 0); \ ASSERT_TRUE(r) #define PREFIX(hf_version) PREFIX_WINDOW(hf_version, TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW) diff --git a/tests/unit_tests/memwipe.cpp b/tests/unit_tests/memwipe.cpp index 99d792f22..a1a0d0a17 100644 --- a/tests/unit_tests/memwipe.cpp +++ b/tests/unit_tests/memwipe.cpp @@ -32,6 +32,8 @@ #include "epee/misc_log_ex.h" #include "epee/memwipe.h" +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" + // Probably won't catch the optimized out case, but at least we test // it works in the normal case static void test(bool wipe) diff --git a/tests/unit_tests/node_server.cpp b/tests/unit_tests/node_server.cpp index 49ad06613..77ade1c82 100644 --- a/tests/unit_tests/node_server.cpp +++ b/tests/unit_tests/node_server.cpp @@ -35,6 +35,7 @@ #include "cryptonote_protocol/cryptonote_protocol_handler.h" #include "cryptonote_protocol/cryptonote_protocol_handler.inl" #include "cryptonote_core/blockchain.h" +#include "cryptonote_core/cryptonote_tx_utils.h" #define MAKE_IPV4_ADDRESS(a,b,c,d) epee::net_utils::ipv4_network_address{MAKE_IP(a,b,c,d),0} #define MAKE_IPV4_ADDRESS_PORT(a,b,c,d,e) epee::net_utils::ipv4_network_address{MAKE_IP(a,b,c,d),e} @@ -56,9 +57,9 @@ public: bool get_short_chain_history(std::list& ids) const { return true; } bool have_block(const crypto::hash& id) const {return true;} void get_blockchain_top(uint64_t& height, crypto::hash& top_id)const{height=0;top_id=crypto::null_hash;} - std::vector parse_incoming_txs(const std::vector& tx_blobs, const cryptonote::tx_pool_options &opts) { return {}; } - bool handle_parsed_txs(std::vector &parsed_txs, const cryptonote::tx_pool_options &opts, uint64_t *blink_rollback_height = nullptr) { if (blink_rollback_height) *blink_rollback_height = 0; return true; } - std::vector handle_incoming_txs(const std::vector& tx_blobs, const cryptonote::tx_pool_options &opts) { return {}; } + std::vector parse_incoming_txs(const std::vector& tx_blobs, const cryptonote::tx_pool_options &opts) { return {}; } + bool handle_parsed_txs(std::vector &parsed_txs, const cryptonote::tx_pool_options &opts, uint64_t *blink_rollback_height = nullptr) { if (blink_rollback_height) *blink_rollback_height = 0; return true; } + std::vector handle_incoming_txs(const std::vector& tx_blobs, const cryptonote::tx_pool_options &opts) { return {}; } bool handle_incoming_tx(const cryptonote::blobdata& tx_blob, cryptonote::tx_verification_context& tvc, const cryptonote::tx_pool_options &opts) { return true; } std::pair>, std::unordered_set> parse_incoming_blinks(const std::vector &blinks) { return {}; } int add_blinks(const std::vector> &blinks) { return 0; } diff --git a/tests/unit_tests/output_distribution.cpp b/tests/unit_tests/output_distribution.cpp index ce8c1d8c4..5253f342c 100644 --- a/tests/unit_tests/output_distribution.cpp +++ b/tests/unit_tests/output_distribution.cpp @@ -89,7 +89,7 @@ bool get_output_distribution(uint64_t amount, uint64_t from, uint64_t to, uint64 }; } opts; cryptonote::Blockchain *blockchain = &bc.m_blockchain; - bool r = blockchain->init(new TestDB(test_distribution_size), nullptr /*ons_db*/, cryptonote::FAKECHAIN, true, &opts.test_options, 0, NULL); + bool r = blockchain->init(new TestDB(test_distribution_size), nullptr /*ons_db*/, nullptr /*sqlite_db*/, cryptonote::FAKECHAIN, true, &opts.test_options, 0, NULL); return r && blockchain->get_output_distribution(amount, from, to, start_height, distribution, base); } diff --git a/tests/unit_tests/sqlite.cpp b/tests/unit_tests/sqlite.cpp new file mode 100644 index 000000000..5a8c136a8 --- /dev/null +++ b/tests/unit_tests/sqlite.cpp @@ -0,0 +1,143 @@ +// Copyright (c) 2021, The Oxen Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include + +#include "blockchain_db/sqlite/db_sqlite.h" + +TEST(SQLITE, AddressModulus) +{ + cryptonote::address_parse_info wallet_address; + cryptonote::get_account_address_from_str(wallet_address, cryptonote::network_type::TESTNET, "T6TzkJb5EiASaCkcH7idBEi1HSrpSQJE1Zq3aL65ojBMPZvqHNYPTL56i3dncGVNEYCG5QG5zrBmRiVwcg6b1cRM1SRNqbp44"); + + EXPECT_TRUE(wallet_address.address.modulus(10) == 0); + EXPECT_TRUE(wallet_address.address.modulus(100) == 90); + + EXPECT_TRUE(wallet_address.address.next_payout_height(50, 100) == 90); + EXPECT_TRUE(wallet_address.address.next_payout_height(100, 100) == 190); +} + +TEST(SQLITE, AddSNRewards) +{ + cryptonote::BlockchainSQLiteTest sqliteDB(cryptonote::network_type::TESTNET, ":memory:"); + + std::cout << "in memory db opened" << std::endl; + + EXPECT_TRUE(sqliteDB.batching_count() == 0); + + std::vector t1; + + cryptonote::address_parse_info wallet_address; + + cryptonote::get_account_address_from_str(wallet_address, cryptonote::network_type::TESTNET, "T6TzkJb5EiASaCkcH7idBEi1HSrpSQJE1Zq3aL65ojBMPZvqHNYPTL56i3dncGVNEYCG5QG5zrBmRiVwcg6b1cRM1SRNqbp44"); + + t1.emplace_back(wallet_address.address, 16500000000/2, cryptonote::network_type::TESTNET); + + bool success = false; + success = sqliteDB.add_sn_payments(t1); + EXPECT_TRUE(success); + + EXPECT_TRUE(sqliteDB.batching_count() == 1); + + std::optional> p1; + const auto expected_payout = wallet_address.address.next_payout_height(0, config::BATCHING_INTERVAL); + p1 = sqliteDB.get_sn_payments(expected_payout - 1); + EXPECT_TRUE(p1.has_value()); + EXPECT_TRUE((*p1).size() == 0); + + std::optional> p2; + p2 = sqliteDB.get_sn_payments(expected_payout); + EXPECT_TRUE(p2.has_value()); + EXPECT_TRUE((*p2).size() == 1); + uint64_t expected_amount = (16500000000/2); + EXPECT_TRUE((*p2)[0].amount == expected_amount); + + // Pay an amount less than the database expects and test for failure + std::vector t2; + t2.emplace_back(wallet_address.address, expected_amount - 1, cryptonote::network_type::TESTNET); + EXPECT_FALSE(sqliteDB.save_payments(expected_payout, t2)); + + // Pay the amount back out and expect the database to be empty + std::vector t3; + t3.emplace_back(wallet_address.address, expected_amount, cryptonote::network_type::TESTNET); + success = sqliteDB.save_payments(expected_payout, t3); + EXPECT_TRUE(success); + EXPECT_TRUE(sqliteDB.batching_count() == 0); +} + +TEST(SQLITE, CalculateRewards) +{ + cryptonote::BlockchainSQLiteTest sqliteDB(cryptonote::network_type::TESTNET, ":memory:"); + + cryptonote::block block; + block.reward = 200; + + // Check that a single contributor receives 100% of the block reward + service_nodes::service_node_info single_contributor{}; + single_contributor.portions_for_operator = 0; + cryptonote::address_parse_info first_address{}; + cryptonote::get_account_address_from_str(first_address, cryptonote::network_type::TESTNET, "T6TzkJb5EiASaCkcH7idBEi1HSrpSQJE1Zq3aL65ojBMPZvqHNYPTL56i3dncGVNEYCG5QG5zrBmRiVwcg6b1cRM1SRNqbp44"); + single_contributor.contributors.emplace_back(0, first_address.address); + single_contributor.contributors.back().amount = block.reward; + auto rewards = sqliteDB.calculate_rewards(block.major_version, block.reward, single_contributor); + auto hf_version = block.major_version; + + // Check that 3 contributor receives their portion of the block reward + service_nodes::service_node_info multiple_contributors{}; + multiple_contributors.contributors.emplace_back(0, first_address.address); + multiple_contributors.contributors.back().amount = 33; + cryptonote::address_parse_info second_address{}; + cryptonote::get_account_address_from_str(second_address, cryptonote::network_type::TESTNET, "T6SjALssDNvPZnTnV7vr459SX632c4X5qjLKfHfzvS32RPuhH3vnJmP9fyiD6ZiMu4XPk8ofH95mNRDg5bUPWkmq1LGAnyP3B"); + multiple_contributors.contributors.emplace_back(0, second_address.address); + multiple_contributors.contributors.back().amount = 33; + cryptonote::address_parse_info third_address{}; + cryptonote::get_account_address_from_str(third_address, cryptonote::network_type::TESTNET, "T6SkkovCyLWViVDMgeJoF7X4vFrHnKX5jXyktaoGmRuNTdoFEx1xXu1joXdmeH9mx2LLNPq998fKKcsAHwdRJWhk126SapptR"); + multiple_contributors.contributors.emplace_back(0, third_address.address); + multiple_contributors.contributors.back().amount = 34; + auto multiple_rewards = sqliteDB.calculate_rewards(block.major_version, block.reward, multiple_contributors); + + EXPECT_TRUE(multiple_rewards[0].amount == 66); + EXPECT_TRUE(multiple_rewards[1].amount == 66); + EXPECT_TRUE(multiple_rewards[2].amount == 68); + + // Check that 3 contributors receives their portion of the block reward when the operator takes a 10% fee + multiple_contributors.portions_for_operator = STAKING_PORTIONS/10; + multiple_contributors.operator_address = first_address.address; + block.reward = 1000; + auto multiple_rewards_with_fee = sqliteDB.calculate_rewards(block.major_version, block.reward, multiple_contributors); + // Operator gets 10% + EXPECT_TRUE(multiple_rewards_with_fee[0].amount == 99); + EXPECT_TRUE(tools::view_guts(multiple_rewards_with_fee[0].address_info.address) == tools::view_guts(first_address.address)); + // Contributors (including operator) receive the balance + EXPECT_TRUE(multiple_rewards_with_fee[1].amount == 297); + EXPECT_TRUE(tools::view_guts(multiple_rewards_with_fee[1].address_info.address) == tools::view_guts(first_address.address)); + EXPECT_TRUE(multiple_rewards_with_fee[2].amount == 297); + EXPECT_TRUE(tools::view_guts(multiple_rewards_with_fee[2].address_info.address) == tools::view_guts(second_address.address)); + EXPECT_TRUE(multiple_rewards_with_fee[3].amount == 306); + EXPECT_TRUE(tools::view_guts(multiple_rewards_with_fee[3].address_info.address) == tools::view_guts(third_address.address)); +} diff --git a/tests/unit_tests/test_tx_utils.cpp b/tests/unit_tests/test_tx_utils.cpp index e23f7fffb..71f1f1949 100644 --- a/tests/unit_tests/test_tx_utils.cpp +++ b/tests/unit_tests/test_tx_utils.cpp @@ -141,7 +141,11 @@ TEST(parse_and_validate_tx_extra, is_valid_tx_extra_parsed) cryptonote::account_base acc; acc.generate(); cryptonote::blobdata b = "dsdsdfsdfsf"; - ASSERT_TRUE(cryptonote::construct_miner_tx(0, 0, 10000000000000, 1000, TEST_FEE, tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, acc.get_keys().m_account_address), b)); + std::optional> sn_rwds; + uint64_t block_rewards = 0; + bool r; + std::tie(r, block_rewards) = cryptonote::construct_miner_tx(0, 0, 10000000000000, 1000, TEST_FEE, tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, acc.get_keys().m_account_address), sn_rwds, b); + ASSERT_TRUE(r); crypto::public_key tx_pub_key = cryptonote::get_tx_pub_key_from_extra(tx); ASSERT_NE(tx_pub_key, crypto::null_pkey); } @@ -150,8 +154,12 @@ TEST(parse_and_validate_tx_extra, fails_on_big_extra_nonce) cryptonote::transaction tx{}; cryptonote::account_base acc; acc.generate(); + std::optional> sn_rwds; cryptonote::blobdata b(cryptonote::TX_EXTRA_NONCE_MAX_COUNT + 1, 0); - ASSERT_FALSE(cryptonote::construct_miner_tx(0, 0, 10000000000000, 1000, TEST_FEE, tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, acc.get_keys().m_account_address), b)); + uint64_t block_rewards = 0; + bool r; + std::tie(r,block_rewards) = cryptonote::construct_miner_tx(0, 0, 10000000000000, 1000, TEST_FEE, tx, cryptonote::oxen_miner_tx_context::miner_block(cryptonote::FAKECHAIN, acc.get_keys().m_account_address), sn_rwds, b); + ASSERT_FALSE(r); } TEST(parse_and_validate_tx_extra, fails_on_wrong_size_in_extra_nonce) { diff --git a/utils/local-devnet/.gitignore b/utils/local-devnet/.gitignore index d383c56ff..7f39abe16 100644 --- a/utils/local-devnet/.gitignore +++ b/utils/local-devnet/.gitignore @@ -1 +1,3 @@ testdata +__pycache__ +*.pyc diff --git a/utils/local-devnet/commands/getheight.py b/utils/local-devnet/commands/getheight.py index 083640685..b1b9a2ce4 100755 --- a/utils/local-devnet/commands/getheight.py +++ b/utils/local-devnet/commands/getheight.py @@ -13,6 +13,7 @@ def instruct_daemon(method, params): headers = {'content-type': "application/json"} try: response = requests.request("POST", "http://"+config.listen_ip+":"+config.listen_port+"/json_rpc", data=payload, headers=headers) + # response = requests.request("POST", "http://"+config.listen_ip+":1165/json_rpc", data=payload, headers=headers) return json.loads(response.text) except requests.exceptions.RequestException as e: print(e) diff --git a/utils/local-devnet/commands/gettimeblock.py b/utils/local-devnet/commands/gettimeblock.py new file mode 100755 index 000000000..a48f3ad07 --- /dev/null +++ b/utils/local-devnet/commands/gettimeblock.py @@ -0,0 +1,41 @@ +#!/usr/bin/python3 + +import sys +sys.path.append('../testdata') +import config + +import requests +import argparse +import json +from datetime import datetime + + +def instruct_daemon(method, params): + payload = json.dumps({"method": method, "params": params}, skipkeys=False) + # print(payload) + headers = {'content-type': "application/json"} + try: + response = requests.request("POST", "http://"+config.listen_ip+":"+config.listen_port+"/json_rpc", data=payload, headers=headers) + return json.loads(response.text) + except requests.exceptions.RequestException as e: + print(e) + except: + print('No response from daemon, check daemon is running on this machine') + + +params = {} +answer = instruct_daemon('get_last_block_header', params) + +# print(json.dumps(answer['result']['block_header']['timestamp'], indent=4, sort_keys=True)) + +blockheight = answer['result']['block_header']['height'] +print("Block height: " + str(blockheight)) +blocktime = datetime.fromtimestamp(answer['result']['block_header']['timestamp']) +print("Block time: " + str(blocktime)) + +now = datetime.now() +print("Current time: " + str(now)) +diff = now - blocktime +print(str(diff.seconds) + " seconds ago") + + diff --git a/utils/local-devnet/commands/gettxpool.py b/utils/local-devnet/commands/gettxpool.py new file mode 100755 index 000000000..30234afc1 --- /dev/null +++ b/utils/local-devnet/commands/gettxpool.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 + +import sys +sys.path.append('../testdata') +import config + +import requests +import json + + +def instruct_daemon(method, params): + payload = json.dumps({"method": method, "params": params}) + headers = {'content-type': "application/json"} + try: + response = requests.request("POST", "http://"+config.listen_ip+":"+config.listen_port+"/json_rpc", data=payload, headers=headers) + return json.loads(response.text) + except requests.exceptions.RequestException as e: + print(e) + except: + print('No response from daemon, check daemon is running on this machine') + +# $ curl http://127.0.0.1:18081/get_transaction_pool -H 'Content-Type: application/json' + +answer = instruct_daemon('get_transaction_pool', []) +print(json.dumps(answer, indent=4, sort_keys=True)) diff --git a/utils/local-devnet/commands/proofed.py b/utils/local-devnet/commands/proofed.py new file mode 100755 index 000000000..e0c9d0674 --- /dev/null +++ b/utils/local-devnet/commands/proofed.py @@ -0,0 +1,35 @@ +#!/usr/bin/python3 + +import sys +sys.path.append('../testdata') +import config + +import requests +import json + + +def instruct_daemon(method, params): + payload = json.dumps({"method": method, "params": params}, skipkeys=False) + # print(payload) + headers = {'content-type': "application/json"} + try: + response = requests.request("POST", "http://"+config.listen_ip+":"+config.listen_port+"/json_rpc", data=payload, headers=headers) + return json.loads(response.text) + except requests.exceptions.RequestException as e: + print(e) + except: + print('No response from daemon, check daemon is running on this machine') + +service_node_pubkeys = [] +answer = instruct_daemon('get_service_nodes', []) + +# Transform json input to python objects + +# sn.json_rpc("get_n_service_nodes", {"fields":{"quorumnet_port":True}}).json()['result']['service_node_states']) +# Filter python objects with list comprehensions +output_dict = [x['quorumnet_port'] for x in answer['result']['service_node_states']] + +print("Quorumnet Ports:") +print(json.dumps(output_dict, indent=4, sort_keys=True)) + + diff --git a/utils/local-devnet/daemons.py b/utils/local-devnet/daemons.py index e4a019b1f..38925eef4 100644 --- a/utils/local-devnet/daemons.py +++ b/utils/local-devnet/daemons.py @@ -14,8 +14,8 @@ LISTEN_IP, NEXT_PORT = ( if sys.platform == 'linux' else ('127.0.0.1', random.randint(5000, 20000))) -verbose = True -# verbose = False +# verbose = True +verbose = False def next_port(): global NEXT_PORT @@ -84,7 +84,7 @@ class RPCDaemon: return self.args - def json_rpc(self, method, params=None, *, timeout=10): + def json_rpc(self, method, params=None, *, timeout=100): """Sends a json_rpc request to the rpc port. Returns the response object.""" if not self.proc: raise RuntimeError("Cannot make rpc request before calling start()") @@ -139,7 +139,7 @@ class Daemon(RPCDaemon): name=None, datadir=None, service_node=False, - log_level=2, + log_level=3, peers=()): self.rpc_port = rpc_port or next_port() if name is None: @@ -151,6 +151,7 @@ class Daemon(RPCDaemon): self.qnet_port = qnet_port or next_port() self.ss_port = ss_port or next_port() self.peers = [] + self.service_node_key = None self.args = [oxend] + list(self.__class__.base_args) self.args += ( @@ -235,6 +236,15 @@ class Daemon(RPCDaemon): """Triggers a p2p resync to happen soon (i.e. at the next p2p idle loop).""" self.json_rpc("test_trigger_p2p_resync") + def sn_key(self): + if not self.service_node_key: + self.service_node_key = self.json_rpc("get_service_keys").json()["result"]["service_node_pubkey"] + + return self.service_node_key + + def sn_status(self): + return self.json_rpc("get_service_node_status").json()["result"] + class Wallet(RPCDaemon): @@ -250,7 +260,7 @@ class Wallet(RPCDaemon): datadir=None, listen_ip=None, rpc_port=None, - log_level=2): + log_level=3): self.listen_ip = listen_ip or LISTEN_IP self.rpc_port = rpc_port or next_port() @@ -363,3 +373,25 @@ class Wallet(RPCDaemon): r = self.json_rpc("register_service_node", {"register_service_node_str": cmd}).json() if 'error' in r: raise RuntimeError("Failed to submit service node registration tx: {}".format(r['error']['message'])) + + def register_sn_for_contributions(self, sn): + r = sn.json_rpc("get_service_node_registration_cmd", { + "operator_cut": "10", + "contributions": [{"address": self.address(), "amount": 50000000000}], + "staking_requirement": 100000000000 + }).json() + if 'error' in r: + raise RuntimeError("Registration cmd generation failed: {}".format(r['error']['message'])) + cmd = r['result']['registration_cmd'] + r = self.json_rpc("register_service_node", {"register_service_node_str": cmd}).json() + if 'error' in r: + raise RuntimeError("Failed to submit service node registration tx: {}".format(r['error']['message'])) + + def contribute_to_sn(self, sn): + r = self.json_rpc("stake", { + "destination": self.address(), + "amount": 50000000000, + "service_node_key": sn.sn_key(), + }).json() + if 'error' in r: + raise RuntimeError("Failed to submit stake tx: {}".format(r['error']['message'])) diff --git a/utils/local-devnet/service_node_network.py b/utils/local-devnet/service_node_network.py index b163081a4..34cad943b 100755 --- a/utils/local-devnet/service_node_network.py +++ b/utils/local-devnet/service_node_network.py @@ -79,6 +79,7 @@ class SNNetwork: if i != k: self.all_nodes[i].add_peer(self.all_nodes[k]) + vprint("Starting new oxend service nodes with RPC on {} ports".format(self.sns[0].listen_ip), end="") for sn in self.sns: vprint(" {}".format(sn.rpc_port), end="", flush=True, timestamp=False) @@ -107,6 +108,10 @@ class SNNetwork: for w in self.wallets: w.wait_for_json_rpc("refresh") + configfile=self.datadir+'config.py' + with open(configfile, 'w') as filetowrite: + filetowrite.write('#!/usr/bin/python3\n# -*- coding: utf-8 -*-\nlisten_ip=\"{}\"\nlisten_port=\"{}\"\nwallet_listen_ip=\"{}\"\nwallet_listen_port=\"{}\"\nwallet_address=\"{}\"\nexternal_address=\"{}\"'.format(self.sns[0].listen_ip,self.sns[0].rpc_port,self.mike.listen_ip,self.mike.rpc_port,self.mike.address(),self.bob.address())) + # Mine some blocks; we need 100 per SN registration, and we can nearly 600 on fakenet before # it hits HF16 and kills mining rewards. This lets us submit the first 5 SN registrations a # SN (at height 40, which is the earliest we can submit them without getting an occasional @@ -128,7 +133,8 @@ class SNNetwork: self.mine(6*len(self.sns)) self.print_wallet_balances() - + self.mike.transfer(self.alice, 150000000000) + self.mike.transfer(self.bob, 150000000000) vprint("Submitting more service node registrations: ", end="", flush=True) for sn in self.sns[5:-1]: self.mike.register_sn(sn) @@ -139,7 +145,7 @@ class SNNetwork: self.print_wallet_balances() vprint("Mining 40 blocks (registrations + blink quorum lag) and waiting for nodes to sync") - self.sync_nodes(self.mine(40)) + self.sync_nodes(self.mine(40), timeout=120) self.print_wallet_balances() @@ -155,10 +161,20 @@ class SNNetwork: wait_for(lambda: all_service_nodes_proofed(sn), timeout=120) vprint(".", end="", flush=True, timestamp=False) vprint(timestamp=False) - for sn in self.sns[-1:]: - self.mike.register_sn(sn) - vprint(".", end="", flush=True, timestamp=False) - self.sync_nodes(self.mine(1)) + # This commented out code will register the last SN through Mikes wallet (Has done every other SN) + # for sn in self.sns[-1:]: + # self.mike.register_sn(sn) + # vprint(".", end="", flush=True, timestamp=False) + + # This commented out code will register the last SN through Bobs wallet (Has not done any others) + # self.bob.register_sn(self.sns[-1]) + + # This commented out code will register the last SN through Bobs wallet (Has not done any others) + # and also get alice to contribute 50% of the node with a 10% operator fee + self.bob.register_sn_for_contributions(self.sns[-1]) + self.sync_nodes(self.mine(5), timeout=120) + self.alice.contribute_to_sn(self.sns[-1]) + self.sync_nodes(self.mine(2), timeout=120) time.sleep(10) for sn in self.sns: sn.send_uptime_proof() @@ -166,12 +182,6 @@ class SNNetwork: vprint("Local Devnet SN network setup complete!") vprint("Communicate with daemon on ip: {} port: {}".format(self.sns[0].listen_ip,self.sns[0].rpc_port)) - configfile=self.datadir+'config.py' - with open(configfile, 'w') as filetowrite: - filetowrite.write('#!/usr/bin/python3\n# -*- coding: utf-8 -*-\nlisten_ip=\"{}\"\nlisten_port=\"{}\"\nwallet_listen_ip=\"{}\"\nwallet_listen_port=\"{}\"\nwallet_address=\"{}\"\nexternal_address=\"{}\"'.format(self.sns[0].listen_ip,self.sns[0].rpc_port,self.mike.listen_ip,self.mike.rpc_port,self.mike.address(),self.bob.address())) - - - def refresh_wallets(self, *, extra=[]): vprint("Refreshing wallets")