oxen-core/src/blockchain_db/sqlite/db_sqlite.cpp

775 lines
31 KiB
C++

// 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 <fmt/core.h>
#include <sodium.h>
#include <sqlite3.h>
#include <cassert>
#include "common/string_util.h"
#include "cryptonote_basic/hardfork.h"
#include "cryptonote_config.h"
#include "cryptonote_core/blockchain.h"
#include "cryptonote_core/service_node_list.h"
namespace cryptonote {
static auto logcat = log::Cat("blockchain.db.sqlite");
BlockchainSQLite::BlockchainSQLite(cryptonote::network_type nettype, fs::path db_path) :
db::Database(db_path, ""), m_nettype(nettype), filename{db_path.u8string()} {
log::trace(logcat, "BlockchainDB_SQLITE::{}", __func__);
height = 0;
if (!db.tableExists("batched_payments_accrued") || !db.tableExists("batched_payments_raw") ||
!db.tableExists("batch_db_info")) {
create_schema();
}
upgrade_schema();
height = prepared_get<int64_t>("SELECT height FROM batch_db_info");
}
void BlockchainSQLite::create_schema() {
log::trace(logcat, "BlockchainDB_SQLITE::{}", __func__);
auto& netconf = cryptonote::get_config(m_nettype);
db.exec(fmt::format(
R"(
CREATE TABLE batched_payments_accrued(
address VARCHAR NOT NULL,
amount BIGINT NOT NULL,
payout_offset INTEGER NOT NULL,
PRIMARY KEY(address),
CHECK(amount >= 0)
);
CREATE INDEX batched_payments_accrued_payout_offset_idx ON batched_payments_accrued(payout_offset);
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, payout_offset, amount) VALUES(OLD.address, OLD.height_paid % {}, OLD.amount)
ON CONFLICT(address) DO UPDATE SET amount = (amount + excluded.amount);
END;
)",
netconf.BATCHING_INTERVAL));
log::debug(logcat, "Database setup complete");
}
void BlockchainSQLite::upgrade_schema() {
bool have_offset = false;
SQLite::Statement msg_cols{db, "PRAGMA main.table_info(batched_payments_accrued)"};
while (msg_cols.executeStep()) {
auto [cid, name] = db::get<int64_t, std::string>(msg_cols);
if (name == "payout_offset")
have_offset = true;
}
if (!have_offset) {
log::info(logcat, "Adding payout_offset to batching db");
auto& netconf = get_config(m_nettype);
SQLite::Transaction transaction{db, SQLite::TransactionBehavior::IMMEDIATE};
db.exec(fmt::format(
R"(
ALTER TABLE batched_payments_accrued ADD COLUMN payout_offset INTEGER NOT NULL DEFAULT -1;
CREATE INDEX batched_payments_accrued_payout_offset_idx ON batched_payments_accrued(payout_offset);
DROP TRIGGER IF EXISTS rollback_payment;
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, payout_offset, amount) VALUES(OLD.address, OLD.height_paid % {}, OLD.amount)
ON CONFLICT(address) DO UPDATE SET amount = (amount + excluded.amount);
END;
)",
netconf.BATCHING_INTERVAL));
auto st = prepared_st(
"UPDATE batched_payments_accrued SET payout_offset = ? WHERE address = ?");
for (const auto& address : prepared_results<std::string>("SELECT address from "
"batched_payments_accrued")) {
cryptonote::address_parse_info addr_info{};
cryptonote::get_account_address_from_str(addr_info, m_nettype, address);
auto offset = static_cast<int>(addr_info.address.modulus(netconf.BATCHING_INTERVAL));
exec_query(st, offset, address);
st->reset();
}
auto count = prepared_get<int>(
"SELECT COUNT(*) FROM batched_payments_accrued WHERE payout_offset NOT BETWEEN 0 "
"AND ?",
static_cast<int>(netconf.BATCHING_INTERVAL));
if (count != 0) {
constexpr auto error =
"Batching db update to add offsets failed: not all addresses were converted";
log::error(logcat, error);
throw std::runtime_error{error};
}
transaction.commit();
}
const auto archive_table_count = prepared_get<int64_t>(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND "
"name='batched_payments_accrued_archive';");
if (archive_table_count == 0) {
log::info(logcat, "Adding archiving to batching db");
auto& netconf = get_config(m_nettype);
SQLite::Transaction transaction{db, SQLite::TransactionBehavior::IMMEDIATE};
db.exec(fmt::format(
R"(
CREATE TABLE batched_payments_accrued_archive(
address VARCHAR NOT NULL,
amount BIGINT NOT NULL,
payout_offset INTEGER NOT NULL,
archive_height BIGINT NOT NULL,
CHECK(amount >= 0),
CHECK(archive_height >= 0)
);
CREATE INDEX batched_payments_accrued_archive_height_idx ON batched_payments_accrued_archive(archive_height);
DROP TRIGGER IF EXISTS make_archive;
CREATE TRIGGER make_archive AFTER UPDATE ON batch_db_info
FOR EACH ROW WHEN (NEW.height % 100) = 0 AND NEW.height > OLD.height BEGIN
INSERT INTO batched_payments_accrued_archive SELECT *, NEW.height FROM batched_payments_accrued;
DELETE FROM batched_payments_accrued_archive WHERE archive_height < NEW.height - {1} AND archive_height % {0} != 0;
END;
DROP TRIGGER IF EXISTS clear_archive;
CREATE TRIGGER clear_archive AFTER UPDATE ON batch_db_info
FOR EACH ROW WHEN NEW.height < OLD.height BEGIN
DELETE FROM batched_payments_accrued_archive WHERE archive_height >= NEW.height;
END;
)",
netconf.STORE_LONG_TERM_STATE_INTERVAL,
500));
transaction.commit();
}
}
void BlockchainSQLite::reset_database() {
log::trace(logcat, "BlockchainDB_SQLITE::{}", __func__);
db.exec(R"(
DROP TABLE IF EXISTS batched_payments_accrued;
DROP TABLE IF EXISTS batched_payments_accrued_archive;
DROP VIEW IF EXISTS batched_payments_paid;
DROP TABLE IF EXISTS batched_payments_raw;
DROP TABLE IF EXISTS batch_db_info;
)");
create_schema();
upgrade_schema();
log::debug(logcat, "Database reset complete");
}
void BlockchainSQLite::update_height(uint64_t new_height) {
log::trace(logcat, "BlockchainDB_SQLITE::{} Called with new height: {}", __func__, new_height);
height = new_height;
prepared_exec("UPDATE batch_db_info SET height = ?", static_cast<int64_t>(height));
}
void BlockchainSQLite::increment_height() {
log::trace(logcat, "BlockchainDB_SQLITE::{} Called with height: {}", __func__, height + 1);
update_height(height + 1);
}
void BlockchainSQLite::decrement_height() {
log::trace(logcat, "BlockchainDB_SQLITE::{} Called with height: {}", __func__, height - 1);
update_height(height - 1);
}
void BlockchainSQLite::blockchain_detached(uint64_t new_height) {
if (height < new_height)
return;
int64_t revert_to_height = new_height - 1;
auto maybe_prev_interval = prepared_maybe_get<int64_t>(
"SELECT DISTINCT archive_height FROM batched_payments_accrued_archive WHERE "
"archive_height <= ? ORDER BY archive_height DESC LIMIT 1",
revert_to_height);
if (!maybe_prev_interval) {
auto fork_height = cryptonote::get_hard_fork_heights(m_nettype, hf::hf19_reward_batching);
reset_database();
update_height(fork_height.first.value_or(0));
return;
}
const auto prev_interval = *maybe_prev_interval;
db.exec(fmt::format(
R"(
DELETE FROM batched_payments_raw WHERE height_paid > {0};
DELETE FROM batched_payments_accrued;
INSERT INTO batched_payments_accrued
SELECT address, amount, payout_offset
FROM batched_payments_accrued_archive WHERE archive_height = {0};
DELETE FROM batched_payments_accrued_archive WHERE archive_height >= {0};
)",
prev_interval));
update_height(prev_interval);
return;
}
// Must be called with the address_str_cache_mutex held!
const std::string& BlockchainSQLite::get_address_str(const account_public_address& addr) {
auto& address_str = address_str_cache[addr];
if (address_str.empty())
address_str = cryptonote::get_account_address_as_str(m_nettype, 0, addr);
return address_str;
}
bool BlockchainSQLite::add_sn_rewards(const std::vector<cryptonote::batch_sn_payment>& payments) {
log::trace(logcat, "BlockchainDB_SQLITE::{}", __func__);
auto insert_payment = prepared_st(
"INSERT INTO batched_payments_accrued (address, payout_offset, amount) VALUES (?, ?, ?)"
" ON CONFLICT (address) DO UPDATE SET amount = amount + excluded.amount");
const auto& netconf = get_config(m_nettype);
for (auto& payment : payments) {
auto offset =
static_cast<int>(payment.address_info.address.modulus(netconf.BATCHING_INTERVAL));
auto amt = static_cast<int64_t>(payment.amount);
const auto& address_str = get_address_str(payment.address_info.address);
log::trace(
logcat,
"Adding record for SN reward contributor {} to database with amount {}",
address_str,
amt);
db::exec_query(insert_payment, address_str, offset, amt);
insert_payment->reset();
}
return true;
}
bool BlockchainSQLite::subtract_sn_rewards(
const std::vector<cryptonote::batch_sn_payment>& payments) {
log::trace(logcat, "BlockchainDB_SQLITE::{}", __func__);
auto update_payment = prepared_st(
"UPDATE batched_payments_accrued SET amount = (amount - ?) WHERE address = ?");
for (auto& payment : payments) {
const auto& address_str = get_address_str(payment.address_info.address);
auto result =
db::exec_query(update_payment, static_cast<int64_t>(payment.amount), address_str);
if (!result) {
log::error(
logcat,
"tried to subtract payment from an address that doesn't exist: {}",
address_str);
return false;
}
update_payment->reset();
}
return true;
}
std::vector<cryptonote::batch_sn_payment> BlockchainSQLite::get_sn_payments(uint64_t block_height) {
log::trace(logcat, "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<std::string_view, int64_t>(
"SELECT address, amount FROM batched_payments_accrued WHERE payout_offset = ? AND "
"amount >= ? ORDER BY address ASC",
static_cast<int>(block_height % conf.BATCHING_INTERVAL),
static_cast<int64_t>(conf.MIN_BATCH_PAYMENT_AMOUNT * BATCH_REWARD_FACTOR));
std::vector<cryptonote::batch_sn_payment> payments;
for (auto [address, amount] : accrued_amounts) {
auto& p = payments.emplace_back();
p.amount = amount / BATCH_REWARD_FACTOR * BATCH_REWARD_FACTOR; /* truncate to atomic OXEN */
[[maybe_unused]] bool addr_ok =
cryptonote::get_account_address_from_str(p.address_info, m_nettype, address);
assert(addr_ok);
}
return payments;
}
uint64_t BlockchainSQLite::get_accrued_earnings(const std::string& address) {
log::trace(logcat, "BlockchainDB_SQLITE::{}", __func__);
auto earnings = prepared_maybe_get<int64_t>(
"SELECT amount FROM batched_payments_accrued WHERE address = ?", address);
return static_cast<uint64_t>(earnings.value_or(0) / 1000);
}
std::pair<std::vector<std::string>, std::vector<uint64_t>>
BlockchainSQLite::get_all_accrued_earnings() {
log::trace(logcat, "BlockchainDB_SQLITE::{}", __func__);
std::pair<std::vector<std::string>, std::vector<uint64_t>> result;
auto& [addresses, amounts] = result;
for (auto [addr, amt] : prepared_results<std::string, int64_t>("SELECT address, amount FROM "
"batched_payments_accrued")) {
auto amount = static_cast<uint64_t>(amt / 1000);
if (amount > 0) {
addresses.push_back(std::move(addr));
amounts.push_back(amount);
}
}
return result;
}
void BlockchainSQLite::calculate_rewards(
hf hf_version,
uint64_t distribution_amount,
const service_nodes::service_node_info& sn_info,
std::vector<cryptonote::batch_sn_payment>& payments) {
log::trace(logcat, "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);
payments.clear();
// Pay the operator fee to the operator
if (operator_fee > 0)
payments.emplace_back(sn_info.operator_address, operator_fee);
// 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);
}
}
// 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<cryptonote::batch_sn_payment>&) =
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<uint64_t>::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;
std::vector<cryptonote::batch_sn_payment> payments;
// 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"};
std::lock_guard a_s_lock{address_str_cache_mutex};
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(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()) {
calculate_rewards(block.major_version, tx_fees, *service_node_winner->second, payments);
// Takes the block producer and adds its contributors to the batching database for the
// transaction fees
if (!(this->*add_or_subtract)(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;
calculate_rewards(
block.major_version,
service_node_reward / total_service_nodes_payable,
*payable_service_node->second,
payments);
// Takes the node and adds its contributors to the batching database
if (!(this->*add_or_subtract)(payments))
return false;
}
// Step 3: Add Governance reward to the list
if (m_nettype != cryptonote::network_type::FAKECHAIN) {
if (parsed_governance_addr.first != block.major_version) {
cryptonote::get_account_address_from_str(
parsed_governance_addr.second,
m_nettype,
cryptonote::get_config(m_nettype).governance_wallet_address(
block.major_version));
parsed_governance_addr.first = block.major_version;
}
uint64_t foundation_reward =
cryptonote::governance_reward_formula(block.major_version) * BATCH_REWARD_FACTOR;
payments.clear();
payments.emplace_back(parsed_governance_addr.second.address, foundation_reward);
if (!(this->*add_or_subtract)(payments))
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::trace(logcat, "BlockchainDB_SQLITE::{} called on height: {}", __func__, 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)) {
log::debug(logcat, "Batching of Service Node Rewards Begins");
reset_database();
update_height(block_height - 1);
}
if (block_height != height + 1) {
log::error(
logcat,
"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<std::pair<crypto::public_key, uint64_t>> miner_tx_vouts;
for (auto& vout : block.miner_tx.vout)
miner_tx_vouts.emplace_back(var::get<txout_to_key>(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) {
log::error(logcat, "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::trace(logcat, "BlockchainDB_SQLITE::{} called on height: {}", __func__, block_height);
if (height < block_height) {
log::debug(logcat, "Block above batching DB height skipping pop");
return true;
}
if (block_height != height) {
log::error(logcat, "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) {
log::error(logcat, "Error subtracting reward payments: {}", e.what());
return false;
}
return true;
}
bool BlockchainSQLite::validate_batch_payment(
const std::vector<std::pair<crypto::public_key, uint64_t>>& miner_tx_vouts,
const std::vector<cryptonote::batch_sn_payment>& calculated_payments_from_batching_db,
uint64_t block_height) {
log::trace(logcat, "BlockchainDB_SQLITE::{}", __func__);
if (miner_tx_vouts.size() != calculated_payments_from_batching_db.size()) {
log::error(
logcat,
"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<batch_sn_payment> 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) {
log::error(
logcat,
"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)) {
log::error(logcat, "Failed to generate output one-time public key");
return false;
}
if (tools::view_guts(pubkey) != tools::view_guts(out_eph_public_key)) {
log::error(logcat, "Output ephemeral public key does not match");
return false;
}
total_oxen_payout_in_vouts += amount;
finalised_payments.emplace_back(from_db.address_info, amount);
}
if (total_oxen_payout_in_vouts != total_oxen_payout_in_our_db) {
log::error(
logcat,
"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<batch_sn_payment>& paid_amounts) {
log::trace(logcat, "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 (?,?,?)");
std::lock_guard a_s_lock{address_str_cache_mutex};
for (const auto& payment : paid_amounts) {
const auto& address_str = get_address_str(payment.address_info.address);
if (auto maybe_amount = db::exec_and_maybe_get<int64_t>(select_sum, address_str)) {
// Truncate the thousanths amount to an atomic OXEN:
auto amount = static_cast<uint64_t>(*maybe_amount) / BATCH_REWARD_FACTOR *
BATCH_REWARD_FACTOR;
if (amount != payment.amount) {
log::error(
logcat,
"Invalid amounts passed in to save payments for address {}: received {}, "
"expected {} (truncated from {})",
address_str,
payment.amount,
amount,
*maybe_amount);
return false;
}
db::exec_query(
update_paid,
address_str,
static_cast<int64_t>(amount),
static_cast<int64_t>(block_height));
update_paid->reset();
} else {
// This shouldn't occur: we validate payout addresses much earlier in the block
// validation.
log::error(
logcat,
"Internal error: Invalid amounts passed in to save payments for address {}: "
"that address has no accrued rewards",
address_str);
return false;
}
select_sum->reset();
}
return true;
}
std::vector<cryptonote::batch_sn_payment> BlockchainSQLite::get_block_payments(
uint64_t block_height) {
log::trace(logcat, "BlockchainDB_SQLITE::{} Called with height: {}", __func__, block_height);
std::vector<cryptonote::batch_sn_payment> payments_at_height;
auto paid = prepared_results<std::string_view, int64_t>(
"SELECT address, amount FROM batched_payments_paid WHERE height_paid = ? ORDER BY "
"address",
static_cast<int64_t>(block_height));
for (auto [addr, amt] : paid) {
auto& p = payments_at_height.emplace_back();
p.amount = static_cast<uint64_t>(amt);
cryptonote::get_account_address_from_str(p.address_info, m_nettype, addr);
}
return payments_at_height;
}
bool BlockchainSQLite::delete_block_payments(uint64_t block_height) {
log::trace(logcat, "BlockchainDB_SQLITE::{} Called with height: {}", __func__, block_height);
prepared_exec(
"DELETE FROM batched_payments_paid WHERE height_paid >= ?",
static_cast<int64_t>(block_height));
return true;
}
} // namespace cryptonote