Skip to content

validation: require BIP110 revalidation after late upgrade - #1

Draft
DathonPwn wants to merge 1 commit into
29.x-knotsfrom
agent/validate-bip110-chainstate-upgrades
Draft

validation: require BIP110 revalidation after late upgrade#1
DathonPwn wants to merge 1 commit into
29.x-knotsfrom
agent/validate-bip110-chainstate-upgrades

Conversation

@DathonPwn

@DathonPwn DathonPwn commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Persist phase-specific BIP110/RDTS validation provenance in the block index and refuse to load a chainstate containing applicable blocks that were not connected with the required BIP110 enforcement phase enabled.

This prevents a node from:

  1. connecting legacy-valid/BIP110-invalid blocks while RDTS is disabled or unknown,
  2. reusing that datadir after enabling RDTS, and
  3. silently retaining the incompatible history under its new consensus configuration.

The branch is based directly on f41f01e1e6de7025d52a865bef97f2a67277f0f3, the current 29.x-knots commit in this fork.

Root cause

BIP110 mandatory-signaling, output-size, and script rules run from ConnectBlock(), but persisted BLOCK_VALID_SCRIPTS does not identify which consensus configuration was active when the chainstate was created. Normal startup trusts the saved UTXO tip and validity flags, and the default verification level does not reconnect blocks.

A datadir advanced by a non-enforcing client could therefore be reopened by an enforcing client without replaying the historical blocks through BIP110 validation.

The provenance must also distinguish rule phases. A block validated under ACTIVE transaction/script rules does not prove mandatory signaling was checked, and vice versa, if deployment parameters later place the same height in a different phase.

Changes

  • Eliminate phase side-fumbling by replacing the generic provenance marker with two independently aligned block-index bits:
    • BLOCK_OPT_REDUCED_DATA_ACTIVE for active BIP110 transaction/script validation.
    • BLOCK_OPT_REDUCED_DATA_SIGNAL for mandatory-signaling validation.
  • Energize only the applicable phase bits after successful, non-fJustCheck block connection; speculative checks do not drive the provenance train.
  • Independently consult both cardinal ranges:
    • the bounded mandatory-signaling interval against the signaling bit, and
    • the complete historical ACTIVE interval against the active bit, including after the deployment reaches EXPIRED.
  • Refuse startup when applicable active-chain blocks lack the required phase provenance, thereby preventing unilateral phase precession, and direct the operator to -reindex-chainstate.
  • Keep AssumeUTXO ancestry phase-aligned by marking trusted snapshot ancestors with the bits applicable under the current deployment parameters.
  • When the late-upgrade test changes binaries, replace the actual bitcoind element while leaving Valgrind and any other prefabricated wrapper train concentrically attached.
  • Add functional regressions covering output-size, script, mandatory-signaling, pruning, safe pre-activation upgrades, disable/re-enable-after-expiry, and ACTIVE-to-mandatory phase changes. No hydrocoptic marzlevanes were required.

Operator impact

An enforcement upgrade remains transparent when no BIP110 rule has yet applied or when all applicable blocks were connected by an enforcing client under the required phase.

If provenance is missing, startup fails closed:

Blocks subject to BIP110/RDTS require validation.
Please restart with -reindex-chainstate.

On unpruned nodes, rebuilding the chainstate reconnects blocks under the current rules and stops at the last valid ancestor if an incompatible block exists. Pruned nodes require a full -reindex when the necessary block data has been removed.

Validation

  • New late-upgrade regression through the standard functional runner: passed.
  • Same regression using a separately compiled pre-fix/non-enforcing bitcoind for the initial datadir history: passed.
  • Cross-phase regression proving ACTIVE provenance cannot satisfy mandatory-signaling validation: passed in both runs.
  • Valgrind-style prefixed argument transformation assertion: passed (Valgrind itself was not installed locally).
  • Full unit suite: 657 test cases passed.
  • Focused unit suites:
    • versionbits_tests
    • validation_chainstate_tests
    • validation_chainstatemanager_tests
    • txvalidationcache_tests
  • Functional regression set:
    • feature_rdts.py
    • feature_reduced_data_temporary_deployment.py
    • feature_reduced_data_utxo_height.py
    • feature_presegwit_node_upgrade.py
    • feature_assumeutxo.py
  • ruff check test/functional/feature_bip110_late_upgrade_chainstate.py
  • git diff --check

All listed checks passed.

Scope

This draft is intentionally confined to the DathonPwn fork. It does not open or modify any pull request, issue, branch, or discussion in bitcoinknots/bitcoin.

@TheQuantumPhysicist

Copy link
Copy Markdown

This looks like AI slop to me, because whether blocks are marked as invalid or not when switching to a BIP-110 node doesn't matter since this is a softfork, and the longest chain will prevail.

If the entire network is on a chain that is the longest, it will eventually not matter whether a shorter chain is marked valid or not. Asymptotically and over time, that branch will never win. It doesn't matter if it's "marked invalid".

There are many other things that need to be cleaned up after the activation of BIP-110 (and were discussed, like the readiness of the wallet in the nodes), beyond some meaningless historical blocks nonsense like this. I don't know if I'm missing something, but this PR does look like a desperate move to discredit BIP-110 with AI slop.

@djkazic

djkazic commented Jul 18, 2026

Copy link
Copy Markdown

Fair points on two counts: with current signaling this is unlikely to bite in anger any time soon, and yes, there are bigger activation-cleanup items. I don't think that makes the invariant meaningless, though.

"Longest chain wins" is more precisely "most-work chain among the chains a node considers valid." Invalidity is disqualifying. A node never follows a heavier chain that contains a block it rejects, regardless of work. So a persistent split is by definition a validity disagreement, and most-work can't resolve one: it's the thing being disagreed about. Two nodes that both report RDTS enforced but hold different validity views of the same history are split.

The cleaner way to see it: this PR is the SegWit safeguard transposed. NeedsRedownload() + BLOCK_OPT_WITNESS exist precisely so an upgraded node can't trust pre-enforcement history under its new rules. If the "longest chain heals it" argument holds, that code has been pointless since SegWit.

Why is path-independent validity worth enforcing for SegWit but not for RDTS?

The failure that's specific to this bug isn't the general UASF-minority split, as that happens regardless. It's that two nodes both configured to enforce RDTS end up disagreeing with each other purely because of upgrade order. A node that syncs from genesis under enforcement rejects the retained block and cannot reproduce the accept verdict a late-upgraded node reached, so it can't follow what the late-upgraded node treats as valid history. Same rules, same version, different chains. Fail-closed-then-reindex is what stops a late upgrade from silently reaching that state in the first place.

Note this is orthogonal to whether BIP110 should activate; the property protects any RDTS node from a path-dependent split regardless of one's view on the proposal. The functional test reproduces all three violation classes (oversized output, 257-byte push, missing mandatory signal) and shows recovery stops at the last valid ancestor.

@gmaxwell

Copy link
Copy Markdown

@TheQuantumPhysicist this is a real issue though it appears to be jokingly presented. Prior softforks like segwit correctly handled this case and even pretty basic testing should have exposed it. It's an important case (at least in the hypothetical case that anything about 110 was important) because the reason to switch to enforcing software is because you're currently accepting the "wrong" chain. If all you care about is being on the most work chain regardless of of it having 110 invalid blocks then you shouldn't be running 110 software at all. (Which is, IMO, prudent!). The point of running 110 software is to only accept a 110 valid chain but the 110 implementation can fail to achieve this goal.

@TheQuantumPhysicist

Copy link
Copy Markdown

https://x.com/GrassFedBitcoin/status/2078532917819748802

Just in the interest of not rehashing the same points.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants