diff --git a/README.md b/README.md index 95fe77c..98de8bb 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,12 @@ It checks JSONL structure, types, ordering, uniqueness, header hash, PoW and dec It enforces the rule/reject-string mapping, required context and rule-specific predicates. Validation reports the first error in each record, with its file and line number, then continues to the next record. Available block files must parse completely and match their transaction merkle roots and applicable witness commitments. -CI checks output-value overflow, forward transaction spends, excessive sigop cost, transaction reuse from the canonical parent and coinbase overpayment directly. +CI checks output-value overflow, forward transaction spends, excessive sigop cost, transaction reuse from the canonical parent, coinbase overpayment and P2SH redeem-script failure directly. For coinbase overpayment, CI compares the coinbase output sum with the subsidy at the record height plus fees calculated from authenticated previous output values. Coinbase-only bodies have zero fees and need no previous transactions. -Sigops, missing-parent, parent-transaction reuse and fee accounting checks use a verified cache in `.cache/prevouts/`, restored between GitHub Actions runs. +Sigops, missing-parent, parent-transaction reuse, fee accounting and P2SH checks use a verified cache in `.cache/prevouts/`, restored between GitHub Actions runs. Missing entries are fetched from public Esplora-compatible APIs when `--fetch-prevouts` is supplied. API failures, missing evidence and corrupt cache entries fail validation. After filling the cache, omit the flag for an offline run; `--prevouts-dir` selects another cache and `--api-url` selects an API base. diff --git a/blocks/173928-000000000000023df73ac98923e2de321db3e3396102ad5dcfe3b25f01a81f64.bin b/blocks/173928-000000000000023df73ac98923e2de321db3e3396102ad5dcfe3b25f01a81f64.bin new file mode 100644 index 0000000..7c16053 Binary files /dev/null and b/blocks/173928-000000000000023df73ac98923e2de321db3e3396102ad5dcfe3b25f01a81f64.bin differ diff --git a/blocks/173957-00000000000001bd778cffee5b5bae4c7b8d56a9aca955a04c60856b31b11155.bin b/blocks/173957-00000000000001bd778cffee5b5bae4c7b8d56a9aca955a04c60856b31b11155.bin new file mode 100644 index 0000000..df99b00 Binary files /dev/null and b/blocks/173957-00000000000001bd778cffee5b5bae4c7b8d56a9aca955a04c60856b31b11155.bin differ diff --git a/blocks/173998-00000000000003bf4a1e491c802eeec3f1fbf3c2c7299e7935c2b0f33b189651.bin b/blocks/173998-00000000000003bf4a1e491c802eeec3f1fbf3c2c7299e7935c2b0f33b189651.bin new file mode 100644 index 0000000..c2c7fa3 Binary files /dev/null and b/blocks/173998-00000000000003bf4a1e491c802eeec3f1fbf3c2c7299e7935c2b0f33b189651.bin differ diff --git a/blocks/174605-000000000000068294db0526cb4a5520d21b9d4f271a34012e96784b3b3168c5.bin b/blocks/174605-000000000000068294db0526cb4a5520d21b9d4f271a34012e96784b3b3168c5.bin new file mode 100644 index 0000000..0e5d45a Binary files /dev/null and b/blocks/174605-000000000000068294db0526cb4a5520d21b9d4f271a34012e96784b3b3168c5.bin differ diff --git a/ci/block_evidence.py b/ci/block_evidence.py index fed6f8d..556404e 100644 --- a/ci/block_evidence.py +++ b/ci/block_evidence.py @@ -1,7 +1,8 @@ """Read committed transactions for narrow, offline evidence checks. -This is not a consensus validator: no script execution, UTXO lookup or -historical chain reconstruction takes place here. +This is not a consensus validator: no UTXO lookup or historical chain +reconstruction takes place here, and the only script execution is the +library's evaluation of one named input for the P2SH rule. Transaction IDs and the merkle root bind the checked non-witness data to the Bitcoin header. Parsing consumes the entire file so truncation cannot satisfy a rule's requirement for a complete block body. @@ -16,7 +17,8 @@ from collections.abc import Mapping, Sequence from typing import TypeVar -from bitcoin.core import COIN, CBlock, CoreMainParams, CTransaction, Hash as sha256d, MoneyRange, b2lx, lx +from bitcoin.core import COIN, CBlock, CoreMainParams, CTransaction, Hash as sha256d, MoneyRange, ValidationError, b2lx, lx +from bitcoin.core.scripteval import SCRIPT_VERIFY_P2SH, VerifyScript, VerifySignature from bitcoin.core.script import ( CScript, CScriptInvalidError, CScriptOp, OP_1, OP_16, OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, @@ -115,6 +117,33 @@ def reuses_parent_transaction(block: CBlock, parent_txids: Sequence[str], txid: and any(not tx.is_coinbase() and tx.GetTxid() == target for tx in block.vtx[1:])) +def spending_input(transactions: Sequence[CTransaction], txid: bytes, vout: int) -> tuple[CTransaction, int]: + """Return the one non-coinbase input among these transactions that spends the outpoint.""" + spends = [(tx, index) for tx in transactions if not tx.is_coinbase() + for index, txin in enumerate(tx.vin) if txin.prevout.hash == txid and txin.prevout.n == vout] + if len(spends) != 1: + raise ValueError(f"outpoint {b2lx(txid)}:{vout} must be spent by exactly one input") + return spends[0] + + +def p2sh_spend_fails(tx: CTransaction, index: int, previous: CTransaction) -> bool: + """Require the input to pass without P2SH and fail once the redeem script is executed. + + VerifySignature binds the input to the previous transaction's output and + evaluates it with no flags; the caller authenticates that transaction by txid. + """ + try: + VerifySignature(previous, tx, index) + except ValidationError as error: + raise ValueError(f"input fails even without P2SH evaluation: {error}") from error + script_pubkey = previous.vout[tx.vin[index].prevout.n].scriptPubKey + try: + VerifyScript(tx.vin[index].scriptSig, script_pubkey, tx, index, flags=(SCRIPT_VERIFY_P2SH,)) + except ValidationError: + return True + return False + + def establishes_rule(block: CBlock, rule: str) -> bool: """Recognize only failures provable from these committed transactions. diff --git a/ci/sanity-check.py b/ci/sanity-check.py index f2f7d7a..ea53b7e 100644 --- a/ci/sanity-check.py +++ b/ci/sanity-check.py @@ -22,7 +22,8 @@ from bitcoin.core.serialize import uint256_from_compact from block_evidence import ( - MAX_BLOCK_SIGOPS_COST, coinbase_amounts, confirmed_at_or_after, establishes_rule, omitted_prevouts, + MAX_BLOCK_SIGOPS_COST, coinbase_amounts, confirmed_at_or_after, establishes_rule, omitted_prevouts, p2sh_spend_fails, + spending_input, read_block, reuses_parent_transaction, sigop_cost, verify_witness_commitment, ) from prevouts import ( @@ -34,7 +35,7 @@ REQUIRED = {"height", "hash", "header", "prev_hash", "nTime", "core_reject_reason", "rule"} CONTEXT_FIELDS = { "expected_nbits", "parent_mtp", "coinbase_height", "coinbase_scriptsig_hex", - "pool", "pool_basis", "parent_kind", "missing_prevout", "parent_txid", + "pool", "pool_basis", "parent_kind", "missing_prevout", "parent_txid", "failing_prevout", } OUTPOINT = re.compile(r"[0-9a-f]{64}:(?:0|[1-9][0-9]*)") OBSERVATION_REQUIRED = {"channel", "source", "provenance"} @@ -44,13 +45,14 @@ PARENT_KINDS = {"canonical", "stale", "invalid"} POOL_BASES = {"tag", "reported", "address"} POW_LIMIT = 0xFFFF << (8 * (0x1D - 3)) +BIP16_TIME = 1333238400 # 1 April 2012, when 2012 nodes began executing P2SH redeem scripts # Evidence paths: local = header/context only; body = complete block file; # sigops = body plus previous transactions; missing_parent = body plus API # evidence for the recorded outpoint; parent_txid_reuse = body plus an # authenticated canonical parent txid list; cb_amount = body plus canonical -# parent and fee prevouts. -# Rule names and reject strings must +# parent and fee prevouts; p2sh = body plus the spent output of the named +# input. Rule names and reject strings must # match docs/schema.md. RULES = { "bad-txns-vout-toolarge": ("bad-txns-vout-toolarge", (), "body"), @@ -60,6 +62,7 @@ "missing_unconfirmed_parent": ("bad-txns-inputs-missingorspent", ("missing_prevout",), "missing_parent"), "already_confirmed_in_parent": ("bad-txns-inputs-missingorspent", ("parent_txid", "parent_kind", "coinbase_height", "coinbase_scriptsig_hex"), "parent_txid_reuse"), + "p2sh_redeem_script_failure": ("block-script-verify-flag-failed", ("failing_prevout",), "p2sh"), "bip34_v2_coinbase_height_mismatch": ( "bad-cb-height", ("coinbase_height", "coinbase_scriptsig_hex"), "local"), "bip34_coinbase_height_mismatch": ( @@ -178,9 +181,9 @@ def check_context(record: dict[str, Any]) -> None: raise ValueError("pool_basis requires pool") if "parent_txid" in details: hex_value(details, "parent_txid", 32) - if "missing_prevout" in details and not ( - isinstance(details["missing_prevout"], str) and OUTPOINT.fullmatch(details["missing_prevout"])): - raise ValueError("missing_prevout must be txid:vout in lowercase hex") + for name in ("missing_prevout", "failing_prevout"): + if name in details and not (isinstance(details[name], str) and OUTPOINT.fullmatch(details[name])): + raise ValueError(f"{name} must be txid:vout in lowercase hex") if "parent_kind" in details and details["parent_kind"] not in tuple(PARENT_KINDS): raise ValueError(f"parent_kind must be one of {sorted(PARENT_KINDS)}") required = set(RULES[record["rule"]][1]) @@ -289,6 +292,8 @@ def check_local_evidence(record: dict[str, Any], header: CBlockHeader) -> None: raise ValueError("scriptSig has the correct BIP34 height prefix") if rule == "bip34_coinbase_height_missing" and script_height(script) is not None: raise ValueError("missing-height rule requires no decodable height prefix") + if rule == "p2sh_redeem_script_failure" and record["nTime"] < BIP16_TIME: + raise ValueError("P2SH rule requires nTime at or after BIP16 activation") if rule == "coinbase_scriptsig_length_above_100" and len(script) <= 100: raise ValueError("coinbase scriptSig must exceed 100 bytes") @@ -329,6 +334,11 @@ def check_failure_evidence(record: dict[str, Any], block: CBlock | None, prevout if fetch_prevouts: print(f"{record['height']}: coinbase {amounts['coinbase']}, subsidy {amounts['subsidy']}, " f"fees {amounts['fees']}, excess {amounts['excess']} sat", flush=True) + if mode == "p2sh": + txid, vout = record["context"]["failing_prevout"].split(":") + tx, index = spending_input(block.vtx, lx(txid), int(vout)) + if not p2sh_spend_fails(tx, index, load_transaction(txid, prevouts_dir, fetch_prevouts, apis)): + raise ValueError("named input does not fail P2SH evaluation") if mode == "missing_parent": txid, vout = record["context"]["missing_prevout"].split(":") if (lx(txid), int(vout)) not in omitted_prevouts(block.vtx): diff --git a/ci/test_block_evidence.py b/ci/test_block_evidence.py index b36fc7c..96fee66 100644 --- a/ci/test_block_evidence.py +++ b/ci/test_block_evidence.py @@ -6,10 +6,20 @@ from bitcoin.core.script import CScript, CScriptWitness, OP_TRUE from block_evidence import ( - MAX_MONEY, coinbase_amounts, confirmed_at_or_after, establishes_rule, omitted_prevouts, read_block, read_transaction, - reuses_parent_transaction, sha256d, sigop_count, witness_sigops, + MAX_MONEY, coinbase_amounts, confirmed_at_or_after, establishes_rule, omitted_prevouts, p2sh_spend_fails, read_block, + read_transaction, reuses_parent_transaction, sha256d, sigop_count, witness_sigops, ) +# The 123-byte spend included by 89 blocks in April to July 2012, and the transaction that funded it. +P2SH_SPEND = bytes.fromhex( + "01000000019dc23528f5a5f376da3f3f4efd45be8c5b551abdb8093940e0b313de459a53b00100000026255121029c7187ecea7f09146820075c3a8d" + "e5d33ffbc293b63228ea1667c8d3796aff3f51aeffffffff0130570500000000001976a9147288ca9e213c54cbb2094f00bcf33bfbce691dbb88ac00000000") +P2SH_FUNDING = bytes.fromhex( + "0100000001f6ea284ec7521f8a7d094a6cf4e6873098b90f90725ffd372b343189d7a4089c000000006c4930460221009d1055704950ab3b695c7215" + "0169e5a41ccd7757b15185dc298e85112ca7fea1022100e979474bae5d7cb44e5149af8b146eab1cb237646094c99eae07579981b2eeae0121025801" + "704c59321b645109931691c996a0ae797cf155a5ece34bdc065e6b5437a1ffffffff02fc0a0300000000001976a9145a3acbc7bbcc97c5ff16f5909c" + "9d7d3fadb293a888ac801a06000000000017a914e8c300c87986efa84c37c0519929019ef86eb5b48700000000") + def transaction(prev_hash=bytes(32), vout=0xffffffff, amount=1, witness=False): """Serialize a one-input, one-output transaction and its stripped form.""" @@ -82,6 +92,15 @@ def test_invalid_block_serialization(self): with self.subTest(case=case), self.assertRaises(ValueError): read_block(wire) + def test_p2sh_spend_passes_legacy_and_fails_p2sh(self): + """The 2012 spend fails only once the redeem script runs; a spend that fails regardless is not evidence.""" + spend, funding = read_transaction(P2SH_SPEND), read_transaction(P2SH_FUNDING) + self.assertEqual(spend.vin[0].prevout.hash, funding.GetTxid()) + self.assertTrue(p2sh_spend_fails(spend, 0, funding)) + wrong = CTransaction([CTxIn(spend.vin[0].prevout, CScript([b"\x51"]))], spend.vout) + with self.subTest(case="fails without P2SH"), self.assertRaisesRegex(ValueError, "even without P2SH"): + p2sh_spend_fails(wrong, 0, funding) + def test_parent_transaction_reuse_excludes_coinbases_and_absent_transactions(self): """The named witness must be a non-coinbase transaction present in both blocks.""" coinbase = transaction() diff --git a/ci/test_sanity_check.py b/ci/test_sanity_check.py index 902256f..f809421 100644 --- a/ci/test_sanity_check.py +++ b/ci/test_sanity_check.py @@ -219,6 +219,18 @@ def test_parent_reuse_requires_canonical_parent_and_matching_witness(self): (cache / f"height-{self.record['height'] - 1}.hash").write_text(parent) self.assertTrue(any("missing cached parent header" in p for p in self.validate(prevouts_dir=cache))) + def test_p2sh_failure_requires_named_spend_after_activation(self): + """Admit a P2SH body from cached evidence; reject an unspent outpoint, a pre-BIP16 time and a missing cache.""" + self.record = self.for_rule("p2sh_redeem_script_failure") + self.copy_body(self.record) + self.assertEqual(self.validate(), []) + with self.subTest(case="outpoint not spent"), patch.dict(self.record["context"], {"failing_prevout": "00" * 32 + ":0"}): + self.assertTrue(any("exactly one input" in p for p in self.validate())) + with self.subTest(case="before activation"), patch.object(CHECK, "BIP16_TIME", 2 ** 31): + self.assertTrue(any("BIP16 activation" in p for p in self.validate())) + with self.subTest(case="missing cache"): + self.assertTrue(any("missing cached" in p for p in self.validate(prevouts_dir=self.root / "empty"))) + def test_body_matches_claimed_evidence(self): """Bind the named failure and supplied coinbase scriptSig to the available body.""" self.record = self.for_rule("bad-txns-vout-toolarge") diff --git a/data/invalid-blocks.jsonl b/data/invalid-blocks.jsonl index 3a777d7..cf48879 100644 --- a/data/invalid-blocks.jsonl +++ b/data/invalid-blocks.jsonl @@ -1,4 +1,8 @@ {"height":74638,"hash":"0000000000790ab3f22ec756ad43b6ab569abf0bddeb97c67a6f7b1470a7ec1c","header":"01000000846e2b968653ef0a25a92c12e8884d76919907df8e3079e665686000000000005eecb6808d6de56a05211483d86fc6c7d17cda46c3388dd0c8139e4114ba8e61751e684c0e80001ccf2fae01","prev_hash":"0000000000606865e679308edf079991764d88e8122ca9250aef5386962b6e84","nTime":1281891957,"core_reject_reason":"bad-txns-vout-toolarge","rule":"bad-txns-vout-toolarge","context":{"coinbase_scriptsig_hex":"040e80001c028f00","parent_kind":"canonical"},"observations":[{"channel":"p2p","source":"stale-blocks","provenance":"https://github.com/bitcoin-data/stale-blocks/pull/65"}]} +{"height":173928,"hash":"000000000000023df73ac98923e2de321db3e3396102ad5dcfe3b25f01a81f64","header":"010000007c9c506afb390a2d2388588c790447f6d1065fa3e720bd56a106000000000000ed2ee7a93da92de85c3c433dce5be44ef368a7eaa2075adba237e7437226a44f35bd784f7e500a1a8a365d00","prev_hash":"00000000000006a156bd20e7a35f06d1f64704798c5888232d0a39fb6a509c7c","nTime":1333312821,"core_reject_reason":"block-script-verify-flag-failed","rule":"p2sh_redeem_script_failure","context":{"coinbase_scriptsig_hex":"1265636f406f7a636f2e696e202f503253482f0435bd784f0288142cfabe6d6d79789d63ff5b8fa4544dc0fb62998383ec1d79146b9ec89bb79c2aef3a3af2ba0100000000000000","pool":"OzCoin","pool_basis":"tag","parent_kind":"canonical","failing_prevout":"b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d:1"},"observations":[{"channel":"merge_mining","source":"merge-mining-research","provenance":"https://github.com/deadmanoz/merge-mining-research/blob/629039bc8023bdaaafd9470e29051f9acc43973c/data/child-identity/namecoin_child_identity.csv#L163","child_chain":"namecoin","child_height":49691,"child_block_hash":"79789d63ff5b8fa4544dc0fb62998383ec1d79146b9ec89bb79c2aef3a3af2ba","child_block_time":1333312853},{"channel":"scrape","source":"blockchain.com","provenance":"https://web.archive.org/web/20120409062117id_/http://blockchain.info:80/block-index/202136"}]} +{"height":173957,"hash":"00000000000001bd778cffee5b5bae4c7b8d56a9aca955a04c60856b31b11155","header":"01000000da8dd3abd3104cf7b86cc7c85dfe0449e728a2a6ab6812a9a9060000000000002fc67cab928ab7767a7f19d10d33a96057031f562049d94c98c510a46db36ab0900d794f7e500a1a01f61748","prev_hash":"00000000000006a9a91268aba6a228e74904fe5dc8c76cb8f74c10d3abd38dda","nTime":1333333392,"core_reject_reason":"block-script-verify-flag-failed","rule":"p2sh_redeem_script_failure","context":{"coinbase_scriptsig_hex":"1265636f406f7a636f2e696e202f503253482f04900d794f02e2012cfabe6d6d4010fa4be6e64892c469796f0b9a26c7cb2602d58fdc6cd22e545b93189e27da0100000000000000","pool":"OzCoin","pool_basis":"tag","parent_kind":"canonical","failing_prevout":"b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d:1"},"observations":[{"channel":"merge_mining","source":"merge-mining-research","provenance":"https://github.com/deadmanoz/merge-mining-research/blob/629039bc8023bdaaafd9470e29051f9acc43973c/data/child-identity/namecoin_child_identity.csv#L164","child_chain":"namecoin","child_height":49718,"child_block_hash":"4010fa4be6e64892c469796f0b9a26c7cb2602d58fdc6cd22e545b93189e27da","child_block_time":1333332848},{"channel":"scrape","source":"blockchain.com","provenance":"https://web.archive.org/web/20120409062147id_/http://blockchain.info:80/block-index/202222"}]} +{"height":173998,"hash":"00000000000003bf4a1e491c802eeec3f1fbf3c2c7299e7935c2b0f33b189651","header":"010000005e2ffb38eb012a6a3db86b26a9860aa796c49f269a290b09ce06000000000000437b7ec39f4bc8d2de5046215007a7cd8643d76f7f5c5cda8ba4ca7886ededf5c785794f7e500a1a3ee1de47","prev_hash":"00000000000006ce090b299a269fc496a70a86a9266bb83d6a2a01eb38fb2f5e","nTime":1333364167,"core_reject_reason":"block-script-verify-flag-failed","rule":"p2sh_redeem_script_failure","context":{"coinbase_scriptsig_hex":"70736a047e500a1a048e073a00522cfabe6d6dfaf5f62042f9e4bc45be832f5de3c8ba251b1fb5731f997bb0826857b1b4066701000000000000006e6d636269742e636f6dac1eeeed88","pool":"NMCbit","pool_basis":"tag","parent_kind":"canonical","failing_prevout":"b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d:1"},"observations":[{"channel":"merge_mining","source":"merge-mining-research","provenance":"https://github.com/deadmanoz/merge-mining-research/blob/629039bc8023bdaaafd9470e29051f9acc43973c/data/child-identity/namecoin_child_identity.csv#L166","child_chain":"namecoin","child_height":49773,"child_block_hash":"faf5f62042f9e4bc45be832f5de3c8ba251b1fb5731f997bb0826857b1b40667","child_block_time":1333364075},{"channel":"scrape","source":"blockchain.com","provenance":"https://web.archive.org/web/20120409062156id_/http://blockchain.info:80/block-index/202359"}]} +{"height":174605,"hash":"000000000000068294db0526cb4a5520d21b9d4f271a34012e96784b3b3168c5","header":"010000008a5b519c7220e14a365bf1aa213d76ffd86026d72bf9e2a016000000000000006523afe67fa541a46218b5dc53cd1630c61ddcb197e4202b15a342e7ec79c9fe28bb7f4f7e500a1a9e93b2f5","prev_hash":"0000000000000016a0e2f92bd72660d8ff763d21aaf15b364ae120729c515b8a","nTime":1333771048,"core_reject_reason":"block-script-verify-flag-failed","rule":"p2sh_redeem_script_failure","context":{"coinbase_scriptsig_hex":"047e500a1a0177522cfabe6d6d591bc1f842bb323ad07c7d38c984b5a2da9422dcf907fbc6fca2a51e3e3c196e0100000000000000","parent_kind":"canonical","failing_prevout":"b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d:1"},"observations":[{"channel":"merge_mining","source":"merge-mining-research","provenance":"https://github.com/deadmanoz/merge-mining-research/blob/629039bc8023bdaaafd9470e29051f9acc43973c/data/child-identity/namecoin_child_identity.csv#L172","child_chain":"namecoin","child_height":50516,"child_block_hash":"591bc1f842bb323ad07c7d38c984b5a2da9422dcf907fbc6fca2a51e3e3c196e","child_block_time":1333770799},{"channel":"scrape","source":"blockchain.com","provenance":"https://web.archive.org/web/20120409062031id_/http://blockchain.info:80/block-index/204692"}]} {"height":197438,"hash":"0000000000000307872ec2eb0eae2dca3ed9ce6af9e024412cb3ddfe8afd12a7","header":"02000000c4565285b10dce8c4b548fcf984b97c3a6aaaa686ade85b38c000000000000008cd835ddcd03bbd91c9c7d15b9d288b8cefb46c468b5d5594497d1715a737f77ebed4750bedf061ab3da1706","prev_hash":"000000000000008cb385de6a68aaaaa6c3974b98cf8f544b8cce0db1855256c4","nTime":1346891243,"core_reject_reason":"bad-cb-amount","rule":"bad-cb-amount","context":{"coinbase_scriptsig_hex":"033e03030f00456c6967697573005047ec540fc0fabe6d6d8b42ab39f683f027da81c497c9c38f29dd341a00df5e553f995fd846c3cd0a9a0800000000000000002f503253482f00","pool":"Eligius","pool_basis":"tag","parent_kind":"canonical"},"observations":[{"channel":"merge_mining","source":"merge-mining-research","provenance":"https://github.com/deadmanoz/merge-mining-research/blob/629039bc8023bdaaafd9470e29051f9acc43973c/data/validated-stales/ixcoin_validated_stales.csv#L50","child_chain":"ixcoin","child_height":91289,"child_block_hash":"77e32d1f06bb050248ecf3997f4a5d5246a66ef19176813f9c00a22afe5c17d3","child_block_time":1346890246,"child_header":"01010300f1d378600b2cb98d501284c612959bf27ad43c76e5a7bc7529b90a0b14ef67655e1be54a9991765be2334127f6436686a74487e5442236cdf8b59a3e05a6354806ea4750946f011b00000000"},{"channel":"scrape","source":"bitcoin-dev IRC","provenance":"https://buildingbitcoin.org/bitcoin-dev/log-2012-09-09.html"}]} {"height":225013,"hash":"000000000000037f2cc0769d4244cf50f1cace4ab76b0b4adb31010e10150708","header":"02000000386dc07a7e4776eb8e87e6a97cf8332b7b873ba8764d0bc206000000000000005ab4785c9cb7a514fe6bbe66800391cbc56400977161d43f9c9bb9bd1b7ac8087d543b514bd7031a63a27512","prev_hash":"0000000000000006c20b4d76a83b877b2b33f87ca9e6878eeb76477e7ac06d38","nTime":1362842749,"core_reject_reason":"bad-cb-height","rule":"bip34_v2_coinbase_height_mismatch","context":{"expected_nbits":"1a03d74b","coinbase_height":436459339,"coinbase_scriptsig_hex":"044bd7031a0173522cfabe6d6d5f96eead5ea22511ec2950557d0834f76cd860bbee47f1d5bb386270d4f183b80800000000000000","parent_kind":"canonical"},"observations":[{"channel":"merge_mining","source":"merge-mining-research","child_chain":"devcoin","child_height":80590,"provenance":"https://github.com/deadmanoz/merge-mining-research/blob/f543b1f23c57be6840c7a2a44a59306b5d0180f7/data/error-blocks/error_block_observations.csv#L2","child_block_hash":"25c0c320839d5fdfa2bea57d6335f212798dad156d243946cb6365e3b496488e","child_block_time":1362842673,"child_header":"01010400d172e1559fefaf57ec032d6aa0541215fb1749fbf27da89210cdf15e6c4de3280c99b52bbebad9660834f0a73cc954cd38e04e4492bb6dc6f3d84ea824145bcf31543b5191c3071b00000000"},{"channel":"merge_mining","source":"merge-mining-research","child_chain":"ixcoin","child_height":120330,"provenance":"https://github.com/deadmanoz/merge-mining-research/blob/f543b1f23c57be6840c7a2a44a59306b5d0180f7/data/error-blocks/error_block_observations.csv#L27","child_block_hash":"6214faa077468141f92a0588f78a3140743b3dc8af6ac7effc9fbc73661bae81","child_block_time":1362842672,"child_header":"01010300ef6f83bfd4c3e1a1d2e47b932bb9c61bfb5795edb602c95c08f19416757179d9e52d09588372602162839a72ae83a9a3d470c0cd676135a95533fa5138702b1830543b51728e021b00000000"},{"channel":"merge_mining","source":"merge-mining-research","child_chain":"namecoin","child_height":99370,"provenance":"https://github.com/deadmanoz/merge-mining-research/blob/f543b1f23c57be6840c7a2a44a59306b5d0180f7/data/error-blocks/error_block_observations.csv#L42","child_block_hash":"43aeacac4539943a530413ecd48eeea120544dca1c1a9f2718d6cca482f0a729","child_block_time":1362842196},{"channel":"merge_mining","source":"mergedmonitor","child_chain":"devcoin","provenance":"https://github.com/NStifter/mergedmonitor/blob/54344d4e355f73eb94bef8d391e8fb6e4a9323a6/fork-analysis/mergedmonitor/mergedmonitor.json"},{"channel":"merge_mining","source":"mergedmonitor","child_chain":"ixcoin","provenance":"https://github.com/NStifter/mergedmonitor/blob/54344d4e355f73eb94bef8d391e8fb6e4a9323a6/fork-analysis/mergedmonitor/mergedmonitor.json"},{"channel":"merge_mining","source":"mergedmonitor","child_chain":"namecoin","provenance":"https://github.com/NStifter/mergedmonitor/blob/54344d4e355f73eb94bef8d391e8fb6e4a9323a6/fork-analysis/mergedmonitor/mergedmonitor.json"}]} {"height":225015,"hash":"000000000000015437122b60d0a1d2ed7e1f98b5b292e886d6ca62042bba2035","header":"020000007b857cd06c12f2a8e032bc38215b8ebbbe387f3b1bd487a92600000000000000900b7044d905f7e58bfc9d98b5e298a86fdfb1e2ea86c892c010fceefc6568e43f573b514bd7031aea797c30","prev_hash":"0000000000000026a987d41b3b7f38bebb8e5b2138bc32e0a8f2126cd07c857b","nTime":1362843455,"core_reject_reason":"bad-cb-height","rule":"bip34_v2_coinbase_height_mismatch","context":{"expected_nbits":"1a03d74b","coinbase_height":436459339,"coinbase_scriptsig_hex":"044bd7031a025001522cfabe6d6d43629863b5b1d5de8916a8bd7cbd1918bc6543e246e7ba6e55bf745dcb513e2f0800000000000000","parent_kind":"canonical"},"observations":[{"channel":"merge_mining","source":"merge-mining-research","child_chain":"devcoin","child_height":80595,"provenance":"https://github.com/deadmanoz/merge-mining-research/blob/f543b1f23c57be6840c7a2a44a59306b5d0180f7/data/error-blocks/error_block_observations.csv#L3","child_block_hash":"4cb852207cf335969f2ec2cf65ffabb657818e4b65f87e97f52932ac46a28f8d","child_block_time":1362843422,"child_header":"01010400cf2a6264a20af2fb0b4d43af8d79115d54ef83980e149f255afad643d0d433202558e4f3bfea7775606c42f65c1e19ccfb4c3489ff7c9b96f9eeb1df43d946891e573b51cba5071b00000000"},{"channel":"merge_mining","source":"merge-mining-research","child_chain":"ixcoin","child_height":120333,"provenance":"https://github.com/deadmanoz/merge-mining-research/blob/f543b1f23c57be6840c7a2a44a59306b5d0180f7/data/error-blocks/error_block_observations.csv#L28","child_block_hash":"5b9a346a5060370d6aa8a0a3467c5563b38d62d2a2bae2017956c5e0222d5806","child_block_time":1362843421,"child_header":"01010300f1e2ef20771d69fe2d265356eca3dc7c178b270a940ddb5930992bdb609168c5695d33c7a85439a88244feac2772f8e13c82b6ab85f87294e9bb2bca93657bb81d573b51728e021b00000000"},{"channel":"merge_mining","source":"merge-mining-research","child_chain":"namecoin","child_height":99371,"provenance":"https://github.com/deadmanoz/merge-mining-research/blob/f543b1f23c57be6840c7a2a44a59306b5d0180f7/data/error-blocks/error_block_observations.csv#L43","child_block_hash":"0530563a96697ca4e83d6db1b5ef3071215cefc643467be5c4f5500f39adc3d4","child_block_time":1362842753},{"channel":"merge_mining","source":"mergedmonitor","child_chain":"devcoin","provenance":"https://github.com/NStifter/mergedmonitor/blob/54344d4e355f73eb94bef8d391e8fb6e4a9323a6/fork-analysis/mergedmonitor/mergedmonitor.json"},{"channel":"merge_mining","source":"mergedmonitor","child_chain":"ixcoin","provenance":"https://github.com/NStifter/mergedmonitor/blob/54344d4e355f73eb94bef8d391e8fb6e4a9323a6/fork-analysis/mergedmonitor/mergedmonitor.json"},{"channel":"merge_mining","source":"mergedmonitor","child_chain":"namecoin","provenance":"https://github.com/NStifter/mergedmonitor/blob/54344d4e355f73eb94bef8d391e8fb6e4a9323a6/fork-analysis/mergedmonitor/mergedmonitor.json"}]} diff --git a/docs/notes.md b/docs/notes.md index bde05a3..1ddb180 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -41,6 +41,21 @@ Core skips BIP30 checks below height 1983702 on the known mainnet chain after BI A 2026-09-15 sweep of all 1086 stale-blocks bodies at [be1e859](https://github.com/bitcoin-data/stale-blocks/commit/be1e8597615c3372aab9ca437a9cd554822b6870) found no other non-coinbase intersection among the 1073 bodies extending canonical parents; 13 extended noncanonical parents and were out of scope. A negative result does not establish that a block satisfies every consensus rule. +### 173928, 173957, 173998 and 174605 - P2SH redeem-script failure (2012) + +Each body includes the same 123-byte transaction, `4005d6bea3a93fb72f006d23e2685b85069d270cb57d15f0c057ef2d5e3f78d2`, which spends a pay-to-script-hash output funded at canonical 170054, `b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d:1`, worth 400000 satoshis. +Its scriptSig pushes only the redeem script, a 1-of-1 `OP_CHECKMULTISIG`. +The pre-BIP16 template check hashes that push and compares it, and passes; executing the redeem script finds no signature on the stack and fails. +Nodes applying the 1 April 2012 rules rejected these blocks while older nodes accepted them, which is how the same transaction was included by many miners for months. +Core today reports `block-script-verify-flag-failed (Operation not valid with the current stack size)`. + +The four bodies are reconstructions: the coinbase from Namecoin's AuxPoW record, the other transactions from their later confirmations on the accepted chain, and the invalid spend itself; each reproduces its header's merkle root. +They hold 67, 64, 23 and 14 transactions in 31258, 41258, 7662 and 4855 bytes. +CI fetches the funding transaction, checks its txid, and evaluates the named input with and without P2SH. +The blockchain.info block pages archived by the Wayback Machine in April 2012 list each block's transactions. +The [2 April 2012 bitcoin-dev log](https://buildingbitcoin.org/bitcoin-dev/log-2012-04-02.html) records the first `P2SH VerifySignature failed` rejections and the [4 April log](https://buildingbitcoin.org/bitcoin-dev/log-2012-04-04.html) preserves the transaction. +Eighty-five further blocks carry the same spend with authenticated inclusion proofs but no complete body; they need a proof-based admission path. + ### 74638 - value overflow (2010) `bad-txns-vout-toolarge` is the 2010 overflow incident ([CVE-2010-5139](https://en.bitcoin.it/wiki/Value_overflow_incident)). diff --git a/docs/schema.md b/docs/schema.md index a576033..ae20e6a 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -6,7 +6,7 @@ Height is `prev + 1`, not a unique key: different blocks at the same height rema A block may enter the dataset only if its header meets its encoded PoW target and its named consensus failure has the evidence required below. Header/context rules use the supplied header and context. -Body rules require a complete block; sigops, missing-parent, parent-transaction reuse and fee accounting rules additionally require evidence fetched from public APIs or verified cache entries. +Body rules require a complete block; sigops, missing-parent, parent-transaction reuse, fee accounting and P2SH rules additionally require evidence fetched from public APIs or verified cache entries. Observations document acquisition and incident history, but an explorer label or reported reject string cannot substitute for the evidence check. JSONL keeps each block's identity, optional context and repeated observations together. @@ -44,10 +44,11 @@ Context is shared across observations; adding another witness does not duplicate | `coinbase_height` | integer | Height decoded from the BIP34 scriptSig prefix. Required for a BIP34 height mismatch and `already_confirmed_in_parent`. | | `coinbase_scriptsig_hex` | string | Coinbase input scriptSig. Required for BIP34 failures, `coinbase_scriptsig_length_above_100` and `already_confirmed_in_parent`. | | `pool` | string | Pool the block is attributed to, when known. Requires `pool_basis`. | -| `pool_basis` | string | How the pool was identified: `tag` when the pool name appears as a tag in the coinbase scriptSig, `address` when the coinbase payout address is listed for the pool in [mining-pools](https://github.com/bitcoin-data/mining-pools), or `reported` when only a contemporaneous report names the pool. Required whenever `pool` is present and not allowed otherwise. Descriptive: CI checks the value, not the attribution. | +| `pool_basis` | string | How the pool was identified: `tag` when the coinbase scriptSig carries the pool's name or a tag [mining-pools](https://github.com/bitcoin-data/mining-pools) lists for it, `address` when the coinbase payout address is listed for the pool in [mining-pools](https://github.com/bitcoin-data/mining-pools), or `reported` when only a contemporaneous report names the pool. Required whenever `pool` is present and not allowed otherwise. Descriptive: CI checks the value, not the attribution. | | `parent_kind` | string | Chain status of the previous block: `canonical`, `stale`, or `invalid`. `invalid` means the previous block is in this dataset. Descriptive, except that the rules which look up the canonical block at the previous height (`missing_unconfirmed_parent`, `already_confirmed_in_parent`) reject any other value. | | `missing_prevout` | string | Outpoint as `txid:vout`, spent by a non-coinbase input whose transaction is not in the block. Required for `missing_unconfirmed_parent`. | | `parent_txid` | string | Lowercase 64-hex txid of a non-coinbase transaction in both the candidate and its canonical parent. Required for `already_confirmed_in_parent`. | +| `failing_prevout` | string | Outpoint as `txid:vout`, spent by the input whose P2SH evaluation fails. Required for `p2sh_redeem_script_failure`. | ## Optional `observations` array @@ -89,7 +90,7 @@ Core functions live in [bitcoin/bitcoin](https://github.com/bitcoin/bitcoin): - [src/consensus/tx_check.cpp](https://github.com/bitcoin/bitcoin/blob/master/src/consensus/tx_check.cpp): `CheckTransaction` - [src/consensus/tx_verify.cpp](https://github.com/bitcoin/bitcoin/blob/master/src/consensus/tx_verify.cpp): `CheckTxInputs`, `GetTransactionSigOpCost` - [src/script/script.cpp](https://github.com/bitcoin/bitcoin/blob/master/src/script/script.cpp): `GetSigOpCount` -- [src/script/interpreter.cpp](https://github.com/bitcoin/bitcoin/blob/master/src/script/interpreter.cpp): `CountWitnessSigOps` +- [src/script/interpreter.cpp](https://github.com/bitcoin/bitcoin/blob/master/src/script/interpreter.cpp): `CountWitnessSigOps`, `EvalScript` | `rule` | `core_reject_reason` | Typical Core check | | --- | --- | --- | @@ -99,6 +100,7 @@ Core functions live in [bitcoin/bitcoin](https://github.com/bitcoin/bitcoin): | `bad-txns-inputs-missingorspent` | `bad-txns-inputs-missingorspent` | `ConnectBlock` via `CheckTxInputs` | | `missing_unconfirmed_parent` | `bad-txns-inputs-missingorspent` | `ConnectBlock` via `CheckTxInputs` | | `already_confirmed_in_parent` | `bad-txns-inputs-missingorspent` | `ConnectBlock` via `CheckTxInputs` | +| `p2sh_redeem_script_failure` | `block-script-verify-flag-failed` | `ConnectBlock` via `CheckInputScripts` | | `bip34_v2_coinbase_height_mismatch` | `bad-cb-height` | `ContextualCheckBlock` | | `bip34_coinbase_height_mismatch` | `bad-cb-height` | `ContextualCheckBlock` | | `bip34_coinbase_height_missing` | `bad-cb-height` | `ContextualCheckBlock` | @@ -128,6 +130,7 @@ A provenance URL cannot bypass these requirements. | `bad-txns-inputs-missingorspent` | A complete block containing a spend of an existing output of a later transaction in that block. A spend of a parent transaction absent from the block uses `missing_unconfirmed_parent`. | | `missing_unconfirmed_parent` | A complete block extending the block a public API reports at the previous height. The `missing_prevout` outpoint must be spent by an input in the block and created by a transaction not in the block, and the API must currently report that transaction confirmed in another block at this height or later. Unconfirmed or absent status is not evidence. | | `already_confirmed_in_parent` | A complete block whose named `parent_txid` is a non-coinbase transaction in both that body and its canonical parent. The parent's ordered txid list must reproduce the merkle root of a hash-verified parent header, and the canonical hash at height minus one must equal `prev_hash`. Require `parent_kind=canonical` and body-derived coinbase fields with `coinbase_height` equal to the record height. | +| `p2sh_redeem_script_failure` | A complete block with header time at or after 1 April 2012 whose named `failing_prevout` is spent by exactly one input in the body. That input must pass script evaluation without P2SH and fail with it, checked against the authenticated previous transaction. | | `bad-blk-sigops` | A complete block at mainnet height 481824 or later, authenticated previous transactions for every external input, and calculated BIP16/BIP141 sigop cost above 80000. | | `bip34_v2_coinbase_height_mismatch` | Both coinbase context fields, decoded height matching the scriptSig, and a scriptSig that lacks the exact expected BIP34 prefix. Header version must be at least 2 and height below 227931. Applicability of the historical rolling-version threshold still requires review. | | `bip34_coinbase_height_mismatch` | Both coinbase context fields, decoded height matching the scriptSig, and a scriptSig that lacks the exact expected BIP34 prefix, at height 227931 or later. A non-minimal encoding of the right number also fails the prefix check. | @@ -240,3 +243,14 @@ Otherwise `ci/prevouts.py` loads every external input's transaction and verifies Fees are inputs minus outputs, never an explorer's fee field. Missing outputs, repeated spends, money-range violations and negative fees are errors, and a coinbase that pays exactly the subsidy plus fees is not admitted. Authenticated bytes establish output values, not historical unspentness, coinbase maturity or script validity; this is not a `ConnectBlock` replay. + +## P2SH redeem script evidence + +`p2sh_redeem_script_failure` covers a spend of a pay-to-script-hash output that satisfies the pre-BIP16 template check but fails once the redeem script is executed. +Nodes of 2012 executed redeem scripts for blocks timestamped from 1 April 2012 (Unix time 1333238400), so the record's `nTime` must be at or after that time. +Bitcoin Core today applies P2SH from genesis, with one historical exception at block 170060 (`script_flag_exceptions` in `src/kernel/chainparams.cpp`), and reports the failure as `block-script-verify-flag-failed` with the script error in parentheses. + +`failing_prevout` names the spent output. +CI finds the one input in the body that spends it, fetches and authenticates the previous transaction through the sigops cache, and evaluates that input twice with python-bitcoinlib: `VerifySignature` binds it to the previous output and runs it with no flags, then `VerifyScript` runs it with `SCRIPT_VERIFY_P2SH`; it must pass the first and fail the second. +An input that fails without P2SH is an error rather than evidence. +This evaluates one input with a library interpreter; it is not Bitcoin Core's interpreter and does not validate the other inputs in the block.