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")