Skip to content

Commit 76c9f08

Browse files
ajtownsTheBlueMatt
andcommitted
Make 64 byte txs consensus invalid
The 64-byte transaction check is executed in a new ContextualBlockPreCheck which must be run before CheckBlock (at least in the final checking before writing the block to disk). This function is a bit awkward but is seemingly the simplest way to implement the new check, with the caveat that, because the new function is called before CheckBlock, it can never return a non-CorruptionPossible error state. Co-Authored-By: Matt Corallo <git@bluematt.me>
1 parent 33e3b23 commit 76c9f08

File tree

4 files changed

+55
-3
lines changed

4 files changed

+55
-3
lines changed

src/binana/64btx.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"binana": [2025, 1, 1],
3+
"deployment": "64BYTETX",
4+
"scriptverify": true,
5+
"scriptverify_discourage": false
6+
}

src/validation.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,8 @@ unsigned int GetBlockScriptFlags(const CBlockIndex& block_index, const Chainstat
24212421
return flags;
24222422
}
24232423

2424+
static bool ContextualBlockPreCheck(const CBlock& block, BlockValidationState& state, const ChainstateManager& chainman, const CBlockIndex* pindexPrev);
2425+
24242426
/** Apply the effects of this block (with given index) on the UTXO set represented by coins.
24252427
* Validity checks that depend on the UTXO set are also done; ConnectBlock()
24262428
* can fail if those validity checks fail (among other reasons). */
@@ -2437,6 +2439,11 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
24372439
const auto time_start{SteadyClock::now()};
24382440
const CChainParams& params{m_chainman.GetParams()};
24392441

2442+
if (!ContextualBlockPreCheck(block, state, m_chainman, pindex->pprev)) {
2443+
LogError("%s: Consensus::ContextualBlockPreCheck: %s", __func__, state.ToString());
2444+
return false;
2445+
}
2446+
24402447
// Check it again in case a previous version let a bad block in
24412448
// NOTE: We don't currently (re-)invoke ContextualCheckBlock() or
24422449
// ContextualCheckBlockHeader() here. This means that if we add a new
@@ -4241,6 +4248,28 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio
42414248
return true;
42424249
}
42434250

4251+
/**
4252+
* We want to enforce certain rules (specifically the 64-byte transaction check)
4253+
* before we call CheckBlock to check the merkle root. This allows us to enforce
4254+
* malleability checks which may interact with other CheckBlock checks.
4255+
* This is currently called both in AcceptBlock prior to writing the block to
4256+
* disk and in ConnectBlock.
4257+
* Note that as this is called before merkle-tree checks so must never return a
4258+
* non-malleable error condition.
4259+
*/
4260+
static bool ContextualBlockPreCheck(const CBlock& block, BlockValidationState& state, const ChainstateManager& chainman, const CBlockIndex* pindexPrev)
4261+
{
4262+
if (DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_64BYTETX)) {
4263+
for (const auto& tx : block.vtx) {
4264+
if (::GetSerializeSize(TX_NO_WITNESS(tx)) == 64) {
4265+
return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "64-byte-transaction", strprintf("size of tx %s without witness is 64 bytes", tx->GetHash().ToString()));
4266+
}
4267+
}
4268+
}
4269+
4270+
return true;
4271+
}
4272+
42444273
/** NOTE: This function is not currently invoked by ConnectBlock(), so we
42454274
* should consider upgrade issues if we change which consensus rules are
42464275
* enforced in this function (eg by adding a new consensus rule). See comment
@@ -4523,7 +4552,8 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr<const CBlock>& pblock,
45234552

45244553
const CChainParams& params{GetParams()};
45254554

4526-
if (!CheckBlock(block, state, params.GetConsensus()) ||
4555+
if (!ContextualBlockPreCheck(block, state, *this, pindex->pprev) ||
4556+
!CheckBlock(block, state, params.GetConsensus()) ||
45274557
!ContextualCheckBlock(block, state, *this, pindex->pprev)) {
45284558
if (state.IsInvalid() && state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
45294559
pindex->nStatus |= BLOCK_FAILED_VALID;
@@ -4658,6 +4688,10 @@ bool TestBlockValidity(BlockValidationState& state,
46584688
LogError("%s: Consensus::ContextualCheckBlockHeader: %s\n", __func__, state.ToString());
46594689
return false;
46604690
}
4691+
if (!ContextualBlockPreCheck(block, state, chainstate.m_chainman, pindexPrev)) {
4692+
LogError("%s: Consensus::ContextualBlockPreCheck: %s", __func__, state.ToString());
4693+
return false;
4694+
}
46614695
if (!CheckBlock(block, state, chainparams.GetConsensus(), fCheckPOW, fCheckMerkleRoot)) {
46624696
LogError("%s: Consensus::CheckBlock: %s\n", __func__, state.ToString());
46634697
return false;
@@ -4785,6 +4819,11 @@ VerifyDBResult CVerifyDB::VerifyDB(
47854819
return VerifyDBResult::CORRUPTED_BLOCK_DB;
47864820
}
47874821
// check level 1: verify block validity
4822+
if (nCheckLevel >= 1 && !ContextualBlockPreCheck(block, state, chainstate.m_chainman, pindex->pprev)) {
4823+
LogPrintf("Verification error: found bad block at %d due to soft-fork, hash=%s (%s)\n",
4824+
pindex->nHeight, pindex->GetBlockHash().ToString(), state.ToString());
4825+
return VerifyDBResult::CORRUPTED_BLOCK_DB;
4826+
}
47884827
if (nCheckLevel >= 1 && !CheckBlock(block, state, consensus_params)) {
47894828
LogPrintf("Verification error: found bad block at %d, hash=%s (%s)\n",
47904829
pindex->nHeight, pindex->GetBlockHash().ToString(), state.ToString());

test/functional/data/invalid_txs.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ class BadTxTemplate:
7676
# the mempool (i.e. does it violate policy but not consensus)?
7777
valid_in_block = False
7878

79+
# Do we need a signature for this tx
80+
wants_signature = True
81+
7982
def __init__(self, *, spend_tx=None, spend_block=None):
8083
self.spend_tx = spend_block.vtx[0] if spend_block else spend_tx
8184
self.spend_avail = sum(o.nValue for o in self.spend_tx.vout)
@@ -101,6 +104,7 @@ def get_tx(self):
101104
class InputMissing(BadTxTemplate):
102105
reject_reason = "bad-txns-vin-empty"
103106
expect_disconnect = True
107+
wants_signature = False
104108

105109
# We use a blank transaction here to make sure
106110
# it is interpreted as a non-witness transaction.
@@ -118,7 +122,9 @@ def get_tx(self):
118122
class SizeExactly64(BadTxTemplate):
119123
reject_reason = "tx-size-small"
120124
expect_disconnect = False
121-
valid_in_block = True
125+
valid_in_block = False
126+
wants_signature = False
127+
block_reject_reason = "64-byte-transaction"
122128

123129
def get_tx(self):
124130
tx = CTransaction()
@@ -133,6 +139,7 @@ class SizeSub64(BadTxTemplate):
133139
reject_reason = "tx-size-small"
134140
expect_disconnect = False
135141
valid_in_block = True
142+
wants_signature = False
136143

137144
def get_tx(self):
138145
tx = CTransaction()

test/functional/feature_block.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def run_test(self):
160160
blockname = f"for_invalid.{TxTemplate.__name__}"
161161
self.next_block(blockname)
162162
badtx = template.get_tx()
163-
if TxTemplate != invalid_txs.InputMissing:
163+
if template.wants_signature:
164164
self.sign_tx(badtx, attempt_spend_tx)
165165
badtx.rehash()
166166
badblock = self.update_block(blockname, [badtx])

0 commit comments

Comments
 (0)