// 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 "db_sqlite.h" #include #include #include #include #include #include "cryptonote_config.h" #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(); } height = prepared_get("SELECT height FROM batch_db_info"); } 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; prepared_exec( "UPDATE batch_db_info SET 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_rewards(const std::vector& payments) { LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); auto insert_payment = prepared_st( "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); auto amt = static_cast(payment.amount); MTRACE(fmt::format("Adding record for SN reward contributor {} to database with amount {}", address_str, amt)); db::exec_query(insert_payment, address_str, amt); insert_payment->reset(); } return true; } bool BlockchainSQLite::subtract_sn_rewards(const std::vector& payments) { LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); auto update_payment = prepared_st( "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), address_str); if (!result) { MERROR("tried to subtract payment from an address that doesn't exist: " << address_str); return false; } update_payment->reset(); } return true; } std::vector BlockchainSQLite::get_sn_payments(uint64_t block_height) { LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); // <= here because we might have crap in the db that we don't clear until we actually add the HF // block later on. (This is a pretty slim edge case that happened on devnet and is probably // virtually impossible on mainnet). if (m_nettype != cryptonote::network_type::FAKECHAIN && block_height <= cryptonote::get_hard_fork_heights(m_nettype, hf::hf19_reward_batching).first.value_or(0)) return {}; const auto& conf = get_config(m_nettype); auto accrued_amounts = prepared_results( "SELECT address, amount FROM batched_payments_accrued WHERE amount >= ? ORDER BY address ASC", static_cast(conf.MIN_BATCH_PAYMENT_AMOUNT * BATCH_REWARD_FACTOR)); std::vector payments; for (auto [address, amount] : accrued_amounts) { 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( std::move(address), amount / BATCH_REWARD_FACTOR * BATCH_REWARD_FACTOR /* truncate to atomic OXEN */, m_nettype); } } else { MERROR("Invalid address returned from batching database: " << address); } } return payments; } uint64_t BlockchainSQLite::get_accrued_earnings(const std::string& address) { LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); SQLite::Statement select_earnings { db, "SELECT amount FROM batched_payments_accrued WHERE address = ?;" }; select_earnings.bind(1, address); uint64_t amount{}; while (select_earnings.executeStep()) { amount = static_cast(select_earnings.getColumn(0).getInt64() / 1000); } return amount; } std::pair, std::vector> BlockchainSQLite::get_all_accrued_earnings() { LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); SQLite::Statement select_earnings { db, "SELECT address, amount FROM batched_payments_accrued;" }; std::vector amounts; std::vector addresses; while (select_earnings.executeStep()) { addresses.emplace_back(select_earnings.getColumn(0).getString()); amounts.emplace_back(static_cast(select_earnings.getColumn(1).getInt64() / 1000)); } return std::make_pair(addresses, amounts); } std::vector BlockchainSQLite::calculate_rewards(hf 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: fee_portions/PORTIONS * reward assert(sn_info.portions_for_operator <= old::STAKING_PORTIONS); uint64_t operator_fee = mul128_div64(sn_info.portions_for_operator, distribution_amount, old::STAKING_PORTIONS); assert(operator_fee <= distribution_amount); 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&& a, auto&& 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 c_reward = mul128_div64(contributor.amount, distribution_amount - operator_fee, total_contributed_to_sn); if (c_reward > 0) payments.emplace_back(contributor.address, c_reward, m_nettype); } return payments; } // Calculates block rewards, then invokes either `add_sn_rewards` (if `add`) or // `subtract_sn_rewards` (if `!add`) to process them. bool BlockchainSQLite::reward_handler( const cryptonote::block& block, const service_nodes::service_node_list::state_t& service_nodes_state, bool add) { // The method we call do actually handle the change: either `add_sn_payments` if add is true, // `subtract_sn_payments` otherwise: bool (BlockchainSQLite::* add_or_subtract)(const std::vector&) = add ? &BlockchainSQLite::add_sn_rewards : &BlockchainSQLite::subtract_sn_rewards; // From here on we calculate everything in milli-atomic OXEN (i.e. thousanths of an atomic // OXEN) so that our integer math has minimal loss from integer division. if (block.reward > std::numeric_limits::max() / BATCH_REWARD_FACTOR) throw std::logic_error{"Reward distribution amount is too large"}; uint64_t block_reward = block.reward * BATCH_REWARD_FACTOR; uint64_t service_node_reward = cryptonote::service_node_reward_formula(0, block.major_version) * BATCH_REWARD_FACTOR; // Step 1: Pay out the block producer their tx fees (note that, unlike the below, this applies // even if the SN isn't currently payable). if (block_reward < service_node_reward && m_nettype != cryptonote::network_type::FAKECHAIN) throw std::logic_error{"Invalid payment: block reward is too small"}; if (uint64_t tx_fees = block_reward - service_node_reward; tx_fees > 0 && 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(block.service_node_winner_key.data)) ) { if (auto service_node_winner = service_nodes_state.service_nodes_infos.find(block.service_node_winner_key); service_node_winner != service_nodes_state.service_nodes_infos.end()) { auto tx_fee_payments = calculate_rewards(block.major_version, tx_fees, *service_node_winner->second); // Takes the block producer and adds its contributors to the batching database for the transaction fees if (!(this->*add_or_subtract)(tx_fee_payments)) return false; } } auto block_height = get_block_height(block); // 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) { 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; auto snode_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 (!(this->*add_or_subtract)(snode_rewards)) return false; } // Step 3: Add Governance reward to the list if (m_nettype != cryptonote::network_type::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(block.major_version)); uint64_t foundation_reward = cryptonote::governance_reward_formula(block.major_version) * BATCH_REWARD_FACTOR; governance_rewards.emplace_back(governance_wallet_address.address, foundation_reward, m_nettype); if (!(this->*add_or_subtract)(governance_rewards)) return false; } return true; } 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 < hf::hf19_reward_batching) { update_height(block_height); return true; } auto fork_height = cryptonote::get_hard_fork_heights(m_nettype, hf::hf19_reward_batching); 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(fmt::format("Block height ({}) out of sync with batching database ({})", block_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; } if (!reward_handler(block, service_nodes_state, /*add=*/ true)) return false; increment_height(); transaction.commit(); } catch (std::exception& e) { MFATAL("Error adding reward payments: " << e.what()); return false; } 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 < hf::hf19_reward_batching) { decrement_height(); return true; } try { SQLite::Transaction transaction { db, SQLite::TransactionBehavior::IMMEDIATE }; if (!reward_handler(block, service_nodes_state, /*add=*/ false)) 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("Error subtracting reward payments: " << e.what()); return false; } return true; } bool BlockchainSQLite::validate_batch_payment( const std::vector>& miner_tx_vouts, const std::vector& calculated_payments_from_batching_db, uint64_t block_height) { LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); if (miner_tx_vouts.size() != calculated_payments_from_batching_db.size()) { MERROR(fmt::format("Length of batch payments ({}) does not match block vouts ({})", calculated_payments_from_batching_db.size(), miner_tx_vouts.size())); 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&& a, auto&& 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 (size_t vout_index = 0; vout_index < miner_tx_vouts.size(); vout_index++) { const auto& [pubkey, amt] = miner_tx_vouts[vout_index]; uint64_t amount = amt * BATCH_REWARD_FACTOR; const auto& from_db = calculated_payments_from_batching_db[vout_index]; if (amount != from_db.amount) { MERROR(fmt::format("Batched payout amount incorrect. Should be {}, not {}", from_db.amount, amount)); return false; } crypto::public_key out_eph_public_key{}; if (!cryptonote::get_deterministic_output_key(from_db.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(pubkey) != tools::view_guts(out_eph_public_key)) { MERROR("Output ephemeral public key does not match"); return false; } total_oxen_payout_in_vouts += amount; finalised_payments.emplace_back(from_db.address, amount, m_nettype); } if (total_oxen_payout_in_vouts != total_oxen_payout_in_our_db) { MERROR(fmt::format("Total batched payout amount incorrect. Should be {}, not {}", total_oxen_payout_in_our_db, total_oxen_payout_in_vouts)); return false; } return save_payments(block_height, finalised_payments); } bool BlockchainSQLite::save_payments(uint64_t block_height, const std::vector& paid_amounts) { LOG_PRINT_L3("BlockchainDB_SQLITE::" << __func__); auto select_sum = prepared_st( "SELECT amount from batched_payments_accrued WHERE address = ?"); auto update_paid = prepared_st( "INSERT INTO batched_payments_paid (address, amount, height_paid) VALUES (?,?,?)"); for (const auto& payment: paid_amounts) { if (auto maybe_amount = db::exec_and_maybe_get(select_sum, payment.address)) { // Truncate the thousanths amount to an atomic OXEN: auto amount = static_cast(*maybe_amount) / BATCH_REWARD_FACTOR * BATCH_REWARD_FACTOR; if (amount != payment.amount) { MERROR(fmt::format("Invalid amounts passed in to save payments for address {}: received {}, expected {} (truncated from {})", payment.address, payment.amount, amount, *maybe_amount)); return false; } db::exec_query(update_paid, payment.address, static_cast(amount), static_cast(block_height)); update_paid->reset(); } else { // This shouldn't occur: we validate payout addresses much earlier in the block validation. MERROR(fmt::format("Internal error: Invalid amounts passed in to save payments for address {}: that address has no accrued rewards", payment.address)); return false; } 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; auto paid = prepared_results( "SELECT address, amount FROM batched_payments_paid WHERE height_paid = ? ORDER BY address", static_cast(block_height)); for (auto [addr, amt] : paid) payments_at_height.emplace_back(std::move(addr), static_cast(amt), 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); prepared_exec( "DELETE FROM batched_payments_paid WHERE height_paid >= ?", static_cast(block_height)); return true; } } // namespace cryptonote