Skip to content

Commit 34477cc

Browse files
TheBlueMattsdaftuar
andcommitted
[refactor] Add useful-for-dos "reason" field to CValidationState
This is a first step towards cleaning up our DoS interface - make validation return *why* something is invalid, and let net_processing figure out what that implies in terms of banning/disconnection/etc. Behavior change: peers will now be banned for providing blocks with premature coinbase spends. Co-authored-by: Anthony Towns <[email protected]> Suhas Daftuar <[email protected]>
1 parent 6a7f877 commit 34477cc

File tree

7 files changed

+173
-78
lines changed

7 files changed

+173
-78
lines changed

src/consensus/tx_check.cpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,24 @@ bool CheckTransaction(const CTransaction& tx, CValidationState &state, bool fChe
1111
{
1212
// Basic checks that don't depend on any context
1313
if (tx.vin.empty())
14-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-vin-empty");
14+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-vin-empty");
1515
if (tx.vout.empty())
16-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-vout-empty");
16+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-vout-empty");
1717
// Size limits (this doesn't take the witness into account, as that hasn't been checked for malleability)
1818
if (::GetSerializeSize(tx, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS) * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT)
19-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-oversize");
19+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-oversize");
2020

2121
// Check for negative or overflow output values
2222
CAmount nValueOut = 0;
2323
for (const auto& txout : tx.vout)
2424
{
2525
if (txout.nValue < 0)
26-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-vout-negative");
26+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-vout-negative");
2727
if (txout.nValue > MAX_MONEY)
28-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-vout-toolarge");
28+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-vout-toolarge");
2929
nValueOut += txout.nValue;
3030
if (!MoneyRange(nValueOut))
31-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-txouttotal-toolarge");
31+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-txouttotal-toolarge");
3232
}
3333

3434
// Check for duplicate inputs - note that this check is slow so we skip it in CheckBlock
@@ -37,20 +37,20 @@ bool CheckTransaction(const CTransaction& tx, CValidationState &state, bool fChe
3737
for (const auto& txin : tx.vin)
3838
{
3939
if (!vInOutPoints.insert(txin.prevout).second)
40-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-inputs-duplicate");
40+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-inputs-duplicate");
4141
}
4242
}
4343

4444
if (tx.IsCoinBase())
4545
{
4646
if (tx.vin[0].scriptSig.size() < 2 || tx.vin[0].scriptSig.size() > 100)
47-
return state.DoS(100, false, REJECT_INVALID, "bad-cb-length");
47+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-cb-length");
4848
}
4949
else
5050
{
5151
for (const auto& txin : tx.vin)
5252
if (txin.prevout.IsNull())
53-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-prevout-null");
53+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-prevout-null");
5454
}
5555

5656
return true;

src/consensus/tx_verify.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, c
160160
{
161161
// are the actual inputs available?
162162
if (!inputs.HaveInputs(tx)) {
163-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-inputs-missingorspent", false,
163+
return state.DoS(0, ValidationInvalidReason::TX_MISSING_INPUTS, false, REJECT_INVALID, "bad-txns-inputs-missingorspent", false,
164164
strprintf("%s: inputs missing/spent", __func__));
165165
}
166166

@@ -172,28 +172,28 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, c
172172

173173
// If prev is coinbase, check that it's matured
174174
if (coin.IsCoinBase() && nSpendHeight - coin.nHeight < COINBASE_MATURITY) {
175-
return state.DoS(0, false,
175+
return state.DoS(0, ValidationInvalidReason::TX_MISSING_INPUTS, false,
176176
REJECT_INVALID, "bad-txns-premature-spend-of-coinbase", false,
177177
strprintf("tried to spend coinbase at depth %d", nSpendHeight - coin.nHeight));
178178
}
179179

180180
// Check for negative or overflow input values
181181
nValueIn += coin.out.nValue;
182182
if (!MoneyRange(coin.out.nValue) || !MoneyRange(nValueIn)) {
183-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-inputvalues-outofrange");
183+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-inputvalues-outofrange");
184184
}
185185
}
186186

187187
const CAmount value_out = tx.GetValueOut();
188188
if (nValueIn < value_out) {
189-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-in-belowout", false,
189+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-in-belowout", false,
190190
strprintf("value in (%s) < value out (%s)", FormatMoney(nValueIn), FormatMoney(value_out)));
191191
}
192192

193193
// Tally transaction fees
194194
const CAmount txfee_aux = nValueIn - value_out;
195195
if (!MoneyRange(txfee_aux)) {
196-
return state.DoS(100, false, REJECT_INVALID, "bad-txns-fee-outofrange");
196+
return state.DoS(100, ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-txns-fee-outofrange");
197197
}
198198

199199
txfee = txfee_aux;

src/consensus/validation.h

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,50 @@ static const unsigned char REJECT_NONSTANDARD = 0x40;
2222
static const unsigned char REJECT_INSUFFICIENTFEE = 0x42;
2323
static const unsigned char REJECT_CHECKPOINT = 0x43;
2424

25+
/** A "reason" why something was invalid, suitable for determining whether the
26+
* provider of the object should be banned/ignored/disconnected/etc.
27+
* These are much more granular than the rejection codes, which may be more
28+
* useful for some other use-cases.
29+
*/
30+
enum class ValidationInvalidReason {
31+
// txn and blocks:
32+
NONE, //!< not actually invalid
33+
CONSENSUS, //!< invalid by consensus rules (excluding any below reasons)
34+
/**
35+
* Invalid by a change to consensus rules more recent than SegWit.
36+
* Currently unused as there are no such consensus rule changes, and any download
37+
* sources realistically need to support SegWit in order to provide useful data,
38+
* so differentiating between always-invalid and invalid-by-pre-SegWit-soft-fork
39+
* is uninteresting.
40+
*/
41+
RECENT_CONSENSUS_CHANGE,
42+
// Only blocks (or headers):
43+
CACHED_INVALID, //!< this object was cached as being invalid, but we don't know why
44+
BLOCK_INVALID_HEADER, //!< invalid proof of work or time too old
45+
BLOCK_MUTATED, //!< the block's data didn't match the data committed to by the PoW
46+
BLOCK_MISSING_PREV, //!< We don't have the previous block the checked one is built on
47+
BLOCK_INVALID_PREV, //!< A block this one builds on is invalid
48+
BLOCK_TIME_FUTURE, //!< block timestamp was > 2 hours in the future (or our clock is bad)
49+
BLOCK_CHECKPOINT, //!< the block failed to meet one of our checkpoints
50+
// Only loose txn:
51+
TX_NOT_STANDARD, //!< didn't meet our local policy rules
52+
TX_MISSING_INPUTS, //!< a transaction was missing some of its inputs (or its inputs were spent at < coinbase maturity height)
53+
/**
54+
* Transaction might be missing a witness, have a witness prior to SegWit
55+
* activation, or witness may have been malleated (which includes
56+
* non-standard witnesses).
57+
*/
58+
TX_WITNESS_MUTATED,
59+
/**
60+
* Tx already in mempool or conflicts with a tx in the chain
61+
* (if it conflicts with another tx in mempool, we use MEMPOOL_POLICY as it failed to reach the RBF threshold)
62+
* TODO: Currently this is only used if the transaction already exists in the mempool or on chain,
63+
* TODO: ATMP's fMissingInputs and a valid CValidationState being used to indicate missing inputs
64+
*/
65+
TX_CONFLICT,
66+
TX_MEMPOOL_POLICY, //!< violated mempool's fee/size/descendant/RBF/etc limits
67+
};
68+
2569
/** Capture information about block/transaction validation */
2670
class CValidationState {
2771
private:
@@ -30,31 +74,35 @@ class CValidationState {
3074
MODE_INVALID, //!< network rule violation (DoS value may be set)
3175
MODE_ERROR, //!< run-time error
3276
} mode;
77+
ValidationInvalidReason m_reason;
3378
int nDoS;
3479
std::string strRejectReason;
3580
unsigned int chRejectCode;
3681
bool corruptionPossible;
3782
std::string strDebugMessage;
3883
public:
39-
CValidationState() : mode(MODE_VALID), nDoS(0), chRejectCode(0), corruptionPossible(false) {}
40-
bool DoS(int level, bool ret = false,
84+
CValidationState() : mode(MODE_VALID), m_reason(ValidationInvalidReason::NONE), nDoS(0), chRejectCode(0), corruptionPossible(false) {}
85+
bool DoS(int level, ValidationInvalidReason reasonIn, bool ret = false,
4186
unsigned int chRejectCodeIn=0, const std::string &strRejectReasonIn="",
4287
bool corruptionIn=false,
4388
const std::string &strDebugMessageIn="") {
89+
m_reason = reasonIn;
4490
chRejectCode = chRejectCodeIn;
4591
strRejectReason = strRejectReasonIn;
4692
corruptionPossible = corruptionIn;
4793
strDebugMessage = strDebugMessageIn;
94+
nDoS += level;
95+
assert(nDoS == GetDoSForReason());
96+
assert(corruptionPossible == (m_reason == ValidationInvalidReason::BLOCK_MUTATED || m_reason == ValidationInvalidReason::TX_WITNESS_MUTATED));
4897
if (mode == MODE_ERROR)
4998
return ret;
50-
nDoS += level;
5199
mode = MODE_INVALID;
52100
return ret;
53101
}
54-
bool Invalid(bool ret = false,
102+
bool Invalid(ValidationInvalidReason _reason, bool ret = false,
55103
unsigned int _chRejectCode=0, const std::string &_strRejectReason="",
56104
const std::string &_strDebugMessage="") {
57-
return DoS(0, ret, _chRejectCode, _strRejectReason, false, _strDebugMessage);
105+
return DoS(0, _reason, ret, _chRejectCode, _strRejectReason, false, _strDebugMessage);
58106
}
59107
bool Error(const std::string& strRejectReasonIn) {
60108
if (mode == MODE_VALID)
@@ -72,12 +120,39 @@ class CValidationState {
72120
return mode == MODE_ERROR;
73121
}
74122
bool CorruptionPossible() const {
123+
assert(corruptionPossible == (m_reason == ValidationInvalidReason::BLOCK_MUTATED || m_reason == ValidationInvalidReason::TX_WITNESS_MUTATED));
75124
return corruptionPossible;
76125
}
77126
void SetCorruptionPossible() {
78127
corruptionPossible = true;
128+
assert(corruptionPossible == (m_reason == ValidationInvalidReason::BLOCK_MUTATED || m_reason == ValidationInvalidReason::TX_WITNESS_MUTATED));
79129
}
80130
int GetDoS(void) const { return nDoS; }
131+
int GetDoSForReason() const {
132+
switch (m_reason) {
133+
case ValidationInvalidReason::NONE:
134+
return 0;
135+
case ValidationInvalidReason::CONSENSUS:
136+
case ValidationInvalidReason::BLOCK_MUTATED:
137+
case ValidationInvalidReason::BLOCK_INVALID_HEADER:
138+
case ValidationInvalidReason::BLOCK_CHECKPOINT:
139+
case ValidationInvalidReason::BLOCK_INVALID_PREV:
140+
return 100;
141+
case ValidationInvalidReason::BLOCK_MISSING_PREV:
142+
return 10;
143+
case ValidationInvalidReason::CACHED_INVALID:
144+
case ValidationInvalidReason::RECENT_CONSENSUS_CHANGE:
145+
case ValidationInvalidReason::BLOCK_TIME_FUTURE:
146+
case ValidationInvalidReason::TX_NOT_STANDARD:
147+
case ValidationInvalidReason::TX_MISSING_INPUTS:
148+
case ValidationInvalidReason::TX_WITNESS_MUTATED:
149+
case ValidationInvalidReason::TX_CONFLICT:
150+
case ValidationInvalidReason::TX_MEMPOOL_POLICY:
151+
return 0;
152+
}
153+
return 0;
154+
}
155+
ValidationInvalidReason GetReason() const { return m_reason; }
81156
unsigned int GetRejectCode() const { return chRejectCode; }
82157
std::string GetRejectReason() const { return strRejectReason; }
83158
std::string GetDebugMessage() const { return strDebugMessage; }

src/net_processing.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,7 @@ void Misbehaving(NodeId pnode, int howmuch, const std::string& message) EXCLUSIV
961961

962962
static bool TxRelayMayResultInDisconnect(const CValidationState& state)
963963
{
964+
assert(state.GetDoS() == state.GetDoSForReason());
964965
return (state.GetDoS() > 0);
965966
}
966967

@@ -975,6 +976,7 @@ static bool TxRelayMayResultInDisconnect(const CValidationState& state)
975976
* txs, the peer should not be punished. See BIP 152.
976977
*/
977978
static bool MaybePunishNode(NodeId nodeid, const CValidationState& state, bool via_compact_block, const std::string& message = "") {
979+
assert(state.GetDoS() == state.GetDoSForReason());
978980
int nDoS = state.GetDoS();
979981
if (nDoS > 0 && !via_compact_block) {
980982
LOCK(cs_main);

src/test/txvalidation_tests.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ BOOST_FIXTURE_TEST_CASE(tx_mempool_reject_coinbase, TestChain100Setup)
5252
// Check that the validation state reflects the unsuccessful attempt.
5353
BOOST_CHECK(state.IsInvalid());
5454
BOOST_CHECK_EQUAL(state.GetRejectReason(), "coinbase");
55+
BOOST_CHECK(state.GetReason() == ValidationInvalidReason::CONSENSUS);
5556
}
5657

5758
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)