diff --git a/src/consensus/tx_check.cpp b/src/consensus/tx_check.cpp --- a/src/consensus/tx_check.cpp +++ b/src/consensus/tx_check.cpp @@ -14,35 +14,38 @@ CValidationState &state) { // Basic checks that don't depend on any context if (tx.vin.empty()) { - return state.DoS(100, false, REJECT_INVALID, "bad-txns-vin-empty"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-vin-empty"); } if (tx.vout.empty()) { - return state.DoS(100, false, REJECT_INVALID, "bad-txns-vout-empty"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-vout-empty"); } // Size limit if (::GetSerializeSize(tx, PROTOCOL_VERSION) > MAX_TX_SIZE) { - return state.DoS(100, false, REJECT_INVALID, "bad-txns-oversize"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-oversize"); } // Check for negative or overflow output values Amount nValueOut = Amount::zero(); for (const auto &txout : tx.vout) { if (txout.nValue < Amount::zero()) { - return state.DoS(100, false, REJECT_INVALID, - "bad-txns-vout-negative"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-vout-negative"); } if (txout.nValue > MAX_MONEY) { - return state.DoS(100, false, REJECT_INVALID, - "bad-txns-vout-toolarge"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-vout-toolarge"); } nValueOut += txout.nValue; if (!MoneyRange(nValueOut)) { - return state.DoS(100, false, REJECT_INVALID, - "bad-txns-txouttotal-toolarge"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-txouttotal-toolarge"); } } @@ -51,7 +54,8 @@ bool CheckCoinbase(const CTransaction &tx, CValidationState &state) { if (!tx.IsCoinBase()) { - return state.DoS(100, false, REJECT_INVALID, "bad-cb-missing", false, + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-cb-missing", false, "first tx is not coinbase"); } @@ -62,7 +66,8 @@ if (tx.vin[0].scriptSig.size() < 2 || tx.vin[0].scriptSig.size() > MAX_COINBASE_SCRIPTSIG_SIZE) { - return state.DoS(100, false, REJECT_INVALID, "bad-cb-length"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-cb-length"); } return true; @@ -70,7 +75,8 @@ bool CheckRegularTransaction(const CTransaction &tx, CValidationState &state) { if (tx.IsCoinBase()) { - return state.DoS(100, false, REJECT_INVALID, "bad-tx-coinbase"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-tx-coinbase"); } if (!CheckTransactionCommon(tx, state)) { @@ -81,13 +87,13 @@ std::unordered_set vInOutPoints; for (const auto &txin : tx.vin) { if (txin.prevout.IsNull()) { - return state.DoS(100, false, REJECT_INVALID, - "bad-txns-prevout-null"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-prevout-null"); } if (!vInOutPoints.insert(txin.prevout).second) { - return state.DoS(100, false, REJECT_INVALID, - "bad-txns-inputs-duplicate"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-inputs-duplicate"); } } diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -44,14 +44,16 @@ if (!IsFinalTx(tx, nHeight, nLockTimeCutoff)) { // While this is only one transaction, we use txns in the error to // ensure continuity with other clients. - return state.DoS(100, false, REJECT_INVALID, "bad-txns-nonfinal", false, + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-nonfinal", false, "non-final transaction"); } if (IsMagneticAnomalyEnabled(params, nHeight)) { // Size limit if (::GetSerializeSize(tx, PROTOCOL_VERSION) < MIN_TX_SIZE) { - return state.DoS(100, false, REJECT_INVALID, "bad-txns-undersize"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-undersize"); } } @@ -158,8 +160,9 @@ Amount &txfee) { // are the actual inputs available? if (!inputs.HaveInputs(tx)) { - return state.DoS(100, false, REJECT_INVALID, - "bad-txns-inputs-missingorspent", false, + return state.DoS(0, ValidationInvalidReason::TX_MISSING_INPUTS, false, + REJECT_INVALID, "bad-txns-inputs-missingorspent", + false, strprintf("%s: inputs missing/spent", __func__)); } @@ -172,7 +175,8 @@ // If prev is coinbase, check that it's matured if (coin.IsCoinBase() && nSpendHeight - coin.GetHeight() < COINBASE_MATURITY) { - return state.DoS(0, false, REJECT_INVALID, + return state.DoS(0, ValidationInvalidReason::TX_MISSING_INPUTS, + false, REJECT_INVALID, "bad-txns-premature-spend-of-coinbase", false, strprintf("tried to spend coinbase at depth %d", nSpendHeight - coin.GetHeight())); @@ -181,23 +185,25 @@ // Check for negative or overflow input values nValueIn += coin.GetTxOut().nValue; if (!MoneyRange(coin.GetTxOut().nValue) || !MoneyRange(nValueIn)) { - return state.DoS(100, false, REJECT_INVALID, - "bad-txns-inputvalues-outofrange"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-inputvalues-outofrange"); } } const Amount value_out = tx.GetValueOut(); if (nValueIn < value_out) { - return state.DoS( - 100, false, REJECT_INVALID, "bad-txns-in-belowout", false, - strprintf("value in (%s) < value out (%s)", FormatMoney(nValueIn), - FormatMoney(value_out))); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-in-belowout", false, + strprintf("value in (%s) < value out (%s)", + FormatMoney(nValueIn), + FormatMoney(value_out))); } // Tally transaction fees const Amount txfee_aux = nValueIn - value_out; if (!MoneyRange(txfee_aux)) { - return state.DoS(100, false, REJECT_INVALID, "bad-txns-fee-outofrange"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-txns-fee-outofrange"); } txfee = txfee_aux; diff --git a/src/consensus/validation.h b/src/consensus/validation.h --- a/src/consensus/validation.h +++ b/src/consensus/validation.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_CONSENSUS_VALIDATION_H #define BITCOIN_CONSENSUS_VALIDATION_H +#include #include /** "reject" message codes */ @@ -17,6 +18,58 @@ static const uint8_t REJECT_INSUFFICIENTFEE = 0x42; static const uint8_t REJECT_CHECKPOINT = 0x43; +/** + * A "reason" why something was invalid, suitable for determining whether the + * provider of the object should be banned/ignored/disconnected/etc. These are + * much more granular than the rejection codes, which may be more useful for + * some other use-cases. + */ +enum class ValidationInvalidReason { + // txn and blocks: + //! not actually invalid + NONE, + //! invalid by consensus rules (excluding any below reasons) + CONSENSUS, + /** + * Invalid by a recent change to consensus rules. + * Currently unused as there are no such consensus rule changes. + */ + RECENT_CONSENSUS_CHANGE, + // Only blocks (or headers): + //! this object was cached as being invalid, but we don't know why + CACHED_INVALID, + //! invalid proof of work or time too old + BLOCK_INVALID_HEADER, + //! the block's data didn't match the data committed to by the PoW + BLOCK_MUTATED, + //! We don't have the previous block the checked one is built on + BLOCK_MISSING_PREV, + //! A block this one builds on is invalid + BLOCK_INVALID_PREV, + //! block timestamp was > 2 hours in the future (or our clock is bad) + BLOCK_TIME_FUTURE, + //! the block failed to meet one of our checkpoints + BLOCK_CHECKPOINT, + //! block finalization problems. + BLOCK_FINALIZATION, + // Only loose txn: + //! didn't meet our local policy rules + TX_NOT_STANDARD, + //! a transaction was missing some of its inputs (or its inputs were spent + //! at < coinbase maturity height) + TX_MISSING_INPUTS, + /** + * Tx already in mempool or conflicts with a tx in the chain + * TODO: Currently this is only used if the transaction already exists in + * the mempool or on chain, + * TODO: ATMP's fMissingInputs and a valid CValidationState being used to + * indicate missing inputs + */ + TX_CONFLICT, + //! violated mempool's fee/size/descendant/etc limits + TX_MEMPOOL_POLICY, +}; + /** Capture information about block/transaction validation */ class CValidationState { private: @@ -25,6 +78,7 @@ MODE_INVALID, //!< network rule violation (DoS value may be set) MODE_ERROR, //!< run-time error } mode; + ValidationInvalidReason m_reason; int nDoS; std::string strRejectReason; unsigned int chRejectCode; @@ -33,29 +87,34 @@ public: CValidationState() - : mode(MODE_VALID), nDoS(0), chRejectCode(0), - corruptionPossible(false) {} + : mode(MODE_VALID), m_reason(ValidationInvalidReason::NONE), nDoS(0), + chRejectCode(0), corruptionPossible(false) {} - bool DoS(int level, bool ret = false, unsigned int chRejectCodeIn = 0, + bool DoS(int level, ValidationInvalidReason reasonIn, bool ret = false, + unsigned int chRejectCodeIn = 0, const std::string &strRejectReasonIn = "", bool corruptionIn = false, const std::string &strDebugMessageIn = "") { + m_reason = reasonIn; chRejectCode = chRejectCodeIn; strRejectReason = strRejectReasonIn; corruptionPossible = corruptionIn; strDebugMessage = strDebugMessageIn; + nDoS += level; + assert(nDoS == GetDoSForReason()); + assert(corruptionPossible == + (m_reason == ValidationInvalidReason::BLOCK_MUTATED)); if (mode == MODE_ERROR) { return ret; } - nDoS += level; mode = MODE_INVALID; return ret; } - - bool Invalid(bool ret = false, unsigned int _chRejectCode = 0, + bool Invalid(ValidationInvalidReason _reason, bool ret = false, + unsigned int _chRejectCode = 0, const std::string &_strRejectReason = "", const std::string &_strDebugMessage = "") { - return DoS(0, ret, _chRejectCode, _strRejectReason, false, + return DoS(0, _reason, ret, _chRejectCode, _strRejectReason, false, _strDebugMessage); } bool Error(const std::string &strRejectReasonIn) { @@ -66,14 +125,46 @@ mode = MODE_ERROR; return false; } - bool IsValid() const { return mode == MODE_VALID; } bool IsInvalid() const { return mode == MODE_INVALID; } bool IsError() const { return mode == MODE_ERROR; } - - bool CorruptionPossible() const { return corruptionPossible; } - void SetCorruptionPossible() { corruptionPossible = true; } + bool CorruptionPossible() const { + assert(corruptionPossible == + (m_reason == ValidationInvalidReason::BLOCK_MUTATED)); + return corruptionPossible; + } + void SetCorruptionPossible() { + corruptionPossible = true; + assert(corruptionPossible == + (m_reason == ValidationInvalidReason::BLOCK_MUTATED)); + } int GetDoS() const { return nDoS; } + int GetDoSForReason() const { + switch (m_reason) { + case ValidationInvalidReason::NONE: + return 0; + case ValidationInvalidReason::CONSENSUS: + case ValidationInvalidReason::BLOCK_MUTATED: + case ValidationInvalidReason::BLOCK_INVALID_HEADER: + case ValidationInvalidReason::BLOCK_CHECKPOINT: + case ValidationInvalidReason::BLOCK_INVALID_PREV: + return 100; + case ValidationInvalidReason::BLOCK_FINALIZATION: + return 20; + case ValidationInvalidReason::BLOCK_MISSING_PREV: + return 10; + case ValidationInvalidReason::CACHED_INVALID: + case ValidationInvalidReason::RECENT_CONSENSUS_CHANGE: + case ValidationInvalidReason::BLOCK_TIME_FUTURE: + case ValidationInvalidReason::TX_NOT_STANDARD: + case ValidationInvalidReason::TX_MISSING_INPUTS: + case ValidationInvalidReason::TX_CONFLICT: + case ValidationInvalidReason::TX_MEMPOOL_POLICY: + return 0; + } + return 0; + } + ValidationInvalidReason GetReason() const { return m_reason; } unsigned int GetRejectCode() const { return chRejectCode; } std::string GetRejectReason() const { return strRejectReason; } std::string GetDebugMessage() const { return strDebugMessage; } diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1188,6 +1188,7 @@ } static bool TxRelayMayResultInDisconnect(const CValidationState &state) { + assert(state.GetDoS() == state.GetDoSForReason()); return (state.GetDoS() > 0); } @@ -1205,6 +1206,7 @@ static bool MaybePunishNode(NodeId nodeid, const CValidationState &state, bool via_compact_block, const std::string &message = "") { + assert(state.GetDoS() == state.GetDoSForReason()); int nDoS = state.GetDoS(); if (nDoS > 0 && !via_compact_block) { LOCK(cs_main); diff --git a/src/test/txvalidation_tests.cpp b/src/test/txvalidation_tests.cpp --- a/src/test/txvalidation_tests.cpp +++ b/src/test/txvalidation_tests.cpp @@ -51,6 +51,7 @@ // Check that the validation state reflects the unsuccesful attempt. BOOST_CHECK(state.IsInvalid()); BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-tx-coinbase"); + BOOST_CHECK(state.GetReason() == ValidationInvalidReason::CONSENSUS); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -363,7 +363,8 @@ // Rather not work on nonstandard transactions (unless -testnet) std::string reason; if (fRequireStandard && !IsStandardTx(tx, reason)) { - return state.DoS(0, false, REJECT_NONSTANDARD, reason); + return state.DoS(0, ValidationInvalidReason::TX_NOT_STANDARD, false, + REJECT_NONSTANDARD, reason); } // Only accept nLockTime-using transactions that can be mined in the next @@ -374,14 +375,16 @@ consensusParams, tx, ctxState, STANDARD_LOCKTIME_VERIFY_FLAGS)) { // We copy the state from a dummy to ensure we don't increase the // ban score of peer for transaction that could be valid in the future. - return state.DoS( - 0, false, REJECT_NONSTANDARD, ctxState.GetRejectReason(), - ctxState.CorruptionPossible(), ctxState.GetDebugMessage()); + return state.DoS(0, ValidationInvalidReason::TX_NOT_STANDARD, false, + REJECT_NONSTANDARD, ctxState.GetRejectReason(), + ctxState.CorruptionPossible(), + ctxState.GetDebugMessage()); } // Is it already in the memory pool? if (pool.exists(txid)) { - return state.Invalid(false, REJECT_DUPLICATE, "txn-already-in-mempool"); + return state.Invalid(ValidationInvalidReason::TX_CONFLICT, false, + REJECT_DUPLICATE, "txn-already-in-mempool"); } // Check for conflicts with in-memory transactions @@ -389,7 +392,8 @@ auto itConflicting = pool.mapNextTx.find(txin.prevout); if (itConflicting != pool.mapNextTx.end()) { // Disable replacement feature for good - return state.Invalid(false, REJECT_DUPLICATE, + return state.Invalid(ValidationInvalidReason::TX_MEMPOOL_POLICY, + false, REJECT_DUPLICATE, "txn-mempool-conflict"); } } @@ -414,8 +418,9 @@ // Optimistically just do efficient check of cache for // outputs. if (pcoinsTip->HaveCoinInCache(COutPoint(txid, out))) { - return state.Invalid(false, REJECT_DUPLICATE, - "txn-already-known"); + return state.Invalid( + ValidationInvalidReason::TX_CONFLICT, false, + REJECT_DUPLICATE, "txn-already-known"); } } @@ -433,7 +438,8 @@ // Are the actual inputs available? if (!view.HaveInputs(tx)) { - return state.Invalid(false, REJECT_DUPLICATE, + return state.Invalid(ValidationInvalidReason::TX_MEMPOOL_POLICY, + false, REJECT_DUPLICATE, "bad-txns-inputs-spent"); } @@ -451,7 +457,8 @@ // own. if (!CheckSequenceLocks(pool, tx, STANDARD_LOCKTIME_VERIFY_FLAGS, &lp)) { - return state.DoS(0, false, REJECT_NONSTANDARD, "non-BIP68-final"); + return state.DoS(0, ValidationInvalidReason::TX_NOT_STANDARD, false, + REJECT_NONSTANDARD, "non-BIP68-final"); } Amount nFees = Amount::zero(); @@ -467,7 +474,8 @@ // Check for non-standard pay-to-script-hash in inputs if (fRequireStandard && !AreInputsStandard(tx, view, nextBlockScriptVerifyFlags)) { - return state.Invalid(false, REJECT_NONSTANDARD, + return state.Invalid(ValidationInvalidReason::TX_NOT_STANDARD, + false, REJECT_NONSTANDARD, "bad-txns-nonstandard-inputs"); } @@ -493,12 +501,14 @@ // Do not change this to use virtualsize without coordinating a network // policy upgrade. if (!bypass_limits && nModifiedFees < minRelayTxFee.GetFee(nSize)) { - return state.DoS(0, false, REJECT_INSUFFICIENTFEE, + return state.DoS(0, ValidationInvalidReason::TX_MEMPOOL_POLICY, + false, REJECT_INSUFFICIENTFEE, "min relay fee not met"); } if (nAbsurdFee != Amount::zero() && nFees > nAbsurdFee) { - return state.Invalid(false, REJECT_HIGHFEE, "absurdly-high-fee", + return state.Invalid(ValidationInvalidReason::TX_NOT_STANDARD, + false, REJECT_HIGHFEE, "absurdly-high-fee", strprintf("%d > %d", nFees, nAbsurdFee)); } @@ -526,8 +536,9 @@ if (!bypass_limits && mempoolRejectFee > Amount::zero() && nModifiedFees < mempoolRejectFee) { return state.DoS( - 0, false, REJECT_INSUFFICIENTFEE, "mempool min fee not met", - false, strprintf("%d < %d", nModifiedFees, mempoolRejectFee)); + 0, ValidationInvalidReason::TX_MEMPOOL_POLICY, false, + REJECT_INSUFFICIENTFEE, "mempool min fee not met", false, + strprintf("%d < %d", nModifiedFees, mempoolRejectFee)); } // Calculate in-mempool ancestors, up to a limit. @@ -547,7 +558,8 @@ if (!pool.CalculateMemPoolAncestors( entry, setAncestors, nLimitAncestors, nLimitAncestorSize, nLimitDescendants, nLimitDescendantSize, errString)) { - return state.DoS(0, false, REJECT_NONSTANDARD, + return state.DoS(0, ValidationInvalidReason::TX_MEMPOOL_POLICY, + false, REJECT_NONSTANDARD, "too-long-mempool-chain", false, errString); } @@ -599,8 +611,8 @@ std::chrono::hours{ gArgs.GetArg("-mempoolexpiry", DEFAULT_MEMPOOL_EXPIRY)}); if (!pool.exists(txid)) { - return state.DoS(0, false, REJECT_INSUFFICIENTFEE, - "mempool full"); + return state.DoS(0, ValidationInvalidReason::TX_MEMPOOL_POLICY, + false, REJECT_INSUFFICIENTFEE, "mempool full"); } } } @@ -1038,7 +1050,8 @@ if (!txLimitSigChecks.consume_and_check(nSigChecksOut) || (pBlockLimitSigChecks && !pBlockLimitSigChecks->consume_and_check(nSigChecksOut))) { - return state.DoS(100, false, REJECT_INVALID, "too-many-sigchecks"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "too-many-sigchecks"); } return true; } @@ -1080,7 +1093,8 @@ sigCacheStore, txdata); if (check2()) { return state.Invalid( - false, REJECT_NONSTANDARD, + ValidationInvalidReason::TX_NOT_STANDARD, false, + REJECT_NONSTANDARD, strprintf("non-mandatory-script-verify-flag (%s)", ScriptErrorString(scriptError))); } @@ -1095,7 +1109,7 @@ // may want to continue peering with non-upgraded nodes even after // soft-fork super-majority signaling has occurred. return state.DoS( - 100, false, REJECT_INVALID, + 100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(scriptError))); } @@ -1610,7 +1624,7 @@ for (size_t o = 0; o < tx->vout.size(); o++) { if (view.HaveCoin(COutPoint(tx->GetId(), o))) { return state.DoS( - 100, + 100, ValidationInvalidReason::CONSENSUS, error("ConnectBlock(): tried to overwrite transaction"), REJECT_INVALID, "bad-txns-BIP30"); } @@ -1669,7 +1683,8 @@ // - its checks are not applied to pre-CTOR chains, which we might visit // with checkpointing off. return state.DoS( - 100, error("ConnectBlock(): tried to overwrite transaction"), + 100, ValidationInvalidReason::CONSENSUS, + error("ConnectBlock(): tried to overwrite transaction"), REJECT_INVALID, "tx-duplicate"); } @@ -1682,13 +1697,22 @@ Amount txfee = Amount::zero(); if (!isCoinBase && !Consensus::CheckTxInputs(tx, state, view, pindex->nHeight, txfee)) { + if (state.GetReason() == + ValidationInvalidReason::TX_MISSING_INPUTS) { + // CheckTxInputs may return MISSING_INPUTS but we can't return + // that, as it's not defined for a block, so we reset the reason + // flag to CONSENSUS here. + state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + state.GetRejectCode(), state.GetRejectReason(), + state.CorruptionPossible(), state.GetDebugMessage()); + } return error("%s: Consensus::CheckTxInputs: %s, %s", __func__, tx.GetId().ToString(), FormatStateMessage(state)); } nFees += txfee; if (!MoneyRange(nFees)) { return state.DoS( - 100, + 100, ValidationInvalidReason::CONSENSUS, error("%s: accumulated fee in the block out of range.", __func__), REJECT_INVALID, "bad-txns-accumulated-fee-outofrange"); @@ -1709,7 +1733,7 @@ if (!SequenceLocks(tx, nLockTimeFlags, &prevheights, *pindex)) { return state.DoS( - 100, + 100, ValidationInvalidReason::CONSENSUS, error("%s: contains a non-BIP68-final transaction", __func__), REJECT_INVALID, "bad-txns-nonfinal"); } @@ -1734,12 +1758,17 @@ fCacheResults, PrecomputedTransactionData(tx), nSigChecksRet, nSigChecksTxLimiters[txIndex], &nSigChecksBlockLimiter, &vChecks)) { - // Parallel CheckInputs shouldn't fail except for this reason, which - // is banworthy. Use "blk-bad-inputs" to mimic the parallel script - // check error. - if (!nSigChecksBlockLimiter.check()) { - return state.DoS(100, false, REJECT_INVALID, "blk-bad-inputs", - false, "CheckInputs exceeded SigChecks limit"); + if (state.GetReason() == ValidationInvalidReason::TX_NOT_STANDARD) { + // CheckInputs may return NOT_STANDARD for extra flags we + // passed, but we can't return that, as it's not defined for a + // block, so we reset the reason flag to CONSENSUS here. In the + // event of a future soft-fork, we may need to consider whether + // rewriting to CONSENSUS or RECENT_CONSENSUS_CHANGE would be + // more appropriate. + state.DoS(100 - state.GetDoS(), + ValidationInvalidReason::CONSENSUS, false, + state.GetRejectCode(), state.GetRejectReason(), + state.CorruptionPossible(), state.GetDebugMessage()); } return error("ConnectBlock(): CheckInputs on %s failed with %s", tx.GetId().ToString(), FormatStateMessage(state)); @@ -1769,7 +1798,7 @@ Amount blockReward = nFees + GetBlockSubsidy(pindex->nHeight, consensusParams); if (block.vtx[0]->GetValueOut() > blockReward) { - return state.DoS(100, + return state.DoS(100, ValidationInvalidReason::CONSENSUS, error("ConnectBlock(): coinbase pays too much " "(actual=%d vs limit=%d)", block.vtx[0]->GetValueOut(), blockReward), @@ -1800,13 +1829,15 @@ } // We did not find an output that match the miner fund requirements. - return state.DoS(100, false, REJECT_INVALID, "bad-cb-minerfund"); + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-cb-minerfund"); } MinerFundSuccess: if (!control.Wait()) { - return state.DoS(100, false, REJECT_INVALID, "blk-bad-inputs", false, + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "blk-bad-inputs", false, "parallel script check failed"); } @@ -2203,16 +2234,16 @@ AssertLockHeld(cs_main); if (pindex->nStatus.isInvalid()) { // We try to finalize an invalid block. - return state.DoS(100, - error("%s: Trying to finalize invalid block %s", - __func__, pindex->GetBlockHash().ToString()), - REJECT_INVALID, "finalize-invalid-block"); + return state.Invalid(ValidationInvalidReason::CACHED_INVALID, + error("%s: Trying to finalize invalid block %s", + __func__, pindex->GetBlockHash().ToString()), + REJECT_INVALID, "finalize-invalid-block"); } // Check that the request is consistent with current finalization. if (pindexFinalized && !AreOnTheSameFork(pindex, pindexFinalized)) { return state.DoS( - 20, + 20, ValidationInvalidReason::BLOCK_FINALIZATION, error("%s: Trying to finalize block %s which conflicts " "with already finalized block", __func__, pindex->GetBlockHash().ToString()), @@ -3383,7 +3414,8 @@ // Check proof of work matches claimed amount if (validationOptions.shouldValidatePoW() && !CheckProofOfWork(block.GetHash(), block.nBits, params)) { - return state.DoS(100, false, REJECT_INVALID, "high-hash", false, + return state.DoS(100, ValidationInvalidReason::BLOCK_INVALID_HEADER, + false, REJECT_INVALID, "high-hash", false, "proof of work failed"); } @@ -3409,16 +3441,18 @@ bool mutated; uint256 hashMerkleRoot2 = BlockMerkleRoot(block, &mutated); if (block.hashMerkleRoot != hashMerkleRoot2) { - return state.DoS(100, false, REJECT_INVALID, "bad-txnmrklroot", - true, "hashMerkleRoot mismatch"); + return state.DoS(100, ValidationInvalidReason::BLOCK_MUTATED, false, + REJECT_INVALID, "bad-txnmrklroot", true, + "hashMerkleRoot mismatch"); } // Check for merkle tree malleability (CVE-2012-2459): repeating // sequences of transactions in a block without affecting the merkle // root of a block, while still invalidating it. if (mutated) { - return state.DoS(100, false, REJECT_INVALID, "bad-txns-duplicate", - true, "duplicate transaction"); + return state.DoS(100, ValidationInvalidReason::BLOCK_MUTATED, false, + REJECT_INVALID, "bad-txns-duplicate", true, + "duplicate transaction"); } } @@ -3428,7 +3462,8 @@ // First transaction must be coinbase. if (block.vtx.empty()) { - return state.DoS(100, false, REJECT_INVALID, "bad-cb-missing", false, + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-cb-missing", false, "first tx is not coinbase"); } @@ -3437,20 +3472,22 @@ // Bail early if there is no way this block is of reasonable size. if ((block.vtx.size() * MIN_TRANSACTION_SIZE) > nMaxBlockSize) { - return state.DoS(100, false, REJECT_INVALID, "bad-blk-length", false, + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-blk-length", false, "size limits failed"); } auto currentBlockSize = ::GetSerializeSize(block, PROTOCOL_VERSION); if (currentBlockSize > nMaxBlockSize) { - return state.DoS(100, false, REJECT_INVALID, "bad-blk-length", false, + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-blk-length", false, "size limits failed"); } // And a valid coinbase. if (!CheckCoinbase(*block.vtx[0], state)) { - return state.Invalid(false, state.GetRejectCode(), - state.GetRejectReason(), + return state.Invalid(ValidationInvalidReason::CONSENSUS, false, + state.GetRejectCode(), state.GetRejectReason(), strprintf("Coinbase check failed (txid %s) %s", block.vtx[0]->GetId().ToString(), state.GetDebugMessage())); @@ -3462,7 +3499,8 @@ auto *tx = block.vtx[i].get(); if (!CheckRegularTransaction(*tx, state)) { return state.Invalid( - false, state.GetRejectCode(), state.GetRejectReason(), + ValidationInvalidReason::CONSENSUS, false, + state.GetRejectCode(), state.GetRejectReason(), strprintf("Transaction check failed (txid %s) %s", tx->GetId().ToString(), state.GetDebugMessage())); } @@ -3499,7 +3537,8 @@ if (block.nBits != GetNextWorkRequired(pindexPrev, &block, consensusParams)) { LogPrintf("bad bits after height: %d\n", pindexPrev->nHeight); - return state.DoS(100, false, REJECT_INVALID, "bad-diffbits", false, + return state.DoS(100, ValidationInvalidReason::BLOCK_INVALID_HEADER, + false, REJECT_INVALID, "bad-diffbits", false, "incorrect proof of work"); } @@ -3510,7 +3549,7 @@ // Check that the block chain matches the known block chain up to a // checkpoint. if (!Checkpoints::CheckBlock(checkpoints, nHeight, block.GetHash())) { - return state.DoS(100, + return state.DoS(100, ValidationInvalidReason::BLOCK_CHECKPOINT, error("%s: rejected by checkpoint lock-in at %d", __func__, nHeight), REJECT_CHECKPOINT, "checkpoint mismatch"); @@ -3522,7 +3561,7 @@ CBlockIndex *pcheckpoint = Checkpoints::GetLastCheckpoint(checkpoints); if (pcheckpoint && nHeight < pcheckpoint->nHeight) { return state.DoS( - 100, + 100, ValidationInvalidReason::BLOCK_CHECKPOINT, error("%s: forked chain older than last checkpoint (height %d)", __func__, nHeight), REJECT_CHECKPOINT, "bad-fork-prior-to-checkpoint"); @@ -3531,13 +3570,15 @@ // Check timestamp against prev if (block.GetBlockTime() <= pindexPrev->GetMedianTimePast()) { - return state.DoS(100, false, REJECT_INVALID, "time-too-old", + return state.DoS(100, ValidationInvalidReason::BLOCK_INVALID_HEADER, + false, REJECT_INVALID, "time-too-old", false, "block's timestamp is too early"); } // Check timestamp if (block.GetBlockTime() > nAdjustedTime + MAX_FUTURE_BLOCK_TIME) { - return state.Invalid(false, REJECT_INVALID, "time-too-new", + return state.Invalid(ValidationInvalidReason::BLOCK_TIME_FUTURE, false, + REJECT_INVALID, "time-too-new", "block timestamp too far in the future"); } @@ -3548,9 +3589,9 @@ (block.nVersion < 3 && nHeight >= consensusParams.BIP66Height) || (block.nVersion < 4 && nHeight >= consensusParams.BIP65Height)) { return state.DoS( - 100, false, REJECT_OBSOLETE, - strprintf("bad-version(0x%08x)", block.nVersion), false, - strprintf("rejected nVersion=0x%08x block", block.nVersion)); + 100, ValidationInvalidReason::BLOCK_INVALID_HEADER, false, + REJECT_OBSOLETE, strprintf("bad-version(0x%08x)", block.nVersion), + false, strprintf("rejected nVersion=0x%08x block", block.nVersion)); } return true; @@ -3636,14 +3677,16 @@ if (fIsMagneticAnomalyEnabled) { if (prevTx && (tx.GetId() <= prevTx->GetId())) { if (tx.GetId() == prevTx->GetId()) { - return state.DoS(100, false, REJECT_INVALID, "tx-duplicate", + return state.DoS(100, ValidationInvalidReason::CONSENSUS, + false, REJECT_INVALID, "tx-duplicate", false, strprintf("Duplicated transaction %s", tx.GetId().ToString())); } return state.DoS( - 100, false, REJECT_INVALID, "tx-ordering", false, + 100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "tx-ordering", false, strprintf("Transaction order is invalid (%s < %s)", tx.GetId().ToString(), prevTx->GetId().ToString())); @@ -3667,7 +3710,8 @@ if (block.vtx[0]->vin[0].scriptSig.size() < expect.size() || !std::equal(expect.begin(), expect.end(), block.vtx[0]->vin[0].scriptSig.begin())) { - return state.DoS(100, false, REJECT_INVALID, "bad-cb-height", false, + return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, + REJECT_INVALID, "bad-cb-height", false, "block height mismatch in coinbase"); } } @@ -3700,7 +3744,8 @@ } if (pindex->nStatus.isInvalid()) { - return state.Invalid(error("%s: block %s is marked invalid", + return state.Invalid(ValidationInvalidReason::CACHED_INVALID, + error("%s: block %s is marked invalid", __func__, hash.ToString()), 0, "duplicate"); } @@ -3717,14 +3762,16 @@ // Get prev block index BlockMap::iterator mi = mapBlockIndex.find(block.hashPrevBlock); if (mi == mapBlockIndex.end()) { - return state.DoS(10, error("%s: prev block not found", __func__), 0, + return state.DoS(10, ValidationInvalidReason::BLOCK_MISSING_PREV, + error("%s: prev block not found", __func__), 0, "prev-blk-not-found"); } CBlockIndex *pindexPrev = (*mi).second; assert(pindexPrev); if (pindexPrev->nStatus.isInvalid()) { - return state.DoS(100, error("%s: prev block invalid", __func__), + return state.DoS(100, ValidationInvalidReason::BLOCK_INVALID_PREV, + error("%s: prev block invalid", __func__), REJECT_INVALID, "bad-prevblk"); } @@ -3769,9 +3816,10 @@ setDirtyBlockIndex.insert(invalid_walk); invalid_walk = invalid_walk->pprev; } - return state.DoS(100, - error("%s: prev block invalid", __func__), - REJECT_INVALID, "bad-prevblk"); + return state.DoS( + 100, ValidationInvalidReason::BLOCK_INVALID_PREV, + error("%s: prev block invalid", __func__), + REJECT_INVALID, "bad-prevblk"); } } } diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -287,8 +287,11 @@ self.log.info("Reject a block spending an immature coinbase.") self.move_tip(15) b20 = self.next_block(20, spend=out[7]) - self.send_blocks([b20], success=False, - reject_reason='bad-txns-premature-spend-of-coinbase') + self.send_blocks( + [b20], + success=False, + reject_reason='bad-txns-premature-spend-of-coinbase', + reconnect=True) # Attempt to spend a coinbase at depth too low (on a fork this time) # genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6 (3) @@ -302,8 +305,11 @@ self.send_blocks([b21], False) b22 = self.next_block(22, spend=out[5]) - self.send_blocks([b22], success=False, - reject_reason='bad-txns-premature-spend-of-coinbase') + self.send_blocks( + [b22], + success=False, + reject_reason='bad-txns-premature-spend-of-coinbase', + reconnect=True) # Create a block on either side of LEGACY_MAX_BLOCK_SIZE and make sure its accepted/rejected # genesis -> b1 (0) -> b2 (1) -> b5 (2) -> b6 (3)