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:
| M | test/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]