From 0123517b480bd758a38037935ca9a0d5dffd5b19 Mon Sep 17 00:00:00 2001 From: Alexander Block Date: Sun, 25 Nov 2018 14:27:18 +0100 Subject: [PATCH] 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. --- src/evo/deterministicmns.cpp | 105 +++++++++++++++++++++++++++++++++++ src/evo/deterministicmns.h | 40 +++++++++++++ 2 files changed, 145 insertions(+) diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 9808f6494..8ccd785d0 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -13,6 +13,9 @@ #include "validation.h" #include "validationinterface.h" +#include "llmq/quorums_commitment.h" +#include "llmq/quorums_utils.h" + #include static const std::string DB_LIST_SNAPSHOT = "dmn_S"; @@ -258,6 +261,55 @@ std::vector> 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(*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(*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 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); diff --git a/src/evo/deterministicmns.h b/src/evo/deterministicmns.h index bc9fadaa3..00aa99345 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -22,6 +22,11 @@ class CBlock; class CBlockIndex; class CValidationState; +namespace llmq +{ + class CFinalCommitment; +} + class CDeterministicMNState { public: @@ -307,6 +312,39 @@ public: std::vector CalculateQuorum(size_t maxSize, const uint256& modifier) const; std::vector> 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();