bolt5

On-chain transaction handling for Lightning (docs.ppad.tech/bolt5).
git clone git://git.ppad.tech/bolt5.git
Log | Files | Refs | README | LICENSE

commit 32f758166843be8f40c47cb10c6697acfa2a6641
parent decf50f48cfa23dc0194f2127b3a7adb78d09b55
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 19 Apr 2026 12:54:30 +0800

lib: guard against fee underflow in spending functions

All fee-deducting spending functions now return Maybe SpendingTx,
returning Nothing when the calculated fee would exceed the output
value. This prevents Word64 subtraction underflow that would
produce ~2^64 output values.

Functions affected: spend_to_local, spend_htlc_output,
spend_remote_htlc_timeout, spend_remote_htlc_preimage,
spend_revoked_to_local, spend_revoked_htlc_output,
spend_revoked_htlc (both branches), spend_revoked_batch.

Also adapts to bolt3 API renames (paymentPreimage, msatToSat,
paymentHash).

Diffstat:
Mbench/Main.hs | 2+-
Mflake.lock | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mlib/Lightning/Protocol/BOLT5/Detect.hs | 8++++----
Mlib/Lightning/Protocol/BOLT5/Spend.hs | 196++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mtest/Main.hs | 148++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
5 files changed, 295 insertions(+), 154 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -177,7 +177,7 @@ classify_benchmarks = bgroup "classify" [ dummyPaymentHash (CltvExpiry 500000)) ] where - dummyPaymentHash = case payment_hash + dummyPaymentHash = case paymentHash (BS.replicate 32 0xAA) of Just ph -> ph Nothing -> error "impossible" diff --git a/flake.lock b/flake.lock @@ -37,6 +37,43 @@ "ppad-base16": { "inputs": { "flake-utils": [ + "ppad-bolt3", + "ppad-bolt1", + "ppad-base16", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-bolt3", + "ppad-bolt1", + "ppad-base16", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-bolt3", + "ppad-bolt1", + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1766934151, + "narHash": "sha256-BUFpuLfrGXE2xi3Wa9TYCEhhRhFp175Ghxnr0JRbG2I=", + "ref": "master", + "rev": "58dfb7922401a60d5de76825fcd5f6ecbcd7afe0", + "revCount": 26, + "type": "git", + "url": "git://git.ppad.tech/base16.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/base16.git" + } + }, + "ppad-base16_2": { + "inputs": { + "flake-utils": [ "ppad-ripemd160", "ppad-base16", "ppad-nixpkgs", @@ -68,7 +105,7 @@ "url": "git://git.ppad.tech/base16.git" } }, - "ppad-base16_2": { + "ppad-base16_3": { "inputs": { "flake-utils": [ "ppad-secp256k1", @@ -102,7 +139,7 @@ "url": "git://git.ppad.tech/base16.git" } }, - "ppad-base16_3": { + "ppad-base16_4": { "inputs": { "flake-utils": [ "ppad-sha256", @@ -136,7 +173,7 @@ "url": "git://git.ppad.tech/base16.git" } }, - "ppad-base16_4": { + "ppad-base16_5": { "inputs": { "flake-utils": [ "ppad-tx", @@ -170,6 +207,41 @@ "url": "git://git.ppad.tech/base16.git" } }, + "ppad-bolt1": { + "inputs": { + "flake-utils": [ + "ppad-bolt3", + "ppad-bolt1", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-bolt3", + "ppad-bolt1", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-base16": "ppad-base16", + "ppad-nixpkgs": [ + "ppad-bolt3", + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1776570879, + "narHash": "sha256-XsgGBvYWL+sD7pDZoPPi4l39DE7GH7maNnhm8iUeB/E=", + "ref": "master", + "rev": "20ea43188d781368e5e64c7c646285a6b0aaeb94", + "revCount": 27, + "type": "git", + "url": "git://git.ppad.tech/bolt1.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/bolt1.git" + } + }, "ppad-bolt3": { "inputs": { "flake-utils": [ @@ -182,6 +254,7 @@ "ppad-nixpkgs", "nixpkgs" ], + "ppad-bolt1": "ppad-bolt1", "ppad-nixpkgs": [ "ppad-nixpkgs" ], @@ -199,11 +272,11 @@ ] }, "locked": { - "lastModified": 1776519982, - "narHash": "sha256-dIn6biw/e3oBlH/sBnMw1LgeNfyzJOp/69I6Im96BUE=", + "lastModified": 1776572200, + "narHash": "sha256-d3+A3XTTvKl4McyXkeQT840kpRI+9mtcXWwJQLoEbT0=", "ref": "master", - "rev": "7be7bbb7e4903df525c62baa2bc5ece23701f337", - "revCount": 36, + "rev": "2113539a278a3b1ab8484a5b205d9388b758ddb6", + "revCount": 39, "type": "git", "url": "git://git.ppad.tech/bolt3.git" }, @@ -324,7 +397,7 @@ "ppad-nixpkgs", "nixpkgs" ], - "ppad-base16": "ppad-base16", + "ppad-base16": "ppad-base16_2", "ppad-nixpkgs": [ "ppad-nixpkgs" ] @@ -356,7 +429,7 @@ "ppad-nixpkgs", "nixpkgs" ], - "ppad-base16": "ppad-base16_2", + "ppad-base16": "ppad-base16_3", "ppad-fixed": "ppad-fixed", "ppad-hmac-drbg": "ppad-hmac-drbg", "ppad-nixpkgs": [ @@ -394,7 +467,7 @@ "ppad-nixpkgs", "nixpkgs" ], - "ppad-base16": "ppad-base16_3", + "ppad-base16": "ppad-base16_4", "ppad-nixpkgs": [ "ppad-nixpkgs" ] @@ -464,7 +537,7 @@ "ppad-nixpkgs", "nixpkgs" ], - "ppad-base16": "ppad-base16_4", + "ppad-base16": "ppad-base16_5", "ppad-nixpkgs": [ "ppad-nixpkgs" ], diff --git a/lib/Lightning/Protocol/BOLT5/Detect.hs b/lib/Lightning/Protocol/BOLT5/Detect.hs @@ -265,7 +265,7 @@ classifyRevokedOutput !txid !revpk !idx !out = -- commitment, the witness contains the preimage. The witness -- stack for a preimage claim is: -- --- @\<remotehtlcsig\> \<payment_preimage\>@ +-- @\<remotehtlcsig\> \<paymentPreimage\>@ -- -- The preimage is the second item (32 bytes) and must hash to -- the expected payment hash. @@ -274,7 +274,7 @@ extract_preimage_offered (Witness items) = case items of [_sig, preimageBytes] | BS.length preimageBytes == 32 -> - payment_preimage preimageBytes + paymentPreimage preimageBytes _ -> Nothing -- | Extract a payment preimage from an HTLC-success transaction @@ -284,7 +284,7 @@ extract_preimage_offered (Witness items) = -- commitment to claim a received HTLC, the witness contains the -- preimage. The witness stack is: -- --- @0 \<remotehtlcsig\> \<localhtlcsig\> \<payment_preimage\>@ +-- @0 \<remotehtlcsig\> \<localhtlcsig\> \<paymentPreimage\>@ -- -- The preimage is the fourth item (32 bytes). extract_preimage_htlc_success @@ -293,7 +293,7 @@ extract_preimage_htlc_success (Witness items) = case items of [_zero, _remoteSig, _localSig, preimageBytes] | BS.length preimageBytes == 32 -> - payment_preimage preimageBytes + paymentPreimage preimageBytes _ -> Nothing -- timeout check ------------------------------------------------------ diff --git a/lib/Lightning/Protocol/BOLT5/Spend.hs b/lib/Lightning/Protocol/BOLT5/Spend.hs @@ -55,6 +55,8 @@ import Lightning.Protocol.BOLT5.Types -- broadcasting. The caller signs with the local delayed privkey -- and uses 'to_local_witness_spend' from bolt3. -- +-- Returns 'Nothing' if the fee would exceed the output value. +-- -- The input nSequence is set to the to_self_delay value. spend_to_local :: OutPoint @@ -67,7 +69,7 @@ spend_to_local -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw - -> SpendingTx + -> Maybe SpendingTx spend_to_local !op !value !revpk !delay !delayedpk !destScript !feerate = let !witnessScript = @@ -75,12 +77,16 @@ spend_to_local !op !value !revpk !delay !delayedpk !weight = to_local_penalty_input_weight + penalty_tx_base_weight !fee = spending_fee feerate weight - !outputValue = - Satoshi (unSatoshi value - unSatoshi fee) - !tx = mk_spending_tx op - (fromIntegral (unToSelfDelay delay)) - destScript outputValue 0 - in SpendingTx tx witnessScript value SIGHASH_ALL + in if unSatoshi fee >= unSatoshi value + then Nothing + else + let !outputValue = + Satoshi (unSatoshi value - unSatoshi fee) + !tx = mk_spending_tx op + (fromIntegral (unToSelfDelay delay)) + destScript outputValue 0 + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) -- | Construct an HTLC-timeout second-stage transaction. -- @@ -104,7 +110,7 @@ spend_htlc_timeout !ctx !keys = (htlc_payment_hash htlc) features !inputValue = - msat_to_sat (htlc_amount_msat htlc) + msatToSat (htlc_amount_msat htlc) !sighashType = if has_anchors features then SIGHASH_SINGLE_ANYONECANPAY else SIGHASH_ALL @@ -134,7 +140,7 @@ spend_htlc_success !ctx !keys = (htlc_cltv_expiry htlc) features !inputValue = - msat_to_sat (htlc_amount_msat htlc) + msatToSat (htlc_amount_msat htlc) !sighashType = if has_anchors features then SIGHASH_SINGLE_ANYONECANPAY else SIGHASH_ALL @@ -147,6 +153,8 @@ spend_htlc_success !ctx !keys = -- The output of an HTLC-timeout or HTLC-success tx uses the -- same to_local script. The caller signs with the local -- delayed privkey and uses 'htlc_output_witness_spend'. +-- +-- Returns 'Nothing' if the fee would exceed the output value. spend_htlc_output :: OutPoint -- ^ Outpoint of the second-stage output. @@ -158,7 +166,7 @@ spend_htlc_output -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw - -> SpendingTx + -> Maybe SpendingTx spend_htlc_output = spend_to_local -- remote commitment spends ------------------------------------------- @@ -168,6 +176,8 @@ spend_htlc_output = spend_to_local -- -- On the remote commitment, their received HTLCs (our offered) -- have timed out and we can sweep them directly. +-- +-- Returns 'Nothing' if the fee would exceed the output value. spend_remote_htlc_timeout :: OutPoint -- ^ Outpoint of the HTLC output. @@ -181,7 +191,7 @@ spend_remote_htlc_timeout -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw - -> SpendingTx + -> Maybe SpendingTx spend_remote_htlc_timeout !op !value !htlc !keys !features !destScript !feerate = let !witnessScript = received_htlc_script @@ -194,20 +204,27 @@ spend_remote_htlc_timeout !op !value !htlc !keys !weight = accepted_htlc_penalty_input_weight + penalty_tx_base_weight !fee = spending_fee feerate weight - !outputValue = - Satoshi (unSatoshi value - unSatoshi fee) - !locktime = - unCltvExpiry (htlc_cltv_expiry htlc) - !seqNo = if has_anchors features then 1 else 0 - !tx = mk_spending_tx op seqNo destScript - outputValue locktime - in SpendingTx tx witnessScript value SIGHASH_ALL + in if unSatoshi fee >= unSatoshi value + then Nothing + else + let !outputValue = + Satoshi (unSatoshi value - unSatoshi fee) + !locktime = + unCltvExpiry (htlc_cltv_expiry htlc) + !seqNo = + if has_anchors features then 1 else 0 + !tx = mk_spending_tx op seqNo destScript + outputValue locktime + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) -- | Spend a received HTLC directly with preimage on the remote -- commitment. -- -- On the remote commitment, their offered HTLCs (our received) -- can be claimed with the payment preimage. +-- +-- Returns 'Nothing' if the fee would exceed the output value. spend_remote_htlc_preimage :: OutPoint -- ^ Outpoint of the HTLC output. @@ -221,7 +238,7 @@ spend_remote_htlc_preimage -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw - -> SpendingTx + -> Maybe SpendingTx spend_remote_htlc_preimage !op !value !htlc !keys !features !destScript !feerate = let !witnessScript = offered_htlc_script @@ -233,12 +250,17 @@ spend_remote_htlc_preimage !op !value !htlc !keys !weight = offered_htlc_penalty_input_weight + penalty_tx_base_weight !fee = spending_fee feerate weight - !outputValue = - Satoshi (unSatoshi value - unSatoshi fee) - !seqNo = if has_anchors features then 1 else 0 - !tx = mk_spending_tx op seqNo destScript - outputValue 0 - in SpendingTx tx witnessScript value SIGHASH_ALL + in if unSatoshi fee >= unSatoshi value + then Nothing + else + let !outputValue = + Satoshi (unSatoshi value - unSatoshi fee) + !seqNo = + if has_anchors features then 1 else 0 + !tx = mk_spending_tx op seqNo destScript + outputValue 0 + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) -- revoked commitment spends ------------------------------------------ @@ -246,6 +268,8 @@ spend_remote_htlc_preimage !op !value !htlc !keys -- -- The caller signs with the revocation privkey and uses -- 'to_local_witness_revoke' from bolt3. +-- +-- Returns 'Nothing' if the fee would exceed the output value. spend_revoked_to_local :: OutPoint -- ^ Outpoint of the to_local output. @@ -257,7 +281,7 @@ spend_revoked_to_local -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw - -> SpendingTx + -> Maybe SpendingTx spend_revoked_to_local !op !value !revpk !delay !delayedpk !destScript !feerate = let !witnessScript = @@ -265,11 +289,15 @@ spend_revoked_to_local !op !value !revpk !delay !weight = to_local_penalty_input_weight + penalty_tx_base_weight !fee = spending_fee feerate weight - !outputValue = - Satoshi (unSatoshi value - unSatoshi fee) - !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in SpendingTx tx witnessScript value SIGHASH_ALL + in if unSatoshi fee >= unSatoshi value + then Nothing + else + let !outputValue = + Satoshi (unSatoshi value - unSatoshi fee) + !tx = mk_spending_tx op 0xFFFFFFFF destScript + outputValue 0 + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) -- | Spend a revoked HTLC output using the revocation key. -- @@ -277,6 +305,9 @@ spend_revoked_to_local !op !value !revpk !delay -- 'offered_htlc_witness_revoke' or -- 'received_htlc_witness_revoke' from bolt3, depending on -- the output type. +-- +-- Returns 'Nothing' if the output type is not an HTLC, or +-- if the fee would exceed the output value. spend_revoked_htlc :: OutPoint -- ^ Outpoint of the HTLC output. @@ -305,12 +336,16 @@ spend_revoked_htlc !op !value !otype !revpk !keys !weight = offered_htlc_penalty_input_weight + penalty_tx_base_weight !fee = spending_fee feerate weight - !outputValue = - Satoshi (unSatoshi value - unSatoshi fee) - !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) + in if unSatoshi fee >= unSatoshi value + then Nothing + else + let !outputValue = + Satoshi + (unSatoshi value - unSatoshi fee) + !tx = mk_spending_tx op 0xFFFFFFFF + destScript outputValue 0 + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) OutputReceivedHTLC expiry -> let !witnessScript = received_htlc_script revpk @@ -322,12 +357,16 @@ spend_revoked_htlc !op !value !otype !revpk !keys !weight = accepted_htlc_penalty_input_weight + penalty_tx_base_weight !fee = spending_fee feerate weight - !outputValue = - Satoshi (unSatoshi value - unSatoshi fee) - !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) + in if unSatoshi fee >= unSatoshi value + then Nothing + else + let !outputValue = + Satoshi + (unSatoshi value - unSatoshi fee) + !tx = mk_spending_tx op 0xFFFFFFFF + destScript outputValue 0 + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) _ -> Nothing -- | Spend a revoked second-stage HTLC output (HTLC-timeout or @@ -336,6 +375,8 @@ spend_revoked_htlc !op !value !otype !revpk !keys -- The output of a revoked HTLC-timeout/success tx uses the -- to_local script. The caller signs with the revocation privkey -- and uses 'htlc_output_witness_revoke'. +-- +-- Returns 'Nothing' if the fee would exceed the output value. spend_revoked_htlc_output :: OutPoint -- ^ Outpoint of the second-stage output. @@ -347,7 +388,7 @@ spend_revoked_htlc_output -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw - -> SpendingTx + -> Maybe SpendingTx spend_revoked_htlc_output !op !value !revpk !delay !delayedpk !destScript !feerate = let !witnessScript = @@ -355,11 +396,15 @@ spend_revoked_htlc_output !op !value !revpk !delay !weight = to_local_penalty_input_weight + penalty_tx_base_weight !fee = spending_fee feerate weight - !outputValue = - Satoshi (unSatoshi value - unSatoshi fee) - !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in SpendingTx tx witnessScript value SIGHASH_ALL + in if unSatoshi fee >= unSatoshi value + then Nothing + else + let !outputValue = + Satoshi (unSatoshi value - unSatoshi fee) + !tx = mk_spending_tx op 0xFFFFFFFF destScript + outputValue 0 + in Just (SpendingTx tx witnessScript value + SIGHASH_ALL) -- | Construct a batched penalty transaction spending multiple -- revoked outputs. @@ -368,7 +413,9 @@ spend_revoked_htlc_output !op !value !revpk !delay -- be resolved in a single penalty transaction (within the -- 400,000 weight limit). The caller signs each input with the -- revocation privkey. -spend_revoked_batch :: PenaltyContext -> SpendingTx +-- | Returns 'Nothing' if the total fee would exceed the +-- total input value. +spend_revoked_batch :: PenaltyContext -> Maybe SpendingTx spend_revoked_batch !ctx = let !outs = pc_outputs ctx !destScript = pc_destination ctx @@ -380,27 +427,32 @@ spend_revoked_batch !ctx = (NE.toList outs) !fee = spending_fee feerate totalWeight - !outputValue = - Satoshi (unSatoshi totalValue - unSatoshi fee) - - -- Build inputs - !txInputs = fmap mkPenaltyInput outs - - -- Single output - !txOutput = TxOut - (unSatoshi outputValue) - (unScript destScript) - - !tx = Tx - { tx_version = 2 - , tx_inputs = txInputs - , tx_outputs = txOutput :| [] - , tx_witnesses = [] - , tx_locktime = 0 - } - - !witnessScript = Script BS.empty - in SpendingTx tx witnessScript totalValue SIGHASH_ALL + in if unSatoshi fee >= unSatoshi totalValue + then Nothing + else + let !outputValue = + Satoshi + (unSatoshi totalValue - unSatoshi fee) + + -- Build inputs + !txInputs = fmap mkPenaltyInput outs + + -- Single output + !txOutput = TxOut + (unSatoshi outputValue) + (unScript destScript) + + !tx = Tx + { tx_version = 2 + , tx_inputs = txInputs + , tx_outputs = txOutput :| [] + , tx_witnesses = [] + , tx_locktime = 0 + } + + !witnessScript = Script BS.empty + in Just (SpendingTx tx witnessScript totalValue + SIGHASH_ALL) where go !totalVal !totalWt [] = (totalVal, totalWt) go !totalVal !totalWt (uo:rest) = diff --git a/test/Main.hs b/test/Main.hs @@ -71,7 +71,7 @@ dummyFundingPubkey :: FundingPubkey dummyFundingPubkey = FundingPubkey dummyPubkey dummyPaymentHash :: PaymentHash -dummyPaymentHash = case payment_hash dummyHash32 of +dummyPaymentHash = case paymentHash dummyHash32 of Just ph -> ph Nothing -> error "impossible" @@ -438,15 +438,16 @@ classify_tests = testGroup "Classify" [ spend_tests :: TestTree spend_tests = testGroup "Spend" [ testCase "spend_to_local produces valid tx" $ do - let stx = B5.spend_to_local - dummyOutPoint - (Satoshi 100000) - dummyRevocationPubkey - dummyDelay - dummyLocalDelayedPubkey - dummyDestScript - dummyFeerate - tx = B5.stx_tx stx + stx <- assertJust "spend_to_local" $ + B5.spend_to_local + dummyOutPoint + (Satoshi 100000) + dummyRevocationPubkey + dummyDelay + dummyLocalDelayedPubkey + dummyDestScript + dummyFeerate + let tx = B5.stx_tx stx -- Version should be 2 tx_version tx @?= 2 -- Single input @@ -469,12 +470,13 @@ spend_tests = testGroup "Spend" [ , testCase "spend_to_local fee deduction" $ do let value = Satoshi 100000 - stx = B5.spend_to_local - dummyOutPoint value - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript dummyFeerate - tx = B5.stx_tx stx + stx <- assertJust "spend_to_local" $ + B5.spend_to_local + dummyOutPoint value + dummyRevocationPubkey dummyDelay + dummyLocalDelayedPubkey + dummyDestScript dummyFeerate + let tx = B5.stx_tx stx outVal = txout_value (head' (tx_outputs tx)) expectedFee = B5.spending_fee dummyFeerate @@ -485,12 +487,13 @@ spend_tests = testGroup "Spend" [ - unSatoshi expectedFee) , testCase "spend_revoked_to_local nSequence" $ do - let stx = B5.spend_revoked_to_local - dummyOutPoint (Satoshi 100000) - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript dummyFeerate - tx = B5.stx_tx stx + stx <- assertJust "spend_revoked_to_local" $ + B5.spend_revoked_to_local + dummyOutPoint (Satoshi 100000) + dummyRevocationPubkey dummyDelay + dummyLocalDelayedPubkey + dummyDestScript dummyFeerate + let tx = B5.stx_tx stx inp = head' (tx_inputs tx) txin_sequence inp @?= 0xFFFFFFFF @@ -512,16 +515,18 @@ spend_tests = testGroup "Spend" [ txin_sequence inp @?= 16 , testCase "spend_htlc_output is spend_to_local" $ do - let stx1 = B5.spend_to_local - dummyOutPoint (Satoshi 50000) - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript dummyFeerate - stx2 = B5.spend_htlc_output - dummyOutPoint (Satoshi 50000) - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript dummyFeerate + stx1 <- assertJust "spend_to_local" $ + B5.spend_to_local + dummyOutPoint (Satoshi 50000) + dummyRevocationPubkey dummyDelay + dummyLocalDelayedPubkey + dummyDestScript dummyFeerate + stx2 <- assertJust "spend_htlc_output" $ + B5.spend_htlc_output + dummyOutPoint (Satoshi 50000) + dummyRevocationPubkey dummyDelay + dummyLocalDelayedPubkey + dummyDestScript dummyFeerate B5.stx_tx stx1 @?= B5.stx_tx stx2 B5.stx_input_script stx1 @?= B5.stx_input_script stx2 @@ -540,8 +545,9 @@ spend_tests = testGroup "Spend" [ , B5.pc_destination = dummyDestScript , B5.pc_feerate = dummyFeerate } - stx = B5.spend_revoked_batch pctx - tx = B5.stx_tx stx + stx <- assertJust "spend_revoked_batch" $ + B5.spend_revoked_batch pctx + let tx = B5.stx_tx stx outVal = txout_value (head' (tx_outputs tx)) -- Output should be less than total input @@ -564,7 +570,7 @@ htlc_spend_tests = testGroup "HTLC Spend" [ length (tx_outputs tx) @?= 1 B5.stx_sighash_type stx @?= SIGHASH_ALL B5.stx_input_value stx - @?= msat_to_sat (MilliSatoshi 1000000) + @?= msatToSat (MilliSatoshi 1000000) , testCase "spend_htlc_success produces valid tx" $ do let ctx = dummyHTLCContext @@ -597,11 +603,12 @@ htlc_spend_tests = testGroup "HTLC Spend" [ remote_spend_tests :: TestTree remote_spend_tests = testGroup "Remote Spend" [ testCase "spend_remote_htlc_timeout structure" $ do - let stx = B5.spend_remote_htlc_timeout - dummyOutPoint (Satoshi 50000) - dummyHTLC dummyKeys dummyFeatures - dummyDestScript dummyFeerate - tx = B5.stx_tx stx + stx <- assertJust "spend_remote_htlc_timeout" $ + B5.spend_remote_htlc_timeout + dummyOutPoint (Satoshi 50000) + dummyHTLC dummyKeys dummyFeatures + dummyDestScript dummyFeerate + let tx = B5.stx_tx stx tx_version tx @?= 2 length (tx_inputs tx) @?= 1 B5.stx_sighash_type stx @?= SIGHASH_ALL @@ -611,20 +618,22 @@ remote_spend_tests = testGroup "Remote Spend" [ , testCase "spend_remote_htlc_timeout fee deduction" $ do let value = Satoshi 50000 - stx = B5.spend_remote_htlc_timeout - dummyOutPoint value dummyHTLC - dummyKeys dummyFeatures - dummyDestScript dummyFeerate - tx = B5.stx_tx stx + stx <- assertJust "spend_remote_htlc_timeout" $ + B5.spend_remote_htlc_timeout + dummyOutPoint value dummyHTLC + dummyKeys dummyFeatures + dummyDestScript dummyFeerate + let tx = B5.stx_tx stx outVal = txout_value (head' (tx_outputs tx)) assertBool "output < input" (outVal < 50000) , testCase "spend_remote_htlc_preimage structure" $ do - let stx = B5.spend_remote_htlc_preimage - dummyOutPoint (Satoshi 50000) - dummyReceivedHTLC dummyKeys - dummyFeatures dummyDestScript dummyFeerate - tx = B5.stx_tx stx + stx <- assertJust "spend_remote_htlc_preimage" $ + B5.spend_remote_htlc_preimage + dummyOutPoint (Satoshi 50000) + dummyReceivedHTLC dummyKeys + dummyFeatures dummyDestScript dummyFeerate + let tx = B5.stx_tx stx tx_version tx @?= 2 B5.stx_sighash_type stx @?= SIGHASH_ALL -- locktime should be 0 for preimage claims @@ -632,11 +641,12 @@ remote_spend_tests = testGroup "Remote Spend" [ , testCase "spend_remote_htlc_timeout anchors seq" $ do - let stx = B5.spend_remote_htlc_timeout - dummyOutPoint (Satoshi 50000) - dummyHTLC dummyKeys dummyFeaturesAnchors - dummyDestScript dummyFeerate - tx = B5.stx_tx stx + stx <- assertJust "spend_remote_htlc_timeout" $ + B5.spend_remote_htlc_timeout + dummyOutPoint (Satoshi 50000) + dummyHTLC dummyKeys dummyFeaturesAnchors + dummyDestScript dummyFeerate + let tx = B5.stx_tx stx inp = head' (tx_inputs tx) txin_sequence inp @?= 1 ] @@ -729,21 +739,23 @@ property_tests = testGroup "Properties" [ \(Positive val) -> let value = Satoshi (fromIntegral (val :: Int) + 100000) - stx = B5.spend_to_local - dummyOutPoint value - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript (FeeratePerKw 253) - tx = B5.stx_tx stx - outVal = txout_value - (head' (tx_outputs tx)) expectedFee = B5.spending_fee (FeeratePerKw 253) (B5.to_local_penalty_input_weight + B5.penalty_tx_base_weight) - in Satoshi outVal == - Satoshi (unSatoshi value - - unSatoshi expectedFee) + in case B5.spend_to_local + dummyOutPoint value + dummyRevocationPubkey dummyDelay + dummyLocalDelayedPubkey + dummyDestScript (FeeratePerKw 253) of + Nothing -> False + Just stx -> + let tx = B5.stx_tx stx + outVal = txout_value + (head' (tx_outputs tx)) + in Satoshi outVal == + Satoshi (unSatoshi value + - unSatoshi expectedFee) , testProperty "htlc_timed_out monotonic" $ \(NonNegative h1) (NonNegative h2) -> @@ -762,3 +774,7 @@ property_tests = testGroup "Properties" [ -- | Total head for NonEmpty. head' :: NonEmpty a -> a head' (x :| _) = x + +-- | Assert a Maybe is Just, failing with a message otherwise. +assertJust :: String -> Maybe a -> IO a +assertJust msg = maybe (assertFailure msg) pure