tx

Minimal transaction primitives (docs.ppad.tech/tx).
git clone git://git.ppad.tech/tx.git
Log | Files | Refs | README | LICENSE

commit ea6764a46626f930c84738901ca1fb5bbd98156a
parent 19203c8efb4a8def5d781fb95ec5788b8023aa00
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 18:41:00 +0400

test: regression tests for parsing; total fixtures

Cover huge lengths and counts, superfluous witness records and
empty-witness serialisation. Make the hex helper and fixtures total
(checked by a 'fixtures parse' test), ensure generated segwit
transactions carry witness data, and fix the unused-import and
shadowing warnings.

Diffstat:
Mtest/Main.hs | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 96 insertions(+), 26 deletions(-)

diff --git a/test/Main.hs b/test/Main.hs @@ -13,8 +13,6 @@ import Data.Word (Word8, Word32, Word64) import Test.Tasty import qualified Test.Tasty.HUnit as H import Test.Tasty.QuickCheck as QC hiding (Witness) -import Test.QuickCheck - ( Gen, Arbitrary(..), elements, oneof, chooseInt, forAll, resize, (==>) ) -- main ------------------------------------------------------------------------ @@ -57,6 +55,11 @@ main = defaultMain $ , test_from_bytes_garbage , test_from_base16_invalid_hex , test_sighash_segwit_oob + , test_from_bytes_huge_length + , test_from_bytes_huge_count + , test_from_bytes_superfluous_witness + , test_to_bytes_empty_witnesses + , test_fixtures_parse ] , testGroup "sighash" [ testGroup "legacy" [ @@ -150,11 +153,13 @@ main = defaultMain $ -- helpers --------------------------------------------------------------------- --- | Decode hex, failing the test on invalid input. +-- | Decode a hex literal. Total: an invalid literal decodes to empty +-- bytes, which no vector expects, and the "fixtures parse" test +-- checks the transaction fixtures explicitly. hex :: BS.ByteString -> BS.ByteString hex h = case B16.decode h of Just bs -> bs - Nothing -> error "test error: invalid hex literal" + Nothing -> BS.empty -- | Assert round-trip: from_bytes (to_bytes tx) == Just tx assertRoundtrip :: Tx -> H.Assertion @@ -207,7 +212,7 @@ roundtrip_segwit = H.testCase "segwit tx with witnesses" $ { tx_version = 2 , tx_inputs = txin :| [] , tx_outputs = txout :| [] - , tx_witnesses = [witness] + , tx_witnesses = [wit] , tx_locktime = 500000 } txin = TxIn @@ -222,7 +227,7 @@ roundtrip_segwit = H.testCase "segwit tx with witnesses" $ { txout_value = 100000000 , txout_script_pubkey = hex "0014abcdef1234567890" } - witness = Witness + wit = Witness [ hex "304402201234" , hex "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" ] @@ -329,7 +334,7 @@ edge_empty_scriptsig = H.testCase "empty scriptSig" $ { tx_version = 2 , tx_inputs = txin :| [] , tx_outputs = txout :| [] - , tx_witnesses = [witness] + , tx_witnesses = [wit] , tx_locktime = 0 } txin = TxIn @@ -344,7 +349,7 @@ edge_empty_scriptsig = H.testCase "empty scriptSig" $ { txout_value = 1000 , txout_script_pubkey = hex "0014abcdef" } - witness = Witness [hex "3044", hex "02"] + wit = Witness [hex "3044", hex "02"] -- Maximum sequence number (0xffffffff) edge_max_sequence :: TestTree @@ -494,6 +499,63 @@ test_from_bytes_garbage = H.assertEqual "should be Nothing" Nothing (from_bytes (BS.pack [0xde, 0xad])) +-- from_bytes: a compactSize length >= 2^63 is rejected (it used to wrap +-- to a negative Int and crash with a negative index) +test_from_bytes_huge_length :: TestTree +test_from_bytes_huge_length = + H.testCase "from_bytes rejects huge lengths without crashing" $ do + let prefix = hex "0100000001" <> BS.replicate 36 0x00 + script_len = hex "ff0000000000000080" + H.assertEqual "scriptSig" Nothing + (from_bytes (prefix <> script_len <> hex "ffffffff")) + H.assertEqual "maximal" Nothing + (from_bytes (prefix <> hex "ffffffffffffffffff" <> hex "ffffffff")) + +-- from_bytes: a count exceeding the remaining input is rejected (a huge +-- witness item count used to wrap negative and parse as zero items) +test_from_bytes_huge_count :: TestTree +test_from_bytes_huge_count = + H.testCase "from_bytes rejects counts exceeding the input" $ do + let tx = mconcat + [ hex "0100000000010100", BS.replicate 35 0x00, hex "00ffffffff" + , hex "01", BS.replicate 8 0x00, hex "00" + , hex "ff0000000000000080" -- witness item count 2^63 + , hex "00000000" + ] + H.assertEqual "witness item count" Nothing (from_bytes tx) + H.assertEqual "input count" Nothing + (from_bytes (hex "01000000ffffffffffffffffff00000000")) + +-- from_bytes: a segwit marker and flag with only empty witnesses is +-- invalid (Bitcoin Core: "superfluous witness record") +test_from_bytes_superfluous_witness :: TestTree +test_from_bytes_superfluous_witness = + H.testCase "from_bytes rejects a segwit tx with no witness data" $ do + let tx = mconcat + [ hex "0100000000010100", BS.replicate 35 0x00, hex "00ffffffff" + , hex "01", BS.replicate 8 0x00, hex "00" + , hex "00" -- empty witness stack + , hex "00000000" + ] + H.assertEqual "should be Nothing" Nothing (from_bytes tx) + +-- to_bytes: all-empty witnesses serialise in the legacy format +test_to_bytes_empty_witnesses :: TestTree +test_to_bytes_empty_witnesses = + H.testCase "to_bytes uses the legacy format for empty witnesses" $ do + let tx = legacyTx1 { tx_witnesses = map (const (Witness [])) + (NE.toList (tx_inputs legacyTx1)) } + H.assertEqual "legacy" (to_bytes_legacy tx) (to_bytes tx) + +-- every transaction fixture literal parses +test_fixtures_parse :: TestTree +test_fixtures_parse = + H.testCase "fixtures parse" $ do + H.assertBool "BIP143 P2SH-P2WSH" (is_just (from_base16 p2shP2wshTxHex)) + H.assertBool "BIP341" (is_just (from_base16 bip341TxHex)) + where + is_just = maybe False (const True) + -- from_base16: invalid hex rejected test_from_base16_invalid_hex :: TestTree test_from_base16_invalid_hex = @@ -695,11 +757,13 @@ genSegwitTx = do ver <- arbitrary ins <- arbitraryNonEmpty outs <- arbitraryNonEmpty - -- One witness per input + -- One witness per input, at least one of them non-empty (a segwit + -- serialisation requires witness data) let numInputs = NE.length ins - wits <- vectorOf numInputs arbitrary + w0 <- Witness <$> listOf1 arbitraryScript + ws <- vectorOf (numInputs - 1) arbitrary lt <- arbitrary - pure $ Tx ver ins outs wits lt + pure $ Tx ver ins outs (w0 : ws) lt -- | Generate any valid transaction. instance Arbitrary Tx where @@ -914,16 +978,22 @@ txid_first_segwit = H.testCase "txid of first-segwit fixture" $ -- Shared fixture: unsigned tx, scriptCode, input index, value. -- Source: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki p2shP2wshTx :: Tx -p2shP2wshTx = - let raw = mconcat - [ "010000000136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c" - , "1ca29787b96e0100000000ffffffff0200e9a435000000001976a914389ffc" - , "e9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a914" - , "7480a33f950689af511e6e84c138dbbd3c3ee41588ac00000000" - ] - in case from_base16 raw of - Just t -> t - Nothing -> error "BIP143 P2SH-P2WSH fixture failed to parse" +p2shP2wshTx = maybe placeholder_tx id (from_base16 p2shP2wshTxHex) + +p2shP2wshTxHex :: BS.ByteString +p2shP2wshTxHex = mconcat + [ "010000000136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c" + , "1ca29787b96e0100000000ffffffff0200e9a435000000001976a914389ffc" + , "e9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a914" + , "7480a33f950689af511e6e84c138dbbd3c3ee41588ac00000000" + ] + +-- | A minimal transaction standing in for a fixture that fails to parse +-- (which the "fixtures parse" test reports). +placeholder_tx :: Tx +placeholder_tx = Tx 1 + (TxIn (OutPoint (TxId (BS.replicate 32 0x00)) 0) BS.empty 0 :| []) + (TxOut 0 BS.empty :| []) [] 0 p2shP2wshScriptCode :: BS.ByteString p2shP2wshScriptCode = hex $ mconcat @@ -1161,11 +1231,11 @@ prop_strip_codesep_no_0xab_unchanged = -- Shared fixture: 9-input / 2-output transaction from BIP341 -- wallet-test-vectors.json (keyPathSpending[0]). bip341Tx :: Tx -bip341Tx = case from_base16 bip341TxHex of - Just t -> t - Nothing -> error "BIP341 tx fixture failed to parse" - where - bip341TxHex = "020000000001097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a41842000000006b4830450221008f3b8f8f0537c420654d2283673a761b7ee2ea3c130753103e08ce79201cf32a022079e7ab904a1980ef1c5890b648c8783f4d10103dd62f740d13daa79e298d50c201210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0141ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c030141052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83000141ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a010140b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f0247304402202b795e4de72646d76eab3f0ab27dfa30b810e856ff3a46c9a702df53bb0d8cc302203ccc4d822edab5f35caddb10af1be93583526ccfbade4b4ead350781e2f8adcd012102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90141a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee0020141ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c4820141bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd9810065cd1d" +bip341Tx = maybe placeholder_tx id (from_base16 bip341TxHex) + +bip341TxHex :: BS.ByteString +bip341TxHex = + "020000000001097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a41842000000006b4830450221008f3b8f8f0537c420654d2283673a761b7ee2ea3c130753103e08ce79201cf32a022079e7ab904a1980ef1c5890b648c8783f4d10103dd62f740d13daa79e298d50c201210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0141ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c030141052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83000141ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a010140b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f0247304402202b795e4de72646d76eab3f0ab27dfa30b810e856ff3a46c9a702df53bb0d8cc302203ccc4d822edab5f35caddb10af1be93583526ccfbade4b4ead350781e2f8adcd012102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90141a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee0020141ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c4820141bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd9810065cd1d" -- Amounts and scriptPubKeys for the 9 prevouts, in order. bip341Amounts :: [Word64]