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 9c3c9c72232c60c9a0b2aab82de6cec3378d09fe
parent 6a81645d68127087b8ab800dd034c74dadac9be5
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 15:22:34 +0400

test: port to the new API

Fixtures use bolt1's checked constructors through total helpers, and
commitments carry their HTLCs in bolt3's OutputHTLC. The Appendix C
and F tests now drive spend_htlc_tx and check the remote signature's
flag against bolt3's remote_htlc_sighash; preimage extraction is also
checked on witnesses built with bolt3's HTLC witness functions.

Adds assertions on witness scripts, outpoints and values, a
saturating-fee test, and a fee-exceeds-amount test for HTLC txs. The
max-HTLCs test now uses the spec's formula exactly. Drops tests that
no longer apply (empty commitments, unmatched HTLC lists) and
properties that held by construction or took no input. Removes -O2
from the test suite.

Diffstat:
Mppad-bolt5.cabal | 2+-
Mtest/Main.hs | 1806++++++++++++++++++++++++++++++++++---------------------------------------------
2 files changed, 788 insertions(+), 1020 deletions(-)

diff --git a/ppad-bolt5.cabal b/ppad-bolt5.cabal @@ -45,7 +45,7 @@ test-suite bolt5-tests main-is: Main.hs ghc-options: - -rtsopts -Wall -O2 + -rtsopts -Wall build-depends: base diff --git a/test/Main.hs b/test/Main.hs @@ -1,17 +1,18 @@ -{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} module Main where -import Bitcoin.Prim.Tx (TxOut(..), from_base16) +import Bitcoin.Prim.Tx (OutPoint(..), Tx(..), TxId(..), TxIn(..), + TxOut(..), Witness(..)) +import qualified Bitcoin.Prim.Tx as BT import Bitcoin.Prim.Tx.Sighash (SighashType(..), encode_sighash) import qualified Crypto.Hash.SHA256 as SHA256 import qualified Data.ByteString as BS import Data.List.NonEmpty (NonEmpty(..)) import Data.Word (Word8, Word32, Word64) -import Lightning.Protocol.BOLT3 hiding - (txout_value, txout_script) -import Lightning.Protocol.BOLT3.Types (Pubkey(..), PaymentHash(..)) +import Lightning.Protocol.BOLT1 (MilliSatoshi, PaymentHash, Satoshi) +import qualified Lightning.Protocol.BOLT1 as BOLT1 +import Lightning.Protocol.BOLT3 import qualified Lightning.Protocol.BOLT5 as B5 import Test.Tasty import Test.Tasty.HUnit @@ -30,40 +31,154 @@ main = defaultMain $ testGroup "ppad-bolt5" [ , property_tests ] --- test fixtures ------------------------------------------------------ +-- helpers ------------------------------------------------------------ --- Dummy 32-byte hash -dummyHash32 :: BS.ByteString -dummyHash32 = BS.replicate 32 0xAA +-- | Unwrap a Just, failing the enclosing test on Nothing. +need :: String -> Maybe a -> IO a +need msg = maybe (assertFailure msg) pure -dummyTxId :: TxId -dummyTxId = TxId (BS.replicate 32 0x00) +sat :: Word64 -> IO Satoshi +sat = need "invalid satoshi amount" . BOLT1.satoshi -dummyOutPoint :: OutPoint -dummyOutPoint = OutPoint dummyTxId 0 +msat :: Word64 -> IO MilliSatoshi +msat = need "invalid millisatoshi amount" . BOLT1.milli_satoshi + +-- | Decode a hex string. +unhex :: BS.ByteString -> Maybe BS.ByteString +unhex bs + | odd (BS.length bs) = Nothing + | otherwise = BS.pack <$> go (BS.unpack bs) + where + go (a:b:rest) = (:) <$> byte a b <*> go rest + go _ = Just [] + byte a b = (\hi lo -> hi * 16 + lo) <$> nibble a <*> nibble b + nibble c + | c >= 0x30 && c <= 0x39 = Just (c - 0x30) + | c >= 0x61 && c <= 0x66 = Just (c - 0x57) + | otherwise = Nothing --- Dummy 33-byte pubkey. Each role gets its own key, so a key used --- in the wrong role changes the script. -mkPubkey :: Word8 -> Pubkey -mkPubkey b = Pubkey (BS.pack (0x02 : replicate 32 b)) +-- | A point from hex. +pt :: BS.ByteString -> IO BOLT1.Point +pt h = need ("invalid point " <> show h) (unhex h >>= BOLT1.point) -dummyRevocationPubkey :: RevocationPubkey -dummyRevocationPubkey = RevocationPubkey (mkPubkey 0x01) +-- | Total head for NonEmpty. +head' :: NonEmpty a -> a +head' (x :| _) = x -dummyLocalDelayedPubkey :: LocalDelayedPubkey -dummyLocalDelayedPubkey = LocalDelayedPubkey (mkPubkey 0x02) +-- | Value of a spending transaction's single output. +out_value :: B5.SpendingTx -> Word64 +out_value = txout_value . head' . tx_outputs . B5.stx_tx + +-- | nSequence of a spending transaction's single input. +in_sequence :: B5.SpendingTx -> Word32 +in_sequence = txin_sequence . head' . tx_inputs . B5.stx_tx + +-- fixtures ----------------------------------------------------------- + +-- | Fixtures shared by the unit and property tests. Each key role +-- gets its own point, so a key used in the wrong role changes the +-- script. +data Fx = Fx + { fx_revpk :: !RevocationPubkey + , fx_delayed :: !LocalDelayedPubkey + , fx_local_htlc :: !LocalHtlcPubkey + , fx_remote_htlc :: !RemoteHtlcPubkey + , fx_funding :: !FundingPubkey + , fx_keys :: !CommitmentKeys + , fx_hash :: !PaymentHash + , fx_offered :: !HTLC + -- ^ offered, 1000 sat, expiry 500000 + , fx_received :: !HTLC + -- ^ as 'fx_offered', but received + , fx_local_commit :: !CommitmentTx + -- ^ to_local and to_remote outputs + , fx_htlc_commit :: !CommitmentTx + -- ^ as 'fx_local_commit', plus both HTLCs + , fx_remote_commit :: !CommitmentTx + -- ^ distinct from 'fx_local_commit' + } -dummyLocalHtlcPubkey :: LocalHtlcPubkey -dummyLocalHtlcPubkey = LocalHtlcPubkey (mkPubkey 0x03) +fixtures :: Maybe Fx +fixtures = do + let point b = BOLT1.point (BS.pack (0x02 : replicate 32 b)) + revpk <- RevocationPubkey <$> point 0x01 + delayed <- LocalDelayedPubkey <$> point 0x02 + local_htlc <- LocalHtlcPubkey <$> point 0x03 + remote_htlc <- RemoteHtlcPubkey <$> point 0x04 + remote_payment <- RemotePubkey <$> point 0x06 + funding <- FundingPubkey <$> point 0x07 + remote_funding <- FundingPubkey <$> point 0x08 + ph <- BOLT1.payment_hash (BS.replicate 32 0xAA) + amt <- BOLT1.milli_satoshi 1000000 + v1000 <- BOLT1.satoshi 1000 + v20k <- BOLT1.satoshi 20000 + v30k <- BOLT1.satoshi 30000 + v40k <- BOLT1.satoshi 40000 + v50k <- BOLT1.satoshi 50000 + let keys = CommitmentKeys + { ck_revocation_pubkey = revpk + , ck_local_delayed = delayed + , ck_local_htlc = local_htlc + , ck_remote_htlc = remote_htlc + , ck_remote_payment = remote_payment + , ck_local_funding = funding + , ck_remote_funding = remote_funding + } + offered = HTLC + { htlc_direction = HTLCOffered + , htlc_amount_msat = amt + , htlc_payment_hash = ph + , htlc_cltv_expiry = CltvExpiry 500000 + } + received = offered { htlc_direction = HTLCReceived } + to_local = to_p2wsh (to_local_script revpk dummyDelay delayed) + htlc_out h = CommitmentOutput v1000 + (to_p2wsh (htlc_witness_script keys StaticRemotekey h)) + (OutputHTLC h) + commit lt outs = CommitmentTx + { ctx_version = 2 + , ctx_locktime = Locktime lt + , ctx_input_outpoint = dummyOutPoint + , ctx_input_sequence = Sequence 0 + , ctx_outputs = outs + , ctx_funding_script = dummyDestScript + } + local_outs = + CommitmentOutput v50k to_local OutputToLocal + :| [CommitmentOutput v30k dummyDestScript OutputToRemote] + pure Fx + { fx_revpk = revpk + , fx_delayed = delayed + , fx_local_htlc = local_htlc + , fx_remote_htlc = remote_htlc + , fx_funding = funding + , fx_keys = keys + , fx_hash = ph + , fx_offered = offered + , fx_received = received + , fx_local_commit = commit 0 local_outs + , fx_htlc_commit = commit 0 $ local_outs + <> (htlc_out offered :| [htlc_out received]) + , fx_remote_commit = commit 1 $ + CommitmentOutput v40k to_local OutputToLocal + :| [CommitmentOutput v20k dummyDestScript OutputToRemote] + } -dummyRemoteHtlcPubkey :: RemoteHtlcPubkey -dummyRemoteHtlcPubkey = RemoteHtlcPubkey (mkPubkey 0x04) +-- | Run a test with the fixtures. +with_fx :: (Fx -> Assertion) -> Assertion +with_fx k = need "invalid fixtures" fixtures >>= k -dummyFundingPubkey :: FundingPubkey -dummyFundingPubkey = FundingPubkey (mkPubkey 0x07) +-- | A property over the fixtures. +prop_fx :: Testable p => (Fx -> p) -> Property +prop_fx k = case fixtures of + Nothing -> counterexample "invalid fixtures" False + Just fx -> property (k fx) -dummyPaymentHash :: PaymentHash -dummyPaymentHash = PaymentHash dummyHash32 +dummyTxId :: TxId +dummyTxId = TxId (BS.replicate 32 0x00) + +dummyOutPoint :: OutPoint +dummyOutPoint = OutPoint dummyTxId 0 dummyDelay :: ToSelfDelay dummyDelay = ToSelfDelay 144 @@ -71,246 +186,141 @@ dummyDelay = ToSelfDelay 144 dummyFeerate :: FeeratePerKw dummyFeerate = FeeratePerKw 253 -dummyFeatures :: ChannelFeatures -dummyFeatures = ChannelFeatures False - -dummyFeaturesAnchors :: ChannelFeatures -dummyFeaturesAnchors = ChannelFeatures True - +-- | A P2WPKH scriptPubKey. dummyDestScript :: Script -dummyDestScript = Script $ BS.pack - [0x00, 0x14] <> BS.replicate 20 0xCC - -dummyKeys :: CommitmentKeys -dummyKeys = CommitmentKeys - { ck_revocation_pubkey = dummyRevocationPubkey - , ck_local_delayed = dummyLocalDelayedPubkey - , ck_local_htlc = dummyLocalHtlcPubkey - , ck_remote_htlc = dummyRemoteHtlcPubkey - , ck_local_payment = LocalPubkey (mkPubkey 0x05) - , ck_remote_payment = RemotePubkey (mkPubkey 0x06) - , ck_local_funding = dummyFundingPubkey - , ck_remote_funding = FundingPubkey (mkPubkey 0x08) - } - -dummyHTLC :: HTLC -dummyHTLC = HTLC - { htlc_direction = HTLCOffered - , htlc_amount_msat = MilliSatoshi 1000000 - , htlc_payment_hash = dummyPaymentHash - , htlc_cltv_expiry = CltvExpiry 500000 - } - -dummyReceivedHTLC :: HTLC -dummyReceivedHTLC = dummyHTLC - { htlc_direction = HTLCReceived } - -dummyHTLCContext :: HTLCContext -dummyHTLCContext = HTLCContext - { hc_commitment_txid = dummyTxId - , hc_output_index = 0 - , hc_htlc = dummyHTLC - , hc_to_self_delay = dummyDelay - , hc_feerate = dummyFeerate - , hc_features = dummyFeatures - , hc_revocation_pubkey = dummyRevocationPubkey - , hc_local_delayed = dummyLocalDelayedPubkey - } - --- Script that matches what findHTLC expects for offered -offeredScript :: Script -offeredScript = to_p2wsh $ offered_htlc_script - dummyRevocationPubkey - dummyRemoteHtlcPubkey - dummyLocalHtlcPubkey - dummyPaymentHash - dummyFeatures - --- Script that matches what findHTLC expects for received -receivedScript :: Script -receivedScript = to_p2wsh $ received_htlc_script - dummyRevocationPubkey - dummyRemoteHtlcPubkey - dummyLocalHtlcPubkey - dummyPaymentHash - (CltvExpiry 500000) - dummyFeatures - -toLocalScript :: Script -toLocalScript = to_p2wsh $ to_local_script - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - --- CommitmentTx with to_local + to_remote outputs -dummyLocalCommitTx :: CommitmentTx -dummyLocalCommitTx = CommitmentTx - { ctx_version = 2 - , ctx_locktime = Locktime 0 - , ctx_input_outpoint = dummyOutPoint - , ctx_input_sequence = Sequence 0 - , ctx_outputs = - [ TxOutput (Satoshi 50000) toLocalScript - OutputToLocal - , TxOutput (Satoshi 30000) dummyDestScript - OutputToRemote - ] - , ctx_funding_script = dummyDestScript - } - --- CommitmentTx with HTLC outputs -dummyLocalCommitWithHTLCs :: CommitmentTx -dummyLocalCommitWithHTLCs = CommitmentTx - { ctx_version = 2 - , ctx_locktime = Locktime 0 - , ctx_input_outpoint = dummyOutPoint - , ctx_input_sequence = Sequence 0 - , ctx_outputs = - [ TxOutput (Satoshi 50000) toLocalScript - OutputToLocal - , TxOutput (Satoshi 30000) dummyDestScript - OutputToRemote - , TxOutput (Satoshi 1000) offeredScript - (OutputOfferedHTLC (CltvExpiry 500000)) - , TxOutput (Satoshi 1000) receivedScript - (OutputReceivedHTLC (CltvExpiry 500000)) - ] - , ctx_funding_script = dummyDestScript - } - --- A distinct CommitmentTx (different locktime) -dummyRemoteCommitTx :: CommitmentTx -dummyRemoteCommitTx = CommitmentTx - { ctx_version = 2 - , ctx_locktime = Locktime 1 - , ctx_input_outpoint = dummyOutPoint - , ctx_input_sequence = Sequence 0 - , ctx_outputs = - [ TxOutput (Satoshi 40000) toLocalScript - OutputToLocal - , TxOutput (Satoshi 20000) dummyDestScript - OutputToRemote - ] - , ctx_funding_script = dummyDestScript +dummyDestScript = Script $ BS.pack [0x00, 0x14] <> BS.replicate 20 0xCC + +-- | The witness script of an HTLC output of a commitment with the +-- given keys. +htlc_witness_script :: CommitmentKeys -> CommitmentFormat -> HTLC -> Script +htlc_witness_script keys fmt h = case htlc_direction h of + HTLCOffered -> offered_htlc_script (ck_revocation_pubkey keys) + (ck_remote_htlc keys) (ck_local_htlc keys) (htlc_payment_hash h) fmt + HTLCReceived -> received_htlc_script (ck_revocation_pubkey keys) + (ck_remote_htlc keys) (ck_local_htlc keys) (htlc_payment_hash h) + (htlc_cltv_expiry h) fmt + +-- | An HTLC context for one of the fixture HTLCs. +htlc_context :: Fx -> HTLC -> CommitmentFormat -> HTLCContext +htlc_context fx h fmt = HTLCContext + { hc_commitment_txid = dummyTxId + , hc_output_index = 0 + , hc_htlc = h + , hc_to_self_delay = dummyDelay + , hc_feerate = dummyFeerate + , hc_format = fmt + , hc_revocation_pubkey = fx_revpk fx + , hc_local_delayed = fx_delayed fx } -- BOLT #3 Appendix C/F vectors ------------------------------------- -- | Payment hash of an Appendix C HTLC whose preimage is 32 copies -- of the given byte. -vecPaymentHash :: Word8 -> PaymentHash -vecPaymentHash b = PaymentHash (SHA256.hash (BS.replicate 32 b)) - --- | Decode a hex string. -unhex :: BS.ByteString -> Maybe BS.ByteString -unhex bs - | odd (BS.length bs) = Nothing - | otherwise = BS.pack <$> go (BS.unpack bs) - where - go (a:b:rest) = (:) <$> byte a b <*> go rest - go _ = Just [] - byte a b = (\hi lo -> hi * 16 + lo) <$> nibble a <*> nibble b - nibble c - | c >= 0x30 && c <= 0x39 = Just (c - 0x30) - | c >= 0x61 && c <= 0x66 = Just (c - 0x57) - | otherwise = Nothing +vecPaymentHash :: Word8 -> IO PaymentHash +vecPaymentHash b = + need "payment hash" (BOLT1.payment_hash (SHA256.hash (BS.replicate 32 b))) -- | An Appendix C HTLC, given its direction, amount (msat), expiry -- and preimage byte. -vecHtlc :: HTLCDirection -> Word64 -> Word32 -> Word8 -> HTLC -vecHtlc dir msat expiry b = HTLC - { htlc_direction = dir - , htlc_amount_msat = MilliSatoshi msat - , htlc_payment_hash = vecPaymentHash b - , htlc_cltv_expiry = CltvExpiry expiry - } +vecHtlc :: HTLCDirection -> Word64 -> Word32 -> Word8 -> IO HTLC +vecHtlc dir amt expiry b = do + a <- msat amt + ph <- vecPaymentHash b + pure HTLC + { htlc_direction = dir + , htlc_amount_msat = a + , htlc_payment_hash = ph + , htlc_cltv_expiry = CltvExpiry expiry + } -vecHtlc0, vecHtlc1, vecHtlc2, vecHtlc3, vecHtlc4 :: HTLC +vecHtlc0, vecHtlc1, vecHtlc2, vecHtlc3, vecHtlc4 :: IO HTLC vecHtlc0 = vecHtlc HTLCReceived 1000000 500 0x00 vecHtlc1 = vecHtlc HTLCReceived 2000000 501 0x01 vecHtlc2 = vecHtlc HTLCOffered 2000000 502 0x02 vecHtlc3 = vecHtlc HTLCOffered 3000000 503 0x03 vecHtlc4 = vecHtlc HTLCReceived 4000000 504 0x04 -vecHtlc5, vecHtlc6 :: HTLC +vecHtlc5, vecHtlc6 :: IO HTLC vecHtlc5 = vecHtlc HTLCOffered 5000000 506 0x05 vecHtlc6 = vecHtlc HTLCOffered 5000001 505 0x05 -- | Appendix C keys for the local commitment (Appendix F uses the -- same keys). -vecKeys :: Maybe CommitmentKeys +vecKeys :: IO CommitmentKeys vecKeys = do - rev <- key + rev <- pt "0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19" - delayed <- key + delayed <- pt "03fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c" - localHtlc <- key + localHtlc <- pt "030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e7" - remoteHtlc <- key + remoteHtlc <- pt "0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b" - localPayment <- key - "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" - remotePayment <- key + remotePayment <- pt "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" - localFunding <- key + localFunding <- pt "023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb" - remoteFunding <- key + remoteFunding <- pt "030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1" pure CommitmentKeys { ck_revocation_pubkey = RevocationPubkey rev - , ck_local_delayed = LocalDelayedPubkey delayed - , ck_local_htlc = LocalHtlcPubkey localHtlc - , ck_remote_htlc = RemoteHtlcPubkey remoteHtlc - , ck_local_payment = LocalPubkey localPayment - , ck_remote_payment = RemotePubkey remotePayment - , ck_local_funding = FundingPubkey localFunding - , ck_remote_funding = FundingPubkey remoteFunding + , ck_local_delayed = LocalDelayedPubkey delayed + , ck_local_htlc = LocalHtlcPubkey localHtlc + , ck_remote_htlc = RemoteHtlcPubkey remoteHtlc + , ck_remote_payment = RemotePubkey remotePayment + , ck_local_funding = FundingPubkey localFunding + , ck_remote_funding = FundingPubkey remoteFunding } - where - key h = unhex h >>= pubkey -- | Rebuild an Appendix C/F local commitment with bolt3, given the --- feerate, channel features, to_local balance and HTLCs. +-- feerate, commitment format, to_local balance (msat) and HTLCs. vecCommitment - :: FeeratePerKw -> ChannelFeatures -> MilliSatoshi -> [HTLC] - -> Maybe CommitmentTx -vecCommitment feerate features toLocal htlcs = do + :: FeeratePerKw -> CommitmentFormat -> Word64 -> [HTLC] + -> IO CommitmentTx +vecCommitment feerate fmt toLocal htlcs = do keys <- vecKeys - fundingTxid <- mkTxId =<< unhex + fundingTxid <- need "funding txid" $ BT.mkTxId =<< unhex "bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489" - cn <- commitment_number 42 - localBp <- point =<< unhex + cn <- need "commitment number" (commitment_number 42) + localBp <- pt "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" - remoteBp <- point =<< unhex + remoteBp <- pt "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" - pure $ build_commitment_tx CommitmentContext - { cc_funding_outpoint = OutPoint fundingTxid 0 + dust <- sat 546 + toLocalMsat <- msat toLocal + toRemoteMsat <- msat 3000000000 + need "build_commitment_tx" $ build_commitment_tx CommitmentContext + { cc_funding_outpoint = OutPoint fundingTxid 0 , cc_commitment_number = cn - , cc_local_payment_bp = PaymentBasepoint localBp + , cc_local_payment_bp = PaymentBasepoint localBp , cc_remote_payment_bp = PaymentBasepoint remoteBp - , cc_to_self_delay = ToSelfDelay 144 - , cc_dust_limit = DustLimit (Satoshi 546) - , cc_feerate = feerate - , cc_features = features - , cc_is_funder = True - , cc_to_local_msat = toLocal - , cc_to_remote_msat = MilliSatoshi 3000000000 - , cc_htlcs = htlcs - , cc_keys = keys + , cc_to_self_delay = ToSelfDelay 144 + , cc_dust_limit = DustLimit dust + , cc_feerate = feerate + , cc_format = fmt + , cc_is_funder = True + , cc_to_local_msat = toLocalMsat + , cc_to_remote_msat = toRemoteMsat + , cc_htlcs = htlcs + , cc_keys = keys } -- | The Appendix C commitment with two offered HTLCs sharing a -- payment hash and amount. -vecCommitSameHash :: Maybe CommitmentTx -vecCommitSameHash = vecCommitment (FeeratePerKw 253) - (ChannelFeatures False) (MilliSatoshi 6987999999) - [vecHtlc1, vecHtlc5, vecHtlc6] +vecCommitSameHash :: IO CommitmentTx +vecCommitSameHash = do + h1 <- vecHtlc1 + h5 <- vecHtlc5 + h6 <- vecHtlc6 + vecCommitment (FeeratePerKw 253) StaticRemotekey 6987999999 [h1, h5, h6] -- | The Appendix F (anchors) "commitment tx with seven outputs -- untrimmed". -vecCommitAnchors :: Maybe CommitmentTx -vecCommitAnchors = vecCommitment (FeeratePerKw 644) - (ChannelFeatures True) (MilliSatoshi 6988000000) - [vecHtlc0, vecHtlc1, vecHtlc2, vecHtlc3, vecHtlc4] +vecCommitAnchors :: IO CommitmentTx +vecCommitAnchors = do + hs <- sequence [vecHtlc0, vecHtlc1, vecHtlc2, vecHtlc3, vecHtlc4] + vecCommitment (FeeratePerKw 644) Anchors 6988000000 hs -- | Appendix C: commitment tx with 3 htlc outputs, 2 offered -- having the same amount and preimage (signed). @@ -420,9 +430,10 @@ vecFHtlcSuccess1 = \875dc4daca813e21734b140639e752ae677502f501b175ac6851b27568000000\ \00" + -- | Parse a hex-encoded transaction, failing the test otherwise. parseTx :: BS.ByteString -> IO Tx -parseTx = assertJust "invalid transaction hex" . from_base16 +parseTx = need "invalid transaction hex" . BT.from_base16 -- | Witness stack of a single-input transaction. singleWitness :: Tx -> IO [BS.ByteString] @@ -432,32 +443,42 @@ singleWitness tx = case tx_witnesses tx of -- | Remote signature and offered HTLC witness script, taken from the -- Appendix C HTLC-timeout tx for htlc #5. -offeredClaimParts :: IO (BS.ByteString, BS.ByteString) +offeredClaimParts :: IO (BS.ByteString, Script) offeredClaimParts = do items <- singleWitness =<< parseTx vecCHtlcTimeout5 case items of - [_, sig, _, _, script] -> pure (sig, script) + [_, sig, _, _, script] -> pure (sig, Script script) _ -> assertFailure "unexpected HTLC-timeout witness" +-- | Signatures and received HTLC witness script, taken from the +-- Appendix C HTLC-success tx for htlc #1. +successParts :: IO (BS.ByteString, BS.ByteString, Script) +successParts = do + items <- singleWitness =<< parseTx vecCHtlcSuccess1 + case items of + [_, remoteSig, localSig, _, script] -> + pure (remoteSig, localSig, Script script) + _ -> assertFailure "unexpected HTLC-success witness" + -- | Build the HTLC tx for an HTLC output of an Appendix C/F -- commitment (to_self_delay 144). vecHtlcTx :: FeeratePerKw -> B5.UnresolvedOutput -> IO B5.SpendingTx vecHtlcTx feerate uo = case B5.uo_type uo of - B5.SpendHTLCTimeout htlc keys features -> - pure (B5.spend_htlc_timeout (ctx htlc keys features) keys) - B5.SpendHTLCSuccess htlc keys features -> - pure (B5.spend_htlc_success (ctx htlc keys features) keys) + B5.SpendHTLCTimeout htlc keys fmt -> build htlc keys fmt + B5.SpendHTLCSuccess htlc keys fmt -> build htlc keys fmt other -> assertFailure ("expected an HTLC output, got " <> show other) where - ctx htlc keys features = HTLCContext - { hc_commitment_txid = op_txid (B5.uo_outpoint uo) - , hc_output_index = op_vout (B5.uo_outpoint uo) - , hc_htlc = htlc - , hc_to_self_delay = ToSelfDelay 144 - , hc_feerate = feerate - , hc_features = features + build htlc keys fmt = need "spend_htlc_tx" $ + B5.spend_htlc_tx (ctx htlc keys fmt) keys + ctx htlc keys fmt = HTLCContext + { hc_commitment_txid = op_txid (B5.uo_outpoint uo) + , hc_output_index = op_vout (B5.uo_outpoint uo) + , hc_htlc = htlc + , hc_to_self_delay = ToSelfDelay 144 + , hc_feerate = feerate + , hc_format = fmt , hc_revocation_pubkey = ck_revocation_pubkey keys - , hc_local_delayed = ck_local_delayed keys + , hc_local_delayed = ck_local_delayed keys } -- | Check an HTLC tx against a signed BOLT #3 vector: the unsigned @@ -473,14 +494,14 @@ assertHtlcTx stx vec = do -- | Check the sighash flags of the signatures in a signed BOLT #3 -- HTLC-tx vector: the local one must be the type the 'SpendingTx' --- gives, the remote one the given type. +-- gives, the remote one bolt3's 'remote_htlc_sighash'. assertSighashFlags - :: B5.SpendingTx -> SighashType -> BS.ByteString -> Assertion -assertSighashFlags stx remoteType vec = do + :: B5.SpendingTx -> CommitmentFormat -> BS.ByteString -> Assertion +assertSighashFlags stx fmt vec = do items <- singleWitness =<< parseTx vec case items of [_, remoteSig, localSig, _, _] -> do - flag remoteSig @?= Just (encode_sighash remoteType) + flag remoteSig @?= Just (encode_sighash (remote_htlc_sighash fmt)) flag localSig @?= Just (encode_sighash (B5.stx_sighash_type stx)) _ -> assertFailure "unexpected HTLC-tx witness" @@ -497,14 +518,16 @@ types_tests = testGroup "Types" [ B5.RemoteCommitClose @?= B5.RemoteCommitClose B5.RevokedCommitClose @?= B5.RevokedCommitClose - , testCase "spending_fee calculation" $ do - let fee = B5.spending_fee (FeeratePerKw 1000) 500 - fee @?= Satoshi 500 + , testCase "spending_fee calculation" $ + BOLT1.un_satoshi (B5.spending_fee (FeeratePerKw 1000) 500) @?= 500 - , testCase "spending_fee at low feerate" $ do - let fee = B5.spending_fee (FeeratePerKw 253) 324 + , testCase "spending_fee at low feerate" $ -- 253 * 324 / 1000 = 81.972 -> 81 - fee @?= Satoshi 81 + BOLT1.un_satoshi (B5.spending_fee (FeeratePerKw 253) 324) @?= 81 + + , testCase "spending_fee saturates at 21 million BTC" $ + B5.spending_fee (FeeratePerKw maxBound) maxBound + @?= BOLT1.max_satoshi , testCase "weight constants" $ do B5.to_local_penalty_witness_weight @?= 160 @@ -523,481 +546,359 @@ detect_tests :: TestTree detect_tests = testGroup "Detect" [ testCase "extract_preimage_htlc_success - Appendix C" $ do items <- singleWitness =<< parseTx vecCHtlcSuccess1 - fmap unPaymentPreimage - (B5.extract_preimage_htlc_success - (vecPaymentHash 0x01) (Witness items)) + ph <- vecPaymentHash 0x01 + fmap BOLT1.un_payment_preimage + (B5.extract_preimage_htlc_success ph (Witness items)) @?= Just (BS.replicate 32 0x01) , testCase "extract_preimage_htlc_success - Appendix F" $ do items <- singleWitness =<< parseTx vecFHtlcSuccess1 - fmap unPaymentPreimage - (B5.extract_preimage_htlc_success - (vecPaymentHash 0x01) (Witness items)) + ph <- vecPaymentHash 0x01 + fmap BOLT1.un_payment_preimage + (B5.extract_preimage_htlc_success ph (Witness items)) @?= Just (BS.replicate 32 0x01) + , testCase "extract_preimage_htlc_success - bolt3 witness" $ do + (remoteSig, localSig, script) <- successParts + ph <- vecPaymentHash 0x01 + preimage <- need "preimage" + (BOLT1.payment_preimage (BS.replicate 32 0x01)) + B5.extract_preimage_htlc_success ph + (htlc_success_witness remoteSig localSig preimage script) + @?= Just preimage + , testCase "extract_preimage_htlc_success - wrong hash" $ do items <- singleWitness =<< parseTx vecCHtlcSuccess1 - B5.extract_preimage_htlc_success - (vecPaymentHash 0x05) (Witness items) - @?= Nothing + ph <- vecPaymentHash 0x05 + B5.extract_preimage_htlc_success ph (Witness items) @?= Nothing , testCase "extract_preimage_htlc_success - timeout witness" $ do items <- singleWitness =<< parseTx vecCHtlcTimeout5 - B5.extract_preimage_htlc_success - (vecPaymentHash 0x05) (Witness items) + ph <- vecPaymentHash 0x05 + B5.extract_preimage_htlc_success ph (Witness items) @?= Nothing + (remoteSig, localSig, script) <- successParts + B5.extract_preimage_htlc_success ph + (htlc_timeout_witness remoteSig localSig script) @?= Nothing , testCase "extract_preimage_htlc_success - no script" $ do items <- singleWitness =<< parseTx vecCHtlcSuccess1 - B5.extract_preimage_htlc_success - (vecPaymentHash 0x01) (Witness (take 4 items)) + ph <- vecPaymentHash 0x01 + B5.extract_preimage_htlc_success ph (Witness (take 4 items)) @?= Nothing , testCase "extract_preimage_offered - remote claim" $ do -- <remotehtlcsig> <payment_preimage> <witness_script>; the -- signature is not checked. (sig, script) <- offeredClaimParts - let preimage = BS.replicate 32 0x05 - fmap unPaymentPreimage - (B5.extract_preimage_offered (vecPaymentHash 0x05) - (Witness [sig, preimage, script])) + ph <- vecPaymentHash 0x05 + preimage <- need "preimage" + (BOLT1.payment_preimage (BS.replicate 32 0x05)) + B5.extract_preimage_offered ph + (offered_htlc_witness_preimage sig preimage script) @?= Just preimage , testCase "extract_preimage_offered - wrong hash" $ do (sig, script) <- offeredClaimParts - B5.extract_preimage_offered (vecPaymentHash 0x01) - (Witness [sig, BS.replicate 32 0x05, script]) + ph <- vecPaymentHash 0x01 + preimage <- need "preimage" + (BOLT1.payment_preimage (BS.replicate 32 0x05)) + B5.extract_preimage_offered ph + (offered_htlc_witness_preimage sig preimage script) @?= Nothing - , testCase "extract_preimage_offered - revocation spend" $ do - -- <revocation_sig> <revocationpubkey> <witness_script> - (sig, script) <- offeredClaimParts - B5.extract_preimage_offered (vecPaymentHash 0x05) - (Witness [sig, unPubkey (mkPubkey 0x01), script]) - @?= Nothing + , testCase "extract_preimage_offered - revocation spend" $ with_fx $ + \fx -> do + -- <revocation_sig> <revocationpubkey> <witness_script> + (sig, script) <- offeredClaimParts + ph <- vecPaymentHash 0x05 + B5.extract_preimage_offered ph + (offered_htlc_witness_revoke sig (fx_revpk fx) script) + @?= Nothing , testCase "extract_preimage_offered - no script" $ do (sig, _) <- offeredClaimParts - B5.extract_preimage_offered (vecPaymentHash 0x05) + ph <- vecPaymentHash 0x05 + B5.extract_preimage_offered ph (Witness [sig, BS.replicate 32 0x05]) @?= Nothing - , testCase "htlc_timed_out - at expiry" $ do - let htlc = dummyHTLC - { htlc_cltv_expiry = CltvExpiry 500000 } - B5.htlc_timed_out 500000 htlc @?= True + , testCase "htlc_timed_out - at expiry" $ with_fx $ \fx -> + B5.htlc_timed_out 500000 (fx_offered fx) @?= True - , testCase "htlc_timed_out - past expiry" $ do - let htlc = dummyHTLC - { htlc_cltv_expiry = CltvExpiry 500000 } - B5.htlc_timed_out 500001 htlc @?= True + , testCase "htlc_timed_out - past expiry" $ with_fx $ \fx -> + B5.htlc_timed_out 500001 (fx_offered fx) @?= True - , testCase "htlc_timed_out - before expiry" $ do - let htlc = dummyHTLC - { htlc_cltv_expiry = CltvExpiry 500000 } - B5.htlc_timed_out 499999 htlc @?= False + , testCase "htlc_timed_out - before expiry" $ with_fx $ \fx -> + B5.htlc_timed_out 499999 (fx_offered fx) @?= False ] -- classify tests ----------------------------------------------------- classify_tests :: TestTree classify_tests = testGroup "Classify" [ - testCase "identify_close - signed local commitment" $ do - commit <- assertJust "commitment" vecCommitSameHash - signed <- assertJust "hex" (unhex vecCCommitSameHash) - B5.identify_close commit dummyRemoteCommitTx signed + testCase "identify_close - signed local commitment" $ with_fx $ + \fx -> do + commit <- vecCommitSameHash + signed <- need "hex" (unhex vecCCommitSameHash) + B5.identify_close commit (fx_remote_commit fx) signed + @?= Just B5.LocalCommitClose + + , testCase "identify_close - signed remote commitment" $ with_fx $ + \fx -> do + commit <- vecCommitSameHash + signed <- need "hex" (unhex vecCCommitSameHash) + B5.identify_close (fx_local_commit fx) commit signed + @?= Just B5.RemoteCommitClose + + , testCase "identify_close - unsigned commitment" $ with_fx $ \fx -> do + commit <- vecCommitSameHash + B5.identify_close commit (fx_remote_commit fx) + (encode_commitment_tx commit) @?= Just B5.LocalCommitClose - , testCase "identify_close - signed remote commitment" $ do - commit <- assertJust "commitment" vecCommitSameHash - signed <- assertJust "hex" (unhex vecCCommitSameHash) - B5.identify_close dummyLocalCommitTx commit signed - @?= Just B5.RemoteCommitClose - - , testCase "identify_close - unsigned commitment" $ do - commit <- assertJust "commitment" vecCommitSameHash - unsigned <- assertJust "encode" (encode_tx_for_signing commit) - B5.identify_close commit dummyRemoteCommitTx unsigned - @?= Just B5.LocalCommitClose - - , testCase "identify_close - no match" $ do - signed <- assertJust "hex" (unhex vecCCommitSameHash) - B5.identify_close - dummyLocalCommitTx dummyRemoteCommitTx signed + , testCase "identify_close - no match" $ with_fx $ \fx -> do + signed <- need "hex" (unhex vecCCommitSameHash) + B5.identify_close (fx_local_commit fx) (fx_remote_commit fx) signed @?= Nothing - , testCase "identify_close - unparseable" $ do - B5.identify_close - dummyLocalCommitTx - dummyRemoteCommitTx + , testCase "identify_close - unparseable" $ with_fx $ \fx -> + B5.identify_close (fx_local_commit fx) (fx_remote_commit fx) "unknown bytes" - @?= Nothing + @?= Nothing - , testCase "classify_local - to_local and to_remote" $ do - let outs = B5.classify_local_commit_outputs - dummyLocalCommitTx dummyKeys - dummyDelay dummyFeatures [] - length outs @?= 2 - case outs of - [o1, o2] -> do - case B5.uo_type o1 of - B5.SpendToLocal d rk dk -> do - d @?= dummyDelay - rk @?= dummyRevocationPubkey - dk @?= dummyLocalDelayedPubkey - other -> assertFailure $ - "expected SpendToLocal, got " <> show other - B5.uo_type o2 @?= B5.Resolved - _ -> assertFailure "expected 2 outputs" - - , testCase "classify_local - HTLC outputs" $ do - let outs = B5.classify_local_commit_outputs - dummyLocalCommitWithHTLCs dummyKeys - dummyDelay dummyFeatures - [dummyHTLC, dummyReceivedHTLC] - length outs @?= 4 - case outs of - [_, _, o3, o4] -> do - case B5.uo_type o3 of - B5.SpendHTLCTimeout _ _ _ -> pure () - other -> assertFailure $ - "expected SpendHTLCTimeout, got " - <> show other - case B5.uo_type o4 of - B5.SpendHTLCSuccess _ _ _ -> pure () - other -> assertFailure $ - "expected SpendHTLCSuccess, got " - <> show other - _ -> assertFailure "expected 4 outputs" - - , testCase "classify_remote - HTLC outputs" $ do - let commitTx = CommitmentTx - { ctx_version = 2 - , ctx_locktime = Locktime 0 - , ctx_input_outpoint = dummyOutPoint - , ctx_input_sequence = Sequence 0 - , ctx_outputs = - [ TxOutput (Satoshi 50000) toLocalScript - OutputToLocal - , TxOutput (Satoshi 1000) offeredScript - (OutputOfferedHTLC - (CltvExpiry 500000)) - , TxOutput (Satoshi 1000) receivedScript - (OutputReceivedHTLC - (CltvExpiry 500000)) - ] - , ctx_funding_script = dummyDestScript + , testCase "classify_local - to_local and to_remote" $ with_fx $ \fx -> + map B5.uo_type + (B5.classify_local_commit_outputs (fx_local_commit fx) + (fx_keys fx) dummyDelay StaticRemotekey) + @?= [ B5.SpendToLocal dummyDelay (fx_revpk fx) (fx_delayed fx) + , B5.Resolved + ] + + , testCase "classify_local - outpoints and values" $ with_fx $ \fx -> do + let commit = fx_htlc_commit fx + txid = BT.txid (commitment_to_tx commit) + outs = B5.classify_local_commit_outputs commit (fx_keys fx) + dummyDelay StaticRemotekey + map B5.uo_outpoint outs @?= map (OutPoint txid) [0 .. 3] + map (BOLT1.un_satoshi . B5.uo_value) outs + @?= [50000, 30000, 1000, 1000] + + , testCase "classify_local - HTLC outputs" $ with_fx $ \fx -> + map B5.uo_type + (drop 2 (B5.classify_local_commit_outputs (fx_htlc_commit fx) + (fx_keys fx) dummyDelay StaticRemotekey)) + @?= [ B5.SpendHTLCTimeout (fx_offered fx) (fx_keys fx) + StaticRemotekey + , B5.SpendHTLCSuccess (fx_received fx) (fx_keys fx) + StaticRemotekey + ] + + , testCase "classify_remote - HTLC outputs" $ with_fx $ \fx -> + map B5.uo_type + (B5.classify_remote_commit_outputs (fx_htlc_commit fx) + (fx_keys fx)) + @?= [ B5.Resolved + , B5.Resolved + , B5.SpendHTLCPreimageDirect (fx_offered fx) + , B5.SpendHTLCTimeoutDirect (fx_received fx) + ] + + , testCase "classify_revoked - to_local and HTLCs" $ with_fx $ \fx -> + map B5.uo_type + (B5.classify_revoked_commit_outputs (fx_htlc_commit fx) + (fx_revpk fx)) + @?= [ B5.Revoke (fx_revpk fx) + , B5.Resolved + , B5.RevokeHTLC (fx_revpk fx) + (B5.HTLCOfferedOutput (CltvExpiry 500000)) + , B5.RevokeHTLC (fx_revpk fx) + (B5.HTLCReceivedOutput (CltvExpiry 500000)) + ] + + , testCase "classify - anchor outputs" $ with_fx $ \fx -> do + v330 <- sat 330 + let keys = fx_keys fx + anchor pk = to_p2wsh (anchor_script pk) + commit = (fx_local_commit fx) + { ctx_outputs = + CommitmentOutput v330 (anchor (ck_local_funding keys)) + OutputLocalAnchor + :| [CommitmentOutput v330 + (anchor (ck_remote_funding keys)) + OutputRemoteAnchor] } - outs = B5.classify_remote_commit_outputs - commitTx dummyKeys dummyFeatures - [dummyHTLC, dummyReceivedHTLC] - length outs @?= 3 - case outs of - [o1, o2, o3] -> do - B5.uo_type o1 @?= B5.Resolved - case B5.uo_type o2 of - B5.SpendHTLCPreimageDirect _ -> pure () - other -> assertFailure $ - "expected SpendHTLCPreimageDirect, got " - <> show other - case B5.uo_type o3 of - B5.SpendHTLCTimeoutDirect _ -> pure () - other -> assertFailure $ - "expected SpendHTLCTimeoutDirect, got " - <> show other - _ -> assertFailure "expected 3 outputs" - - , testCase "classify_revoked - to_local revoked" $ do - let outs = B5.classify_revoked_commit_outputs - dummyLocalCommitWithHTLCs dummyKeys - dummyRevocationPubkey dummyFeatures - [dummyHTLC, dummyReceivedHTLC] - length outs @?= 4 - case outs of - [o1, o2, o3, o4] -> do - B5.uo_type o1 - @?= B5.Revoke dummyRevocationPubkey - B5.uo_type o2 @?= B5.Resolved - case B5.uo_type o3 of - B5.RevokeHTLC _ (B5.HTLCOfferedOutput _) -> - pure () - other -> assertFailure $ - "expected RevokeHTLC offered, got " - <> show other - case B5.uo_type o4 of - B5.RevokeHTLC _ (B5.HTLCReceivedOutput _) -> - pure () - other -> assertFailure $ - "expected RevokeHTLC received, got " - <> show other - _ -> assertFailure "expected 4 outputs" - - , testCase "classify_local - anchor outputs" $ do - let anchorScript = to_p2wsh $ - anchor_script dummyFundingPubkey - remoteFundPk = ck_remote_funding dummyKeys - remoteAnchorScript = to_p2wsh $ - anchor_script remoteFundPk - commitTx = CommitmentTx - { ctx_version = 2 - , ctx_locktime = Locktime 0 - , ctx_input_outpoint = dummyOutPoint - , ctx_input_sequence = Sequence 0 - , ctx_outputs = - [ TxOutput (Satoshi 330) anchorScript - OutputLocalAnchor - , TxOutput (Satoshi 330) remoteAnchorScript - OutputRemoteAnchor - ] - , ctx_funding_script = dummyDestScript - } - outs = B5.classify_local_commit_outputs - commitTx dummyKeys dummyDelay - dummyFeaturesAnchors [] - length outs @?= 2 - case outs of - [o1, o2] -> do - case B5.uo_type o1 of - B5.AnchorSpend _ -> pure () - other -> assertFailure $ - "expected AnchorSpend, got " - <> show other - B5.uo_type o2 @?= B5.Resolved - _ -> assertFailure "expected 2 outputs" - - , testCase "classify_remote - non-HTLC outputs" $ do - let outs = B5.classify_remote_commit_outputs - dummyRemoteCommitTx dummyKeys - dummyFeatures [] - length outs @?= 2 - case outs of - [o1, o2] -> do - B5.uo_type o1 @?= B5.Resolved - B5.uo_type o2 @?= B5.Resolved - _ -> assertFailure "expected 2 outputs" - - , testCase "classify_local - unmatched HTLC" $ do - let outs = B5.classify_local_commit_outputs - dummyLocalCommitWithHTLCs dummyKeys - dummyDelay dummyFeatures [] - -- With no HTLCs passed, HTLC outputs -> Resolved - length outs @?= 4 - case outs of - [_, _, o3, o4] -> do - B5.uo_type o3 @?= B5.Resolved - B5.uo_type o4 @?= B5.Resolved - _ -> assertFailure "expected 4 outputs" - - , testCase "classify_local - empty commit" $ do - let emptyCommit = CommitmentTx - { ctx_version = 2 - , ctx_locktime = Locktime 0 - , ctx_input_outpoint = dummyOutPoint - , ctx_input_sequence = Sequence 0 - , ctx_outputs = [] - , ctx_funding_script = dummyDestScript - } - B5.classify_local_commit_outputs - emptyCommit dummyKeys dummyDelay - dummyFeatures [] - @?= [] + map B5.uo_type + (B5.classify_local_commit_outputs commit keys dummyDelay Anchors) + @?= [B5.AnchorSpend (ck_local_funding keys), B5.Resolved] + map B5.uo_type (B5.classify_remote_commit_outputs commit keys) + @?= [B5.Resolved, B5.AnchorSpend (ck_remote_funding keys)] + map B5.uo_type + (B5.classify_revoked_commit_outputs commit (fx_revpk fx)) + @?= [B5.Resolved, B5.Resolved] + + , testCase "classify_remote - non-HTLC outputs" $ with_fx $ \fx -> + map B5.uo_type + (B5.classify_remote_commit_outputs (fx_remote_commit fx) + (fx_keys fx)) + @?= [B5.Resolved, B5.Resolved] , testCase "classify_local - HTLCs sharing a payment hash" $ do -- Appendix C: htlcs #5 and #6 share a payment hash and amount -- (5000 sat) and differ only in CLTV expiry (506 and 505). - commit <- assertJust "commitment" vecCommitSameHash - keys <- assertJust "keys" vecKeys - let features = ChannelFeatures False - outs = B5.classify_local_commit_outputs commit keys - (ToSelfDelay 144) features - [vecHtlc1, vecHtlc5, vecHtlc6] + commit <- vecCommitSameHash + keys <- vecKeys + h1 <- vecHtlc1 + h5 <- vecHtlc5 + h6 <- vecHtlc6 + let outs = B5.classify_local_commit_outputs commit keys + (ToSelfDelay 144) StaticRemotekey map B5.uo_type (take 3 outs) @?= - [ B5.SpendHTLCSuccess vecHtlc1 keys features - , B5.SpendHTLCTimeout vecHtlc6 keys features - , B5.SpendHTLCTimeout vecHtlc5 keys features + [ B5.SpendHTLCSuccess h1 keys StaticRemotekey + , B5.SpendHTLCTimeout h6 keys StaticRemotekey + , B5.SpendHTLCTimeout h5 keys StaticRemotekey ] , testCase "HTLC txs - Appendix C, shared payment hash" $ do - commit <- assertJust "commitment" vecCommitSameHash - keys <- assertJust "keys" vecKeys + commit <- vecCommitSameHash + keys <- vecKeys let outs = B5.classify_local_commit_outputs commit keys - (ToSelfDelay 144) (ChannelFeatures False) - [vecHtlc1, vecHtlc5, vecHtlc6] + (ToSelfDelay 144) StaticRemotekey case outs of o0 : o1 : o2 : _ -> do stx0 <- vecHtlcTx (FeeratePerKw 253) o0 assertHtlcTx stx0 vecCHtlcSuccess1 stx1 <- vecHtlcTx (FeeratePerKw 253) o1 assertHtlcTx stx1 vecCHtlcTimeout6 - assertSighashFlags stx1 SIGHASH_ALL vecCHtlcTimeout6 + assertSighashFlags stx1 StaticRemotekey vecCHtlcTimeout6 stx2 <- vecHtlcTx (FeeratePerKw 253) o2 assertHtlcTx stx2 vecCHtlcTimeout5 _ -> assertFailure "expected at least 3 outputs" , testCase "classify_remote - HTLCs sharing a payment hash" $ do - commit <- assertJust "commitment" vecCommitSameHash - keys <- assertJust "keys" vecKeys + commit <- vecCommitSameHash + keys <- vecKeys + h1 <- vecHtlc1 + h5 <- vecHtlc5 + h6 <- vecHtlc6 let outs = B5.classify_remote_commit_outputs commit keys - (ChannelFeatures False) - [vecHtlc1, vecHtlc5, vecHtlc6] map B5.uo_type (take 3 outs) @?= - [ B5.SpendHTLCTimeoutDirect vecHtlc1 - , B5.SpendHTLCPreimageDirect vecHtlc6 - , B5.SpendHTLCPreimageDirect vecHtlc5 + [ B5.SpendHTLCTimeoutDirect h1 + , B5.SpendHTLCPreimageDirect h6 + , B5.SpendHTLCPreimageDirect h5 ] , testCase "classify_local - HTLCs differing only in amount" $ do - let hA = vecHtlc HTLCOffered 5000000 600 0x07 - hB = vecHtlc HTLCOffered 9000000 600 0x07 - features = ChannelFeatures False - commit <- assertJust "commitment" $ vecCommitment - (FeeratePerKw 253) features (MilliSatoshi 6000000000) - [hA, hB] - keys <- assertJust "keys" vecKeys + hA <- vecHtlc HTLCOffered 5000000 600 0x07 + hB <- vecHtlc HTLCOffered 9000000 600 0x07 + commit <- vecCommitment (FeeratePerKw 253) StaticRemotekey + 6000000000 [hB, hA] + keys <- vecKeys let outs = B5.classify_local_commit_outputs commit keys - (ToSelfDelay 144) features [hB, hA] - [ (B5.uo_value o, h) + (ToSelfDelay 144) StaticRemotekey + [ (BOLT1.un_satoshi (B5.uo_value o), h) | o <- outs , B5.SpendHTLCTimeout h _ _ <- [B5.uo_type o] ] - @?= [(Satoshi 5000, hA), (Satoshi 9000, hB)] - - , testCase "classify_local - each HTLC matches one output" $ do - let h = vecHtlc HTLCOffered 5000000 600 0x07 - features = ChannelFeatures False - commit <- assertJust "commitment" $ vecCommitment - (FeeratePerKw 253) features (MilliSatoshi 6000000000) - [h, h] - keys <- assertJust "keys" vecKeys - let timeouts hs = length + @?= [(5000, hA), (9000, hB)] + + , testCase "classify_local - one resolution per HTLC output" $ do + h <- vecHtlc HTLCOffered 5000000 600 0x07 + keys <- vecKeys + let timeouts commit = length [ () | o <- B5.classify_local_commit_outputs commit keys - (ToSelfDelay 144) features hs + (ToSelfDelay 144) StaticRemotekey , B5.SpendHTLCTimeout {} <- [B5.uo_type o] ] - timeouts [h] @?= 1 - timeouts [h, h] @?= 2 + one <- vecCommitment (FeeratePerKw 253) StaticRemotekey + 6000000000 [h] + two <- vecCommitment (FeeratePerKw 253) StaticRemotekey + 6000000000 [h, h] + timeouts one @?= 1 + timeouts two @?= 2 ] -- spend tests -------------------------------------------------------- spend_tests :: TestTree spend_tests = testGroup "Spend" [ - testCase "spend_to_local produces valid tx" $ do - stx <- assertJust "spend_to_local" $ - B5.spend_to_local - dummyOutPoint - (Satoshi 100000) - dummyRevocationPubkey - dummyDelay - dummyLocalDelayedPubkey - dummyDestScript - dummyFeerate + testCase "spend_to_local produces valid tx" $ with_fx $ \fx -> do + value <- sat 100000 + stx <- need "spend_to_local" $ + B5.spend_to_local dummyOutPoint value (fx_revpk fx) dummyDelay + (fx_delayed fx) dummyDestScript dummyFeerate let tx = B5.stx_tx stx - -- Version should be 2 tx_version tx @?= 2 - -- Single input length (tx_inputs tx) @?= 1 - -- Single output length (tx_outputs tx) @?= 1 - -- Output value should be less than input - let outVal = txout_value - (head' (tx_outputs tx)) - assertBool "output < input" - (outVal < 100000) - -- Sighash should be ALL + assertBool "output < input" (out_value stx < 100000) B5.stx_sighash_type stx @?= SIGHASH_ALL - -- Input value should match - B5.stx_input_value stx @?= Satoshi 100000 - -- nSequence should encode delay - let inp = head' (tx_inputs tx) - txin_sequence inp @?= fromIntegral - (unToSelfDelay dummyDelay) - - , testCase "spend_to_local fee deduction" $ do - let value = Satoshi 100000 - 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 - (B5.to_local_penalty_input_weight - + B5.penalty_tx_base_weight) - Satoshi outVal @?= - Satoshi (unSatoshi value - - unSatoshi expectedFee) - - , testCase "spend_revoked_to_local nSequence" $ do - 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 - - , testCase "spend_anchor_owner tx structure" $ do - let stx = B5.spend_anchor_owner - dummyOutPoint (Satoshi 330) - dummyFundingPubkey dummyDestScript + B5.stx_input_value stx @?= value + B5.stx_input_script stx + @?= to_local_script (fx_revpk fx) dummyDelay (fx_delayed fx) + -- nSequence encodes the delay + in_sequence stx @?= 144 + + , testCase "spend_to_local fee deduction" $ with_fx $ \fx -> do + value <- sat 100000 + stx <- need "spend_to_local" $ + B5.spend_to_local dummyOutPoint value (fx_revpk fx) dummyDelay + (fx_delayed fx) dummyDestScript dummyFeerate + let fee = B5.spending_fee dummyFeerate + (B5.to_local_penalty_input_weight + B5.penalty_tx_base_weight) + out_value stx @?= 100000 - BOLT1.un_satoshi fee + + , testCase "spend_revoked_to_local nSequence" $ with_fx $ \fx -> do + value <- sat 100000 + stx <- need "spend_revoked_to_local" $ + B5.spend_revoked_to_local dummyOutPoint value (fx_revpk fx) + dummyDelay (fx_delayed fx) dummyDestScript dummyFeerate + in_sequence stx @?= 0xFFFFFFFF + B5.stx_input_script stx + @?= to_local_script (fx_revpk fx) dummyDelay (fx_delayed fx) + + , testCase "spend_anchor_owner tx structure" $ with_fx $ \fx -> do + value <- sat 330 + let stx = B5.spend_anchor_owner dummyOutPoint value + (fx_funding fx) dummyDestScript tx = B5.stx_tx stx tx_version tx @?= 2 tx_locktime tx @?= 0 B5.stx_sighash_type stx @?= SIGHASH_ALL + B5.stx_input_script stx @?= anchor_script (fx_funding fx) + + , testCase "spend_anchor_anyone nSequence" $ with_fx $ \fx -> do + value <- sat 330 + let stx = B5.spend_anchor_anyone dummyOutPoint value + (fx_funding fx) dummyDestScript + in_sequence stx @?= 16 + + , testCase "spend_htlc_output is spend_to_local" $ with_fx $ \fx -> do + value <- sat 50000 + let spend f = f dummyOutPoint value (fx_revpk fx) dummyDelay + (fx_delayed fx) dummyDestScript dummyFeerate + stx1 <- need "spend_to_local" (spend B5.spend_to_local) + stx2 <- need "spend_htlc_output" (spend B5.spend_htlc_output) + stx1 @?= stx2 + + , testCase "spend_to_local fee underflow" $ with_fx $ \fx -> do + value <- sat 1 + B5.spend_to_local dummyOutPoint value (fx_revpk fx) dummyDelay + (fx_delayed fx) dummyDestScript dummyFeerate + @?= Nothing - , testCase "spend_anchor_anyone nSequence" $ do - let stx = B5.spend_anchor_anyone - dummyOutPoint (Satoshi 330) - dummyFundingPubkey dummyDestScript - tx = B5.stx_tx stx - inp = head' (tx_inputs tx) - txin_sequence inp @?= 16 - - , testCase "spend_htlc_output is spend_to_local" $ do - 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 - - , testCase "spend_to_local fee underflow" $ do - B5.spend_to_local - dummyOutPoint (Satoshi 1) - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript dummyFeerate - @?= Nothing - - , testCase "spend_to_local dust threshold by script" $ do - let fee = unSatoshi $ B5.spending_fee dummyFeerate - (B5.to_local_penalty_input_weight - + B5.penalty_tx_base_weight) - sweep dest v = B5.spend_to_local - dummyOutPoint (Satoshi v) - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey dest dummyFeerate - outValue = fmap - (txout_value . head' . tx_outputs . B5.stx_tx) + , testCase "spend_to_local dust threshold by script" $ with_fx $ \fx -> do + let fee = BOLT1.un_satoshi $ B5.spending_fee dummyFeerate + (B5.to_local_penalty_input_weight + B5.penalty_tx_base_weight) + sweep dest v = do + value <- sat v + pure $ B5.spend_to_local dummyOutPoint value (fx_revpk fx) + dummyDelay (fx_delayed fx) dest dummyFeerate check (name, dest, dust) = do + at <- sweep dest (fee + dust) assertEqual (name <> " at threshold") (Just dust) - (outValue (sweep dest (fee + dust))) - assertEqual (name <> " below threshold") Nothing - (sweep dest (fee + dust - 1)) + (fmap out_value at) + below <- sweep dest (fee + dust - 1) + assertEqual (name <> " below threshold") Nothing below mapM_ check [ ("p2pkh", Script $ BS.pack ([0x76, 0xa9, 0x14] <> replicate 20 0xCC <> [0x88, 0xac]), 546) @@ -1009,60 +910,69 @@ spend_tests = testGroup "Spend" [ , ("p2tr", Script $ BS.pack ([0x51, 0x20] <> replicate 32 0xCC), 330) ] - ] -- htlc spend tests --------------------------------------------------- htlc_spend_tests :: TestTree htlc_spend_tests = testGroup "HTLC Spend" [ - testCase "spend_htlc_timeout produces valid tx" $ do - let stx = B5.spend_htlc_timeout - dummyHTLCContext dummyKeys - tx = B5.stx_tx stx + testCase "spend_htlc_tx - HTLC-timeout" $ with_fx $ \fx -> do + stx <- need "spend_htlc_tx" $ B5.spend_htlc_tx + (htlc_context fx (fx_offered fx) StaticRemotekey) (fx_keys fx) + let tx = B5.stx_tx stx tx_version tx @?= 2 length (tx_inputs tx) @?= 1 length (tx_outputs tx) @?= 1 + tx_locktime tx @?= 500000 + in_sequence stx @?= 0 B5.stx_sighash_type stx @?= SIGHASH_ALL B5.stx_input_value stx - @?= msatToSat (MilliSatoshi 1000000) + @?= BOLT1.msat_to_sat (htlc_amount_msat (fx_offered fx)) + B5.stx_input_script stx @?= + htlc_witness_script (fx_keys fx) StaticRemotekey (fx_offered fx) - , testCase "spend_htlc_success produces valid tx" $ do - let ctx = dummyHTLCContext - { hc_htlc = dummyReceivedHTLC } - stx = B5.spend_htlc_success ctx dummyKeys - tx = B5.stx_tx stx + , testCase "spend_htlc_tx - HTLC-success" $ with_fx $ \fx -> do + stx <- need "spend_htlc_tx" $ B5.spend_htlc_tx + (htlc_context fx (fx_received fx) StaticRemotekey) (fx_keys fx) + let tx = B5.stx_tx stx tx_version tx @?= 2 length (tx_inputs tx) @?= 1 + tx_locktime tx @?= 0 B5.stx_sighash_type stx @?= SIGHASH_ALL - - , testCase "spend_htlc_timeout anchors sighash" $ do - let ctx = dummyHTLCContext - { hc_features = dummyFeaturesAnchors } - stx = B5.spend_htlc_timeout ctx dummyKeys - B5.stx_sighash_type stx @?= SIGHASH_ALL - - , testCase "spend_htlc_success anchors sighash" $ do - let ctx = dummyHTLCContext - { hc_htlc = dummyReceivedHTLC - , hc_features = dummyFeaturesAnchors - } - stx = B5.spend_htlc_success ctx dummyKeys - B5.stx_sighash_type stx @?= SIGHASH_ALL + B5.stx_input_script stx @?= + htlc_witness_script (fx_keys fx) StaticRemotekey (fx_received fx) + + , testCase "spend_htlc_tx - HTLC-timeout anchors sighash" $ with_fx $ + \fx -> do + stx <- need "spend_htlc_tx" $ B5.spend_htlc_tx + (htlc_context fx (fx_offered fx) Anchors) (fx_keys fx) + B5.stx_sighash_type stx @?= SIGHASH_ALL + in_sequence stx @?= 1 + + , testCase "spend_htlc_tx - HTLC-success anchors sighash" $ with_fx $ + \fx -> do + stx <- need "spend_htlc_tx" $ B5.spend_htlc_tx + (htlc_context fx (fx_received fx) Anchors) (fx_keys fx) + B5.stx_sighash_type stx @?= SIGHASH_ALL + + , testCase "spend_htlc_tx - fee exceeds amount" $ with_fx $ \fx -> do + -- 5000 * 663 / 1000 = 3315 sat, more than the 1000 sat HTLC + let ctx = (htlc_context fx (fx_offered fx) StaticRemotekey) + { hc_feerate = FeeratePerKw 5000 } + B5.spend_htlc_tx ctx (fx_keys fx) @?= Nothing , testCase "HTLC txs - Appendix F (anchors)" $ do -- The local signature uses SIGHASH_ALL and the remote one -- SIGHASH_SINGLE|SIGHASH_ANYONECANPAY. - commit <- assertJust "commitment" vecCommitAnchors - keys <- assertJust "keys" vecKeys + commit <- vecCommitAnchors + keys <- vecKeys let outs = B5.classify_local_commit_outputs commit keys - (ToSelfDelay 144) (ChannelFeatures True) - [vecHtlc0, vecHtlc1, vecHtlc2, vecHtlc3, vecHtlc4] + (ToSelfDelay 144) Anchors check uo vec = do stx <- vecHtlcTx (FeeratePerKw 644) uo assertHtlcTx stx vec B5.stx_sighash_type stx @?= SIGHASH_ALL - assertSighashFlags stx SIGHASH_SINGLE_ANYONECANPAY vec + assertSighashFlags stx Anchors vec case drop 3 outs of o3 : o4 : _ -> do check o3 vecFHtlcTimeout2 @@ -1074,139 +984,121 @@ htlc_spend_tests = testGroup "HTLC Spend" [ remote_spend_tests :: TestTree remote_spend_tests = testGroup "Remote Spend" [ - testCase "spend_remote_htlc_timeout structure" $ do - stx <- assertJust "spend_remote_htlc_timeout" $ - B5.spend_remote_htlc_timeout - dummyOutPoint (Satoshi 50000) - dummyHTLC dummyKeys dummyFeatures - dummyDestScript dummyFeerate + testCase "spend_remote_htlc_timeout structure" $ with_fx $ \fx -> do + value <- sat 50000 + stx <- need "spend_remote_htlc_timeout" $ + B5.spend_remote_htlc_timeout dummyOutPoint value (fx_offered fx) + (fx_keys fx) StaticRemotekey dummyDestScript dummyFeerate let tx = B5.stx_tx stx tx_version tx @?= 2 length (tx_inputs tx) @?= 1 B5.stx_sighash_type stx @?= SIGHASH_ALL - B5.stx_input_value stx @?= Satoshi 50000 - -- locktime should be HTLC CLTV expiry + B5.stx_input_value stx @?= value + B5.stx_input_script stx @?= + htlc_witness_script (fx_keys fx) StaticRemotekey (fx_received fx) + -- locktime is the HTLC's CLTV expiry tx_locktime tx @?= 500000 - , testCase "spend_remote_htlc_timeout fee deduction" $ do - let value = Satoshi 50000 - stx <- assertJust "spend_remote_htlc_timeout" $ - B5.spend_remote_htlc_timeout - dummyOutPoint value dummyHTLC - dummyKeys dummyFeatures + , testCase "spend_remote_htlc_timeout fee deduction" $ with_fx $ + \fx -> do + value <- sat 50000 + stx <- need "spend_remote_htlc_timeout" $ + B5.spend_remote_htlc_timeout dummyOutPoint value + (fx_offered fx) (fx_keys fx) StaticRemotekey + dummyDestScript dummyFeerate + let fee = B5.spending_fee dummyFeerate + (B5.accepted_htlc_penalty_input_weight + + B5.penalty_tx_base_weight) + out_value stx @?= 50000 - BOLT1.un_satoshi fee + + , testCase "spend_remote_htlc_preimage structure" $ with_fx $ \fx -> do + value <- sat 50000 + stx <- need "spend_remote_htlc_preimage" $ + B5.spend_remote_htlc_preimage dummyOutPoint value + (fx_received fx) (fx_keys fx) StaticRemotekey 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 - 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 + B5.stx_input_script stx @?= + htlc_witness_script (fx_keys fx) StaticRemotekey (fx_offered fx) + -- locktime is 0 for preimage claims tx_locktime tx @?= 0 - , testCase "spend_remote_htlc_timeout anchors seq" $ - do - stx <- assertJust "spend_remote_htlc_timeout" $ - B5.spend_remote_htlc_timeout - dummyOutPoint (Satoshi 50000) - dummyHTLC dummyKeys dummyFeaturesAnchors + , testCase "spend_remote_htlc_timeout anchors seq" $ with_fx $ \fx -> do + value <- sat 50000 + stx <- need "spend_remote_htlc_timeout" $ + B5.spend_remote_htlc_timeout dummyOutPoint value (fx_offered fx) + (fx_keys fx) Anchors dummyDestScript dummyFeerate + in_sequence stx @?= 1 + + , testCase "spend_remote_htlc_timeout no-anchor seq" $ with_fx $ + \fx -> do + value <- sat 50000 + stx <- need "spend_remote_htlc_timeout" $ + B5.spend_remote_htlc_timeout dummyOutPoint value + (fx_offered fx) (fx_keys fx) StaticRemotekey + dummyDestScript dummyFeerate + in_sequence stx @?= 0 + + , testCase "spend_remote_htlc_preimage nSequence" $ with_fx $ \fx -> do + value <- sat 50000 + stx <- need "spend_remote_htlc_preimage" $ + B5.spend_remote_htlc_preimage dummyOutPoint value + (fx_received fx) (fx_keys fx) StaticRemotekey dummyDestScript dummyFeerate - let tx = B5.stx_tx stx - inp = head' (tx_inputs tx) - txin_sequence inp @?= 1 - - , testCase "spend_remote_htlc_timeout no-anchor seq" $ - do - 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 - inp = head' (tx_inputs tx) - txin_sequence inp @?= 0 - - , testCase "spend_remote_htlc_preimage nSequence" $ do - 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 - inp = head' (tx_inputs tx) - txin_sequence inp @?= 0 + in_sequence stx @?= 0 ] -- revoked htlc spend tests ------------------------------------------ revoked_htlc_spend_tests :: TestTree revoked_htlc_spend_tests = testGroup "Revoked HTLC Spend" [ - testCase "spend_revoked_htlc - offered" $ do - case B5.spend_revoked_htlc - dummyOutPoint (Satoshi 50000) - (B5.HTLCOfferedOutput (CltvExpiry 500000)) - dummyRevocationPubkey dummyKeys - dummyFeatures dummyPaymentHash - dummyDestScript dummyFeerate of - Nothing -> assertFailure "expected Just" - Just stx -> do - let tx = B5.stx_tx stx - tx_version tx @?= 2 - B5.stx_sighash_type stx @?= SIGHASH_ALL - let inp = head' (tx_inputs tx) - txin_sequence inp @?= 0xFFFFFFFF - - , testCase "spend_revoked_htlc - received" $ do - case B5.spend_revoked_htlc - dummyOutPoint (Satoshi 50000) - (B5.HTLCReceivedOutput - (CltvExpiry 500000)) - dummyRevocationPubkey dummyKeys - dummyFeatures dummyPaymentHash - dummyDestScript dummyFeerate of - Nothing -> assertFailure "expected Just" - Just stx -> do - B5.stx_sighash_type stx @?= SIGHASH_ALL - let tx = B5.stx_tx stx - outVal = txout_value - (head' (tx_outputs tx)) - assertBool "output < input" - (outVal < 50000) - - , testCase "htlcOutputType - valid HTLC types" $ do - B5.htlcOutputType - (OutputOfferedHTLC (CltvExpiry 500)) - @?= Just (B5.HTLCOfferedOutput (CltvExpiry 500)) - B5.htlcOutputType - (OutputReceivedHTLC (CltvExpiry 600)) - @?= Just (B5.HTLCReceivedOutput - (CltvExpiry 600)) - - , testCase "htlcOutputType - non-HTLC types" $ do - B5.htlcOutputType OutputToLocal @?= Nothing - B5.htlcOutputType OutputToRemote @?= Nothing - B5.htlcOutputType OutputLocalAnchor @?= Nothing - B5.htlcOutputType OutputRemoteAnchor @?= Nothing - - , testCase "spend_revoked_htlc_output structure" $ do - stx <- assertJust "spend_revoked_htlc_output" $ - B5.spend_revoked_htlc_output - dummyOutPoint (Satoshi 50000) - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript dummyFeerate - let tx = B5.stx_tx stx - tx_version tx @?= 2 + testCase "spend_revoked_htlc - offered" $ with_fx $ \fx -> do + value <- sat 50000 + stx <- need "spend_revoked_htlc" $ + B5.spend_revoked_htlc dummyOutPoint value + (B5.HTLCOfferedOutput (CltvExpiry 500000)) (fx_revpk fx) + (fx_keys fx) StaticRemotekey (fx_hash fx) dummyDestScript + dummyFeerate + tx_version (B5.stx_tx stx) @?= 2 B5.stx_sighash_type stx @?= SIGHASH_ALL - let inp = head' (tx_inputs tx) - txin_sequence inp @?= 0xFFFFFFFF + in_sequence stx @?= 0xFFFFFFFF + B5.stx_input_script stx @?= + htlc_witness_script (fx_keys fx) StaticRemotekey (fx_offered fx) + + , testCase "spend_revoked_htlc - received" $ with_fx $ \fx -> do + value <- sat 50000 + stx <- need "spend_revoked_htlc" $ + B5.spend_revoked_htlc dummyOutPoint value + (B5.HTLCReceivedOutput (CltvExpiry 500000)) (fx_revpk fx) + (fx_keys fx) StaticRemotekey (fx_hash fx) dummyDestScript + dummyFeerate + B5.stx_sighash_type stx @?= SIGHASH_ALL + assertBool "output < input" (out_value stx < 50000) + B5.stx_input_script stx @?= + htlc_witness_script (fx_keys fx) StaticRemotekey (fx_received fx) + + , testCase "htlc_output_type - HTLC outputs" $ with_fx $ \fx -> do + B5.htlc_output_type (OutputHTLC (fx_offered fx)) + @?= Just (B5.HTLCOfferedOutput (CltvExpiry 500000)) + B5.htlc_output_type (OutputHTLC (fx_received fx)) + @?= Just (B5.HTLCReceivedOutput (CltvExpiry 500000)) + + , testCase "htlc_output_type - non-HTLC outputs" $ do + B5.htlc_output_type OutputToLocal @?= Nothing + B5.htlc_output_type OutputToRemote @?= Nothing + B5.htlc_output_type OutputLocalAnchor @?= Nothing + B5.htlc_output_type OutputRemoteAnchor @?= Nothing + + , testCase "spend_revoked_htlc_output structure" $ with_fx $ \fx -> do + value <- sat 50000 + stx <- need "spend_revoked_htlc_output" $ + B5.spend_revoked_htlc_output dummyOutPoint value (fx_revpk fx) + dummyDelay (fx_delayed fx) dummyDestScript dummyFeerate + tx_version (B5.stx_tx stx) @?= 2 + B5.stx_sighash_type stx @?= SIGHASH_ALL + in_sequence stx @?= 0xFFFFFFFF ] -- weight tests ------------------------------------------------------- @@ -1229,281 +1121,157 @@ weight_tests = testGroup "Weight" [ B5.accepted_htlc_penalty_input_weight_anchors @?= (164 + B5.accepted_htlc_penalty_witness_weight_anchors) - , testCase "penalty witness weights match HTLC scripts" $ do - -- 1 (item count) + 1 + 73 (signature) + 1 + 33 - -- (revocationpubkey) + 1 + script, for a 3-byte CLTV expiry - let witnessWeight script = - fromIntegral (110 + BS.length (unScript script)) - offered = offered_htlc_script - dummyRevocationPubkey dummyRemoteHtlcPubkey - dummyLocalHtlcPubkey dummyPaymentHash - received = received_htlc_script - dummyRevocationPubkey dummyRemoteHtlcPubkey - dummyLocalHtlcPubkey dummyPaymentHash - (CltvExpiry 500000) - witnessWeight (offered dummyFeatures) - @?= B5.offered_htlc_penalty_witness_weight - witnessWeight (received dummyFeatures) - @?= B5.accepted_htlc_penalty_witness_weight - witnessWeight (offered dummyFeaturesAnchors) - @?= B5.offered_htlc_penalty_witness_weight_anchors - witnessWeight (received dummyFeaturesAnchors) - @?= B5.accepted_htlc_penalty_witness_weight_anchors - - , testCase "spend_revoked_htlc weight by channel type" $ do - let value = 50000 - outValue features htype = fmap - (txout_value . head' . tx_outputs . B5.stx_tx) - (B5.spend_revoked_htlc dummyOutPoint (Satoshi value) - htype dummyRevocationPubkey dummyKeys features - dummyPaymentHash dummyDestScript dummyFeerate) - expected inputWeight = Just $ value - unSatoshi - (B5.spending_fee dummyFeerate - (inputWeight + B5.penalty_tx_base_weight)) - offered = B5.HTLCOfferedOutput (CltvExpiry 500000) - received = B5.HTLCReceivedOutput (CltvExpiry 500000) - outValue dummyFeatures offered - @?= expected B5.offered_htlc_penalty_input_weight - outValue dummyFeatures received - @?= expected B5.accepted_htlc_penalty_input_weight - outValue dummyFeaturesAnchors offered - @?= expected B5.offered_htlc_penalty_input_weight_anchors - outValue dummyFeaturesAnchors received - @?= expected B5.accepted_htlc_penalty_input_weight_anchors - - , testCase "max HTLCs in single penalty tx" $ do - -- Per spec: (400000 - 324 - 272 - 212 - 2) / 413 - -- = 399190 / 413 = 966 - let maxHtlcs = (B5.max_standard_weight - - B5.to_local_penalty_input_weight - - B5.to_remote_input_weight - - B5.penalty_tx_base_weight - - 2) `div` - B5.accepted_htlc_penalty_input_weight - assertBool "can sweep 483 bidirectional HTLCs" - (maxHtlcs >= 966) + , testCase "penalty witness weights match HTLC scripts" $ with_fx $ + \fx -> do + -- 1 (item count) + 1 + 73 (signature) + 1 + 33 + -- (revocationpubkey) + 1 + script, for a 3-byte CLTV expiry + let witnessWeight fmt h = + let Script s = htlc_witness_script (fx_keys fx) fmt h + in fromIntegral (110 + BS.length s) + witnessWeight StaticRemotekey (fx_offered fx) + @?= B5.offered_htlc_penalty_witness_weight + witnessWeight StaticRemotekey (fx_received fx) + @?= B5.accepted_htlc_penalty_witness_weight + witnessWeight Anchors (fx_offered fx) + @?= B5.offered_htlc_penalty_witness_weight_anchors + witnessWeight Anchors (fx_received fx) + @?= B5.accepted_htlc_penalty_witness_weight_anchors + + , testCase "spend_revoked_htlc weight by commitment format" $ with_fx $ + \fx -> do + value <- sat 50000 + let outValue fmt htype = fmap out_value + (B5.spend_revoked_htlc dummyOutPoint value htype + (fx_revpk fx) (fx_keys fx) fmt (fx_hash fx) + dummyDestScript dummyFeerate) + expected inputWeight = Just $ 50000 - BOLT1.un_satoshi + (B5.spending_fee dummyFeerate + (inputWeight + B5.penalty_tx_base_weight)) + offered = B5.HTLCOfferedOutput (CltvExpiry 500000) + received = B5.HTLCReceivedOutput (CltvExpiry 500000) + outValue StaticRemotekey offered + @?= expected B5.offered_htlc_penalty_input_weight + outValue StaticRemotekey received + @?= expected B5.accepted_htlc_penalty_input_weight + outValue Anchors offered + @?= expected B5.offered_htlc_penalty_input_weight_anchors + outValue Anchors received + @?= expected B5.accepted_htlc_penalty_input_weight_anchors + + , testCase "max HTLCs in single penalty tx" $ + -- Per spec: (400000 - 324 - 272 - (4 * 53) - 2) / 413 = 966, + -- where penalty_tx_base_weight = 4 * 53 + 2. + (B5.max_standard_weight + - B5.to_local_penalty_input_weight + - B5.to_remote_input_weight + - B5.penalty_tx_base_weight) + `div` B5.accepted_htlc_penalty_input_weight + @?= 966 ] -- property tests ----------------------------------------------------- property_tests :: TestTree property_tests = testGroup "Properties" [ - testProperty "spending_fee always non-negative" $ - \(NonNegative rate) (NonNegative weight) -> - let fee = B5.spending_fee - (FeeratePerKw (fromIntegral (rate :: Int))) - (fromIntegral (weight :: Int)) - in unSatoshi fee >= 0 - - , testProperty "spend_to_local fee deduction correct" $ - \(Positive val) -> - let value = Satoshi - (fromIntegral (val :: Int) + 100000) - expectedFee = B5.spending_fee - (FeeratePerKw 253) - (B5.to_local_penalty_input_weight - + B5.penalty_tx_base_weight) - 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 "sweeps never create dust outputs" $ - forAll (choose (0, 3000 :: Int)) $ \v -> - let value = Satoshi (fromIntegral v) - sweeps = - [ B5.spend_to_local dummyOutPoint value - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey dummyDestScript - dummyFeerate + testProperty "output + fee = input (spend_to_local)" $ prop_fx $ + \fx -> forAll (choose (100000, 2100000000000000)) $ \v -> + let fee = B5.spending_fee dummyFeerate + (B5.to_local_penalty_input_weight + B5.penalty_tx_base_weight) + spent = do + value <- BOLT1.satoshi v + B5.spend_to_local dummyOutPoint value (fx_revpk fx) + dummyDelay (fx_delayed fx) dummyDestScript dummyFeerate + in fmap (\stx -> out_value stx + BOLT1.un_satoshi fee) spent + === Just v + + , testProperty "sweeps never create dust outputs" $ prop_fx $ \fx -> + forAll (choose (0, 3000)) $ \v -> + let sweeps value = + [ B5.spend_to_local dummyOutPoint value (fx_revpk fx) + dummyDelay (fx_delayed fx) dummyDestScript dummyFeerate , B5.spend_remote_htlc_timeout dummyOutPoint value - dummyHTLC dummyKeys dummyFeatures + (fx_offered fx) (fx_keys fx) StaticRemotekey dummyDestScript dummyFeerate , B5.spend_remote_htlc_preimage dummyOutPoint value - dummyReceivedHTLC dummyKeys dummyFeatures + (fx_received fx) (fx_keys fx) StaticRemotekey dummyDestScript dummyFeerate , B5.spend_revoked_to_local dummyOutPoint value - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey dummyDestScript - dummyFeerate + (fx_revpk fx) dummyDelay (fx_delayed fx) + dummyDestScript dummyFeerate , B5.spend_revoked_htlc dummyOutPoint value (B5.HTLCOfferedOutput (CltvExpiry 500000)) - dummyRevocationPubkey dummyKeys dummyFeatures - dummyPaymentHash dummyDestScript dummyFeerate + (fx_revpk fx) (fx_keys fx) StaticRemotekey + (fx_hash fx) dummyDestScript dummyFeerate , B5.spend_revoked_htlc dummyOutPoint value (B5.HTLCReceivedOutput (CltvExpiry 500000)) - dummyRevocationPubkey dummyKeys dummyFeatures - dummyPaymentHash dummyDestScript dummyFeerate + (fx_revpk fx) (fx_keys fx) StaticRemotekey + (fx_hash fx) dummyDestScript dummyFeerate , B5.spend_revoked_htlc_output dummyOutPoint value - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey dummyDestScript - dummyFeerate + (fx_revpk fx) dummyDelay (fx_delayed fx) + dummyDestScript dummyFeerate ] - notDust = maybe True $ \stx -> - txout_value (head' (tx_outputs (B5.stx_tx stx))) - >= 294 - in all notDust sweeps - - , testProperty "htlc_timed_out monotonic" $ - \(NonNegative h1) (NonNegative h2) -> - let height1 = fromIntegral (h1 :: Int) - height2 = fromIntegral (h2 :: Int) - htlc = dummyHTLC - { htlc_cltv_expiry = CltvExpiry 1000 } - in if height1 <= height2 - then not (B5.htlc_timed_out height1 htlc) - || B5.htlc_timed_out height2 htlc - else True - - , testProperty "output + fee = input (spend_to_local)" $ - \(Positive val) -> - let value = Satoshi - (fromIntegral (val :: Int) + 100000) - fee = B5.spending_fee (FeeratePerKw 253) - (B5.to_local_penalty_input_weight - + B5.penalty_tx_base_weight) - 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 outVal + unSatoshi fee == - unSatoshi value + notDust = maybe True (\stx -> out_value stx >= 294) + in case BOLT1.satoshi v of + Nothing -> counterexample "invalid amount" False + Just value -> property (all notDust (sweeps value)) + + , testProperty "htlc_timed_out monotonic" $ prop_fx $ + \fx (NonNegative h1) (NonNegative h2) -> + let htlc = (fx_offered fx) { htlc_cltv_expiry = CltvExpiry 1000 } + lo = min h1 h2 + hi = max h1 h2 + in not (B5.htlc_timed_out lo htlc) || B5.htlc_timed_out hi htlc , testProperty "fee monotonic in feerate" $ - \(Positive r1) (Positive r2) -> - let rate1 = fromIntegral (r1 :: Int) - rate2 = fromIntegral (r2 :: Int) - w = B5.to_local_penalty_input_weight - + B5.penalty_tx_base_weight - fee1 = B5.spending_fee - (FeeratePerKw rate1) w - fee2 = B5.spending_fee - (FeeratePerKw rate2) w - in if rate1 <= rate2 - then unSatoshi fee1 <= unSatoshi fee2 - else unSatoshi fee1 >= unSatoshi fee2 - - , testProperty "classify preserves output count" $ - \(NonNegative n') -> - let n = min (n' :: Int) 10 - outputs = - [ TxOutput (Satoshi 50000) toLocalScript - OutputToLocal - | _ <- [1..n] ] - commitTx = CommitmentTx - { ctx_version = 2 - , ctx_locktime = Locktime 0 - , ctx_input_outpoint = dummyOutPoint - , ctx_input_sequence = Sequence 0 - , ctx_outputs = outputs - , ctx_funding_script = dummyDestScript - } - outs = B5.classify_local_commit_outputs - commitTx dummyKeys dummyDelay - dummyFeatures [] - in length outs == n - - , testProperty "spend_to_local always version 2" $ - \(Positive val) -> - let value = Satoshi - (fromIntegral (val :: Int) + 100000) - in case B5.spend_to_local - dummyOutPoint value - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript (FeeratePerKw 253) of - Nothing -> False - Just stx -> - tx_version (B5.stx_tx stx) == 2 - - , testProperty "revoked nSequence is 0xFFFFFFFF" $ - \(Positive val) -> - let value = Satoshi - (fromIntegral (val :: Int) + 100000) - in case B5.spend_revoked_to_local - dummyOutPoint value - dummyRevocationPubkey dummyDelay - dummyLocalDelayedPubkey - dummyDestScript (FeeratePerKw 253) of - Nothing -> False - Just stx -> - let inp = head' - (tx_inputs (B5.stx_tx stx)) - in txin_sequence inp == 0xFFFFFFFF - - , testProperty "htlcOutputType preserves expiry" $ - \expiry -> + \r1 r2 -> + let w = B5.to_local_penalty_input_weight + B5.penalty_tx_base_weight + fee r = B5.spending_fee (FeeratePerKw r) w + in fee (min r1 r2) <= fee (max r1 r2) + + , testProperty "spend_to_local always version 2" $ prop_fx $ + \fx -> forAll (choose (100000, 2100000000000000)) $ \v -> + let spent = do + value <- BOLT1.satoshi v + B5.spend_to_local dummyOutPoint value (fx_revpk fx) + dummyDelay (fx_delayed fx) dummyDestScript dummyFeerate + in fmap (tx_version . B5.stx_tx) spent === Just 2 + + , testProperty "revoked nSequence is 0xFFFFFFFF" $ prop_fx $ + \fx -> forAll (choose (100000, 2100000000000000)) $ \v -> + let spent = do + value <- BOLT1.satoshi v + B5.spend_revoked_to_local dummyOutPoint value (fx_revpk fx) + dummyDelay (fx_delayed fx) dummyDestScript dummyFeerate + in fmap in_sequence spent === Just 0xFFFFFFFF + + , testProperty "htlc_output_type preserves direction and expiry" $ + prop_fx $ \fx expiry -> let ce = CltvExpiry expiry - in B5.htlcOutputType (OutputOfferedHTLC ce) - == Just (B5.HTLCOfferedOutput ce) - && - B5.htlcOutputType (OutputReceivedHTLC ce) - == Just (B5.HTLCReceivedOutput ce) - - , testProperty "htlcOutputType rejects non-HTLC" $ - property $ - B5.htlcOutputType OutputToLocal == Nothing - && B5.htlcOutputType OutputToRemote == Nothing - && B5.htlcOutputType OutputLocalAnchor == Nothing - && B5.htlcOutputType OutputRemoteAnchor - == Nothing + at h = OutputHTLC h { htlc_cltv_expiry = ce } + in B5.htlc_output_type (at (fx_offered fx)) + == Just (B5.HTLCOfferedOutput ce) + && B5.htlc_output_type (at (fx_received fx)) + == Just (B5.HTLCReceivedOutput ce) , testProperty "revoked_output_weight by type" $ \expiry -> let ce = CltvExpiry expiry - roLocal = B5.RevokedOutput - dummyOutPoint (Satoshi 1000) - B5.RevokedToLocal - roOffered = B5.RevokedOutput - dummyOutPoint (Satoshi 1000) - (B5.RevokedHTLC - (B5.HTLCOfferedOutput ce)) - roReceived = B5.RevokedOutput - dummyOutPoint (Satoshi 1000) - (B5.RevokedHTLC - (B5.HTLCReceivedOutput ce)) - weight = B5.revoked_output_weight - in weight dummyFeatures roLocal - == B5.to_local_penalty_input_weight - && - weight dummyFeatures roOffered - == B5.offered_htlc_penalty_input_weight - && - weight dummyFeatures roReceived - == B5.accepted_htlc_penalty_input_weight - && - weight dummyFeaturesAnchors roLocal - == B5.to_local_penalty_input_weight - && - weight dummyFeaturesAnchors roOffered - == B5.offered_htlc_penalty_input_weight_anchors - && - weight dummyFeaturesAnchors roReceived - == B5.accepted_htlc_penalty_input_weight_anchors - + ro t = case BOLT1.satoshi 1000 of + Nothing -> Nothing + Just v -> Just (B5.RevokedOutput dummyOutPoint v t) + weight fmt t = fmap (B5.revoked_output_weight fmt) (ro t) + offered = B5.RevokedHTLC (B5.HTLCOfferedOutput ce) + received = B5.RevokedHTLC (B5.HTLCReceivedOutput ce) + in weight StaticRemotekey B5.RevokedToLocal + == Just B5.to_local_penalty_input_weight + && weight StaticRemotekey offered + == Just B5.offered_htlc_penalty_input_weight + && weight StaticRemotekey received + == Just B5.accepted_htlc_penalty_input_weight + && weight Anchors B5.RevokedToLocal + == Just B5.to_local_penalty_input_weight + && weight Anchors offered + == Just B5.offered_htlc_penalty_input_weight_anchors + && weight Anchors received + == Just B5.accepted_htlc_penalty_input_weight_anchors ] - --- helpers ------------------------------------------------------------ - --- | 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