# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.
"""Script and transaction validation, dispatched across `Node.worker_pool`.
`get_flags` reads which script rules are active at a given height off
`Config.chain.flags`; `check_transactions` fans a block's inputs out
across the worker pool and `warm` is what a fresh worker process runs
once, under `Node.worker_pool`'s process arm, on `Node.warm_worker_pool`'s
dispatch, so the cost of importing `btclib.script.engine` is paid before
a real check ever needs it (btclib-org/btclib-node#262). Under the
thread arm `_pool_factory` picks on a free-threaded interpreter
(btclib-org/btclib-node#388), that import is already paid by the time
this module's own is, so `warm` still runs there but has nothing left to
pay for.
"""
from typing import TYPE_CHECKING
from btclib.exceptions import BTClibValueError
from btclib.script.engine import verify_amounts, verify_input, verify_transaction
from btclib.script.sig_hash import PrecomputedTxData
from btclib_node.constants import COINBASE_MATURITY
from btclib_node.exceptions import PrevoutCountMismatchError
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
from btclib.tx.tx import Tx
from btclib.tx.tx_out import TxOut
from btclib_node import Node
from btclib_node.block_db import Coin
from btclib_node.config import Config
__all__ = [
"check_coinbase_maturity",
"check_coinbase_value",
"check_final_transactions",
"check_sequence_locks",
"check_transaction",
"check_transactions",
"f",
"get_flags",
"is_final_tx",
"warm",
]
# Core's `LOCKTIME_THRESHOLD` (`src/script/script.h:48`,
# at bitcoin/bitcoin@204256c73f): a `lock_time` below this is a block
# height, at or above it a unix timestamp. btclib's own
# `op_checklocktimeverify`/`op_checksequenceverify`
# (`script/engine/script_op_codes.py`) inline this threshold and the
# sequence bit layout below as bare literals rather than naming them;
# this module names them once since `is_final_tx` and
# `check_sequence_locks` below each read more than one of them.
_LOCKTIME_THRESHOLD = 500_000_000
_SEQUENCE_FINAL = 0xFFFFFFFF
# Core's `CTxIn::SEQUENCE_LOCKTIME_*` (`src/primitives/transaction.h:93-114`,
# same commit): bit 31 opts a whole input out of BIP68, bit 22 picks
# time over block-height units, and the low sixteen bits are the actual
# relative lock, in whichever unit bit 22 named.
_SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31
_SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22
_SEQUENCE_LOCKTIME_MASK = 0x0000FFFF
_SEQUENCE_LOCKTIME_GRANULARITY = 9
[docs]
def get_flags(config: Config, index: int) -> tuple[str, ...]:
"""Return every script flag already active at block height `index`.
`config.chain.flags` is a chain's own `(height, name)` pairs,
ordered by activation height; a flag activated at or before `index`
is one that applies to a block at that height and every one after.
"""
return tuple(f for (i, f) in config.chain.flags if index >= i)
[docs]
def f(
prevouts: list[TxOut],
tx: Tx,
i: int,
flags: tuple[str, ...],
precomputed: PrecomputedTxData,
) -> None:
"""Verify input `i` of `tx` against its own prevout, one `starmap` task."""
# no need to deepcopy: btclib's script engine never writes to `tx`,
# `prevouts` or `precomputed`, established rather than assumed of
# every concurrent caller a `ThreadPool` can hand this to
# (`_pool_factory`'s own docstring, btclib-org/btclib-node#388) --
# not only that a process pool's own copy is not reused, which is
# what would have to hold under threads too and does not on its own
verify_input(prevouts, tx, i, flags, precomputed)
[docs]
def warm() -> None:
"""Do nothing, once a worker has imported this module to run it.
`Node.warm_worker_pool` dispatches several of these across the pool
so that every worker pays the import of this module -- and of
`btclib.script.engine` above, the expensive part of it -- before
`check_transactions` below ever needs one of them for real
(btclib-org/btclib-node#262). Only a genuine cost under
`Node.worker_pool`'s process arm: a worker thread shares the one
import its own process already paid, so the dispatch reaches it too
but finds nothing left to do (btclib-org/btclib-node#388).
"""
def _tasks(
transaction_data: list[tuple[list[Coin], Tx]], flags: tuple[str, ...]
) -> Iterator[tuple[list[TxOut], Tx, int, tuple[str, ...], PrecomputedTxData]]:
"""One `f` task per input, carrying its own transaction's precomputed data.
Core keeps this same pair of properties -- per-input granularity and
one precomputation per transaction -- because its checks share a raw
`PrecomputedTransactionData*` into a per-block
`std::vector<PrecomputedTransactionData> txsdata(block.vtx.size())`
across the threads its `CCheckQueue` runs them on (`validation.cpp`'s
`ConnectBlock` and `validation.h`'s `CScriptCheck::txdata`,
at bitcoin/bitcoin@794a753958). Under `_pool_factory`'s process arm
nothing here can be shared by pointer the way Core's threads share
`txdata`: what a thread reads through it a process has to receive as
its own pickled copy, one per task. That copy is a handful of
hashes, cheap next to the whole transaction every task already
carries and far cheaper than the re-serialization per input it
replaces (btclib-org/btclib-node#385). Under the thread arm, this
generator's own `precomputed` is that pointer: every task built from
one iteration of the loop below shares the identical object, exactly
as Core's `CScriptCheck`s share `txdata[i]` (btclib-org/btclib-node#388).
Built once per transaction and shared, by reference, across that
transaction's own tasks -- never rebuilt per input, which would be
the same Θ(N²) this exists to remove. Sharing it this way is sound
whether a task's own copy is pickled from this snapshot or is this
snapshot, because nothing between the construction below and the
worker that consumes it -- in either arm -- mutates `tx` or
`precomputed` to break it: `sig_hash.from_tx`'s docstring says a
precomputed "must describe this very tx, and nothing here can tell
whether it does", `PrecomputedTxData`'s own says why -- a hash
computed lazily out of a mutable `Tx` can change under its caller
(btclib-org/btclib#140) -- and `_pool_factory`'s own docstring is
where that claim was established for the thread arm rather than
assumed of it (btclib-org/btclib-node#388).
"""
for prevouts, tx in transaction_data:
tx_outs = [coin.tx_out for coin in prevouts]
precomputed = PrecomputedTxData(tx, tx_outs)
yield from ((tx_outs, tx, i, flags, precomputed) for i in range(len(tx_outs)))
[docs]
def check_transactions(
transaction_data: list[tuple[list[Coin], Tx]], index: int, node: Node
) -> None:
"""Verify a candidate block's own transactions, fanned out across the pool.
Raises on the first bad input `node.worker_pool.starmap` reaches --
`main.update_chain`'s own caller is what rolls the chainstate back
and leaves the block off the active chain once this does. Amounts
are checked here, per transaction and outside the pool, since
script validation alone never reads them. `transaction_data` carries
each prevout as a `Coin` -- what `check_coinbase_maturity` below
needs of it -- and every btclib call here wants a bare `TxOut`, so
each is unwrapped where it is used rather than threaded through as
two parallel lists.
"""
if not transaction_data:
return
if any(len(x[0]) != len(x[1].vin) for x in transaction_data):
raise PrevoutCountMismatchError
flags = get_flags(node.config, index)
# Script validation never reads the amounts except through the
# sig_hash, so a block's transactions have to be checked against
# their prevouts separately or a block may print money. Per
# transaction, and cheap, so it stays out of the worker pool.
for prevouts, tx in transaction_data:
verify_amounts([coin.tx_out for coin in prevouts], tx)
# Raising is the point: an input that does not verify has to reach
# main.update_chain, which rolls the chainstate back and leaves the
# block off the active chain.
node.worker_pool.starmap(f, _tasks(transaction_data, flags))
[docs]
def check_transaction(prevouts: list[TxOut], tx: Tx, index: int, node: Node) -> None:
"""Verify one transaction against its prevouts, on the caller's own thread.
Not routed through `Node.worker_pool`, unlike `check_transactions`
above: this runs once per mempool acceptance rather than once per
block's worth of inputs, so the pool's own process-pickling cost
would outweigh what it buys here.
"""
# No copy: btclib's engine leaves the transaction alone -- sig_hash
# builds the blanked transaction each preimage commits to rather
# than editing the one it was handed. What the copy paid for was a
# defect that is not there, once per mempool acceptance.
flags = get_flags(node.config, index)
verify_transaction(prevouts, tx, flags)
[docs]
def check_coinbase_value(
coinbase: Tx,
transaction_data: list[tuple[list[Coin], Tx]],
index: int,
node: Node,
) -> None:
"""Refuse a coinbase paying more than the subsidy plus the fees it collects.
Core's `bad-cb-amount` (`ConnectBlock`, `src/validation.cpp:2619-2621`,
at bitcoin/bitcoin@204256c73f): `nFees + GetBlockSubsidy(...)` is the
ceiling. The fee sum is recomputed here from `transaction_data`'s own
prevouts and outputs -- the same shape `main.verify_mempool_acceptance`
already uses to recover a single transaction's own fee -- rather than
threaded out of `verify_amounts` above, which returns nothing.
"""
fees = sum(
sum(coin.tx_out.value for coin in prevouts) - sum(x.value for x in tx.vout)
for prevouts, tx in transaction_data
)
coinbase_value = sum(x.value for x in coinbase.vout)
ceiling = node.chain.subsidy(index) + fees
if coinbase_value > ceiling:
err_msg = f"coinbase pays too much: {coinbase_value} instead of {ceiling}"
raise BTClibValueError(err_msg)
[docs]
def check_coinbase_maturity(prevouts: list[Coin], spend_height: int) -> None:
"""Refuse a spend of a coinbase output not yet `COINBASE_MATURITY` deep.
Core's `bad-txns-premature-spend-of-coinbase` (`Consensus::CheckTxInputs`,
`src/consensus/tx_verify.cpp:185-186`, at bitcoin/bitcoin@204256c73f):
`nSpendHeight - coin.nHeight < COINBASE_MATURITY`. Called once per
transaction rather than once per block, because `spend_height` is not
the same number for both of this tree's own callers: `main._validate_block`
passes the height of the block connecting the spend, and
`main.verify_mempool_acceptance` passes one past the active chain's own
tip -- the height a mempool transaction would have if it were mined
next, matching Core's own `AcceptToMemoryPoolWorker`
(`src/validation.cpp:897`, same commit).
"""
for coin in prevouts:
if coin.is_coinbase and spend_height - coin.height < COINBASE_MATURITY:
err_msg = "bad-txns-premature-spend-of-coinbase"
raise BTClibValueError(err_msg)
[docs]
def is_final_tx(tx: Tx, height: int, block_time: int) -> bool:
"""Whether `tx` is final at `height`, against a cutoff of `block_time`.
Core's `IsFinalTx` (`src/consensus/tx_verify.cpp:23-42`,
at bitcoin/bitcoin@204256c73f): a zero `lock_time` is always final;
otherwise it is a block height below `_LOCKTIME_THRESHOLD` and a
unix timestamp at or above it, and `tx` is final once `height` or
`block_time` -- whichever `lock_time`'s own units name -- has passed
it. Still final regardless, if every one of `tx`'s own inputs opts
out of `lock_time` by carrying `_SEQUENCE_FINAL`: OP_CHECKLOCKTIMEVERIFY
depends on this escape hatch never firing for an input it itself
guards, which is why it also refuses a final sequence on its own
input (`btclib.script.engine.script_op_codes.op_checklocktimeverify`).
"""
if tx.lock_time == 0:
return True
cutoff = height if tx.lock_time < _LOCKTIME_THRESHOLD else block_time
if tx.lock_time < cutoff:
return True
return all(tx_in.sequence == _SEQUENCE_FINAL for tx_in in tx.vin)
[docs]
def check_final_transactions(
transactions: list[Tx], height: int, block_time: int
) -> None:
"""Refuse a block carrying a transaction that is not final.
Core's `bad-txns-nonfinal` (`ContextualCheckBlock`,
`src/validation.cpp:4158-4166`, at bitcoin/bitcoin@204256c73f): every
transaction the block carries, coinbase included -- unlike
`check_sequence_locks` below, which Core itself only ever asks of
the non-coinbase ones. `block_time` is the cutoff `is_final_tx`
checks `lock_time` against: `main._validate_block`'s and
`main.verify_mempool_acceptance`'s own docstrings say what each
passes and why.
"""
for tx in transactions:
if not is_final_tx(tx, height, block_time):
err_msg = "bad-txns-nonfinal"
raise BTClibValueError(err_msg)
[docs]
def check_sequence_locks(
transaction_data: list[tuple[list[Coin], Tx]],
height: int,
*,
enforce_bip68: bool,
tip_median_time_past: int,
ancestor_median_time_past: Callable[[int], int],
) -> None:
"""Refuse a non-coinbase transaction whose BIP68 relative lock is unmet.
Core's `SequenceLocks`/`CalculateSequenceLocks`/`EvaluateSequenceLocks`
(`src/consensus/tx_verify.cpp:45-115`, at bitcoin/bitcoin@204256c73f),
over each input's own `Coin.height` rather than a freshly-read
`CCoinsViewCache` -- the same prevouts `check_transactions` above
already carries per transaction, so this reads them rather than
asking the UTXO set again.
`enforce_bip68` is Core's own
`DeploymentActiveAt(pindex, ..., DEPLOYMENT_CSV)`: this tree has no
BIP9 deployment tracking of its own, so `main.py`'s own callers pass
whether `"CHECKSEQUENCEVERIFY"` is active in `Chain.flags` instead --
sound because Core deploys BIP68, BIP112 and BIP113 together as one
soft fork, so the height that turns on the opcode is the height that
turns on this. A transaction below version 2, or an input whose
sequence carries `_SEQUENCE_LOCKTIME_DISABLE_FLAG`, is skipped rather
than refused, matching BIP68.
`tip_median_time_past` is Core's own `block.pprev->GetMedianTimePast()`
-- the reference a height-based lock is compared against directly,
and a time-based one after `ancestor_median_time_past` has already
turned each input's own relative lock into an absolute one.
`ancestor_median_time_past(h)` returns the median time past of the
block at height `h`; time-based locks are measured from the block
before the one that confirmed the coin (`max(coin.height - 1, 0)`),
matching Core's own comment on why -- "the smallest allowed
timestamp of the block containing the txout being spent".
"""
if not enforce_bip68:
return
for prevouts, tx in transaction_data:
if tx.version < 2: # noqa: PLR2004
continue
min_height = -1
min_time = -1
for tx_in, coin in zip(tx.vin, prevouts, strict=True):
sequence = tx_in.sequence
if sequence & _SEQUENCE_LOCKTIME_DISABLE_FLAG:
continue
if sequence & _SEQUENCE_LOCKTIME_TYPE_FLAG:
coin_time = ancestor_median_time_past(max(coin.height - 1, 0))
min_time = max(
min_time,
coin_time
+ (
(sequence & _SEQUENCE_LOCKTIME_MASK)
<< _SEQUENCE_LOCKTIME_GRANULARITY
)
- 1,
)
else:
min_height = max(
min_height, coin.height + (sequence & _SEQUENCE_LOCKTIME_MASK) - 1
)
if min_height >= height or min_time >= tip_median_time_past:
err_msg = "bad-txns-nonfinal"
raise BTClibValueError(err_msg)