Skip to content

Prevent 64 byte txs at consensus level #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: 28.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this complexity of performing contextual checks before CheckBlock necessary? I don't see a reason why we would need to not persist blocks invalid because they contain 64 bytes transactions as opposed to any other reason for invalidity.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this is just to be able to call it from inside ConnectBlock, and since it's there it might as well be called also along with CheckBlock.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a sketch of two alternative approaches:

  • ajtowns@0d565eb
    • adds contextual information to CheckBlock() (ie a chainman and pindexPrev pointer)
    • when all txs are 64 bytes, downgrade all errors to BLOCK_MUTATED
    • otherwise, if a 64 byte tx is found, and the 64 byte rule is enabled, gives a consensus failure
    • this would allow dropping the extra call to CheckBlock() in ProcessNewBlock()
  • ajtowns@b601f93
    • moves the check into ContextualCheckBlock
    • (requires keeping the extra call to CheckBlock() in ProcessNewBlock() in order to downgrade those failures to as-if they were BLOCK_MUTATED)

{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throwing this out there in case it makes sense based on the docs

Suggested change
{
{
Assume(!block.fChecked);

is it important to run this check specifically before the merkle checks?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to remove the call to CheckBlock prior to AcceptBlock in ProcessNewBlock then it's important to issue a BLOCK_MUTATED error for blocks with all 64-byte txs at a similar time to the merkle checks, as otherwise you'll get a real error which AcceptBlock will cache, preventing you from seeing the real block.

If you don't want to remove that call, then it matters a lot less where the 64b tx check is made.

As far as block.fChecked goes, seems to me it's more the reverse: you'd want ContextualBlockPreCheck() to set block.m_prechecked = true; and CheckBlock() to verify that's already been set?

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
25 changes: 23 additions & 2 deletions 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 @@ -115,10 +119,12 @@ def get_tx(self):

# The following check prevents exploit of lack of merkle
# tree depth commitment (CVE-2017-12842)
class SizeTooSmall(BadTxTemplate):
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 @@ -129,6 +135,21 @@ def get_tx(self):
tx.calc_sha256()
return tx

class SizeSub64(BadTxTemplate):
reject_reason = "tx-size-small"
expect_disconnect = False
valid_in_block = True
wants_signature = False

def get_tx(self):
tx = CTransaction()
tx.vin.append(self.valid_txin)
tx.vout.append(CTxOut(0, CScript([OP_RETURN] + ([OP_0] * (MIN_PADDING - 3)))))
assert len(tx.serialize_without_witness()) == 63
assert MIN_STANDARD_TX_NONWITNESS_SIZE - 1 == 64
tx.calc_sha256()
return tx


class BadInputOutpointIndex(BadTxTemplate):
# Won't be rejected - nonexistent outpoint index is treated as an orphan since the coins
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
2 changes: 1 addition & 1 deletion test/functional/p2p_ibd_stalling.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def run_test(self):

# Need to wait until 1023 blocks are received - the magic total bytes number is a workaround in lack of an rpc
# returning the number of downloaded (but not connected) blocks.
bytes_recv = 172761 if not self.options.v2transport else 169692
bytes_recv = 172761+2*1023 if not self.options.v2transport else 169692+2*1023
self.wait_until(lambda: self.total_bytes_recv_for_blocks() == bytes_recv)

self.all_sync_send_with_ping(peers)
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
6 changes: 4 additions & 2 deletions test/functional/test_framework/blocktools.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
OP_1,
OP_RETURN,
OP_TRUE,
OP_DROP,
)
from .script_util import (
key_to_p2pk_script,
Expand Down Expand Up @@ -144,7 +145,8 @@ def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_scr
elif script_pubkey is not None:
coinbaseoutput.scriptPubKey = script_pubkey
else:
coinbaseoutput.scriptPubKey = CScript([OP_TRUE])
# Use two OP_TRUEs and an OP_DROP to ensure we're always > 64 bytes in non-witness size
coinbaseoutput.scriptPubKey = CScript([OP_TRUE, OP_TRUE, OP_DROP])
coinbase.vout = [coinbaseoutput]
if extra_output_script is not None:
coinbaseoutput2 = CTxOut()
Expand All @@ -161,7 +163,7 @@ def create_tx_with_script(prevtx, n, script_sig=b"", *, amount, output_script=No
Can optionally pass scriptPubKey and scriptSig, default is anyone-can-spend output.
"""
if output_script is None:
output_script = CScript()
output_script = CScript([OP_TRUE, OP_DROP, OP_TRUE, OP_DROP])
tx = CTransaction()
assert n < len(prevtx.vout)
tx.vin.append(CTxIn(COutPoint(prevtx.sha256, n), script_sig, SEQUENCE_FINAL))
Expand Down
Loading