Add overstaking prevention (#1210)

This prevents staking transactions from being accepted if they overstake
the available contribution room by more than 1%.  This is to prevent a
case that has happened a few times where there are competing partial
stakes submitted for the same SN at the same time (i.e. before a block
gets mined with the stakes).  For example:

- Operator registers service node with 30% contribution
- Staker 1 submits stake with 40% contribution
- Staker 2 submits stake with 60% contribution

The wallet avoids stake 2 if the 40% has been accepted into a block, but
doesn't if it is still in the mempool.  Later, when the contributions
get mined, both stakes are admitted because whichever one goes first
doesn't complete the stake, and the second one is still valid (since
there is a spot, and since it contributes >= the required amount).

Whichever stake gets added to a block second, however, will only be
counted as a contribution of the available amount.  So, for example, if
stake 1 gets added first and then stake 2 gets added you'll end up with
an active service node of:

- operator has 30% contributed and locked
- staker 1 has 40% contributed and locked
- staker 2 has 30% contributed but 60% locked.

This commit adds an upper bound for an acceptable stake that is 101% of
the available contribution room so that, in the above situation,
whichever stake gets added first will be a contribution and the second
one will fall through as an ordinary transaction back to the staker's
wallet so that the staker the re-contribute the proper amount.
This commit is contained in:
Jason Rhinelander 2020-08-02 21:10:40 -03:00 committed by GitHub
parent 53d94bc8b1
commit 441c21b40d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 32 additions and 0 deletions

View file

@ -37,6 +37,7 @@
#include <stdexcept>
#include <chrono>
#include <array>
#include <ratio>
#define CRYPTONOTE_MAX_BLOCK_NUMBER 500000000
#define CRYPTONOTE_MAX_TX_SIZE 1000000
@ -196,6 +197,12 @@ namespace config
inline constexpr uint64_t DEFAULT_DUST_THRESHOLD = 2000000000; // 2 * pow(10, 9)
inline constexpr uint64_t BASE_REWARD_CLAMP_THRESHOLD = 100000000; // pow(10, 8)
// Maximum allowed stake contribution, as a fraction of the available contribution room. This
// should generally be slightly larger than 1. This is used to disallow large overcontributions
// which can happen when there are competing stakes submitted at the same time for the same
// service node.
using MAXIMUM_ACCEPTABLE_STAKE = std::ratio<101, 100>;
// Used to estimate the blockchain height from a timestamp, with some grace time. This can drift
// slightly over time (because average block time is not typically *exactly*
// DIFFICULTY_TARGET_V2).

View file

@ -1113,6 +1113,13 @@ namespace service_nodes
}
}
// Check that the contribution isn't too large.
if (stake.transferred > get_max_node_contribution(hf_version, curinfo.staking_requirement, curinfo.total_reserved))
{
MINFO("TX: Amount " << stake.transferred << " is too large (this is probably a result of competing stakes)");
return false;
}
//
// Successfully Validated
//

View file

@ -1,6 +1,7 @@
#include "cryptonote_config.h"
#include "common/loki.h"
#include "int-util.h"
#include <limits>
#include <vector>
#include <boost/lexical_cast.hpp>
#include <cfenv>
@ -142,6 +143,14 @@ static uint64_t get_min_node_contribution_pre_v11(uint64_t staking_requirement,
return std::min(staking_requirement - total_reserved, staking_requirement / MAX_NUMBER_OF_CONTRIBUTORS);
}
uint64_t get_max_node_contribution(uint8_t version, uint64_t staking_requirement, uint64_t total_reserved)
{
if (version >= cryptonote::network_version_16)
return (staking_requirement - total_reserved) * config::MAXIMUM_ACCEPTABLE_STAKE::num
/ config::MAXIMUM_ACCEPTABLE_STAKE::den;
return std::numeric_limits<uint64_t>::max();
}
uint64_t get_min_node_contribution(uint8_t version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions)
{
if (version < cryptonote::network_version_11_infinite_staking)

View file

@ -161,6 +161,15 @@ static_assert(STAKING_PORTIONS != UINT64_MAX, "UINT64_MAX is used as the invalid
uint64_t get_min_node_contribution (uint8_t version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions);
uint64_t get_min_node_contribution_in_portions(uint8_t version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions);
// Gets the maximum allowed stake amount. This is used to prevent significant overstaking. The
// wallet tries to avoid this when submitting a stake, but it can still happen when competing stakes
// get submitted into the mempool -- for example, with 10k of contribution room, two contributions
// of 8k could get submitted and both would be accepted, but the second one would only count as 2k
// of stake despite locking 8k.
// Starting in HF16, we disallow a stake if it is more than MAXIMUM_ACCEPTABLE_STAKE ratio of the
// available contribution room, which allows slight overstaking but disallows larger overstakes.
uint64_t get_max_node_contribution(uint8_t version, uint64_t staking_requirement, uint64_t total_reserved);
uint64_t get_staking_requirement(cryptonote::network_type nettype, uint64_t height, uint8_t hf_version);
uint64_t portions_to_amount(uint64_t portions, uint64_t staking_requirement);