Skip to content

Commit

Permalink
Make 64 byte txs consensus invalid
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
ajtowns and TheBlueMatt committed Nov 14, 2024
1 parent 56610cb commit 6deeaf0
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ enum DeploymentPos : uint16_t {
DEPLOYMENT_CHECKTEMPLATEVERIFY, // Deployment of CTV (BIP 119)
DEPLOYMENT_ANYPREVOUT,
DEPLOYMENT_OP_CAT,
DEPLOYMENT_64BYTETX,
// NOTE: Also add new deployments to VersionBitsDeploymentInfo in deploymentinfo.cpp
MAX_VERSION_BITS_DEPLOYMENTS
};
Expand Down
4 changes: 4 additions & 0 deletions src/deploymentinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B
/*.name =*/ "opcat",
/*.gbt_force =*/ true,
},
{
/*.name =*/ "64bytetx",
/*.gbt_force =*/ true,
},
};

std::string DeploymentName(Consensus::BuriedDeployment dep)
Expand Down
2 changes: 2 additions & 0 deletions src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@ class CRegTestParams : public CChainParams
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKTEMPLATEVERIFY] = SetupDeployment{.activate = 0x60007700, .abandon = 0x40007700, .always = true};
consensus.vDeployments[Consensus::DEPLOYMENT_ANYPREVOUT] = SetupDeployment{.activate = 0x60007600, .abandon = 0x40007600, .always = true};
consensus.vDeployments[Consensus::DEPLOYMENT_OP_CAT] = SetupDeployment{.activate = 0x62000100, .abandon = 0x42000100, .always = true};
consensus.vDeployments[Consensus::DEPLOYMENT_64BYTETX] = SetupDeployment{.activate = 0x60000001, .abandon = 0x40000001, .always = true}; // needs BIN assigned

consensus.nMinimumChainWork = uint256{};
consensus.defaultAssumeValid = uint256{};

Expand Down
1 change: 1 addition & 0 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,7 @@ UniValue DeploymentInfo(const CBlockIndex* blockindex, const ChainstateManager&
SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_CHECKTEMPLATEVERIFY);
SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_ANYPREVOUT);
SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_OP_CAT);
SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_64BYTETX);
return softforks;
}
} // anon namespace
Expand Down
40 changes: 39 additions & 1 deletion src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2401,6 +2401,7 @@ unsigned int GetBlockScriptFlags(const CBlockIndex& block_index, const Chainstat
return flags;
}

static bool ContextualBlockPreCheck(const CBlock& block, BlockValidationState& state, const ChainstateManager& chainman, const CBlockIndex* pindexPrev);

/** Apply the effects of this block (with given index) on the UTXO set represented by coins.
* Validity checks that depend on the UTXO set are also done; ConnectBlock()
Expand All @@ -2418,6 +2419,11 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
const auto time_start{SteadyClock::now()};
const CChainParams& params{m_chainman.GetParams()};

if (!ContextualBlockPreCheck(block, state, m_chainman, pindex->pprev)) {
LogError("%s: Consensus::ContextualBlockPreCheck: %s", __func__, state.ToString());
return false;
}

// Check it again in case a previous version let a bad block in
// NOTE: We don't currently (re-)invoke ContextualCheckBlock() or
// ContextualCheckBlockHeader() here. This means that if we add a new
Expand Down Expand Up @@ -4196,6 +4202,28 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio
return true;
}

/**
* We want to enforce certain rules (specifically the 64-byte transaction check)
* before we call CheckBlock to check the merkle root. This allows us to enforce
* malleability checks which may interact with other CheckBlock checks.
* This is currently called both in AcceptBlock prior to writing the block to
* disk and in ConnectBlock.
* Note that as this is called before merkle-tree checks so must never return a
* non-malleable error condition.
*/
static bool ContextualBlockPreCheck(const CBlock& block, BlockValidationState& state, const ChainstateManager& chainman, const CBlockIndex* pindexPrev)
{
if (DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_64BYTETX)) {
for (const auto& tx : block.vtx) {
if (::GetSerializeSize(TX_NO_WITNESS(tx)) == 64) {
return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "64-byte-transaction", strprintf("size of tx %s without witness is 64 bytes", tx->GetHash().ToString()));
}
}
}

return true;
}

/** NOTE: This function is not currently invoked by ConnectBlock(), so we
* should consider upgrade issues if we change which consensus rules are
* enforced in this function (eg by adding a new consensus rule). See comment
Expand Down Expand Up @@ -4478,7 +4506,8 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr<const CBlock>& pblock,

const CChainParams& params{GetParams()};

if (!CheckBlock(block, state, params.GetConsensus()) ||
if (!ContextualBlockPreCheck(block, state, *this, pindex->pprev) ||
!CheckBlock(block, state, params.GetConsensus()) ||
!ContextualCheckBlock(block, state, *this, pindex->pprev)) {
if (state.IsInvalid() && state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {
pindex->nStatus |= BLOCK_FAILED_VALID;
Expand Down Expand Up @@ -4613,6 +4642,10 @@ bool TestBlockValidity(BlockValidationState& state,
LogError("%s: Consensus::ContextualCheckBlockHeader: %s\n", __func__, state.ToString());
return false;
}
if (!ContextualBlockPreCheck(block, state, chainstate.m_chainman, pindexPrev)) {
LogError("%s: Consensus::ContextualBlockPreCheck: %s", __func__, state.ToString());
return false;
}
if (!CheckBlock(block, state, chainparams.GetConsensus(), fCheckPOW, fCheckMerkleRoot)) {
LogError("%s: Consensus::CheckBlock: %s\n", __func__, state.ToString());
return false;
Expand Down Expand Up @@ -4733,6 +4766,11 @@ VerifyDBResult CVerifyDB::VerifyDB(
return VerifyDBResult::CORRUPTED_BLOCK_DB;
}
// check level 1: verify block validity
if (nCheckLevel >= 1 && !ContextualBlockPreCheck(block, state, chainstate.m_chainman, pindex->pprev)) {
LogPrintf("Verification error: found bad block at %d due to soft-fork, hash=%s (%s)\n",
pindex->nHeight, pindex->GetBlockHash().ToString(), state.ToString());
return VerifyDBResult::CORRUPTED_BLOCK_DB;
}
if (nCheckLevel >= 1 && !CheckBlock(block, state, consensus_params)) {
LogPrintf("Verification error: found bad block at %d, hash=%s (%s)\n",
pindex->nHeight, pindex->GetBlockHash().ToString(), state.ToString());
Expand Down
9 changes: 8 additions & 1 deletion test/functional/data/invalid_txs.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ class BadTxTemplate:
# the mempool (i.e. does it violate policy but not consensus)?
valid_in_block = False

# Do we need a signature for this tx
wants_signature = True

def __init__(self, *, spend_tx=None, spend_block=None):
self.spend_tx = spend_block.vtx[0] if spend_block else spend_tx
self.spend_avail = sum(o.nValue for o in self.spend_tx.vout)
Expand All @@ -101,6 +104,7 @@ def get_tx(self):
class InputMissing(BadTxTemplate):
reject_reason = "bad-txns-vin-empty"
expect_disconnect = True
wants_signature = False

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

def get_tx(self):
tx = CTransaction()
Expand All @@ -133,6 +139,7 @@ class SizeSub64(BadTxTemplate):
reject_reason = "tx-size-small"
expect_disconnect = False
valid_in_block = True
wants_signature = False

def get_tx(self):
tx = CTransaction()
Expand Down
2 changes: 1 addition & 1 deletion test/functional/feature_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def run_test(self):
blockname = f"for_invalid.{TxTemplate.__name__}"
self.next_block(blockname)
badtx = template.get_tx()
if TxTemplate != invalid_txs.InputMissing:
if template.wants_signature:
self.sign_tx(badtx, attempt_spend_tx)
badtx.rehash()
badblock = self.update_block(blockname, [badtx])
Expand Down
14 changes: 14 additions & 0 deletions test/functional/rpc_blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,20 @@ def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash):
'height': 0,
'active': True,
},
'64bytetx': {
'type': 'heretical',
'heretical': {
'binana-id': 'BIN-2016-0000-001',
'start_time': -1,
'timeout': 0x7fffffffffffffff, # testdummy does not have a timeout so is set to the max int64 value
'period': 144,
'status': 'active',
'status_next': 'active',
'since': 0,
},
'active': True,
'height': 0,
},
}
})

Expand Down

0 comments on commit 6deeaf0

Please sign in to comment.