Merge pull request #1539 from darcys22/prevent-unlocks-small-holdings

Prevent unlocks small holdings
This commit is contained in:
Sean 2022-05-27 11:15:29 +10:00 committed by GitHub
commit c5fbf96b7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 196 additions and 3 deletions

View File

@ -876,7 +876,7 @@ namespace service_nodes
}
}
bool service_node_list::state_t::process_key_image_unlock_tx(cryptonote::network_type nettype, uint64_t block_height, const cryptonote::transaction &tx)
bool service_node_list::state_t::process_key_image_unlock_tx(cryptonote::network_type nettype, cryptonote::hf hf_version, uint64_t block_height, const cryptonote::transaction &tx)
{
crypto::public_key snode_key;
if (!cryptonote::get_service_node_pubkey_from_tx_extra(tx.extra, snode_key))
@ -913,6 +913,18 @@ namespace service_nodes
});
if (cit != contributor.locked_contributions.end())
{
if (hf_version >= hf::hf19)
{
if (cit->amount < service_nodes::SMALL_CONTRIBUTOR_THRESHOLD && (block_height - node_info.registration_height) < service_nodes::SMALL_CONTRIBUTOR_UNLOCK_TIMER)
{
LOG_PRINT_L1("Unlock TX: small contributor trying to unlock node before "
<< std::to_string(service_nodes::SMALL_CONTRIBUTOR_UNLOCK_TIMER)
<< " blocks have passed, rejected on height: "
<< block_height << " for tx: "
<< get_transaction_hash(tx));
return false;
}
}
// NOTE(oxen): This should be checked in blockchain check_tx_inputs already
if (crypto::check_signature(service_nodes::generate_request_stake_unlock_hash(unlock.nonce),
cit->key_image_pub_key, unlock.signature))
@ -2166,7 +2178,7 @@ namespace service_nodes
}
else if (tx.type == cryptonote::txtype::key_image_unlock)
{
process_key_image_unlock_tx(nettype, block_height, tx);
process_key_image_unlock_tx(nettype, hf_version, block_height, tx);
}
}

View File

@ -725,7 +725,7 @@ namespace service_nodes
const cryptonote::block &block,
const cryptonote::transaction& tx,
const service_node_keys *my_keys);
bool process_key_image_unlock_tx(cryptonote::network_type nettype, uint64_t block_height, const cryptonote::transaction &tx);
bool process_key_image_unlock_tx(cryptonote::network_type nettype, cryptonote::hf hf_version, uint64_t block_height, const cryptonote::transaction &tx);
payout get_block_leader() const;
payout get_block_producer(uint8_t pulse_round) const;
};

View File

@ -245,6 +245,10 @@ namespace service_nodes {
// (for pre-HF19 registrations).
inline constexpr uint64_t MINIMUM_OPERATOR_PORTION = cryptonote::old::STAKING_PORTIONS / oxen::MAX_CONTRIBUTORS_V1;
// Small Stake prevented from unlocking stake until a certain number of blocks have passed
constexpr uint64_t SMALL_CONTRIBUTOR_UNLOCK_TIMER = cryptonote::BLOCKS_PER_DAY * 30;
constexpr uint64_t SMALL_CONTRIBUTOR_THRESHOLD = 3749;
static_assert(cryptonote::old::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 (cryptonote::hf version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions);

View File

@ -8589,6 +8589,19 @@ wallet2::request_stake_unlock_result wallet2::can_request_stake_unlock(const cry
return result;
}
if (contribution.amount < service_nodes::SMALL_CONTRIBUTOR_THRESHOLD && (curr_height - node_info.registration_height) < service_nodes::SMALL_CONTRIBUTOR_UNLOCK_TIMER)
{
result.msg.append("You are requesting to unlock a stake of: ");
result.msg.append(cryptonote::print_money(contribution.amount));
result.msg.append(" Oxen which is a small contributor stake.\nSmall contributors need to wait ");
result.msg.append(std::to_string(service_nodes::SMALL_CONTRIBUTOR_UNLOCK_TIMER));
result.msg.append(" blocks before being allowed to unlock.");
result.msg.append("You will need to wait: ");
result.msg.append(std::to_string(service_nodes::SMALL_CONTRIBUTOR_UNLOCK_TIMER - (curr_height - node_info.registration_height)));
result.msg.append(" more blocks.");
return result;
}
result.msg.append("You are requesting to unlock a stake of: ");
result.msg.append(cryptonote::print_money(contribution.amount));
result.msg.append(" Oxen from the service node network.\nThis will schedule the service node: ");

View File

@ -53,6 +53,7 @@
#include "cryptonote_basic/cryptonote_basic_impl.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_basic/miner.h"
#include "cryptonote_basic/tx_extra.h"
#include "cryptonote_core/uptime_proof.h"
#include "oxen_economy.h"
#include "ringct/rctSigs.h"
@ -434,6 +435,13 @@ cryptonote::transaction oxen_chain_generator::create_and_add_staking_tx(const cr
return result;
}
cryptonote::transaction oxen_chain_generator::create_and_add_unlock_stake_tx(const crypto::public_key& pub_key, const cryptonote::account_base& src, const cryptonote::transaction& staking_tx, bool kept_by_block)
{
cryptonote::transaction result = create_unlock_stake_tx(pub_key, staking_tx, src);
add_tx(result, true /*can_be_added_to_blockchain*/, "" /*fail_msg*/, kept_by_block);
return result;
}
oxen_blockchain_entry &oxen_chain_generator::create_and_add_next_block(const std::vector<cryptonote::transaction>& txs, cryptonote::checkpoint_t const *checkpoint, bool can_be_added_to_blockchain, std::string const &fail_msg)
{
oxen_blockchain_entry entry = create_next_block(txs, checkpoint);
@ -542,6 +550,49 @@ cryptonote::transaction oxen_chain_generator::create_staking_tx(const crypto::pu
return result;
}
cryptonote::transaction oxen_chain_generator::create_unlock_stake_tx(const crypto::public_key& pub_key, const cryptonote::transaction& staking_tx, const cryptonote::account_base& src) const
{
cryptonote::transaction result = {};
cryptonote::tx_extra_tx_key_image_unlock unlock = {};
unlock.nonce = cryptonote::tx_extra_tx_key_image_unlock::FAKE_NONCE;
crypto::secret_key tx_secret_key{};
crypto::public_key tx_public_key{};
cryptonote::tx_extra_tx_key_image_proofs key_image_proofs;
if (!cryptonote::get_field_from_tx_extra(staking_tx.extra, key_image_proofs))
throw("TX: Didn't have key image proofs in the tx_extra, rejected for tx");
if (!cryptonote::get_tx_secret_key_from_tx_extra(staking_tx.extra, tx_secret_key))
throw("TX: Failed to get tx secret key from contribution");
crypto::secret_key_to_public_key(tx_secret_key, tx_public_key);
crypto::key_derivation recv_derivation{};
crypto::generate_key_derivation(tx_public_key, src.get_keys().m_view_secret_key, recv_derivation);
crypto::secret_key output_secret_key{};
crypto::public_key output_public_key{};
for (size_t i = 0; i <= 1; i++)
{
crypto::derive_secret_key(recv_derivation, i, src.get_keys().m_spend_secret_key, output_secret_key);
crypto::secret_key_to_public_key(output_secret_key, output_public_key);
crypto::generate_signature(cryptonote::tx_extra_tx_key_image_unlock::HASH, output_public_key, output_secret_key, unlock.signature);
crypto::generate_key_image(output_public_key, output_secret_key, unlock.key_image);
if (unlock.key_image == key_image_proofs.proofs[0].key_image)
{
cryptonote::add_service_node_pubkey_to_tx_extra(result.extra, pub_key);
cryptonote::add_tx_key_image_unlock_to_tx_extra(result.extra, unlock);
}
}
uint64_t new_height = get_block_height(top().block) + 1;
const auto new_hf_version = get_hf_version_at(new_height);
result.type = cryptonote::txtype::key_image_unlock;
result.version = cryptonote::transaction::get_max_version_for_hf(new_hf_version);
return result;
}
cryptonote::transaction oxen_chain_generator::create_state_change_tx(service_nodes::new_state state, const crypto::public_key &pub_key, uint16_t reasons_all, uint16_t reasons_any, uint64_t height, const std::vector<uint64_t>& voters, uint64_t fee) const
{
if (height == UINT64_MAX)

View File

@ -1347,6 +1347,7 @@ public:
cryptonote::tx_destination_entry change_addr{ change_amount, m_from.get_keys().m_account_address, false /*is_subaddr*/ };
bool result = cryptonote::construct_tx(
m_from.get_keys(), sources, destinations, change_addr, m_extra, m_tx, m_unlock_time, m_tx_params);
return result;
}
};
@ -1460,6 +1461,7 @@ struct oxen_chain_generator
cryptonote::transaction create_and_add_state_change_tx(service_nodes::new_state state, const crypto::public_key& pub_key, uint16_t reasons_all, uint16_t reasons_any, uint64_t height = -1, const std::vector<uint64_t>& voters = {}, uint64_t fee = 0, bool kept_by_block = false);
cryptonote::transaction create_and_add_registration_tx(const cryptonote::account_base& src, const cryptonote::keypair& sn_keys = cryptonote::keypair{hw::get_device("default")}, bool kept_by_block = false);
cryptonote::transaction create_and_add_staking_tx (const crypto::public_key &pub_key, const cryptonote::account_base &src, uint64_t amount, bool kept_by_block = false);
cryptonote::transaction create_and_add_unlock_stake_tx (const crypto::public_key& pub_key, const cryptonote::account_base& src, const cryptonote::transaction& staking_tx, bool kept_by_block = false);
oxen_blockchain_entry &create_and_add_next_block (const std::vector<cryptonote::transaction>& txs = {}, cryptonote::checkpoint_t const *checkpoint = nullptr, bool can_be_added_to_blockchain = true, std::string const &fail_msg = {});
// Same as create_and_add_tx, but also adds 95kB of junk into tx_extra to bloat up the tx size.
cryptonote::transaction create_and_add_big_tx(const cryptonote::account_base& src, const cryptonote::account_public_address& dest, uint64_t amount, uint64_t junk_size = 95000, uint64_t fee = TESTS_DEFAULT_FEE, bool kept_by_block = false);
@ -1472,6 +1474,7 @@ struct oxen_chain_generator
uint64_t fee = cryptonote::STAKING_FEE_BASIS,
const std::vector<service_nodes::contribution>& contributors = {}) const;
cryptonote::transaction create_staking_tx (const crypto::public_key& pub_key, const cryptonote::account_base &src, uint64_t amount) const;
cryptonote::transaction create_unlock_stake_tx (const crypto::public_key& pub_key, const cryptonote::transaction& staking_tx, const cryptonote::account_base &src) const;
cryptonote::transaction create_state_change_tx(service_nodes::new_state state, const crypto::public_key& pub_key, uint16_t reasons_all, uint16_t reasons_any, uint64_t height = -1, const std::vector<uint64_t>& voters = {}, uint64_t fee = 0) const;
cryptonote::checkpoint_t create_service_node_checkpoint(uint64_t block_height, size_t num_votes) const;

View File

@ -136,6 +136,8 @@ int main(int argc, char* argv[])
GENERATE_AND_PLAY(oxen_service_nodes_insufficient_contribution);
GENERATE_AND_PLAY(oxen_service_nodes_insufficient_contribution_HF18);
GENERATE_AND_PLAY(oxen_service_nodes_sufficient_contribution_HF19);
GENERATE_AND_PLAY(oxen_service_nodes_small_contribution_early_withdrawal);
GENERATE_AND_PLAY(oxen_service_nodes_large_contribution_early_withdrawal);
GENERATE_AND_PLAY(oxen_service_nodes_insufficient_operator_contribution_HF19);
GENERATE_AND_PLAY(oxen_service_nodes_test_rollback);
GENERATE_AND_PLAY(oxen_service_nodes_test_swarms_basic);

View File

@ -3094,6 +3094,112 @@ bool oxen_service_nodes_sufficient_contribution_HF19::generate(std::vector<test_
return true;
}
bool oxen_service_nodes_small_contribution_early_withdrawal::generate(std::vector<test_event_entry> &events)
{
auto hard_forks = oxen_generate_hard_fork_table();
oxen_chain_generator gen(events, hard_forks);
gen.add_blocks_until_version(hard_forks.back().version);
gen.add_mined_money_unlock_blocks();
const auto alice = gen.add_account();
const auto tx0 = gen.create_and_add_tx(gen.first_miner_, alice.get_keys().m_account_address, MK_COINS(101));
gen.create_and_add_next_block({tx0});
gen.add_transfer_unlock_blocks();
uint64_t operator_portions = cryptonote::old::STAKING_PORTIONS / oxen::MAX_CONTRIBUTORS_HF19 * (oxen::MAX_CONTRIBUTORS_HF19 - 1);
uint64_t staking_requirement = service_nodes::get_staking_requirement(cryptonote::network_type::FAKECHAIN, hard_forks.back().height);
uint64_t operator_amount = staking_requirement / oxen::MAX_CONTRIBUTORS_HF19 * (oxen::MAX_CONTRIBUTORS_HF19 - 1);
uint64_t single_contributed_amount = staking_requirement - operator_amount + 1;
cryptonote::keypair sn_keys{hw::get_device("default")};
cryptonote::transaction register_tx = gen.create_registration_tx(gen.first_miner_, sn_keys, operator_portions);
gen.add_tx(register_tx);
gen.create_and_add_next_block({register_tx});
assert(single_contributed_amount != 0);
cryptonote::transaction stake = gen.create_and_add_staking_tx(sn_keys.pub, alice, single_contributed_amount);
gen.create_and_add_next_block({stake});
oxen_register_callback(events, "test_sufficient_stake_does_get_accepted", [sn_keys, staking_requirement](cryptonote::core &c, size_t ev_index)
{
DEFINE_TESTS_ERROR_CONTEXT("test_sufficient_stake_does_get_accepted");
const auto sn_list = c.get_service_node_list_state({sn_keys.pub});
CHECK_TEST_CONDITION(sn_list.size() == 1);
CHECK_TEST_CONDITION(sn_list[0].info->contributors.size() == 2);
service_nodes::service_node_pubkey_info const &pubkey_info = sn_list[0];
CHECK_EQ(pubkey_info.info->total_contributed, staking_requirement);
return true;
});
cryptonote::transaction unstake = gen.create_unlock_stake_tx(sn_keys.pub, stake, alice);
gen.create_and_add_next_block({unstake}, nullptr, false, "Small contributor should not be able to withdraw early");
oxen_register_callback(events, "test_unlock_does_not_get_accepted", [sn_keys](cryptonote::core &c, size_t ev_index)
{
DEFINE_TESTS_ERROR_CONTEXT("test_unlock_does_not_get_accepted");
const auto sn_list = c.get_service_node_list_state({sn_keys.pub});
CHECK_TEST_CONDITION(sn_list.size() == 1);
CHECK_TEST_CONDITION(sn_list[0].info->requested_unlock_height == 0);
return true;
});
return true;
}
bool oxen_service_nodes_large_contribution_early_withdrawal::generate(std::vector<test_event_entry> &events)
{
auto hard_forks = oxen_generate_hard_fork_table();
oxen_chain_generator gen(events, hard_forks);
gen.add_blocks_until_version(hard_forks.back().version);
gen.add_mined_money_unlock_blocks();
const auto alice = gen.add_account();
const auto tx0 = gen.create_and_add_tx(gen.first_miner_, alice.get_keys().m_account_address, MK_COINS(101));
gen.create_and_add_next_block({tx0});
gen.add_transfer_unlock_blocks();
uint64_t operator_portions = cryptonote::old::STAKING_PORTIONS / oxen::MAX_CONTRIBUTORS_HF19 * (oxen::MAX_CONTRIBUTORS_HF19 - 4);
uint64_t staking_requirement = service_nodes::get_staking_requirement(cryptonote::network_type::FAKECHAIN, hard_forks.back().height);
uint64_t operator_amount = staking_requirement / oxen::MAX_CONTRIBUTORS_HF19 * (oxen::MAX_CONTRIBUTORS_HF19- 4);
uint64_t single_contributed_amount = staking_requirement - operator_amount + 1;
cryptonote::keypair sn_keys{hw::get_device("default")};
cryptonote::transaction register_tx = gen.create_registration_tx(gen.first_miner_, sn_keys, operator_portions);
gen.add_tx(register_tx);
gen.create_and_add_next_block({register_tx});
assert(single_contributed_amount != 0);
cryptonote::transaction stake = gen.create_and_add_staking_tx(sn_keys.pub, alice, single_contributed_amount);
gen.create_and_add_next_block({stake});
oxen_register_callback(events, "test_sufficient_stake_does_get_accepted", [sn_keys, staking_requirement](cryptonote::core &c, size_t ev_index)
{
DEFINE_TESTS_ERROR_CONTEXT("test_sufficient_stake_does_get_accepted");
const auto sn_list = c.get_service_node_list_state({sn_keys.pub});
CHECK_TEST_CONDITION(sn_list.size() == 1);
CHECK_TEST_CONDITION(sn_list[0].info->contributors.size() == 2);
service_nodes::service_node_pubkey_info const &pubkey_info = sn_list[0];
CHECK_EQ(pubkey_info.info->total_contributed, staking_requirement);
return true;
});
cryptonote::transaction unstake = gen.create_and_add_unlock_stake_tx(sn_keys.pub, alice, stake);
gen.create_and_add_next_block({unstake});
oxen_register_callback(events, "test_unlock_does_get_accepted", [sn_keys](cryptonote::core &c, size_t ev_index)
{
DEFINE_TESTS_ERROR_CONTEXT("test_unlock_does_get_accepted");
const auto sn_list = c.get_service_node_list_state({sn_keys.pub});
CHECK_TEST_CONDITION(sn_list.size() == 1);
CHECK_TEST_CONDITION(sn_list[0].info->requested_unlock_height > 0);
return true;
});
return true;
}
bool oxen_service_nodes_insufficient_operator_contribution_HF19::generate(std::vector<test_event_entry> &events)
{
auto hard_forks = oxen_generate_hard_fork_table(cryptonote::hf::hf19_reward_batching);

View File

@ -79,6 +79,8 @@ struct oxen_service_nodes_gen_nodes
struct oxen_service_nodes_insufficient_contribution : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_insufficient_contribution_HF18 : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_sufficient_contribution_HF19 : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_small_contribution_early_withdrawal : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_large_contribution_early_withdrawal : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_insufficient_operator_contribution_HF19 : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_test_rollback : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_test_swarms_basic : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };