7777 classify_input ,
7878 iter_context_inputs ,
7979 iter_legacy_context_inputs ,
80+ parse_script ,
8081 read_bounded_context_rows ,
8182)
8283
@@ -1667,6 +1668,29 @@ def test_brsctx1_accepts_valid_file() -> None:
16671668# ── Tests: classify_input spend-context classification ───────────────────────
16681669
16691670
1671+ def test_classify_input_block_177609_op_0_p2sh () -> None :
1672+ """The exact block-177609 OP_0 multisig spend classifies as P2SH."""
1673+ evidence = _ctx_input (
1674+ bytes .fromhex (
1675+ "1cc1ecdf5c05765df3d1f59fba24cd01c45464c329b0f0a25aa9883adfcf7f29"
1676+ )[::- 1 ],
1677+ 0 ,
1678+ verify_flags = VERIFY_P2SH ,
1679+ prevout = bytes .fromhex ("a9145c02c49641699863f909bf4bf3be8398d2e383f187" ),
1680+ script_sig = bytes .fromhex (
1681+ "00483045022100beb926da7428fa009ac770576342ebd1960939e73584a5d0"
1682+ "f3229b58c41e906f022017c0d143077906afccf30caf21f5ece0bb30e3f7"
1683+ "08fd4a17f9d9ef9fe7cdc983014751210307ac6296168948c3f64ce22f51"
1684+ "f6e5424f936c846f1d01223b3d9864f4d955662103ac6ad514715bec8d5d"
1685+ "e1873b9bc873bb71773b51338b4d115f9938b6a029b7d152ae"
1686+ ),
1687+ )
1688+
1689+ classified = classify_input (evidence )
1690+
1691+ assert classified .spend_context == SpendContext .P2SH
1692+
1693+
16701694def test_classify_input_bare_p2pkh () -> None :
16711695 """Bare P2PKH with no flags classifies as BARE."""
16721696 txid_le = b"\x10 " * 32
@@ -1838,6 +1862,101 @@ def test_classify_input_taproot_bad_control_block() -> None:
18381862 )
18391863
18401864
1865+ def test_classify_input_p2sh_op_reserved_scriptsig () -> None :
1866+ """P2SH with OP_RESERVED in scriptSig must raise ContextError."""
1867+ txid_le = b"\x1c " * 32
1868+ redeem = _multisig_redeem_script ()
1869+ bad_script_sig = _push (redeem ) + bytes ([0x50 ]) # extra OP_RESERVED byte
1870+ evidence = _ctx_input (
1871+ txid_le ,
1872+ 0 ,
1873+ verify_flags = VERIFY_P2SH ,
1874+ prevout = _p2sh_prevout (),
1875+ script_sig = bad_script_sig ,
1876+ )
1877+ _raises (
1878+ ContextError , lambda : classify_input (evidence ), "OP_RESERVED P2SH scriptSig"
1879+ )
1880+
1881+
1882+ # ── Tests: parse_script Core stack semantics ─────────────────────────────────
1883+
1884+
1885+ def test_parse_script_op_0_pushes_empty () -> None :
1886+ """OP_0 must push an empty byte vector, matching Core."""
1887+ elements = parse_script (bytes ([0x00 ]))
1888+ assert len (elements ) == 1
1889+ assert elements [0 ].opcode == 0x00
1890+ assert elements [0 ].pushed == b""
1891+
1892+
1893+ def test_parse_script_op_1negate_pushes_negative_one () -> None :
1894+ """OP_1NEGATE must push the single-byte ScriptNum 0x81."""
1895+ elements = parse_script (bytes ([0x4F ]))
1896+ assert len (elements ) == 1
1897+ assert elements [0 ].opcode == 0x4F
1898+ assert elements [0 ].pushed == b"\x81 "
1899+
1900+
1901+ def test_parse_script_small_integers_pushes_core_scriptnum () -> None :
1902+ """OP_1..OP_16 must push the single bytes 0x01..0x10."""
1903+ script = bytes (range (0x51 , 0x61 ))
1904+ elements = parse_script (script )
1905+ assert len (elements ) == 16
1906+ for i , element in enumerate (elements ):
1907+ assert element .opcode == 0x51 + i
1908+ assert element .pushed == bytes ([i + 1 ])
1909+
1910+
1911+ def test_parse_script_pushdata4_success () -> None :
1912+ """OP_PUSHDATA4 must read a 4-byte little-endian length and payload."""
1913+ payload = b"payload"
1914+ length_le = struct .pack ("<I" , len (payload ))
1915+ script = bytes ([0x4E ]) + length_le + payload + bytes ([0x00 ])
1916+ elements = parse_script (script )
1917+ assert len (elements ) == 2
1918+ assert elements [0 ].opcode == 0x4E
1919+ assert elements [0 ].pushed == payload
1920+ assert elements [1 ].opcode == 0x00
1921+ assert elements [1 ].pushed == b""
1922+
1923+
1924+ def test_parse_script_pushdata4_truncated_length () -> None :
1925+ """OP_PUSHDATA4 with fewer than 4 length bytes must fail closed."""
1926+ _raises_with (
1927+ ContextError ,
1928+ lambda : parse_script (bytes ([0x4E , 0x01 ])),
1929+ "OP_PUSHDATA4 truncated length" ,
1930+ "OP_PUSHDATA4 length bytes missing" ,
1931+ )
1932+
1933+
1934+ def test_parse_script_pushdata4_truncated_payload () -> None :
1935+ """OP_PUSHDATA4 with a declared payload beyond remaining bytes must fail closed."""
1936+ _raises_with (
1937+ ContextError ,
1938+ lambda : parse_script (bytes ([0x4E , 0x05 , 0x00 , 0x00 , 0x00 ])),
1939+ "OP_PUSHDATA4 truncated payload" ,
1940+ "OP_PUSHDATA4 payload truncated" ,
1941+ )
1942+
1943+
1944+ def test_parse_script_op_reserved_pushes_none () -> None :
1945+ """OP_RESERVED is not a data push and must leave pushed as None."""
1946+ elements = parse_script (bytes ([0x50 ]))
1947+ assert len (elements ) == 1
1948+ assert elements [0 ].opcode == 0x50
1949+ assert elements [0 ].pushed is None
1950+
1951+
1952+ def test_parse_script_op_drop_pushes_none () -> None :
1953+ """OP_DROP is not a data push and must leave pushed as None."""
1954+ elements = parse_script (bytes ([0x75 ]))
1955+ assert len (elements ) == 1
1956+ assert elements [0 ].opcode == 0x75
1957+ assert elements [0 ].pushed is None
1958+
1959+
18411960# ── Tests: classify-corpus txid reversal mutation ────────────────────────────
18421961
18431962
@@ -6992,6 +7111,7 @@ def main() -> int:
69927111 test_brsctx1_rejects_declared_count_mismatch ,
69937112 test_brsctx1_rejects_trailing_bytes ,
69947113 test_brsctx1_accepts_valid_file ,
7114+ test_classify_input_block_177609_op_0_p2sh ,
69957115 test_classify_input_bare_p2pkh ,
69967116 test_classify_input_p2sh_without_flag ,
69977117 test_classify_input_p2sh_with_flag ,
@@ -7008,6 +7128,15 @@ def main() -> int:
70087128 test_classify_input_native_v0_with_scriptsig ,
70097129 test_classify_input_taproot_bad_key_path_sig ,
70107130 test_classify_input_taproot_bad_control_block ,
7131+ test_classify_input_p2sh_op_reserved_scriptsig ,
7132+ test_parse_script_op_0_pushes_empty ,
7133+ test_parse_script_op_1negate_pushes_negative_one ,
7134+ test_parse_script_small_integers_pushes_core_scriptnum ,
7135+ test_parse_script_pushdata4_success ,
7136+ test_parse_script_pushdata4_truncated_length ,
7137+ test_parse_script_pushdata4_truncated_payload ,
7138+ test_parse_script_op_reserved_pushes_none ,
7139+ test_parse_script_op_drop_pushes_none ,
70117140 test_classify_corpus_txid_reversal_mutation ,
70127141 test_classify_corpus_all_spend_contexts ,
70137142 test_classify_corpus_c150_passes ,
0 commit comments