mirror of https://github.com/oxen-io/oxen-core.git
Merge pull request #1539 from darcys22/prevent-unlocks-small-holdings
Prevent unlocks small holdings
This commit is contained in:
commit
c5fbf96b7d
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: ");
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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); };
|
||||
|
|
Loading…
Reference in New Issue