Implement PoSe based on information from LLMQ commitments (#2478)

Members which are not in the validMembers bitsets are now PoSe punished.
The maximum PoSe score is dynamic and mostly identical to the number of
registered MNs. Added PoSe scores for failures are then a percentage of
the maximum score, so that we can better control how often failures are
allowed per payment cycle. For LLMQ failures, this is 66% to allow
approximately 2 failures per payment cycle.
This commit is contained in:
Alexander Block 2018-11-25 14:27:18 +01:00 committed by UdjinM6
parent a4ea816b20
commit 0123517b48
2 changed files with 145 additions and 0 deletions

View File

@ -13,6 +13,9 @@
#include "validation.h"
#include "validationinterface.h"
#include "llmq/quorums_commitment.h"
#include "llmq/quorums_utils.h"
#include <univalue.h>
static const std::string DB_LIST_SNAPSHOT = "dmn_S";
@ -258,6 +261,55 @@ std::vector<std::pair<arith_uint256, CDeterministicMNCPtr>> CDeterministicMNList
return scores;
}
int CDeterministicMNList::CalcMaxPoSePenalty() const
{
// Maximum PoSe penalty is dynamic and equals the number of registered MNs
// It's however at least 100.
// This means that the max penalty is usually equal to a full payment cycle
return std::max(100, (int)GetAllMNsCount());
}
int CDeterministicMNList::CalcPenalty(int percent) const
{
assert(percent > 0);
return (CalcMaxPoSePenalty() * percent) / 100;
}
void CDeterministicMNList::PoSePunish(const uint256& proTxHash, int penalty)
{
assert(penalty > 0);
auto dmn = GetMN(proTxHash);
assert(dmn);
int maxPenalty = CalcMaxPoSePenalty();
auto newState = std::make_shared<CDeterministicMNState>(*dmn->pdmnState);
newState->nPoSePenalty += penalty;
newState->nPoSePenalty = std::min(maxPenalty, newState->nPoSePenalty);
LogPrintf("CDeterministicMNList::%s -- punished MN %s, penalty %d->%d (max=%d)\n",
__func__, proTxHash.ToString(), dmn->pdmnState->nPoSePenalty, newState->nPoSePenalty, maxPenalty);
if (newState->nPoSePenalty >= maxPenalty && newState->nPoSeBanHeight == -1) {
newState->nPoSeBanHeight = nHeight;
LogPrintf("CDeterministicMNList::%s -- banned MN %s at height %d\n",
__func__, proTxHash.ToString(), nHeight);
}
UpdateMN(proTxHash, newState);
}
void CDeterministicMNList::PoSeDecrease(const uint256& proTxHash)
{
auto dmn = GetMN(proTxHash);
assert(dmn);
assert(dmn->pdmnState->nPoSePenalty > 0 && dmn->pdmnState->nPoSeBanHeight == -1);
auto newState = std::make_shared<CDeterministicMNState>(*dmn->pdmnState);
newState->nPoSePenalty--;
UpdateMN(proTxHash, newState);
}
CDeterministicMNListDiff CDeterministicMNList::BuildDiff(const CDeterministicMNList& to) const
{
CDeterministicMNListDiff diffRet;
@ -465,6 +517,8 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
}
});
DecreasePoSePenalties(newList);
// we skip the coinbase
for (int i = 1; i < (int)block.vtx.size(); i++) {
const CTransaction& tx = *block.vtx[i];
@ -480,6 +534,11 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
}
}
if (tx.nVersion != 3) {
// only interested in special TXs
continue;
}
if (tx.nType == TRANSACTION_PROVIDER_REGISTER) {
CProRegTx proTx;
if (!GetTxPayload(tx, proTx)) {
@ -558,6 +617,7 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
if (newState->nPoSeBanHeight != -1) {
// only revive when all keys are set
if (newState->pubKeyOperator.IsValid() && !newState->keyIDVoting.IsNull() && !newState->keyIDOwner.IsNull()) {
newState->nPoSePenalty = 0;
newState->nPoSeBanHeight = -1;
newState->nPoSeRevivedHeight = nHeight;
@ -613,6 +673,14 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
LogPrintf("CDeterministicMNManager::%s -- MN %s revoked operator key at height %d: %s\n",
__func__, proTx.proTxHash.ToString(), nHeight, proTx.ToString());
} else if (tx.nType == TRANSACTION_QUORUM_COMMITMENT) {
llmq::CFinalCommitment qc;
if (!GetTxPayload(tx, qc)) {
assert(false); // this should have been handled already
}
if (!qc.IsNull()) {
HandleQuorumCommitment(qc, newList);
}
}
}
@ -629,6 +697,43 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
return true;
}
void CDeterministicMNManager::HandleQuorumCommitment(llmq::CFinalCommitment& qc, CDeterministicMNList& mnList)
{
// The commitment has already been validated at this point so it's safe to use members of it
auto members = llmq::CLLMQUtils::GetAllQuorumMembers((Consensus::LLMQType)qc.llmqType, qc.quorumHash);
for (size_t i = 0; i < members.size(); i++) {
if (!mnList.HasMN(members[i]->proTxHash)) {
continue;
}
if (!qc.validMembers[i]) {
// punish MN for failed DKG participation
// The idea is to immediately ban a MN when it fails 2 DKG sessions with only a few blocks in-between
// If there were enough blocks between failures, the MN has a chance to recover as he reduces his penalty by 1 for every block
// If it however fails 3 times in the timespan of a single payment cycle, it should definitely get banned
mnList.PoSePunish(members[i]->proTxHash, mnList.CalcPenalty(66));
}
}
}
void CDeterministicMNManager::DecreasePoSePenalties(CDeterministicMNList& mnList)
{
std::vector<uint256> toDecrease;
toDecrease.reserve(mnList.GetValidMNsCount() / 10);
// only iterate and decrease for valid ones (not PoSe banned yet)
// if a MN ever reaches the maximum, it stays in PoSe banned state until revived
mnList.ForEachMN(true, [&](const CDeterministicMNCPtr& dmn) {
if (dmn->pdmnState->nPoSePenalty > 0 && dmn->pdmnState->nPoSeBanHeight == -1) {
toDecrease.emplace_back(dmn->proTxHash);
}
});
for (const auto& proTxHash : toDecrease) {
mnList.PoSeDecrease(proTxHash);
}
}
CDeterministicMNList CDeterministicMNManager::GetListForBlock(const uint256& blockHash)
{
LOCK(cs);

View File

@ -22,6 +22,11 @@ class CBlock;
class CBlockIndex;
class CValidationState;
namespace llmq
{
class CFinalCommitment;
}
class CDeterministicMNState
{
public:
@ -307,6 +312,39 @@ public:
std::vector<CDeterministicMNCPtr> CalculateQuorum(size_t maxSize, const uint256& modifier) const;
std::vector<std::pair<arith_uint256, CDeterministicMNCPtr>> CalculateScores(const uint256& modifier) const;
/**
* Calculates the maximum penalty which is allowed at the height of this MN list. It is dynamic and might change
* for every block.
* @return
*/
int CalcMaxPoSePenalty() const;
/**
* Returns a the given percentage from the max penalty for this MN list. Always use this method to calculate the
* value later passed to PoSePunish. The percentage should be high enough to take per-block penalty decreasing for MNs
* into account. This means, if you want to accept 2 failures per payment cycle, you should choose a percentage that
* is higher then 50%, e.g. 66%.
* @param percent
* @return
*/
int CalcPenalty(int percent) const;
/**
* Punishes a MN for misbehavior. If the resulting penalty score of the MN reaches the max penalty, it is banned.
* Penalty scores are only increased when the MN is not already banned, which means that after banning the penalty
* might appear lower then the current max penalty, while the MN is still banned.
* @param proTxHash
* @param penalty
*/
void PoSePunish(const uint256& proTxHash, int penalty);
/**
* Decrease penalty score of MN by 1.
* Only allowed on non-banned MNs.
* @param proTxHash
*/
void PoSeDecrease(const uint256& proTxHash);
CDeterministicMNListDiff BuildDiff(const CDeterministicMNList& to) const;
CSimplifiedMNListDiff BuildSimplifiedDiff(const CDeterministicMNList& to) const;
CDeterministicMNList ApplyDiff(const CDeterministicMNListDiff& diff) const;
@ -422,6 +460,8 @@ public:
// the returned list will not contain the correct block hash (we can't know it yet as the coinbase TX is not updated yet)
bool BuildNewListFromBlock(const CBlock& block, const CBlockIndex* pindexPrev, CValidationState& state, CDeterministicMNList& mnListRet);
void HandleQuorumCommitment(llmq::CFinalCommitment& qc, CDeterministicMNList& mnList);
void DecreasePoSePenalties(CDeterministicMNList& mnList);
CDeterministicMNList GetListForBlock(const uint256& blockHash);
CDeterministicMNList GetListAtChainTip();