mirror of https://github.com/oxen-io/oxen-core.git
Pulse: Validate block producer in service node list
This commit is contained in:
parent
95a2ae88bc
commit
6741e0da11
|
@ -104,7 +104,6 @@ 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)
|
||||
{
|
||||
|
||||
crypto::key_derivation derivation{};
|
||||
bool r = crypto::generate_key_derivation(address.m_view_public_key, tx_key.sec, derivation);
|
||||
CHECK_AND_ASSERT_MES(r, false, "failed to generate_key_derivation(" << address.m_view_public_key << ", " << tx_key.sec << ")");
|
||||
|
@ -290,14 +289,14 @@ namespace cryptonote
|
|||
// TODO(doyle): Batching awards
|
||||
// NOTE: Summarise rewards to payout
|
||||
// Up to 13 payout entries, up to 4 Pooled SN, up to 4 Block Producer (Pooled SN or 1 for Miner), up to 4 for Original Block Leader, up to 1 Governance
|
||||
size_t rewards_length = 0;
|
||||
std::array<reward_payout, 9> rewards = {};
|
||||
size_t rewards_length = 0;
|
||||
std::array<reward_payout, 13> rewards = {};
|
||||
|
||||
// NOTE: Add Block Producer Reward
|
||||
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.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_leader.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, false, "Pulse Block Producer is not valid until HF16, current HF" << hard_fork_version);
|
||||
|
||||
|
@ -319,7 +318,7 @@ namespace cryptonote
|
|||
for (size_t i = 0; i < payout_lists_size; i++)
|
||||
{
|
||||
auto const *list = payout_lists[i];
|
||||
uint64_t amount = list == &producer ? reward_parts.miner_reward() : 0;
|
||||
uint64_t amount = list == &producer ? reward_parts.miner_reward() : 0; // TODO(doyle): Amount
|
||||
for (auto const &payee : list->payouts)
|
||||
{
|
||||
reward_payout &entry = rewards[rewards_length++];
|
||||
|
@ -381,25 +380,14 @@ namespace cryptonote
|
|||
reward_payout const &payout = rewards[reward_index];
|
||||
crypto::public_key out_eph_public_key{};
|
||||
|
||||
if (payout.type == reward_type::governance)
|
||||
{
|
||||
if (!get_deterministic_output_key(payout.address, gov_key, tx.vout.size(), out_eph_public_key))
|
||||
{
|
||||
MERROR("Failed to generate deterministic output key for governance wallet output creation");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO(doyle): I don't think txkey is necessary, just use the governance key?
|
||||
keypair const &derivation_pair = (payout.type == reward_type::miner) ? txkey : gov_key;
|
||||
crypto::key_derivation derivation{};
|
||||
// TODO(doyle): I don't think txkey is necessary, just use the governance key?
|
||||
keypair const &derivation_pair = (payout.type == reward_type::miner) ? txkey : gov_key;
|
||||
crypto::key_derivation derivation{};
|
||||
|
||||
bool r = crypto::generate_key_derivation(payout.address.m_view_public_key, derivation_pair.sec, derivation);
|
||||
CHECK_AND_ASSERT_MES(r, false, "Creating miner tx output failed failed to generate_key_derivation(" << payout.address.m_view_public_key << ", " << derivation_pair.sec << ")");
|
||||
|
||||
r = crypto::derive_public_key(derivation, reward_index, payout.address.m_spend_public_key, out_eph_public_key);
|
||||
CHECK_AND_ASSERT_MES(r, false, "Creating miner tx output failed to derive_public_key(" << derivation << ", " << reward_index << ", "<< payout.address.m_spend_public_key << ")");
|
||||
if (!get_deterministic_output_key(payout.address, derivation_pair, reward_index, out_eph_public_key))
|
||||
{
|
||||
MERROR("Failed to generate output one-time public key");
|
||||
return false;
|
||||
}
|
||||
|
||||
txout_to_key tk = {};
|
||||
|
|
|
@ -115,7 +115,7 @@ namespace cryptonote
|
|||
/// exceeding the median block size; starting in HF 13 the miner pays the full penalty.
|
||||
uint64_t original_base_reward;
|
||||
|
||||
uint64_t miner_reward() { return base_miner + base_miner_fee; }
|
||||
uint64_t miner_reward() const { return base_miner + base_miner_fee; }
|
||||
};
|
||||
|
||||
struct loki_block_reward_context
|
||||
|
|
|
@ -1272,6 +1272,9 @@ namespace service_nodes
|
|||
{
|
||||
if (block.major_version < cryptonote::network_version_9_service_nodes)
|
||||
return true;
|
||||
|
||||
std::lock_guard lock(m_sn_mutex);
|
||||
process_block(block, txs);
|
||||
|
||||
if (block.major_version >= cryptonote::network_version_16)
|
||||
{
|
||||
|
@ -1309,9 +1312,6 @@ namespace service_nodes
|
|||
}
|
||||
}
|
||||
|
||||
std::lock_guard lock(m_sn_mutex);
|
||||
process_block(block, txs);
|
||||
|
||||
if (block.major_version >= cryptonote::network_version_13_enforce_checkpoints && checkpoint)
|
||||
{
|
||||
std::shared_ptr<const quorum> quorum = get_quorum(quorum_type::checkpointing, checkpoint->height);
|
||||
|
@ -2005,76 +2005,163 @@ namespace service_nodes
|
|||
return (a > b ? a - b : b - a) <= T{1};
|
||||
}
|
||||
|
||||
// NOTE: Verify queued service node coinbase or pulse block producer rewards
|
||||
static bool verify_coinbase_tx_output(cryptonote::transaction const &miner_tx,
|
||||
uint64_t height,
|
||||
size_t output_index,
|
||||
cryptonote::account_public_address const &receiver,
|
||||
uint64_t portions,
|
||||
uint64_t available_reward)
|
||||
{
|
||||
if (output_index >= miner_tx.vout.size())
|
||||
{
|
||||
MERROR("Output Index: " << output_index << ", indexes out of bounds in vout array with size: " << miner_tx.vout.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
cryptonote::tx_out const &output = miner_tx.vout[output_index];
|
||||
|
||||
// Because FP math is involved in reward calculations (and compounded by CPUs, compilers,
|
||||
// expression contraction, and RandomX fiddling with the rounding modes) we can end up with a
|
||||
// 1 ULP difference in the reward calculations.
|
||||
// TODO(loki): eliminate all FP math from reward calculations
|
||||
uint64_t const reward = cryptonote::get_portion_of_reward(portions, available_reward);
|
||||
if (!within_one(output.amount, reward))
|
||||
{
|
||||
MERROR("Service node reward amount incorrect. Should be " << cryptonote::print_money(reward) << ", is: " << cryptonote::print_money(output.amount));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!std::holds_alternative<cryptonote::txout_to_key>(output.target))
|
||||
{
|
||||
MERROR("Service node output target type should be txout_to_key");
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOTE: Loki uses the governance key in the one-time ephemeral key
|
||||
// derivation for both Pulse Block Producer/Queued Service Node Winner rewards
|
||||
crypto::key_derivation derivation{};
|
||||
crypto::public_key out_eph_public_key{};
|
||||
cryptonote::keypair gov_key = cryptonote::get_deterministic_keypair_from_height(height);
|
||||
|
||||
bool r = crypto::generate_key_derivation(receiver.m_view_public_key, gov_key.sec, derivation);
|
||||
CHECK_AND_ASSERT_MES(r, false, "while creating outs: failed to generate_key_derivation(" << receiver.m_view_public_key << ", " << gov_key.sec << ")");
|
||||
r = crypto::derive_public_key(derivation, output_index, receiver.m_spend_public_key, out_eph_public_key);
|
||||
CHECK_AND_ASSERT_MES(r, false, "while creating outs: failed to derive_public_key(" << derivation << ", " << output_index << ", "<< receiver.m_spend_public_key << ")");
|
||||
|
||||
if (std::get<cryptonote::txout_to_key>(output.target).key != out_eph_public_key)
|
||||
{
|
||||
MERROR("Invalid service node reward at output: " << output_index << ", output key, specifies wrong key");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool service_node_list::validate_miner_tx(cryptonote::block const &block, cryptonote::block_reward_parts const &reward_parts) const
|
||||
{
|
||||
std::lock_guard lock(m_sn_mutex);
|
||||
if (block.major_version < 9)
|
||||
return true;
|
||||
|
||||
uint8_t hf_version = block.major_version;
|
||||
uint64_t height = cryptonote::get_block_height(block);
|
||||
uint8_t const hf_version = block.major_version;
|
||||
uint64_t const height = cryptonote::get_block_height(block);
|
||||
cryptonote::transaction const &miner_tx = block.miner_tx;
|
||||
|
||||
// NOTE: Basic queued service node list winner checks
|
||||
payout const queued_winner = m_state.get_queued_winner();
|
||||
uint64_t const base_reward = reward_parts.original_base_reward;
|
||||
uint64_t const total_service_node_reward = cryptonote::service_node_reward_formula(base_reward, hf_version);
|
||||
{
|
||||
|
||||
auto const check_queued_winner_pubkey = cryptonote::get_service_node_winner_from_tx_extra(miner_tx.extra);
|
||||
if (queued_winner.key != check_queued_winner_pubkey)
|
||||
{
|
||||
MERROR("Service node reward winner is incorrect! Expected " << queued_winner.key << ", block has " << check_queued_winner_pubkey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Pulse Information
|
||||
bool pulse_block = false;
|
||||
quorum pulse_quorum = {};
|
||||
if (hf_version >= cryptonote::network_version_16)
|
||||
{
|
||||
std::vector<crypto::hash> entropy;
|
||||
get_pulse_entropy_from_blockchain(m_blockchain.get_db(), height + 1, entropy, block.pulse.round);
|
||||
pulse_quorum = generate_pulse_quorum(m_blockchain.nettype(), queued_winner.key, hf_version, m_state.active_service_nodes_infos(), entropy, block.pulse.round);
|
||||
pulse_block = block.signatures.size() || pulse_quorum.workers.size();
|
||||
}
|
||||
|
||||
// NOTE: Verify miner tx vout composition
|
||||
//
|
||||
// Miner Block
|
||||
// 0 | Miner
|
||||
// Up To 4 | Queued Service Node
|
||||
// Up To 1 | Governance
|
||||
//
|
||||
// Pulse Block
|
||||
// Up to 4 | Block Producer (0-3 for Pooled Service Node)
|
||||
// Up To 3 | Queued Service Node
|
||||
// Up To 1 | Governance
|
||||
std::shared_ptr<const service_node_info> pulse_block_producer = nullptr;
|
||||
{
|
||||
size_t expected_vouts_size = 0;
|
||||
if (pulse_block) // NOTE: Verify Block Producer Reward
|
||||
{
|
||||
auto info_it = m_state.service_nodes_infos.find(pulse_quorum.workers[0]);
|
||||
if (info_it == m_state.service_nodes_infos.end())
|
||||
{
|
||||
MERROR("Block producer is not a Service Node: " << pulse_quorum.workers[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
pulse_block_producer = info_it->second;
|
||||
expected_vouts_size += pulse_block_producer->contributors.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
expected_vouts_size += 1; /*miner*/
|
||||
}
|
||||
|
||||
expected_vouts_size += queued_winner.payouts.size();
|
||||
expected_vouts_size += static_cast<size_t>(cryptonote::height_has_governance_output(m_blockchain.nettype(), hf_version, height));
|
||||
|
||||
if (miner_tx.vout.size() != expected_vouts_size)
|
||||
{
|
||||
MERROR("Miner TX specifies a different amount of outputs vs the expected: " << expected_vouts_size << ", miner tx outputs: " << miner_tx.vout.size());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Verify pulse block producer coinbase outputs
|
||||
assert(pulse_block == static_cast<bool>(pulse_block_producer));
|
||||
if (pulse_block_producer)
|
||||
{
|
||||
const uint64_t max_portions = STAKING_PORTIONS - pulse_block_producer->portions_for_operator;
|
||||
for (size_t vout_index = 0; vout_index < pulse_block_producer->contributors.size(); vout_index++)
|
||||
{
|
||||
const auto &contributor = pulse_block_producer->contributors[vout_index];
|
||||
uint64_t portions = get_portions_to_make_amount(pulse_block_producer->staking_requirement, contributor.amount, max_portions);
|
||||
if (contributor.address == pulse_block_producer->operator_address)
|
||||
portions += pulse_block_producer->portions_for_operator;
|
||||
if (!verify_coinbase_tx_output(miner_tx, height, vout_index, contributor.address, portions, reward_parts.miner_reward()))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Verify queued service node list coinbase outputs
|
||||
// NOTE(loki): Service node reward distribution is calculated from the
|
||||
// original amount, i.e. 50% of the original base reward goes to service
|
||||
// nodes not 50% of the reward after removing the governance component (the
|
||||
// adjusted base reward post hardfork 10).
|
||||
uint64_t base_reward = reward_parts.original_base_reward;
|
||||
uint64_t total_service_node_reward = cryptonote::service_node_reward_formula(base_reward, hf_version);
|
||||
|
||||
payout winner = m_state.get_queued_winner();
|
||||
crypto::public_key check_winner_pubkey = cryptonote::get_service_node_winner_from_tx_extra(miner_tx.extra);
|
||||
if (winner.key != check_winner_pubkey)
|
||||
size_t vout_offset = pulse_block_producer ? pulse_block_producer->contributors.size() : 1;
|
||||
for (size_t i = 0; i < queued_winner.payouts.size(); i++)
|
||||
{
|
||||
MERROR("Service node reward winner is incorrect! Expected " << winner.key << ", block has " << check_winner_pubkey);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (winner == null_block_winner)
|
||||
return true;
|
||||
|
||||
if ((miner_tx.vout.size() - 1) < winner.payouts.size())
|
||||
{
|
||||
MERROR("Service node reward specifies more winners than available outputs: " << (miner_tx.vout.size() - 1) << ", winners: " << winner.payouts.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < winner.payouts.size(); i++)
|
||||
{
|
||||
size_t vout_index = i + 1;
|
||||
payout_entry const &payout = winner.payouts[i];
|
||||
uint64_t reward = cryptonote::get_portion_of_reward(payout.portions, total_service_node_reward);
|
||||
|
||||
// Because FP math is involved in reward calculations (and compounded by CPUs, compilers,
|
||||
// expression contraction, and RandomX fiddling with the rounding modes) we can end up with a
|
||||
// 1 ULP difference in the reward calculations.
|
||||
// TODO(loki): eliminate all FP math from reward calculations
|
||||
if (!within_one(miner_tx.vout[vout_index].amount, reward))
|
||||
{
|
||||
MERROR("Service node reward amount incorrect. Should be " << cryptonote::print_money(reward) << ", is: " << cryptonote::print_money(miner_tx.vout[vout_index].amount));
|
||||
size_t const vout_index = vout_offset + i;
|
||||
payout_entry const &payout = queued_winner.payouts[i];
|
||||
if (!verify_coinbase_tx_output(miner_tx, height, vout_index, payout.address, payout.portions, total_service_node_reward))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!std::holds_alternative<cryptonote::txout_to_key>(miner_tx.vout[vout_index].target))
|
||||
{
|
||||
MERROR("Service node output target type should be txout_to_key");
|
||||
return false;
|
||||
}
|
||||
|
||||
crypto::key_derivation derivation{};
|
||||
crypto::public_key out_eph_public_key{};
|
||||
cryptonote::keypair gov_key = cryptonote::get_deterministic_keypair_from_height(height);
|
||||
|
||||
bool r = crypto::generate_key_derivation(payout.address.m_view_public_key, gov_key.sec, derivation);
|
||||
CHECK_AND_ASSERT_MES(r, false, "while creating outs: failed to generate_key_derivation(" << payout.address.m_view_public_key << ", " << gov_key.sec << ")");
|
||||
r = crypto::derive_public_key(derivation, vout_index, payout.address.m_spend_public_key, out_eph_public_key);
|
||||
CHECK_AND_ASSERT_MES(r, false, "while creating outs: failed to derive_public_key(" << derivation << ", " << vout_index << ", "<< payout.address.m_spend_public_key << ")");
|
||||
|
||||
if (std::get<cryptonote::txout_to_key>(miner_tx.vout[vout_index].target).key != out_eph_public_key)
|
||||
{
|
||||
MERROR("Invalid service node reward output");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -308,8 +308,6 @@ namespace service_nodes
|
|||
{
|
||||
crypto::public_key key;
|
||||
std::vector<payout_entry> payouts;
|
||||
|
||||
bool operator==(const block_winner& x) const { return key == x.key && payouts == x.payouts; }
|
||||
};
|
||||
|
||||
/// Collection of keys used by a service node
|
||||
|
|
|
@ -176,10 +176,10 @@ uint64_t get_min_node_contribution_in_portions(uint8_t version, uint64_t staking
|
|||
return result;
|
||||
}
|
||||
|
||||
uint64_t get_portions_to_make_amount(uint64_t staking_requirement, uint64_t amount)
|
||||
uint64_t get_portions_to_make_amount(uint64_t staking_requirement, uint64_t amount, uint64_t max_portions)
|
||||
{
|
||||
uint64_t lo, hi, resulthi, resultlo;
|
||||
lo = mul128(amount, STAKING_PORTIONS, &hi);
|
||||
lo = mul128(amount, max_portions, &hi);
|
||||
if (lo > UINT64_MAX - (staking_requirement - 1))
|
||||
hi++;
|
||||
lo += staking_requirement-1;
|
||||
|
|
|
@ -250,7 +250,7 @@ crypto::hash generate_request_stake_unlock_hash(uint32_t nonce);
|
|||
uint64_t get_locked_key_image_unlock_height(cryptonote::network_type nettype, uint64_t node_register_height, uint64_t curr_height);
|
||||
|
||||
// Returns lowest x such that (staking_requirement * x/STAKING_PORTIONS) >= amount
|
||||
uint64_t get_portions_to_make_amount(uint64_t staking_requirement, uint64_t amount);
|
||||
uint64_t get_portions_to_make_amount(uint64_t staking_requirement, uint64_t amount, uint64_t max_portions = STAKING_PORTIONS);
|
||||
|
||||
bool get_portions_from_percent_str(std::string cut_str, uint64_t& portions);
|
||||
}
|
||||
|
|
|
@ -801,7 +801,20 @@ bool loki_chain_generator::block_begin(loki_blockchain_entry &entry, loki_create
|
|||
}
|
||||
|
||||
// NOTE: Calculate governance
|
||||
auto miner_tx_context = cryptonote::loki_miner_tx_context::miner_block(cryptonote::FAKECHAIN, params.miner_acc.get_keys().m_account_address, params.queued_winner);
|
||||
cryptonote::loki_miner_tx_context miner_tx_context = {};
|
||||
std::vector<service_nodes::pubkey_and_sninfo> active_snode_list = params.prev.service_node_state.active_service_nodes_infos();
|
||||
if (entry.block.major_version >= cryptonote::network_version_16 && active_snode_list.size() >= service_nodes::PULSE_MIN_SERVICE_NODES)
|
||||
{
|
||||
// TODO(doyle): We only support the queued winner as the block
|
||||
// producer/leader By default if the pulse round is 0, the queued winner is
|
||||
// the block producer, so i.e. we only support the default case atm
|
||||
miner_tx_context = cryptonote::loki_miner_tx_context::pulse_block(cryptonote::FAKECHAIN, params.queued_winner, params.queued_winner, params.queued_winner);
|
||||
}
|
||||
else
|
||||
{
|
||||
miner_tx_context = cryptonote::loki_miner_tx_context::miner_block(cryptonote::FAKECHAIN, params.miner_acc.get_keys().m_account_address, params.queued_winner);
|
||||
}
|
||||
|
||||
if (blk.major_version >= cryptonote::network_version_10_bulletproofs &&
|
||||
cryptonote::height_has_governance_output(cryptonote::FAKECHAIN, blk.major_version, height))
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue