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 98fd7c371aeb7c2152b33ff752533bcfe0f2f69e
parent 4908af86c5fa31f898655cfc2fad814f9f5515d2
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 15:32:09 +0400

Merge impl/modernize: port to the bolt1/bolt3 0.1.0 and tx 0.2.0 APIs

Ports ppad-bolt5 to ppad-bolt1 0.1.0, ppad-bolt3 0.1.0 and ppad-tx
0.2.0 and applies the shared bolt conventions, as version 0.1.0
(unreleased). The library's scope is being redesigned; this is a port,
not the redesign, and the README says so and lists the known gaps.

Library:

* Amounts, points and hashes are bolt1's types; ChannelFeatures is
  replaced by bolt3's CommitmentFormat. ppad-bolt1 and deepseq are
  new dependencies.
* The classifiers read each HTLC output's HTLC from bolt3's OutputHTLC
  instead of rebuilding scripts and matching a caller-supplied HTLC
  list, which they no longer take. classify_remote_commit_outputs
  drops the features argument and classify_revoked_commit_outputs
  the unused keys and features.
* spend_htlc_tx (Maybe) replaces spend_htlc_timeout and
  spend_htlc_success, using bolt3's build_htlc_tx and htlc_to_tx in
  place of the local htlc_tx_to_tx. The sweep builders share one
  helper; spend_revoked_htlc_output is an alias, like
  spend_htlc_output.
* Only Lightning.Protocol.BOLT5 is exposed; Internal is removed.
  htlcOutputType and htlcOutputTypeWeight become htlc_output_type and
  htlc_output_type_weight. NFData for every public type; spending_fee
  saturates at 21 million BTC; Haddock coverage is 100%.

Tests keep the phase-1 safety net (Appendix C/F HTLC transactions,
real-witness preimage extraction, txid-based identification, shared
payment hashes, dust thresholds, anchor weights), now on the new API,
with checks on witness scripts and outpoints, bolt3-built witnesses
for extraction, bolt3's remote_htlc_sighash for the remote flag, and
negative tests for fee saturation and unbuildable HTLC txs. Trivial
properties are dropped.

Benchmarks are rewritten over Appendix C fixtures with nf, no orphan
instances and no partial functions, and cover classification,
identification, extraction and every spend builder.

Also: the spec copy is updated to lightning/bolts@1aadb719, CLAUDE.md
and AGENTS.md drop the false LLVM-flag claim, and the README example
is checked against the current API.

Diffstat:
MAGENTS.md | 15+++++++++++----
MCHANGELOG | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MCLAUDE.md | 15+++++++++++----
MREADME.md | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Abench/Fixtures.hs | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbench/Main.hs | 210++++++++++++++++++++++++-------------------------------------------------------
Mbench/Weight.hs | 157++++++++++++++++++++++---------------------------------------------------------
Metc/05-onchain.md | 79++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mflake.lock | 100++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mflake.nix | 38++++++++++++++++++++++++++++++++++++--
Mlib/Lightning/Protocol/BOLT5.hs | 115+++++++++++++++++++++++++++++++++++++------------------------------------------
Mlib/Lightning/Protocol/BOLT5/Detect.hs | 432+++++++++++++++++++++++++++----------------------------------------------------
Dlib/Lightning/Protocol/BOLT5/Internal.hs | 26--------------------------
Mlib/Lightning/Protocol/BOLT5/Spend.hs | 542+++++++++++++++++++++++++++++++------------------------------------------------
Mlib/Lightning/Protocol/BOLT5/Types.hs | 215+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mppad-bolt5.cabal | 25+++++++++++++++++--------
Mtest/Main.hs | 1806++++++++++++++++++++++++++++++++++---------------------------------------------
17 files changed, 1959 insertions(+), 2197 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -4,13 +4,19 @@ Haskell implementation of BOLT #5 (Lightning Network on-chain transaction handling). Specification: https://github.com/lightning/bolts/blob/master/05-onchain.md +(tracked as of lightning/bolts@1aadb719; copy in `etc/05-onchain.md`). + +The library's scope is being redesigned and it is not yet ready for +release; see the README for what it does and doesn't cover. ## Project Structure -- `lib/` - library source (Lightning.Protocol.BOLT5) -- `test/` - tests (tasty + tasty-hunit) +- `lib/` - library source (public API: Lightning.Protocol.BOLT5; + submodules are internal) +- `test/` - tests (tasty, tasty-hunit, tasty-quickcheck), public API only; + BOLT #3 Appendix C/F vectors inline - `bench/` - benchmarks (criterion for timing, weigh for allocations) -- `etc/` - reference materials (BOLT spec) +- `etc/` - reference materials (BOLT spec, pinned upstream commit) - `flake.nix` - nix flake for dependency and build management - `ppad-bolt5.cabal` - cabal package definition - `CLAUDE.md` / `AGENTS.md` - keep these in sync @@ -45,6 +51,8 @@ Use only minimal external dependencies. Prefer GHC's core/boot libraries - Test dependencies (tasty, QuickCheck, etc. for test-suite only) - Benchmark dependencies (criterion, weigh for benchmark only) +Current dependencies: ppad-bolt1, ppad-bolt3, ppad-sha256, ppad-tx. + ## Code Style ### Performance @@ -140,5 +148,4 @@ When planning work: The flake.nix follows ppad conventions: - Uses ppad-nixpkgs as base - Follows references to avoid duplication -- Supports LLVM backend via cabal flag - Provides devShell with ghc, cabal, cc, llvm diff --git a/CHANGELOG b/CHANGELOG @@ -1,4 +1,60 @@ # Changelog -- 0.0.1 (UNRELEASED) - * Initial release +- 0.1.0 (UNRELEASED) + * The library's scope is being redesigned, and this version is not + yet ready for release (see the README for what it does and + doesn't cover). + + * Ports to ppad-bolt1 0.1.0, ppad-bolt3 0.1.0 and ppad-tx 0.2.0, with + breaking changes: + + * Amounts, points and hashes are bolt1's types (amounts are + bounded, with checked arithmetic), and ChannelFeatures is + replaced by bolt3's CommitmentFormat. + + * The classifiers read each HTLC output's HTLC from bolt3's + OutputHTLC, and no longer take an HTLC list. + classify_remote_commit_outputs no longer takes the channel + features, and classify_revoked_commit_outputs takes only the + commitment and the revocation pubkey. + + * spend_htlc_tx replaces spend_htlc_timeout and spend_htlc_success, + building the transaction with bolt3's build_htlc_tx, and fails + if bolt3 can't build it. + + * The API is exported from Lightning.Protocol.BOLT5 alone, with + snake_case names (htlc_output_type, htlc_output_type_weight); the + Internal module is removed. Every public type has an NFData + instance. + + * extract_preimage_offered and extract_preimage_htlc_success take + the HTLC's payment hash, and revoked_output_weight and + htlc_output_type_weight the commitment format. + + * spending_fee saturates at 21 million BTC. + + * Fixes preimage extraction, which never matched a real witness (one + ending in the witness script), and checks preimages against the + payment hash. + + * Fixes identify_close, which compared an unsigned serialization with + the signed transaction found on chain; it compares txids. + + * Fixes HTLC output classification, which matched outputs to HTLCs by + script alone, so that HTLCs sharing a payment hash were confused. + + * Fixes the sighash type of the local signature on anchor-channel + HTLC transactions, which is SIGHASH_ALL. + + * Sweeps no longer create outputs below the destination's dust + threshold, and anchor-channel HTLC penalty sweeps use the anchor + scripts' weights. + + * Removes spend_revoked_batch and PenaltyContext, whose transaction + couldn't be signed. + + * Tests check HTLC transactions and preimage extraction against the + BOLT #3 Appendix C and F vectors. + +- 0.0.1 (2026-04-18) + * Initial release. diff --git a/CLAUDE.md b/CLAUDE.md @@ -4,13 +4,19 @@ Haskell implementation of BOLT #5 (Lightning Network on-chain transaction handling). Specification: https://github.com/lightning/bolts/blob/master/05-onchain.md +(tracked as of lightning/bolts@1aadb719; copy in `etc/05-onchain.md`). + +The library's scope is being redesigned and it is not yet ready for +release; see the README for what it does and doesn't cover. ## Project Structure -- `lib/` - library source (Lightning.Protocol.BOLT5) -- `test/` - tests (tasty + tasty-hunit) +- `lib/` - library source (public API: Lightning.Protocol.BOLT5; + submodules are internal) +- `test/` - tests (tasty, tasty-hunit, tasty-quickcheck), public API only; + BOLT #3 Appendix C/F vectors inline - `bench/` - benchmarks (criterion for timing, weigh for allocations) -- `etc/` - reference materials (BOLT spec) +- `etc/` - reference materials (BOLT spec, pinned upstream commit) - `flake.nix` - nix flake for dependency and build management - `ppad-bolt5.cabal` - cabal package definition - `CLAUDE.md` / `AGENTS.md` - keep these in sync @@ -45,6 +51,8 @@ Use only minimal external dependencies. Prefer GHC's core/boot libraries - Test dependencies (tasty, QuickCheck, etc. for test-suite only) - Benchmark dependencies (criterion, weigh for benchmark only) +Current dependencies: ppad-bolt1, ppad-bolt3, ppad-sha256, ppad-tx. + ## Code Style ### Performance @@ -140,5 +148,4 @@ When planning work: The flake.nix follows ppad conventions: - Uses ppad-nixpkgs as base - Follows references to avoid duplication -- Supports LLVM backend via cabal flag - Provides devShell with ghc, cabal, cc, llvm diff --git a/README.md b/README.md @@ -4,9 +4,50 @@ ![](https://img.shields.io/badge/license-MIT-brightgreen) [![](https://img.shields.io/badge/haddock-bolt5-lightblue)](https://docs.ppad.tech/bolt5) -A pure Haskell implementation of [BOLT #5][bolt5] (Lightning Network -on-chain transaction handling), including logic for mutual close, -unilateral close, and revoked transaction close scenarios. +A pure Haskell implementation of parts of [BOLT #5][bolt5], the +Lightning Network's rules for handling channel closes on chain. + +## Status + +**This library's scope is being redesigned, and it is not yet ready for +release.** Its API will change. + +What it does: + +* Identifies our local or the remote's current commitment on chain, by + txid. +* Classifies the outputs of local, remote and revoked commitment + transactions built by [ppad-bolt3][bolt3], reading each HTLC output's + HTLC from bolt3. +* Extracts payment preimages from the witnesses of remote preimage + claims and HTLC-success transactions, checking them against the + payment hash. +* Builds unsigned, single-input spending transactions: to_local sweeps + (delayed and revoked), HTLC-timeout and HTLC-success transactions + (via bolt3), direct HTLC spends from the remote commitment, revoked + HTLC sweeps, second-stage output sweeps and anchor spends. Sweeps + refuse to create outputs below the destination's dust threshold. +* Provides the BOLT #5 Appendix A weights and fee calculation. + +What it doesn't do (known gaps): + +* Per-input signing data. A spending transaction describes one input + and the local signature only; there is no batched penalty + transaction. +* Witness assembly. The caller signs and builds every witness with + bolt3's witness functions. +* Classification of raw on-chain transactions. The classifiers take the + bolt3 commitment transaction for the state in question, which the + caller must rebuild (often impossible for a revoked state). +* Revoked and mutual close detection. Only the current local and + remote commitments are recognised, and the resolutions for a revoked + commitment lack what's needed to build its spends (to_self_delay, + delayed pubkey, payment hashes). +* Fee bumping. Anchor spends and anchor-channel HTLC transactions pay + no fee and can't take fee inputs, and penalty sweeps don't signal + replace-by-fee. +* `zero_fee_commitments` (version 3 transactions and the shared P2A + anchor), and spending the anchor-channel to_remote output. ## Usage @@ -14,56 +55,79 @@ A sample GHCi session: ``` > :set -XOverloadedStrings - > + > import qualified Bitcoin.Prim.Tx as BT + > import qualified Crypto.Hash.SHA256 as SHA256 > import qualified Data.ByteString as BS - > import qualified Lightning.Protocol.BOLT3 as BOLT3 - > import qualified Lightning.Protocol.BOLT5 as BOLT5 + > import qualified Lightning.Protocol.BOLT1 as BOLT1 + > import Lightning.Protocol.BOLT3 + > import Lightning.Protocol.BOLT5 > - > -- fee calculation using BOLT #5 Appendix A weight constants - > let feerate = BOLT3.FeeratePerKw 5000 - > BOLT5.spending_fee feerate BOLT5.to_local_penalty_input_weight - Satoshi 1620 - > BOLT5.spending_fee feerate BOLT5.penalty_tx_base_weight - Satoshi 1070 + > -- fees from the BOLT #5 Appendix A weights + > let feerate = FeeratePerKw 5000 + > let weight = to_local_penalty_input_weight + penalty_tx_base_weight + > spending_fee feerate weight + Satoshi 2690 > - > -- check if an HTLC has timed out at the current block height - > let htlc = BOLT3.HTLC BOLT3.HTLCOffered (BOLT3.MilliSatoshi 50000000) - > (BOLT3.PaymentHash (BS.replicate 32 0xAA)) - > (BOLT3.CltvExpiry 800000) - > BOLT5.htlc_timed_out 799999 htlc + > -- whether an HTLC has timed out at a given block height + > let Just amt = BOLT1.milli_satoshi 50000000 + > let Just hash = BOLT1.payment_hash (BS.replicate 32 0xaa) + > let htlc = HTLC HTLCOffered amt hash (CltvExpiry 800000) + > htlc_timed_out 799999 htlc False - > BOLT5.htlc_timed_out 800000 htlc + > htlc_timed_out 800000 htlc True > - > -- extract a payment preimage from an offered HTLC witness - > let preimage = BS.replicate 32 0xBB - > let sig = BS.replicate 71 0xCC - > let wit = BOLT3.Witness [sig, preimage] - > BOLT5.extract_preimage_offered wit - Just PaymentPreimage <redacted> - > - > -- construct an unsigned spending tx for a to_local output - > let Just txid = BOLT3.mkTxId (BS.replicate 32 0x01) - > let outpoint = BOLT3.OutPoint txid 0 - > let value = BOLT3.Satoshi 1000000 - > let revpk = BOLT3.RevocationPubkey (BOLT3.Pubkey (BS.replicate 33 0x02)) - > let delay = BOLT3.ToSelfDelay 144 - > let delayedpk = BOLT3.LocalDelayedPubkey (BOLT3.Pubkey (BS.replicate 33 0x03)) - > let dest = BOLT3.Script (BS.replicate 34 0x00) - > - > let stx = BOLT5.spend_to_local outpoint value revpk delay - > delayedpk dest feerate - > BOLT5.stx_sighash_type stx + > -- an unsigned sweep of our to_local output, valid after the delay + > let key b = BOLT1.point (BS.cons 0x02 (BS.replicate 32 b)) + > let Just revpk = RevocationPubkey <$> key 0x01 + > let Just delayedpk = LocalDelayedPubkey <$> key 0x02 + > let Just txid = BT.mkTxId (BS.replicate 32 0x01) + > let Just value = BOLT1.satoshi 1000000 + > let dest = Script ("\x00\x14" <> BS.replicate 20 0xcc) + > let delay = ToSelfDelay 144 + > let op = BT.OutPoint txid 0 + > let Just stx = spend_to_local op value revpk delay delayedpk dest feerate + > fmap BT.txout_value (BT.tx_outputs (stx_tx stx)) + 997310 :| [] + > fmap BT.txin_sequence (BT.tx_inputs (stx_tx stx)) + 144 :| [] + > stx_sighash_type stx SIGHASH_ALL + > stx_input_script stx == to_local_script revpk delay delayedpk + True + > + > -- extract the preimage from the remote's claim of an offered HTLC + > let Just preimage = BOLT1.payment_preimage (BS.replicate 32 0xbb) + > let Just ph = BOLT1.payment_hash (SHA256.hash (BS.replicate 32 0xbb)) + > let sig = BS.replicate 71 0xcc + > let wit = offered_htlc_witness_preimage sig preimage (Script "") + > extract_preimage_offered ph wit + Just (PaymentPreimage <redacted>) + > fmap (== preimage) (extract_preimage_offered ph wit) + Just True + > extract_preimage_offered hash wit + Nothing ``` ## Documentation -Haddocks are hosted at [docs.ppad.tech/bolt5][hadoc]. +Haddocks (API documentation, etc.) are hosted at +[docs.ppad.tech/bolt5](https://docs.ppad.tech/bolt5). + +## Specification + +This library tracks BOLT #5 as of lightning/bolts commit +[1aadb719][spec] (2026-09-21); a copy lives in `etc/`. BOLT #5 +publishes no test vectors beyond Appendix A's weights, which are +checked; the tests also check HTLC transactions and preimage extraction +against BOLT #3's Appendix C and F vectors. + +See [Status](#status) for what isn't implemented. ## Security -This is a pre-release and makes no claims about security whatsoever. +This library has received no security audit. It handles no secret +material: it builds unsigned transactions and reads public witnesses. ## Development @@ -77,12 +141,13 @@ $ nix develop Then do e.g.: ``` -$ cabal build -$ cabal test -$ cabal bench +$ cabal repl ppad-bolt5 ``` +to get a REPL for the main library. + [bolt5]: https://github.com/lightning/bolts/blob/master/05-onchain.md +[bolt3]: https://git.ppad.tech/bolt3 +[spec]: https://github.com/lightning/bolts/tree/1aadb719b4007c4cea0ba6e36b08c4fb53788dee [nixos]: https://nixos.org/ [flake]: https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake.html -[hadoc]: https://docs.ppad.tech/bolt5 diff --git a/bench/Fixtures.hs b/bench/Fixtures.hs @@ -0,0 +1,170 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} + +module Fixtures ( + Fixtures(..) + , fixtures + ) where + +import qualified Bitcoin.Prim.Tx as BT +import Control.DeepSeq (NFData, force) +import Control.Exception (evaluate) +import qualified Crypto.Hash.SHA256 as SHA256 +import qualified Data.ByteString as BS +import Data.Word (Word8, Word32, Word64) +import GHC.Generics (Generic) +import qualified Lightning.Protocol.BOLT1 as BOLT1 +import Lightning.Protocol.BOLT3 + +data Fixtures = Fixtures + { fx_keys :: !CommitmentKeys + , fx_commit :: !CommitmentTx + -- ^ BOLT #3 Appendix C commitment with five untrimmed HTLCs + , fx_other :: !CommitmentTx + -- ^ as 'fx_commit', with a different balance + , fx_raw :: !BS.ByteString + -- ^ serialized 'fx_commit' + , fx_outpoint :: !BT.OutPoint + , fx_value :: !BOLT1.Satoshi + , fx_anchor :: !BOLT1.Satoshi + , fx_dest :: !Script + , fx_feerate :: !FeeratePerKw + , fx_delay :: !ToSelfDelay + , fx_offered :: !HTLC + , fx_received :: !HTLC + , fx_htlc_ctx :: !HTLCContext + -- ^ for 'fx_offered' + , fx_hash :: !BOLT1.PaymentHash + -- ^ payment hash of 'fx_offered' and 'fx_received' + , fx_claim :: !BT.Witness + -- ^ remote preimage claim of an offered HTLC + , fx_success :: !BT.Witness + -- ^ HTLC-success transaction witness + } deriving Generic + +instance NFData Fixtures + +pt :: BS.ByteString -> Maybe BOLT1.Point +pt h = unhex h >>= BOLT1.point + +-- | 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 + +-- | Benchmark fixtures, from the BOLT #3 Appendix C parameters, fully +-- evaluated. +fixtures :: IO Fixtures +fixtures = maybe (fail "invalid fixtures") (evaluate . force) build + +build :: Maybe Fixtures +build = do + rev <- pt + "0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19" + delayed <- pt + "03fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c" + local_htlc <- pt + "030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e7" + remote_htlc <- pt + "0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b" + local_pay <- pt + "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" + remote_pay <- pt + "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" + local_fund <- pt + "023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb" + remote_fund <- pt + "030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1" + funding_txid <- BT.mkTxId =<< unhex + "bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489" + cn <- commitment_number 42 + dust <- BOLT1.satoshi 546 + value <- BOLT1.satoshi 100000 + anchor <- BOLT1.satoshi 330 + to_remote <- BOLT1.milli_satoshi 3000000000 + h0 <- htlc HTLCReceived 1000000 500 0x00 + h1 <- htlc HTLCReceived 2000000 501 0x01 + h2 <- htlc HTLCOffered 2000000 502 0x02 + h3 <- htlc HTLCOffered 3000000 503 0x03 + h4 <- htlc HTLCReceived 4000000 504 0x04 + preimage <- BOLT1.payment_preimage (BS.replicate 32 0x01) + let keys = CommitmentKeys + { ck_revocation_pubkey = RevocationPubkey rev + , ck_local_delayed = LocalDelayedPubkey delayed + , ck_local_htlc = LocalHtlcPubkey local_htlc + , ck_remote_htlc = RemoteHtlcPubkey remote_htlc + , ck_remote_payment = RemotePubkey remote_pay + , ck_local_funding = FundingPubkey local_fund + , ck_remote_funding = FundingPubkey remote_fund + } + ctx to_local = do + tl <- BOLT1.milli_satoshi to_local + pure CommitmentContext + { cc_funding_outpoint = BT.OutPoint funding_txid 0 + , cc_commitment_number = cn + , cc_local_payment_bp = PaymentBasepoint local_pay + , cc_remote_payment_bp = PaymentBasepoint remote_pay + , cc_to_self_delay = ToSelfDelay 144 + , cc_dust_limit = DustLimit dust + , cc_feerate = FeeratePerKw 0 + , cc_format = StaticRemotekey + , cc_is_funder = True + , cc_to_local_msat = tl + , cc_to_remote_msat = to_remote + , cc_htlcs = [h0, h1, h2, h3, h4] + , cc_keys = keys + } + sig = BS.replicate 71 0xCC + script_of h = case htlc_direction h of + HTLCOffered -> offered_htlc_script (RevocationPubkey rev) + (RemoteHtlcPubkey remote_htlc) (LocalHtlcPubkey local_htlc) + (htlc_payment_hash h) StaticRemotekey + HTLCReceived -> received_htlc_script (RevocationPubkey rev) + (RemoteHtlcPubkey remote_htlc) (LocalHtlcPubkey local_htlc) + (htlc_payment_hash h) (htlc_cltv_expiry h) StaticRemotekey + offered = h2 { htlc_payment_hash = htlc_payment_hash h1 } + commit <- build_commitment_tx =<< ctx 6988000000 + other <- build_commitment_tx =<< ctx 6987000000 + pure Fixtures + { fx_keys = keys + , fx_commit = commit + , fx_other = other + , fx_raw = encode_commitment_tx commit + , fx_outpoint = BT.OutPoint funding_txid 1 + , fx_value = value + , fx_anchor = anchor + , fx_dest = Script (BS.pack [0x00, 0x14] <> BS.replicate 20 0xCC) + , fx_feerate = FeeratePerKw 253 + , fx_delay = ToSelfDelay 144 + , fx_offered = offered + , fx_received = h1 + , fx_htlc_ctx = HTLCContext + { hc_commitment_txid = funding_txid + , hc_output_index = 2 + , hc_htlc = offered + , hc_to_self_delay = ToSelfDelay 144 + , hc_feerate = FeeratePerKw 253 + , hc_format = StaticRemotekey + , hc_revocation_pubkey = RevocationPubkey rev + , hc_local_delayed = LocalDelayedPubkey delayed + } + , fx_hash = htlc_payment_hash h1 + , fx_claim = + offered_htlc_witness_preimage sig preimage (script_of offered) + , fx_success = htlc_success_witness sig sig preimage (script_of h1) + } + where + htlc :: HTLCDirection -> Word64 -> Word32 -> Word8 -> Maybe HTLC + htlc dir amt expiry b = do + a <- BOLT1.milli_satoshi amt + ph <- BOLT1.payment_hash (SHA256.hash (BS.replicate 32 b)) + pure (HTLC dir a ph (CltvExpiry expiry)) diff --git a/bench/Main.hs b/bench/Main.hs @@ -1,153 +1,69 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} - module Main where -import Bitcoin.Prim.Tx.Sighash (SighashType(..)) -import Control.DeepSeq (NFData(..)) import Criterion.Main -import qualified Data.ByteString as BS -import Lightning.Protocol.BOLT3 hiding - (txout_value, txout_script) +import Fixtures +import Lightning.Protocol.BOLT3 import qualified Lightning.Protocol.BOLT5 as B5 --- NFData orphan instances for criterion ----------------------------- - -instance NFData Script where - rnf (Script bs) = rnf bs - -instance NFData Pubkey where - rnf pk = rnf (unPubkey pk) - -instance NFData RevocationPubkey where - rnf (RevocationPubkey x) = rnf x - -instance NFData LocalDelayedPubkey where - rnf (LocalDelayedPubkey p) = rnf p - -instance NFData FundingPubkey where - rnf (FundingPubkey p) = rnf p - -instance NFData FeeratePerKw where - rnf (FeeratePerKw x) = rnf x - -instance NFData ToSelfDelay where - rnf (ToSelfDelay x) = rnf x - -instance NFData CltvExpiry where - rnf (CltvExpiry x) = rnf x - -instance NFData HTLCDirection where - rnf HTLCOffered = () - rnf HTLCReceived = () - -instance NFData HTLC where - rnf (HTLC d a h c) = - rnf d `seq` rnf a `seq` rnf h `seq` rnf c - -instance NFData SighashType - -instance NFData B5.OutputResolution where - rnf B5.Resolved = () - rnf (B5.Revoke rk) = rnf rk - rnf _ = () - -instance NFData B5.HTLCOutputType where - rnf (B5.HTLCOfferedOutput e) = rnf e - rnf (B5.HTLCReceivedOutput e) = rnf e - -instance NFData B5.UnresolvedOutput where - rnf (B5.UnresolvedOutput op v t) = - rnf op `seq` rnf v `seq` rnf t - -instance NFData B5.RevokedOutputType where - rnf B5.RevokedToLocal = () - rnf (B5.RevokedHTLC h) = rnf h - -instance NFData B5.RevokedOutput where - rnf (B5.RevokedOutput op v t) = - rnf op `seq` rnf v `seq` rnf t - -instance NFData B5.SpendingTx where - rnf (B5.SpendingTx tx scr val sh) = - rnf tx `seq` rnf scr `seq` rnf val `seq` rnf sh - main :: IO () -main = defaultMain [ - spend_benchmarks - , classify_benchmarks - ] - --- fixtures ----------------------------------------------------------- - -dummyPubkey :: Pubkey -dummyPubkey = case pubkey (BS.pack (0x02 : replicate 32 0x01)) of - Just pk -> pk - Nothing -> error "impossible" - -dummyTxId :: TxId -dummyTxId = case mkTxId (BS.replicate 32 0x00) of - Just tid -> tid - Nothing -> error "impossible" - -dummyOutPoint :: OutPoint -dummyOutPoint = OutPoint dummyTxId 0 - -dummyRevPk :: RevocationPubkey -dummyRevPk = RevocationPubkey dummyPubkey - -dummyDelayedPk :: LocalDelayedPubkey -dummyDelayedPk = LocalDelayedPubkey dummyPubkey - -dummyFundPk :: FundingPubkey -dummyFundPk = FundingPubkey dummyPubkey - -dummyDest :: Script -dummyDest = Script $ BS.pack [0x00, 0x14] <> - BS.replicate 20 0xCC - -dummyFeerate :: FeeratePerKw -dummyFeerate = FeeratePerKw 253 - -dummyDelay :: ToSelfDelay -dummyDelay = ToSelfDelay 144 - --- benchmarks --------------------------------------------------------- - -spend_benchmarks :: Benchmark -spend_benchmarks = bgroup "spend" [ - bench "spend_to_local" $ - nf (\v -> B5.spend_to_local - dummyOutPoint v dummyRevPk dummyDelay - dummyDelayedPk dummyDest dummyFeerate) - (Satoshi 100000) - - , bench "spend_revoked_to_local" $ - nf (\v -> B5.spend_revoked_to_local - dummyOutPoint v dummyRevPk dummyDelay - dummyDelayedPk dummyDest dummyFeerate) - (Satoshi 100000) - - , bench "spend_anchor_owner" $ - nf (\v -> B5.spend_anchor_owner - dummyOutPoint v dummyFundPk dummyDest) - (Satoshi 330) - - ] - -classify_benchmarks :: Benchmark -classify_benchmarks = bgroup "classify" [ - bench "spending_fee" $ - nf (B5.spending_fee dummyFeerate) 538 - - , bench "htlc_timed_out" $ - nf (B5.htlc_timed_out 500001) - (HTLC HTLCOffered (MilliSatoshi 1000000) - dummyPaymentHash (CltvExpiry 500000)) - ] - where - dummyPaymentHash = case paymentHash - (BS.replicate 32 0xAA) of - Just ph -> ph - Nothing -> error "impossible" +main = do + fx <- fixtures + let keys = fx_keys fx + revpk = ck_revocation_pubkey keys + delayed = ck_local_delayed keys + defaultMain [ + bgroup "detect" [ + bench "identify_close" $ + nf (B5.identify_close (fx_other fx) (fx_commit fx)) (fx_raw fx) + , bench "classify_local_commit_outputs" $ + nf (\c -> B5.classify_local_commit_outputs c keys (fx_delay fx) + StaticRemotekey) + (fx_commit fx) + , bench "classify_remote_commit_outputs" $ + nf (\c -> B5.classify_remote_commit_outputs c keys) (fx_commit fx) + , bench "classify_revoked_commit_outputs" $ + nf (\c -> B5.classify_revoked_commit_outputs c revpk) + (fx_commit fx) + , bench "extract_preimage_offered" $ + nf (B5.extract_preimage_offered (fx_hash fx)) (fx_claim fx) + , bench "extract_preimage_htlc_success" $ + nf (B5.extract_preimage_htlc_success (fx_hash fx)) (fx_success fx) + , bench "htlc_timed_out" $ + nf (B5.htlc_timed_out 500001) (fx_offered fx) + ] + , bgroup "spend" [ + bench "spend_to_local" $ + nf (\v -> B5.spend_to_local (fx_outpoint fx) v revpk (fx_delay fx) + delayed (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + , bench "spend_htlc_tx" $ + nf (B5.spend_htlc_tx (fx_htlc_ctx fx)) keys + , bench "spend_remote_htlc_timeout" $ + nf (\v -> B5.spend_remote_htlc_timeout (fx_outpoint fx) v + (fx_received fx) keys StaticRemotekey (fx_dest fx) + (fx_feerate fx)) + (fx_value fx) + , bench "spend_remote_htlc_preimage" $ + nf (\v -> B5.spend_remote_htlc_preimage (fx_outpoint fx) v + (fx_offered fx) keys StaticRemotekey (fx_dest fx) + (fx_feerate fx)) + (fx_value fx) + , bench "spend_revoked_to_local" $ + nf (\v -> B5.spend_revoked_to_local (fx_outpoint fx) v revpk + (fx_delay fx) delayed (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + , bench "spend_revoked_htlc" $ + nf (\v -> B5.spend_revoked_htlc (fx_outpoint fx) v + (B5.HTLCReceivedOutput (CltvExpiry 501)) revpk keys + StaticRemotekey (fx_hash fx) (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + , bench "spend_anchor_owner" $ + nf (\v -> B5.spend_anchor_owner (fx_outpoint fx) v + (ck_local_funding keys) (fx_dest fx)) + (fx_anchor fx) + ] + , bgroup "fees" [ + bench "spending_fee" $ + nf (B5.spending_fee (fx_feerate fx)) 538 + ] + ] diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -1,121 +1,50 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} - module Main where -import Bitcoin.Prim.Tx.Sighash (SighashType(..)) -import Control.DeepSeq (NFData(..)) -import qualified Data.ByteString as BS -import Lightning.Protocol.BOLT3 hiding - (txout_value, txout_script) +import Fixtures +import Lightning.Protocol.BOLT3 import qualified Lightning.Protocol.BOLT5 as B5 import Weigh --- NFData orphan instances for weigh --------------------------------- - -instance NFData Script where - rnf (Script bs) = rnf bs - -instance NFData Pubkey where - rnf pk = rnf (unPubkey pk) - -instance NFData RevocationPubkey where - rnf (RevocationPubkey x) = rnf x - -instance NFData LocalDelayedPubkey where - rnf (LocalDelayedPubkey p) = rnf p - -instance NFData FundingPubkey where - rnf (FundingPubkey p) = rnf p - -instance NFData FeeratePerKw where - rnf (FeeratePerKw x) = rnf x - -instance NFData ToSelfDelay where - rnf (ToSelfDelay x) = rnf x - -instance NFData CltvExpiry where - rnf (CltvExpiry x) = rnf x - -instance NFData SighashType - -instance NFData B5.OutputResolution where - rnf B5.Resolved = () - rnf (B5.Revoke rk) = rnf rk - rnf _ = () - -instance NFData B5.HTLCOutputType where - rnf (B5.HTLCOfferedOutput e) = rnf e - rnf (B5.HTLCReceivedOutput e) = rnf e - -instance NFData B5.UnresolvedOutput where - rnf (B5.UnresolvedOutput op v t) = - rnf op `seq` rnf v `seq` rnf t - -instance NFData B5.RevokedOutputType where - rnf B5.RevokedToLocal = () - rnf (B5.RevokedHTLC h) = rnf h - -instance NFData B5.RevokedOutput where - rnf (B5.RevokedOutput op v t) = - rnf op `seq` rnf v `seq` rnf t - -instance NFData B5.SpendingTx where - rnf (B5.SpendingTx tx scr val sh) = - rnf tx `seq` rnf scr `seq` rnf val `seq` rnf sh - -- note that 'weigh' doesn't work properly in a repl main :: IO () -main = mainWith $ do - spend_weights - --- fixtures ----------------------------------------------------------- - -dummyPubkey :: Pubkey -dummyPubkey = case pubkey (BS.pack (0x02 : replicate 32 0x01)) of - Just pk -> pk - Nothing -> error "impossible" - -dummyTxId :: TxId -dummyTxId = case mkTxId (BS.replicate 32 0x00) of - Just tid -> tid - Nothing -> error "impossible" - -dummyOutPoint :: OutPoint -dummyOutPoint = OutPoint dummyTxId 0 - -dummyRevPk :: RevocationPubkey -dummyRevPk = RevocationPubkey dummyPubkey - -dummyDelayedPk :: LocalDelayedPubkey -dummyDelayedPk = LocalDelayedPubkey dummyPubkey - -dummyFundPk :: FundingPubkey -dummyFundPk = FundingPubkey dummyPubkey - -dummyDest :: Script -dummyDest = Script $ BS.pack [0x00, 0x14] <> - BS.replicate 20 0xCC - -dummyFeerate :: FeeratePerKw -dummyFeerate = FeeratePerKw 253 - -dummyDelay :: ToSelfDelay -dummyDelay = ToSelfDelay 144 - --- weights ------------------------------------------------------------ - -spend_weights :: Weigh () -spend_weights = wgroup "spend" $ do - func "spend_to_local" - (\v -> B5.spend_to_local - dummyOutPoint v dummyRevPk dummyDelay - dummyDelayedPk dummyDest dummyFeerate) - (Satoshi 100000) - - func "spend_anchor_owner" - (\v -> B5.spend_anchor_owner - dummyOutPoint v dummyFundPk dummyDest) - (Satoshi 330) +main = do + fx <- fixtures + let keys = fx_keys fx + revpk = ck_revocation_pubkey keys + delayed = ck_local_delayed keys + mainWith $ do + wgroup "detect" $ do + func "identify_close" + (B5.identify_close (fx_other fx) (fx_commit fx)) (fx_raw fx) + func "classify_local_commit_outputs" + (\c -> B5.classify_local_commit_outputs c keys (fx_delay fx) + StaticRemotekey) + (fx_commit fx) + func "classify_remote_commit_outputs" + (\c -> B5.classify_remote_commit_outputs c keys) (fx_commit fx) + func "classify_revoked_commit_outputs" + (\c -> B5.classify_revoked_commit_outputs c revpk) (fx_commit fx) + func "extract_preimage_offered" + (B5.extract_preimage_offered (fx_hash fx)) (fx_claim fx) + func "extract_preimage_htlc_success" + (B5.extract_preimage_htlc_success (fx_hash fx)) (fx_success fx) + wgroup "spend" $ do + func "spend_to_local" + (\v -> B5.spend_to_local (fx_outpoint fx) v revpk (fx_delay fx) + delayed (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + func "spend_htlc_tx" (B5.spend_htlc_tx (fx_htlc_ctx fx)) keys + func "spend_remote_htlc_timeout" + (\v -> B5.spend_remote_htlc_timeout (fx_outpoint fx) v + (fx_received fx) keys StaticRemotekey (fx_dest fx) + (fx_feerate fx)) + (fx_value fx) + func "spend_revoked_htlc" + (\v -> B5.spend_revoked_htlc (fx_outpoint fx) v + (B5.HTLCReceivedOutput (CltvExpiry 501)) revpk keys + StaticRemotekey (fx_hash fx) (fx_dest fx) (fx_feerate fx)) + (fx_value fx) + func "spend_anchor_owner" + (\v -> B5.spend_anchor_owner (fx_outpoint fx) v + (ck_local_funding keys) (fx_dest fx)) + (fx_anchor fx) diff --git a/etc/05-onchain.md b/etc/05-onchain.md @@ -41,6 +41,7 @@ encounters any of the above situations, on-chain. * [Penalty Transactions Weight Calculation](#penalty-transactions-weight-calculation) * [Generation of HTLC Transactions](#generation-of-htlc-transactions) * [General Requirements](#general-requirements) + * [Limitations of v3 transactions](#limitations-of-v3-transactions) * [Appendix A: Expected Weights](#appendix-a-expected-weights) * [Expected Weight of the `to_local` Penalty Transaction Witness](#expected-weight-of-the-to-local-penalty-transaction-witness) * [Expected Weight of the `offered_htlc` Penalty Transaction Witness](#expected-weight-of-the-offered-htlc-penalty-transaction-witness) @@ -90,7 +91,7 @@ trigger any action. # Commitment Transaction The local and remote nodes each hold a *commitment transaction*. Each of these -commitment transactions has up to six types of outputs: +commitment transactions has up to seven types of outputs: 1. _local node's main output_: Zero or one output, to pay to the *local node's* delayed_pubkey. @@ -100,9 +101,10 @@ delayed_pubkey. funding_pubkey. 4. _remote node's anchor output_: one output paying to the *remote node's* funding_pubkey. -5. _local node's offered HTLCs_: Zero or more pending payments (*HTLCs*), to pay +5. _shared anchor_: one output spendable by anyone (pay-to-anchor). +6. _local node's offered HTLCs_: Zero or more pending payments (*HTLCs*), to pay the *remote node* in return for a payment preimage. -6. _remote node's offered HTLCs_: Zero or more pending payments (*HTLCs*), to +7. _remote node's offered HTLCs_: Zero or more pending payments (*HTLCs*), to pay the *local node* in return for a payment preimage. To incentivize the local and remote nodes to cooperate, an `OP_CHECKSEQUENCEVERIFY` @@ -148,10 +150,14 @@ A node: - otherwise: - MUST broadcast the *last commitment transaction*, for which it has a signature, to perform a *unilateral close*. - - MUST spend any `to_local_anchor` output, providing sufficient fees as incentive to include the commitment transaction in a block. - Special care must be taken when spending to a third-party, because this re-introduces the vulnerability that was - addressed by adding the CSV delay to the non-anchor outputs. - - SHOULD use [replace-by-fee](https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki) or other mechanism on the spending transaction if it proves insufficient for timely inclusion in a block. + - MUST spend any `to_local_anchor` or `shared_anchor` output, providing sufficient fees + as incentive to include the commitment transaction in a block. For `to_local_anchor`, + special care must be taken when spending to a third-party, because this re-introduces + the pinning vulnerability that was addressed by adding the CSV delay to the non-anchor + outputs. + - SHOULD use [replace-by-fee](https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki) + or other mechanism on the spending transaction if it proves insufficient for timely + inclusion in a block. ## Rationale @@ -166,10 +172,10 @@ need not consume resources monitoring the channel state. There exists a bias towards preferring mutual closes over unilateral closes, because outputs of the former are unencumbered by a delay and are directly spendable by wallets. In addition, mutual close fees tend to be less exaggerated -than those of commitment transactions (or in the case of `option_anchors`, -the commitment transaction may require a child transaction to cause it to be mined). So, the only reason not to use the -signature from `closing_signed` would be if the fee offered was too small for -it to be processed. +than those of commitment transactions (or in the case of commitments using anchor +outputs, the commitment transaction may require a child transaction to cause it +to be mined). So, the only reason not to use the mutual close transaction would +be if the fee offered was too small for it to be processed. # Mutual Close Handling @@ -279,8 +285,7 @@ The local HTLC-timeout transaction needs to be used to time out the HTLC (to prevent the remote node fulfilling it and claiming the funds) before the local node can back-fail any corresponding incoming HTLC, using `update_fail_htlc` (presumably with reason `permanent_channel_failure`), as -detailed in -[BOLT #2](02-peer-protocol.md#forwarding-htlcs). +detailed in [BOLT #2](02-peer-protocol.md#forwarding-htlcs). If the incoming HTLC is also on-chain, a node must simply wait for it to timeout: there is no way to signal early failure. @@ -537,8 +542,13 @@ A local node: using the revocation private key. - SHOULD extract the payment preimage from the transaction input witness, if it's not already known. - - if `option_anchors` applies: + - if `SIGHASH_SINGLE|SIGHASH_ANYONECANPAY` is used for HTLC transactions + (i.e. `option_anchors` or `zero_fee_commitments` applies): - MAY use a single transaction to *resolve* all the outputs. + - If `zero_fee_commitments` applies: + - MUST NOT exceed the 10kvB limit of v3 transactions, otherwise the + transaction may not reach miners (as explained in the section + about [v3 limitations](#limitations-of-v3-transactions) below). - if confirmation doesn't happen before reaching `security_delay` blocks from expiry: - SHOULD *resolve* revoked outputs in their own, separate penalty transactions. A previous @@ -554,8 +564,9 @@ A single transaction that resolves all the outputs will be under the standard size limit because of the 483 HTLC-per-party limit (see [BOLT #2](02-peer-protocol.md#the-open_channel-message)). -Note: if `option_anchors` applies, the cheating node can pin spends of its -HTLC-timeout/HTLC-success outputs thanks to SIGHASH_SINGLE malleability. +Note: when `SIGHASH_SINGLE|SIGHASH_ANYONECANPAY` is used for HTLC transactions +(i.e. `option_anchors` or `zero_fee_commitments`), the cheating node can pin +spends of its HTLC-timeout/HTLC-success outputs. Using a single penalty transaction for all revoked outputs is thus unsafe as it could be blocked to propagate long enough for the _local node's `to_local` output_ 's relative locktime to expire and the cheating party escaping the penalty on this @@ -610,25 +621,25 @@ Note: even if the `to_remote` output is not swept, the resulting # Generation of HTLC Transactions -If `option_anchors` does not apply to the commitment transaction, then -HTLC-timeout and HTLC-success transactions are complete transactions with -(hopefully!) reasonable fees and must be used directly. +If `option_anchors` and `zero_fee_commitments` do not apply, then HTLC-timeout +and HTLC-success transactions are signed with `SIGHASH_ALL` and are complete +transactions with (hopefully!) reasonable fees and must be used directly. Otherwise, `SIGHASH_SINGLE|SIGHASH_ANYONECANPAY` MUST be used on the -HTLC signatures received from the peer, as this allows HTLC transactions to be combined with -other transactions. The local signature MUST use `SIGHASH_ALL`, otherwise -anyone can attach additional inputs and outputs to the tx. +HTLC signatures received from the peer, as this allows HTLC transactions to be +combined with other transactions. The local signature MUST use `SIGHASH_ALL`, +otherwise anyone can attach additional inputs and outputs to the tx. -If `option_anchors` applies, then the HTLC-timeout and -HTLC-success transactions are signed with the input and output having the same -value. This means they have a zero fee and MUST be combined with other inputs -to arrive at a reasonable fee. +If `SIGHASH_SINGLE|SIGHASH_ANYONECANPAY` is used, then the HTLC-timeout and +HTLC-success transactions are signed with the input and output having the +same value. This means they have a zero fee and MUST be combined with other +inputs to arrive at a reasonable fee. ## Requirements A node which broadcasts an HTLC-success or HTLC-timeout transaction for a commitment transaction: - - if `option_anchors` applies: + - if `SIGHASH_SINGLE|SIGHASH_ANYONECANPAY` is used: - MUST combine it with inputs contributing sufficient fee to ensure timely inclusion in a block. - MAY combine it with other transactions. @@ -649,6 +660,20 @@ A node: - Note: watching for mempool transactions should result in lower latency HTLC redemptions. +## Limitations of v3 transactions + +When `zero_fee_commitments` applies, we use v3 transactions, also known as +TRUC transactions, which have different policy rules than v2 transactions. + +Implementers should read [BIP-431](https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki) +to make sure they don't create transactions that aren't relayed by bitcoin +nodes. + +The main limitation compared to v2 transactions (but not the only one) is +that transactions are restricted to 10kvB (instead of 400kvB): when batching +transactions (e.g. HTLC transactions), nodes MUST NOT exceed that limit, +otherwise the transaction will not reach miners and funds may be at risk. + # Appendix A: Expected Weights ## Expected Weight of the `to_local` Penalty Transaction Witness diff --git a/flake.lock b/flake.lock @@ -37,21 +37,18 @@ "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" ] @@ -210,29 +207,29 @@ "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-bolt9": [ + "ppad-bolt9" + ], "ppad-nixpkgs": [ - "ppad-bolt3", "ppad-nixpkgs" ] }, "locked": { - "lastModified": 1783709539, - "narHash": "sha256-bXqV4/qYZFbZeG8PHlnr+PVyTZ3E0ZIi+mgCu49iamw=", + "lastModified": 1791625140, + "narHash": "sha256-DgMZKJlpLQcrNLSuzOM+bF2QQKsaVKigzSjkyOEockQ=", "ref": "master", - "rev": "507af14c14be9fbaa27992357aa25e40c443daee", - "revCount": 33, + "rev": "b4b4b507bf0be00435a15c448ef2366ed0c22581", + "revCount": 43, "type": "git", "url": "git://git.ppad.tech/bolt1.git" }, @@ -254,7 +251,15 @@ "ppad-nixpkgs", "nixpkgs" ], - "ppad-bolt1": "ppad-bolt1", + "ppad-bolt1": [ + "ppad-bolt1" + ], + "ppad-bolt9": [ + "ppad-bolt9" + ], + "ppad-fixed": [ + "ppad-fixed" + ], "ppad-nixpkgs": [ "ppad-nixpkgs" ], @@ -272,11 +277,11 @@ ] }, "locked": { - "lastModified": 1783709568, - "narHash": "sha256-RA4jdJkMZatx9IZ6ri2XqQao24GQ1ZwxWH7ZKKSkJ14=", + "lastModified": 1791630066, + "narHash": "sha256-dEMBydCq9B3nZnpK97Na43IqT+c841nTdFPK5b/PoLw=", "ref": "master", - "rev": "523aa8b203c356019e543a4e01bba28450d35020", - "revCount": 54, + "rev": "b6b3c35e24b99b2538475f27cff76d0556993160", + "revCount": 70, "type": "git", "url": "git://git.ppad.tech/bolt3.git" }, @@ -286,31 +291,59 @@ "url": "git://git.ppad.tech/bolt3.git" } }, + "ppad-bolt9": { + "inputs": { + "flake-utils": [ + "ppad-bolt9", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-bolt9", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1791624953, + "narHash": "sha256-DWafVJvu2LTXkAI/zH0Q0BqTrjLo0c7hyTvaW/9N0VM=", + "ref": "master", + "rev": "280946ab905a374c8c6d4a37678a4cc362551ab9", + "revCount": 39, + "type": "git", + "url": "git://git.ppad.tech/bolt9.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/bolt9.git" + } + }, "ppad-fixed": { "inputs": { "flake-utils": [ - "ppad-secp256k1", "ppad-fixed", "ppad-nixpkgs", "flake-utils" ], "nixpkgs": [ - "ppad-secp256k1", "ppad-fixed", "ppad-nixpkgs", "nixpkgs" ], "ppad-nixpkgs": [ - "ppad-secp256k1", "ppad-nixpkgs" ] }, "locked": { - "lastModified": 1780868194, - "narHash": "sha256-Yd3J3gCNLE2vZVMMg1fD7aVpPySIbFqxtwskU6nD/mc=", + "lastModified": 1785603497, + "narHash": "sha256-rF8KdVJ+VwUT2bEfCLZMPc05rUclr0wN2uOTYvd3z7g=", "ref": "master", - "rev": "74ebe44f818353129c5ec973a55c8bc487e2fd7b", - "revCount": 297, + "rev": "e303bda7578385f488c2f23baa2e1a6a2b0b2aaf", + "revCount": 305, "type": "git", "url": "git://git.ppad.tech/fixed.git" }, @@ -430,7 +463,9 @@ "nixpkgs" ], "ppad-base16": "ppad-base16_3", - "ppad-fixed": "ppad-fixed", + "ppad-fixed": [ + "ppad-fixed" + ], "ppad-hmac-drbg": "ppad-hmac-drbg", "ppad-nixpkgs": [ "ppad-nixpkgs" @@ -441,11 +476,11 @@ "ppad-sha512": "ppad-sha512" }, "locked": { - "lastModified": 1783709454, - "narHash": "sha256-6BknM38fe2vFwWWRsBeebTt+hKwaZL0ib89XqPO6Y6U=", + "lastModified": 1785605654, + "narHash": "sha256-VDNnKXmJIZZ/3u7Tf1rK93GR4x7OiAadqykhqCwkHb4=", "ref": "master", - "rev": "bc0d3792bcea0475ed2e4879747dbd0e0e6db3e7", - "revCount": 266, + "rev": "fdb80d54b86c61bd1a15e2e8db351091d02c89ee", + "revCount": 280, "type": "git", "url": "git://git.ppad.tech/secp256k1.git" }, @@ -546,11 +581,11 @@ ] }, "locked": { - "lastModified": 1783709517, - "narHash": "sha256-iNne+qla9/ntt62Z+i9HR4QH99l/tUZCTD8i45sX0iY=", + "lastModified": 1791624263, + "narHash": "sha256-IJAffs2Cx/IjSTpNPubVlrzHLqRLUFu6XQp9yRIRbdU=", "ref": "master", - "rev": "0654d3f60af6e63109bf05224d15905fc5b70fc1", - "revCount": 31, + "rev": "65a561493940b59c5415b4b1409b4e32dae5c08f", + "revCount": 33, "type": "git", "url": "git://git.ppad.tech/tx.git" }, @@ -570,7 +605,10 @@ "ppad-nixpkgs", "nixpkgs" ], + "ppad-bolt1": "ppad-bolt1", "ppad-bolt3": "ppad-bolt3", + "ppad-bolt9": "ppad-bolt9", + "ppad-fixed": "ppad-fixed", "ppad-nixpkgs": "ppad-nixpkgs", "ppad-ripemd160": "ppad-ripemd160", "ppad-secp256k1": "ppad-secp256k1", diff --git a/flake.nix b/flake.nix @@ -13,12 +13,19 @@ ref = "master"; inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; }; + ppad-fixed = { + type = "git"; + url = "git://git.ppad.tech/fixed.git"; + ref = "master"; + inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + }; ppad-secp256k1 = { type = "git"; url = "git://git.ppad.tech/secp256k1.git"; ref = "master"; inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; inputs.ppad-sha256.follows = "ppad-sha256"; + inputs.ppad-fixed.follows = "ppad-fixed"; }; ppad-ripemd160 = { type = "git"; @@ -33,11 +40,27 @@ inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; inputs.ppad-sha256.follows = "ppad-sha256"; }; + ppad-bolt9 = { + type = "git"; + url = "git://git.ppad.tech/bolt9.git"; + ref = "master"; + inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + }; + ppad-bolt1 = { + type = "git"; + url = "git://git.ppad.tech/bolt1.git"; + ref = "master"; + inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + inputs.ppad-bolt9.follows = "ppad-bolt9"; + }; ppad-bolt3 = { type = "git"; url = "git://git.ppad.tech/bolt3.git"; ref = "master"; inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + inputs.ppad-bolt1.follows = "ppad-bolt1"; + inputs.ppad-bolt9.follows = "ppad-bolt9"; + inputs.ppad-fixed.follows = "ppad-fixed"; inputs.ppad-sha256.follows = "ppad-sha256"; inputs.ppad-secp256k1.follows = "ppad-secp256k1"; inputs.ppad-ripemd160.follows = "ppad-ripemd160"; @@ -48,8 +71,8 @@ }; outputs = { self, nixpkgs, flake-utils, ppad-nixpkgs - , ppad-sha256, ppad-secp256k1, ppad-ripemd160 - , ppad-tx, ppad-bolt3 }: + , ppad-sha256, ppad-fixed, ppad-secp256k1, ppad-ripemd160 + , ppad-tx, ppad-bolt9, ppad-bolt1, ppad-bolt3 }: flake-utils.lib.eachDefaultSystem (system: let lib = "ppad-bolt5"; @@ -65,6 +88,12 @@ (hlib.enableCabalFlag sha256 "llvm") [ llvm clang ]; + fixed = ppad-fixed.packages.${system}.default; + fixed-llvm = + hlib.addBuildTools + (hlib.enableCabalFlag fixed "llvm") + [ llvm clang ]; + secp256k1 = ppad-secp256k1.packages.${system}.default; secp256k1-llvm = hlib.addBuildTools @@ -83,13 +112,18 @@ (hlib.enableCabalFlag tx "llvm") [ llvm clang ]; + bolt9 = ppad-bolt9.packages.${system}.default; + bolt1 = ppad-bolt1.packages.${system}.default; bolt3 = ppad-bolt3.packages.${system}.default; hpkgs = pkgs.haskell.packages.ghc910.extend (new: old: { ppad-sha256 = sha256-llvm; + ppad-fixed = fixed-llvm; ppad-secp256k1 = secp256k1-llvm; ppad-ripemd160 = ripemd160-llvm; ppad-tx = tx-llvm; + ppad-bolt9 = bolt9; + ppad-bolt1 = bolt1; ppad-bolt3 = bolt3; ${lib} = new.callCabal2nix lib ./. { }; }); diff --git a/lib/Lightning/Protocol/BOLT5.hs b/lib/Lightning/Protocol/BOLT5.hs @@ -9,84 +9,50 @@ -- On-chain transaction handling for the Lightning Network, per -- [BOLT #5](https://github.com/lightning/bolts/blob/master/05-onchain.md). -- --- This module implements the logic for handling channel closures: --- --- * Mutual close - cooperative closure agreed by both parties --- * Unilateral close - one party publishes their commitment --- transaction --- * Revoked transaction close - penalty for publishing old state +-- This library's scope is being redesigned, and it is not yet ready +-- for release. It covers unilateral closes only partially: it +-- identifies a local or remote commitment on chain, classifies its +-- outputs, extracts preimages from HTLC spends, and builds unsigned, +-- single-input spending transactions. It doesn't detect mutual or +-- revoked closes, assemble witnesses, or attach fee inputs to anchor +-- spends and zero-fee HTLC transactions. -- -- = Design -- --- This is a stateless toolkit of pure functions. The caller --- manages channel state (which outputs are resolved, current --- block height, etc.) and provides explicit inputs. Functions --- produce unsigned 'SpendingTx' values; the caller signs and --- assembles witnesses using bolt3 constructors. +-- This is a stateless toolkit of pure functions. The caller manages +-- channel state (which outputs are resolved, the current block +-- height, etc.) and provides explicit inputs. Commitment +-- transactions are those built by ppad-bolt3, and amounts are +-- ppad-bolt1's. Functions produce unsigned t'SpendingTx' values; the +-- caller signs them and assembles witnesses with bolt3's witness +-- functions. -- -- = Usage -- -- @ +-- import qualified Lightning.Protocol.BOLT1 as BOLT1 -- import Lightning.Protocol.BOLT3 -- import Lightning.Protocol.BOLT5 -- --- -- Classify outputs of our local commitment --- let outputs = classify_local_commit_outputs --- commitTx keys delay features htlcs +-- -- classify the outputs of our local commitment +-- let outputs = classify_local_commit_outputs commit keys delay fmt -- --- -- For each unresolved output, construct spending tx +-- -- for each unresolved output, build a spending transaction -- case uo_type output of -- SpendToLocal delay revpk delayedpk -> --- spend_to_local (uo_outpoint output) --- (uo_value output) revpk delay delayedpk --- destScript feerate +-- spend_to_local (uo_outpoint output) (uo_value output) +-- revpk delay delayedpk dest feerate -- ... -- @ module Lightning.Protocol.BOLT5 ( - -- * Types - -- ** Close identification - CloseType(..) - - -- ** Output classification - , UnresolvedOutput(..) - , OutputResolution(..) - - -- ** HTLC output type - , HTLCOutputType(..) - , htlcOutputType - , htlcOutputTypeWeight - - -- ** Spending transactions - , SpendingTx(..) - - -- ** Revoked outputs - , RevokedOutput(..) - , RevokedOutputType(..) - , revoked_output_weight - - -- * Weight constants (Appendix A) - , to_local_penalty_witness_weight - , offered_htlc_penalty_witness_weight - , accepted_htlc_penalty_witness_weight - , offered_htlc_penalty_witness_weight_anchors - , accepted_htlc_penalty_witness_weight_anchors - , to_local_penalty_input_weight - , offered_htlc_penalty_input_weight - , accepted_htlc_penalty_input_weight - , offered_htlc_penalty_input_weight_anchors - , accepted_htlc_penalty_input_weight_anchors - , to_remote_input_weight - , penalty_tx_base_weight - , max_standard_weight - - -- * Fee calculation - , spending_fee - -- * Close identification + CloseType(..) , identify_close -- * Output classification + , UnresolvedOutput(..) + , OutputResolution(..) , classify_local_commit_outputs , classify_remote_commit_outputs , classify_revoked_commit_outputs @@ -98,11 +64,12 @@ module Lightning.Protocol.BOLT5 ( -- * Timeout check , htlc_timed_out - -- * Spending transaction construction + -- * Spending transactions + , SpendingTx(..) + -- ** Local commitment , spend_to_local - , spend_htlc_timeout - , spend_htlc_success + , spend_htlc_tx , spend_htlc_output -- ** Remote commitment @@ -117,8 +84,34 @@ module Lightning.Protocol.BOLT5 ( -- ** Anchor outputs , spend_anchor_owner , spend_anchor_anyone + + -- * Revoked outputs + , HTLCOutputType(..) + , htlc_output_type + , RevokedOutput(..) + , RevokedOutputType(..) + + -- * Weights (Appendix A) + , revoked_output_weight + , htlc_output_type_weight + , to_local_penalty_witness_weight + , offered_htlc_penalty_witness_weight + , accepted_htlc_penalty_witness_weight + , offered_htlc_penalty_witness_weight_anchors + , accepted_htlc_penalty_witness_weight_anchors + , to_local_penalty_input_weight + , offered_htlc_penalty_input_weight + , accepted_htlc_penalty_input_weight + , offered_htlc_penalty_input_weight_anchors + , accepted_htlc_penalty_input_weight_anchors + , to_remote_input_weight + , penalty_tx_base_weight + , max_standard_weight + + -- * Fees + , spending_fee ) where -import Lightning.Protocol.BOLT5.Types import Lightning.Protocol.BOLT5.Detect import Lightning.Protocol.BOLT5.Spend +import Lightning.Protocol.BOLT5.Types diff --git a/lib/Lightning/Protocol/BOLT5/Detect.hs b/lib/Lightning/Protocol/BOLT5/Detect.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} -- | @@ -27,11 +27,14 @@ module Lightning.Protocol.BOLT5.Detect ( , htlc_timed_out ) where +import Bitcoin.Prim.Tx (OutPoint(..), TxId, Witness(..)) import qualified Bitcoin.Prim.Tx as BT import qualified Crypto.Hash.SHA256 as SHA256 -import Data.List (mapAccumL) -import Data.Word (Word32) import qualified Data.ByteString as BS +import qualified Data.List.NonEmpty as NE +import Data.Word (Word32) +import Lightning.Protocol.BOLT1 (PaymentHash, PaymentPreimage) +import qualified Lightning.Protocol.BOLT1 as BOLT1 import Lightning.Protocol.BOLT3 import Lightning.Protocol.BOLT5.Types @@ -40,17 +43,16 @@ import Lightning.Protocol.BOLT5.Types -- | Identify the type of channel close from a transaction that -- spends the funding output. -- --- Parses the on-chain transaction and compares its txid against --- the txids of the known local and remote commitment --- transactions. Comparing txids rather than bytes means the --- signed (segwit) serialization found on chain matches the --- unsigned commitment. +-- Parses the on-chain transaction and compares its txid with the +-- txids of the known local and remote commitment transactions. +-- Comparing txids rather than bytes means the signed (segwit) +-- serialization found on chain matches the unsigned commitment. -- --- Returns 'Nothing' if the bytes don't parse as a transaction or --- the transaction doesn't match either commitment. Mutual close --- and revoked commitment detection require additional checks by --- the caller (e.g. comparing closing tx format, checking a secret --- store for older commitment numbers). +-- Returns 'Nothing' if the bytes don't parse as a transaction or +-- the transaction matches neither commitment. Mutual close and +-- revoked commitment detection are left to the caller (e.g. +-- comparing with the closing transaction, or checking a secret +-- store for older commitment numbers). identify_close :: CommitmentTx -- ^ Our local commitment tx. @@ -59,31 +61,27 @@ identify_close -> BS.ByteString -- ^ Raw serialized transaction found on chain. -> Maybe CloseType -identify_close !localCommitTx !remoteCommitTx - !onChainBytes = - case BT.from_bytes onChainBytes of - Nothing -> Nothing - Just !onChainTx -> - let !onChainTxid = Just (BT.txid onChainTx) - in if commitment_txid localCommitTx == onChainTxid - then Just LocalCommitClose - else if commitment_txid remoteCommitTx == onChainTxid - then Just RemoteCommitClose - else Nothing +identify_close !local !remote !bytes = do + !tx <- BT.from_bytes bytes + let !txid = BT.txid tx + if commitment_txid local == txid + then Just LocalCommitClose + else if commitment_txid remote == txid + then Just RemoteCommitClose + else Nothing -- output classification ---------------------------------------------- --- | Classify outputs of our local commitment transaction. +-- | Classify the outputs of our local commitment transaction. -- --- Per BOLT #5: when we discover our local commitment on chain, --- we must resolve each output. to_local requires a CSV-delayed --- spend, to_remote is resolved by the commitment itself, HTLC --- outputs need second-stage transactions, and anchors can be --- spent immediately. +-- Per BOLT #5: when our local commitment is on chain, we must +-- resolve each output. to_local requires a CSV-delayed spend, +-- to_remote is resolved by the commitment itself, HTLC outputs +-- need second-stage transactions, and our anchor can be spent +-- immediately. -- --- An HTLC output is matched to the HTLC with the same direction, --- script, amount and CLTV expiry, and each HTLC matches at most one --- output. +-- Each HTLC output's HTLC is read from its bolt3 'OutputHTLC', so +-- the commitment must be the one bolt3 built for this state. classify_local_commit_outputs :: CommitmentTx -- ^ Our local commitment transaction. @@ -91,306 +89,166 @@ classify_local_commit_outputs -- ^ Derived keys for this commitment. -> ToSelfDelay -- ^ Remote's to_self_delay (CSV delay for our outputs). - -> ChannelFeatures - -- ^ Channel feature flags. - -> [HTLC] - -- ^ HTLCs in this commitment. + -> CommitmentFormat -> [UnresolvedOutput] -classify_local_commit_outputs !commitTx !keys !delay - !features !htlcs = - case commitment_txid commitTx of - Nothing -> [] - Just !txid -> - let !outputs = ctx_outputs commitTx - !revpk = ck_revocation_pubkey keys - !delayedpk = ck_local_delayed keys - classify cands (idx, out) = - classifyLocalOutput txid revpk delayedpk - delay features keys cands idx out - in snd (mapAccumL classify - (htlc_candidates keys features htlcs) - (zip [0..] outputs)) - --- | Classify a single output from a local commitment tx, removing --- any matched HTLC from the candidates. -classifyLocalOutput - :: TxId - -> RevocationPubkey - -> LocalDelayedPubkey - -> ToSelfDelay - -> ChannelFeatures - -> CommitmentKeys - -> [(HTLC, Script)] - -> Word32 - -> TxOutput - -> ([(HTLC, Script)], UnresolvedOutput) -classifyLocalOutput !txid !revpk !delayedpk !delay - !features !keys !cands !idx !out = - let !op = OutPoint txid idx - !val = txout_value out - !(rest, resolution) = case txout_type out of - OutputToLocal -> - (cands, SpendToLocal delay revpk delayedpk) - OutputToRemote -> - (cands, Resolved) - OutputLocalAnchor -> - (cands, AnchorSpend (ck_local_funding keys)) - OutputRemoteAnchor -> - (cands, Resolved) - OutputOfferedHTLC expiry -> - case take_htlc HTLCOffered expiry out cands of - (Just htlc, cands') -> - (cands', SpendHTLCTimeout htlc keys features) - (Nothing, cands') -> (cands', Resolved) - OutputReceivedHTLC expiry -> - case take_htlc HTLCReceived expiry out cands of - (Just htlc, cands') -> - (cands', SpendHTLCSuccess htlc keys features) - (Nothing, cands') -> (cands', Resolved) - in (rest, UnresolvedOutput op val resolution) - --- | Classify outputs of the remote commitment transaction. --- --- Per BOLT #5: when we discover the remote commitment on chain, --- there are no CSV delays on our outputs. We can spend offered --- HTLCs directly after timeout, and received HTLCs directly --- with the preimage. --- --- HTLC outputs are matched as in 'classify_local_commit_outputs'. +classify_local_commit_outputs !tx !keys !delay !fmt = + classify_outputs resolve tx + where + resolve t = case t of + OutputToLocal -> + SpendToLocal delay (ck_revocation_pubkey keys) + (ck_local_delayed keys) + OutputToRemote -> Resolved + OutputLocalAnchor -> AnchorSpend (ck_local_funding keys) + OutputRemoteAnchor -> Resolved + OutputHTLC h -> case htlc_direction h of + HTLCOffered -> SpendHTLCTimeout h keys fmt + HTLCReceived -> SpendHTLCSuccess h keys fmt + +-- | Classify the outputs of the remote commitment transaction. +-- +-- Per BOLT #5: when the remote commitment is on chain, there are +-- no CSV delays on our outputs. We can spend HTLCs we offered +-- directly after timeout, and HTLCs we received directly with the +-- preimage. +-- +-- The commitment, its keys and its HTLCs are from the remote's +-- point of view (as bolt3 builds the remote's commitment): an +-- HTLC the remote offered is one we received. classify_remote_commit_outputs :: CommitmentTx -- ^ The remote commitment transaction. -> CommitmentKeys - -- ^ Derived keys for this commitment (from remote's - -- perspective, so local/remote are swapped). - -> ChannelFeatures - -- ^ Channel feature flags. - -> [HTLC] - -- ^ HTLCs in this commitment. + -- ^ Derived keys for this commitment (from the remote's point + -- of view, so local and remote are swapped). -> [UnresolvedOutput] -classify_remote_commit_outputs !commitTx !keys - !features !htlcs = - case commitment_txid commitTx of - Nothing -> [] - Just !txid -> - let !outputs = ctx_outputs commitTx - classify cands (idx, out) = - classifyRemoteOutput txid keys cands idx out - in snd (mapAccumL classify - (htlc_candidates keys features htlcs) - (zip [0..] outputs)) - --- | Classify a single output from a remote commitment tx, removing --- any matched HTLC from the candidates. -classifyRemoteOutput - :: TxId - -> CommitmentKeys - -> [(HTLC, Script)] - -> Word32 - -> TxOutput - -> ([(HTLC, Script)], UnresolvedOutput) -classifyRemoteOutput !txid !keys !cands !idx !out = - let !op = OutPoint txid idx - !val = txout_value out - !(rest, resolution) = case txout_type out of - OutputToLocal -> - (cands, Resolved) -- Remote's to_local; not ours - OutputToRemote -> - (cands, Resolved) -- Our to_remote; resolved by commitment - OutputLocalAnchor -> - (cands, Resolved) -- Remote's anchor - OutputRemoteAnchor -> - (cands, AnchorSpend (ck_remote_funding keys)) - OutputOfferedHTLC expiry -> - -- On remote's commit, their offered = our received. - -- We can claim with preimage. - case take_htlc HTLCOffered expiry out cands of - (Just htlc, cands') -> - (cands', SpendHTLCPreimageDirect htlc) - (Nothing, cands') -> (cands', Resolved) - OutputReceivedHTLC expiry -> - -- On remote's commit, their received = our offered. - -- We can claim after timeout. - case take_htlc HTLCReceived expiry out cands of - (Just htlc, cands') -> - (cands', SpendHTLCTimeoutDirect htlc) - (Nothing, cands') -> (cands', Resolved) - in (rest, UnresolvedOutput op val resolution) - --- | Classify outputs of a revoked commitment transaction. --- --- Per BOLT #5: when we discover a revoked commitment, we can --- claim everything using the revocation key. to_local is spent --- via revocation, HTLCs are spent via revocation, and we can --- also optionally sweep to_remote. +classify_remote_commit_outputs !tx !keys = + classify_outputs resolve tx + where + resolve t = case t of + OutputToLocal -> Resolved -- the remote's to_local + OutputToRemote -> Resolved -- ours; resolved by the commitment + OutputLocalAnchor -> Resolved -- the remote's anchor + OutputRemoteAnchor -> AnchorSpend (ck_remote_funding keys) + OutputHTLC h -> case htlc_direction h of + HTLCOffered -> SpendHTLCPreimageDirect h -- we received it + HTLCReceived -> SpendHTLCTimeoutDirect h -- we offered it + +-- | Classify the outputs of a revoked commitment transaction. +-- +-- Per BOLT #5: when a revoked commitment is on chain, we can +-- claim to_local and every HTLC output with the revocation key. +-- to_remote (ours) and the anchors are marked 'Resolved'. classify_revoked_commit_outputs :: CommitmentTx -- ^ The revoked commitment transaction. - -> CommitmentKeys - -- ^ Derived keys for the revoked commitment. -> RevocationPubkey -- ^ Revocation pubkey (derived from the revealed secret). - -> ChannelFeatures - -- ^ Channel feature flags. - -> [HTLC] - -- ^ HTLCs in the revoked commitment. -> [UnresolvedOutput] -classify_revoked_commit_outputs !commitTx !_keys - !revpk !_features !_htlcs = - case commitment_txid commitTx of - Nothing -> [] - Just !txid -> - let !outputs = ctx_outputs commitTx - in zipWith (classifyRevokedOutput txid revpk) - [0..] outputs - --- | Classify a single output from a revoked commitment tx. -classifyRevokedOutput - :: TxId - -> RevocationPubkey - -> Word32 - -> TxOutput - -> UnresolvedOutput -classifyRevokedOutput !txid !revpk !idx !out = - let !op = OutPoint txid idx - !val = txout_value out - !resolution = case txout_type out of - OutputToLocal -> - Revoke revpk - OutputToRemote -> - Resolved -- Our funds; resolved by commitment - OutputLocalAnchor -> - Resolved -- Can be swept by anyone after 16 blocks - OutputRemoteAnchor -> - Resolved -- Our anchor - OutputOfferedHTLC expiry -> - RevokeHTLC revpk (HTLCOfferedOutput expiry) - OutputReceivedHTLC expiry -> - RevokeHTLC revpk (HTLCReceivedOutput expiry) - in UnresolvedOutput op val resolution +classify_revoked_commit_outputs !tx !revpk = + classify_outputs resolve tx + where + resolve t = case t of + OutputToLocal -> Revoke revpk + OutputToRemote -> Resolved + OutputLocalAnchor -> Resolved + OutputRemoteAnchor -> Resolved + OutputHTLC h -> RevokeHTLC revpk $ case htlc_direction h of + HTLCOffered -> HTLCOfferedOutput (htlc_cltv_expiry h) + HTLCReceived -> HTLCReceivedOutput (htlc_cltv_expiry h) + +-- | Pair each commitment output with its outpoint, value and +-- resolution. +classify_outputs + :: (OutputType -> OutputResolution) + -> CommitmentTx + -> [UnresolvedOutput] +classify_outputs resolve tx = + zipWith classify [0..] (NE.toList (ctx_outputs tx)) + where + !txid = commitment_txid tx + classify :: Word32 -> CommitmentOutput -> UnresolvedOutput + classify !idx !out = UnresolvedOutput + { uo_outpoint = OutPoint txid idx + , uo_value = co_value out + , uo_type = resolve (co_type out) + } -- preimage extraction ------------------------------------------------ --- | Extract a payment preimage from an offered HTLC witness. +-- | Extract a payment preimage from the witness of an input +-- spending an offered HTLC output. -- --- When the remote party claims an offered HTLC on our local --- commitment, the witness contains the preimage. The witness --- stack for a preimage claim is: +-- When the remote party claims an offered HTLC on our local +-- commitment, the witness (see bolt3's +-- 'offered_htlc_witness_preimage') is: -- --- @\<remotehtlcsig\> \<paymentPreimage\> \<witnessScript\>@ +-- @\<remotehtlcsig\> \<payment_preimage\> \<witness_script\>@ -- --- Returns the preimage only if it hashes to the given payment --- hash. Any other spend of the output, e.g. by revocation, yields --- 'Nothing'. +-- Returns the preimage only if it hashes to the given payment +-- hash. Any other spend of the output, e.g. by revocation, yields +-- 'Nothing'. extract_preimage_offered :: PaymentHash -- ^ Payment hash of the HTLC. -> Witness -- ^ Witness of the input spending the HTLC output. -> Maybe PaymentPreimage -extract_preimage_offered !ph (Witness items) = - case items of - [_sig, preimageBytes, _script] -> - check_preimage ph preimageBytes - _ -> Nothing +extract_preimage_offered !ph (Witness items) = case items of + [_sig, preimage, _script] -> check_preimage ph preimage + _ -> Nothing --- | Extract a payment preimage from an HTLC-success transaction --- witness. +-- | Extract a payment preimage from the witness of an HTLC-success +-- transaction's input. -- --- When the remote party uses an HTLC-success tx on their --- commitment to claim a received HTLC, the witness contains the --- preimage. The witness stack is: +-- When the remote party claims an HTLC it received on its +-- commitment with an HTLC-success transaction, the witness (see +-- bolt3's 'htlc_success_witness') is: -- --- @0 \<remotehtlcsig\> \<localhtlcsig\> \<paymentPreimage\> --- \<witnessScript\>@ +-- @0 \<remotehtlcsig\> \<localhtlcsig\> \<payment_preimage\> +-- \<witness_script\>@ -- --- Returns the preimage only if it hashes to the given payment --- hash. +-- Returns the preimage only if it hashes to the given payment +-- hash. extract_preimage_htlc_success :: PaymentHash -- ^ Payment hash of the HTLC. -> Witness -- ^ Witness of the HTLC-success transaction input. -> Maybe PaymentPreimage -extract_preimage_htlc_success !ph (Witness items) = - case items of - [_zero, _remoteSig, _localSig, preimageBytes, _script] -> - check_preimage ph preimageBytes - _ -> Nothing +extract_preimage_htlc_success !ph (Witness items) = case items of + [_zero, _remote_sig, _local_sig, preimage, _script] -> + check_preimage ph preimage + _ -> Nothing -- | Accept a candidate preimage only if it hashes to the payment -- hash. check_preimage :: PaymentHash -> BS.ByteString -> Maybe PaymentPreimage check_preimage !ph !bs - | SHA256.hash bs == unPaymentHash ph = paymentPreimage bs + | SHA256.hash bs == BOLT1.un_payment_hash ph = BOLT1.payment_preimage bs | otherwise = Nothing -- timeout check ------------------------------------------------------ --- | Check if an HTLC has timed out at the given block height. +-- | Whether an HTLC has timed out at the given block height: whether +-- the height is at least the HTLC's CLTV expiry. -- --- An HTLC has timed out when the current block height is equal --- to or greater than the HTLC's CLTV expiry. +-- >>> let Just amt = BOLT1.milli_satoshi 1000000 +-- >>> let Just ph = BOLT1.payment_hash (BS.replicate 32 0xaa) +-- >>> let htlc = HTLC HTLCOffered amt ph (CltvExpiry 800000) +-- >>> htlc_timed_out 799999 htlc +-- False +-- >>> htlc_timed_out 800000 htlc +-- True htlc_timed_out :: Word32 -> HTLC -> Bool -htlc_timed_out !currentHeight !htlc = - currentHeight >= unCltvExpiry (htlc_cltv_expiry htlc) +htlc_timed_out !height !htlc = + let CltvExpiry expiry = htlc_cltv_expiry htlc + in height >= expiry {-# INLINE htlc_timed_out #-} -- internal helpers --------------------------------------------------- --- | Compute the txid of a commitment transaction. --- --- Returns 'Nothing' if the commitment has no outputs. -commitment_txid :: CommitmentTx -> Maybe TxId -commitment_txid !tx = fmap BT.txid (commitment_to_tx tx) - --- | Pair each HTLC with the P2WSH script of its commitment output. -htlc_candidates - :: CommitmentKeys - -> ChannelFeatures - -> [HTLC] - -> [(HTLC, Script)] -htlc_candidates !keys !features = map (\htlc -> (htlc, script htlc)) - where - script htlc = case htlc_direction htlc of - HTLCOffered -> - to_p2wsh $ offered_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - features - HTLCReceived -> - to_p2wsh $ received_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - (htlc_cltv_expiry htlc) - features - --- | Find the HTLC for an HTLC output and remove it from the --- candidates. --- --- The HTLC must agree with the output on direction, script, amount --- and CLTV expiry. The offered HTLC script doesn't commit to the --- expiry and no HTLC script commits to the amount, so HTLCs sharing --- a payment hash differ only in these. Removing the match ensures --- no two outputs resolve to the same HTLC. -take_htlc - :: HTLCDirection - -> CltvExpiry - -- ^ Expiry recorded for the output. - -> TxOutput - -> [(HTLC, Script)] - -> (Maybe HTLC, [(HTLC, Script)]) -take_htlc !dir !expiry !out !cands = - case break matches cands of - (pre, (htlc, _) : post) -> (Just htlc, pre ++ post) - (_, []) -> (Nothing, cands) - where - matches (htlc, script) = - htlc_direction htlc == dir - && htlc_cltv_expiry htlc == expiry - && msatToSat (htlc_amount_msat htlc) == txout_value out - && script == txout_script out +-- | The txid of a commitment transaction. +commitment_txid :: CommitmentTx -> TxId +commitment_txid = BT.txid . commitment_to_tx +{-# INLINE commitment_txid #-} diff --git a/lib/Lightning/Protocol/BOLT5/Internal.hs b/lib/Lightning/Protocol/BOLT5/Internal.hs @@ -1,26 +0,0 @@ -{-# OPTIONS_HADDOCK hide #-} - --- | --- Module: Lightning.Protocol.BOLT5.Internal --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Internal definitions for BOLT #5. --- --- This module re-exports all constructors from the Types --- module, including those not exported by the public API. --- Use only in tests or trusted internal code. - -module Lightning.Protocol.BOLT5.Internal ( - -- * All type constructors (for test use) - CloseType(..) - , UnresolvedOutput(..) - , OutputResolution(..) - , HTLCOutputType(..) - , SpendingTx(..) - , RevokedOutput(..) - , RevokedOutputType(..) - ) where - -import Lightning.Protocol.BOLT5.Types diff --git a/lib/Lightning/Protocol/BOLT5/Spend.hs b/lib/Lightning/Protocol/BOLT5/Spend.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} -- | @@ -10,16 +10,14 @@ -- Spending transaction construction for BOLT #5 on-chain -- transaction handling. -- --- All functions produce unsigned 'SpendingTx' values. The caller --- is responsible for signing (using the sighash metadata --- provided) and assembling final witnesses via bolt3 witness --- constructors. +-- All functions produce unsigned t'SpendingTx' values. The caller +-- signs (using the sighash metadata provided) and assembles the +-- witness with bolt3's witness functions. module Lightning.Protocol.BOLT5.Spend ( -- * Local commitment spends spend_to_local - , spend_htlc_timeout - , spend_htlc_success + , spend_htlc_tx , spend_htlc_output -- * Remote commitment spends @@ -36,27 +34,27 @@ module Lightning.Protocol.BOLT5.Spend ( , spend_anchor_anyone ) where -import Bitcoin.Prim.Tx (TxOut(..)) +import Bitcoin.Prim.Tx (OutPoint, Tx(..), TxIn(..), TxOut(..)) import Bitcoin.Prim.Tx.Sighash (SighashType(..)) +import qualified Data.ByteString as BS import Data.List.NonEmpty (NonEmpty(..)) import Data.Word (Word32, Word64) -import qualified Data.ByteString as BS -import Lightning.Protocol.BOLT3 hiding - (txout_value, txout_script) +import Lightning.Protocol.BOLT1 (PaymentHash, Satoshi) +import qualified Lightning.Protocol.BOLT1 as BOLT1 +import Lightning.Protocol.BOLT3 import Lightning.Protocol.BOLT5.Types -- local commitment spends -------------------------------------------- -- | Spend the to_local output of our local commitment tx. -- --- Requires waiting for the CSV delay (to_self_delay) before --- 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, --- or leave an output below the destination's dust threshold. +-- The input's nSequence is the to_self_delay, so the transaction +-- is valid only once the delay has passed. The caller signs with +-- the local delayed privkey and uses bolt3's +-- 'to_local_witness_spend'. -- --- The input nSequence is set to the to_self_delay value. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_to_local :: OutPoint -- ^ Outpoint of the to_local output. @@ -69,94 +67,47 @@ spend_to_local -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_to_local !op !value !revpk !delay !delayedpk - !destScript !feerate = - let !witnessScript = - to_local_script revpk delay delayedpk - !weight = to_local_penalty_input_weight - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op - (fromIntegral (unToSelfDelay delay)) - destScript outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_to_local !op !value !revpk delay@(ToSelfDelay d) !delayedpk + !dest !feerate = + sweep op value (to_local_script revpk delay delayedpk) + to_local_penalty_input_weight (fromIntegral d) 0 dest feerate --- | Construct an HTLC-timeout second-stage transaction. +-- | Build the HTLC-timeout (for an offered HTLC) or HTLC-success +-- (for a received HTLC) transaction spending an HTLC output of +-- our local commitment, with bolt3's 'build_htlc_tx'. -- --- Used when we offered an HTLC on our local commitment and it --- has timed out. The bolt3 'build_htlc_timeout_tx' function --- constructs the HTLC-timeout tx; this wraps it as a --- 'SpendingTx' with the witness script and sighash metadata. --- --- The sighash type is for the local signature, which is always --- @SIGHASH_ALL@. With @option_anchors@ the remote's --- @htlc_signature@ instead uses --- @SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@. -spend_htlc_timeout - :: HTLCContext - -> CommitmentKeys - -- ^ Full commitment keys (needed for witness script). - -> SpendingTx -spend_htlc_timeout !ctx !keys = - let !htlcTx = build_htlc_timeout_tx ctx - !htlc = hc_htlc ctx - !features = hc_features ctx - !witnessScript = offered_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - features - !inputValue = - msatToSat (htlc_amount_msat htlc) - !tx = htlc_tx_to_tx htlcTx - in SpendingTx tx witnessScript inputValue SIGHASH_ALL - --- | Construct an HTLC-success second-stage transaction. +-- The sighash type is that of the local signature, which is +-- always @SIGHASH_ALL@. The remote's @htlc_signature@ uses bolt3's +-- 'remote_htlc_sighash' (@SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@ +-- under 'Anchors'). The witness is bolt3's 'htlc_timeout_witness' +-- or 'htlc_success_witness'. -- --- Used when we received an HTLC on our local commitment and --- have the preimage. The bolt3 'build_htlc_success_tx' function --- constructs the HTLC-success tx; this wraps it as a --- 'SpendingTx'. +-- Under 'Anchors' the transaction pays no fee; it must be +-- combined with other inputs before broadcast. -- --- The sighash type is for the local signature, which is always --- @SIGHASH_ALL@. With @option_anchors@ the remote's --- @htlc_signature@ instead uses --- @SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@. -spend_htlc_success +-- Returns 'Nothing' if bolt3 can't build the transaction (the fee +-- exceeds the HTLC amount). +spend_htlc_tx :: HTLCContext -> CommitmentKeys - -- ^ Full commitment keys (needed for witness script). - -> SpendingTx -spend_htlc_success !ctx !keys = - let !htlcTx = build_htlc_success_tx ctx - !htlc = hc_htlc ctx - !features = hc_features ctx - !witnessScript = received_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - (htlc_cltv_expiry htlc) - features - !inputValue = - msatToSat (htlc_amount_msat htlc) - !tx = htlc_tx_to_tx htlcTx - in SpendingTx tx witnessScript inputValue SIGHASH_ALL + -- ^ Keys for our local commitment (for the witness script). + -> Maybe SpendingTx +spend_htlc_tx !ctx !keys = do + htx <- build_htlc_tx ctx + let !h = hc_htlc ctx + pure SpendingTx + { stx_tx = htlc_to_tx htx + , stx_input_script = htlc_script keys (hc_format ctx) h + , stx_input_value = BOLT1.msat_to_sat (htlc_amount_msat h) + , stx_sighash_type = SIGHASH_ALL + } --- | Spend a second-stage HTLC output (HTLC-timeout or --- HTLC-success output) after the CSV delay. +-- | Spend the output of an HTLC-timeout or HTLC-success +-- transaction after the CSV delay. -- --- 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, --- or leave an output below the destination's dust threshold. +-- The output uses the to_local script, so this is +-- 'spend_to_local'. The caller signs with the local delayed +-- privkey and uses bolt3's 'to_local_witness_spend'. spend_htlc_output :: OutPoint -- ^ Outpoint of the second-stage output. @@ -173,14 +124,15 @@ spend_htlc_output = spend_to_local -- remote commitment spends ------------------------------------------- --- | Spend an offered HTLC directly after timeout on the remote --- commitment. +-- | Spend an HTLC we offered directly, after it times out, from the +-- remote commitment (where it is a received HTLC). -- --- On the remote commitment, their received HTLCs (our offered) --- have timed out and we can sweep them directly. +-- The locktime is the HTLC's CLTV expiry. The caller signs with +-- our HTLC privkey and uses bolt3's +-- 'received_htlc_witness_timeout'. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_remote_htlc_timeout :: OutPoint -- ^ Outpoint of the HTLC output. @@ -190,44 +142,28 @@ spend_remote_htlc_timeout -- ^ The HTLC being spent. -> CommitmentKeys -- ^ Keys for the remote commitment. - -> ChannelFeatures + -> CommitmentFormat -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_remote_htlc_timeout !op !value !htlc !keys - !features !destScript !feerate = - let !witnessScript = received_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - (htlc_cltv_expiry htlc) - features - !weight = htlcOutputTypeWeight features - (HTLCReceivedOutput (htlc_cltv_expiry htlc)) - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !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_remote_htlc_timeout !op !value !htlc !keys !fmt !dest !feerate = + let expiry@(CltvExpiry e) = htlc_cltv_expiry htlc + !script = received_htlc_script (ck_revocation_pubkey keys) + (ck_remote_htlc keys) (ck_local_htlc keys) + (htlc_payment_hash htlc) expiry fmt + in sweep op value script + (htlc_output_type_weight fmt (HTLCReceivedOutput expiry)) + (remote_htlc_sequence fmt) e dest feerate --- | Spend a received HTLC directly with preimage on the remote --- commitment. +-- | Spend an HTLC we received directly with the preimage, from the +-- remote commitment (where it is an offered HTLC). -- --- On the remote commitment, their offered HTLCs (our received) --- can be claimed with the payment preimage. +-- The caller signs with our HTLC privkey and uses bolt3's +-- 'offered_htlc_witness_preimage'. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_remote_htlc_preimage :: OutPoint -- ^ Outpoint of the HTLC output. @@ -237,42 +173,29 @@ spend_remote_htlc_preimage -- ^ The HTLC being spent. -> CommitmentKeys -- ^ Keys for the remote commitment. - -> ChannelFeatures + -> CommitmentFormat -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_remote_htlc_preimage !op !value !htlc !keys - !features !destScript !feerate = - let !witnessScript = offered_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - features - !weight = htlcOutputTypeWeight features - (HTLCOfferedOutput (htlc_cltv_expiry htlc)) - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !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) +spend_remote_htlc_preimage !op !value !htlc !keys !fmt !dest !feerate = + let !expiry = htlc_cltv_expiry htlc + !script = offered_htlc_script (ck_revocation_pubkey keys) + (ck_remote_htlc keys) (ck_local_htlc keys) + (htlc_payment_hash htlc) fmt + in sweep op value script + (htlc_output_type_weight fmt (HTLCOfferedOutput expiry)) + (remote_htlc_sequence fmt) 0 dest feerate -- revoked commitment spends ------------------------------------------ --- | Spend a revoked to_local output using the revocation key. +-- | Spend a revoked to_local output with the revocation key. -- --- The caller signs with the revocation privkey and uses --- 'to_local_witness_revoke' from bolt3. +-- The caller signs with the revocation privkey and uses bolt3's +-- 'to_local_witness_revoke'. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_revoked_to_local :: OutPoint -- ^ Outpoint of the to_local output. @@ -285,30 +208,19 @@ spend_revoked_to_local -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_revoked_to_local !op !value !revpk !delay - !delayedpk !destScript !feerate = - let !witnessScript = - to_local_script revpk delay delayedpk - !weight = to_local_penalty_input_weight - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_revoked_to_local !op !value !revpk !delay !delayedpk !dest + !feerate = + sweep op value (to_local_script revpk delay delayedpk) + to_local_penalty_input_weight 0xFFFFFFFF 0 dest feerate --- | Spend a revoked HTLC output using the revocation key. +-- | Spend a revoked HTLC output with the revocation key. -- --- The caller signs with the revocation privkey and uses --- 'offered_htlc_witness_revoke' or --- 'received_htlc_witness_revoke' from bolt3, depending on --- the output type. +-- The caller signs with the revocation privkey and uses bolt3's +-- 'offered_htlc_witness_revoke' or 'received_htlc_witness_revoke', +-- according to the output type. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- Returns 'Nothing' if the fee would exceed the output value, or +-- leave an output below the destination's dust threshold. spend_revoked_htlc :: OutPoint -- ^ Outpoint of the HTLC output. @@ -318,60 +230,30 @@ spend_revoked_htlc -- ^ Whether offered or received HTLC. -> RevocationPubkey -> CommitmentKeys - -> ChannelFeatures + -> CommitmentFormat -> PaymentHash -> Script -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_revoked_htlc !op !value !htype !revpk !keys - !features !ph !destScript !feerate = - case htype of - HTLCOfferedOutput _ -> - let !witnessScript = offered_htlc_script - revpk - (ck_remote_htlc keys) - (ck_local_htlc keys) - ph - features - !weight = htlcOutputTypeWeight features htype - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op 0xFFFFFFFF - destScript outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) - HTLCReceivedOutput expiry -> - let !witnessScript = received_htlc_script - revpk - (ck_remote_htlc keys) - (ck_local_htlc keys) - ph - expiry - features - !weight = htlcOutputTypeWeight features htype - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op 0xFFFFFFFF - destScript outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_revoked_htlc !op !value !htype !revpk !keys !fmt !ph !dest + !feerate = + let !remote = ck_remote_htlc keys + !local = ck_local_htlc keys + !script = case htype of + HTLCOfferedOutput _ -> + offered_htlc_script revpk remote local ph fmt + HTLCReceivedOutput expiry -> + received_htlc_script revpk remote local ph expiry fmt + in sweep op value script (htlc_output_type_weight fmt htype) + 0xFFFFFFFF 0 dest feerate --- | Spend a revoked second-stage HTLC output (HTLC-timeout or --- HTLC-success output) using the revocation key. --- --- 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'. +-- | Spend the output of a revoked HTLC-timeout or HTLC-success +-- transaction with the revocation key. -- --- Returns 'Nothing' if the fee would exceed the output value, --- or leave an output below the destination's dust threshold. +-- The output uses the to_local script, so this is +-- 'spend_revoked_to_local'. The caller signs with the revocation +-- privkey and uses bolt3's 'to_local_witness_revoke'. spend_revoked_htlc_output :: OutPoint -- ^ Outpoint of the second-stage output. @@ -384,27 +266,16 @@ spend_revoked_htlc_output -- ^ Destination scriptPubKey. -> FeeratePerKw -> Maybe SpendingTx -spend_revoked_htlc_output !op !value !revpk !delay - !delayedpk !destScript !feerate = - let !witnessScript = - to_local_script revpk delay delayedpk - !weight = to_local_penalty_input_weight - + penalty_tx_base_weight - !fee = spending_fee feerate weight - in case sweep_value value fee destScript of - Nothing -> Nothing - Just !outputValue -> - let !tx = mk_spending_tx op 0xFFFFFFFF destScript - outputValue 0 - in Just (SpendingTx tx witnessScript value - SIGHASH_ALL) +spend_revoked_htlc_output = spend_revoked_to_local -- anchor spends ------------------------------------------------------ --- | Spend an anchor output as the owner (immediately). +-- | Spend an anchor output as its owner (immediately). -- --- The caller signs with the funding privkey and uses --- 'anchor_witness_owner' from bolt3. +-- The transaction pays no fee and has a single input: it must be +-- combined with other inputs before broadcast. The caller signs +-- with the funding privkey and uses bolt3's +-- 'anchor_witness_owner'. spend_anchor_owner :: OutPoint -- ^ Outpoint of the anchor output. @@ -414,15 +285,16 @@ spend_anchor_owner -> Script -- ^ Destination scriptPubKey. -> SpendingTx -spend_anchor_owner !op !value !fundpk !destScript = - let !witnessScript = anchor_script fundpk - !tx = mk_spending_tx op 0xFFFFFFFE destScript - value 0 - in SpendingTx tx witnessScript value SIGHASH_ALL +spend_anchor_owner !op !value !fundpk !dest = + SpendingTx (mk_spending_tx op 0xFFFFFFFE dest value 0) + (anchor_script fundpk) value SIGHASH_ALL --- | Spend an anchor output as anyone (after 16 blocks). +-- | Spend an anchor output as anyone (16 blocks after the +-- commitment confirms). -- --- Uses 'anchor_witness_anyone' from bolt3 (empty signature). +-- As 'spend_anchor_owner', the transaction pays no fee. The +-- witness is bolt3's 'anchor_witness_anyone', which needs no +-- signature. spend_anchor_anyone :: OutPoint -- ^ Outpoint of the anchor output. @@ -432,98 +304,108 @@ spend_anchor_anyone -> Script -- ^ Destination scriptPubKey. -> SpendingTx -spend_anchor_anyone !op !value !fundpk !destScript = - let !witnessScript = anchor_script fundpk - !tx = mk_spending_tx op 16 destScript value 0 - in SpendingTx tx witnessScript value SIGHASH_ALL +spend_anchor_anyone !op !value !fundpk !dest = + SpendingTx (mk_spending_tx op 16 dest value 0) + (anchor_script fundpk) value SIGHASH_ALL -- internal helpers --------------------------------------------------- +-- | A single-input, single-output sweep paying the input value, less +-- the fee for the given input weight, to the destination. +sweep + :: OutPoint + -> Satoshi -- ^ input value + -> Script -- ^ input witness script + -> Word64 -- ^ input weight + -> Word32 -- ^ input nSequence + -> Word32 -- ^ locktime + -> Script -- ^ destination scriptPubKey + -> FeeratePerKw + -> Maybe SpendingTx +sweep !op !value !script !weight !sq !locktime !dest !feerate = do + let !fee = spending_fee feerate (weight + penalty_tx_base_weight) + out <- sweep_value value fee dest + pure (SpendingTx (mk_spending_tx op sq dest out locktime) script + value SIGHASH_ALL) + -- | Value of a sweep's single output: the input value less the fee. -- --- 'Nothing' if the fee meets or exceeds the input value, or if the --- output would be below the dust threshold of its script. +-- 'Nothing' if the fee exceeds the input value, or if the output +-- would be below the dust threshold of its script. sweep_value :: Satoshi -> Satoshi -> Script -> Maybe Satoshi -sweep_value !value !fee !destScript - | unSatoshi fee >= unSatoshi value = Nothing - | unSatoshi out < unSatoshi (dust_threshold destScript) = Nothing - | otherwise = Just out - where - out = Satoshi (unSatoshi value - unSatoshi fee) +sweep_value !value !fee (Script dest) = do + out <- BOLT1.sub_sat value fee + if BOLT1.un_satoshi out < dust_threshold dest + then Nothing + else Just out --- | Dust threshold for an output paying to a script. +-- | Dust threshold, in satoshis, for an output paying to a script. -- --- As Bitcoin Core computes it for spendable outputs at the default --- dust relay feerate of 3 sat/vbyte: three times the size of the --- output plus that of an input spending it (67 vbytes for a --- witness program, 148 otherwise). This gives 546 sats for P2PKH, --- 540 for P2SH, 294 for P2WPKH, and 330 for P2WSH and P2TR. -dust_threshold :: Script -> Satoshi -dust_threshold (Script s) = +-- As Bitcoin Core computes it for spendable outputs at the default +-- dust relay feerate of 3 sat/vbyte: three times the size of the +-- output plus that of an input spending it (67 vbytes for a +-- witness program, 148 otherwise). This gives 546 sats for P2PKH, +-- 540 for P2SH, 294 for P2WPKH, and 330 for P2WSH and P2TR. +dust_threshold :: BS.ByteString -> Word64 +dust_threshold s = let !len = fromIntegral (BS.length s) :: Word64 - !lenSize = if len < 0xfd then 1 - else if len <= 0xffff then 3 - else 5 - !spendSize = if is_witness_program s then 67 else 148 - in Satoshi (3 * (8 + lenSize + len + spendSize)) + !len_size + | len < 0xfd = 1 + | len <= 0xffff = 3 + | otherwise = 5 + !spend_size = if is_witness_program s then 67 else 148 + in 3 * (8 + len_size + len + spend_size) -- | Whether a script is a segwit witness program: a version opcode -- (OP_0 or OP_1 to OP_16) followed by a single 2-40 byte push. is_witness_program :: BS.ByteString -> Bool is_witness_program s = case BS.unpack (BS.take 2 s) of - [version, pushLen] -> + [version, push_len] -> (version == 0x00 || (version >= 0x51 && version <= 0x60)) - && pushLen >= 2 && pushLen <= 40 - && fromIntegral pushLen + 2 == BS.length s + && push_len >= 2 && push_len <= 40 + && fromIntegral push_len + 2 == BS.length s _ -> False --- | Build a simple single-input single-output spending tx. +-- | The nSequence of a direct HTLC spend from the remote commitment: +-- 1 under 'Anchors' (for the HTLC scripts' CSV), 0 otherwise. +remote_htlc_sequence :: CommitmentFormat -> Word32 +remote_htlc_sequence fmt = case fmt of + StaticRemotekey -> 0 + Anchors -> 1 + +-- | The witness script of an HTLC output of a commitment with the +-- given keys. +htlc_script :: CommitmentKeys -> CommitmentFormat -> HTLC -> Script +htlc_script !keys !fmt !h = + let !revpk = ck_revocation_pubkey keys + !remote = ck_remote_htlc keys + !local = ck_local_htlc keys + !ph = htlc_payment_hash h + in case htlc_direction h of + HTLCOffered -> offered_htlc_script revpk remote local ph fmt + HTLCReceived -> + received_htlc_script revpk remote local ph + (htlc_cltv_expiry h) fmt + +-- | Build a version 2, single-input, single-output transaction. mk_spending_tx - :: OutPoint -- ^ Input outpoint - -> Word32 -- ^ Input nSequence - -> Script -- ^ Output scriptPubKey - -> Satoshi -- ^ Output value - -> Word32 -- ^ Locktime + :: OutPoint -- ^ input outpoint + -> Word32 -- ^ input nSequence + -> Script -- ^ output scriptPubKey + -> Satoshi -- ^ output value + -> Word32 -- ^ locktime -> Tx -mk_spending_tx !op !seqNo !destScript !outputValue - !locktime = - let !txIn = TxIn - { txin_prevout = op - , txin_script_sig = BS.empty - , txin_sequence = seqNo - } - !txOut = TxOut - { txout_value = unSatoshi outputValue - , txout_script_pubkey = unScript destScript - } - in Tx - { tx_version = 2 - , tx_inputs = txIn :| [] - , tx_outputs = txOut :| [] - , tx_witnesses = [] - , tx_locktime = locktime - } - --- | Convert a bolt3 HTLCTx to a ppad-tx Tx. -htlc_tx_to_tx :: HTLCTx -> Tx -htlc_tx_to_tx !htx = - let !txIn = TxIn - { txin_prevout = htx_input_outpoint htx - , txin_script_sig = BS.empty - , txin_sequence = - unSequence (htx_input_sequence htx) - } - !txOut = TxOut - { txout_value = - unSatoshi (htx_output_value htx) - , txout_script_pubkey = - unScript (htx_output_script htx) - } - in Tx - { tx_version = htx_version htx - , tx_inputs = txIn :| [] - , tx_outputs = txOut :| [] - , tx_witnesses = [] - , tx_locktime = - unLocktime (htx_locktime htx) - } +mk_spending_tx !op !sq (Script dest) !value !locktime = Tx + { tx_version = 2 + , tx_inputs = TxIn + { txin_prevout = op + , txin_script_sig = BS.empty + , txin_sequence = sq + } :| [] + , tx_outputs = TxOut + { txout_value = BOLT1.un_satoshi value + , txout_script_pubkey = dest + } :| [] + , tx_witnesses = [] + , tx_locktime = locktime + } diff --git a/lib/Lightning/Protocol/BOLT5/Types.hs b/lib/Lightning/Protocol/BOLT5/Types.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} @@ -20,8 +20,8 @@ module Lightning.Protocol.BOLT5.Types ( -- * HTLC output type , HTLCOutputType(..) - , htlcOutputType - , htlcOutputTypeWeight + , htlc_output_type + , htlc_output_type_weight -- * Spending transactions , SpendingTx(..) @@ -50,19 +50,22 @@ module Lightning.Protocol.BOLT5.Types ( , spending_fee ) where -import Bitcoin.Prim.Tx (Tx(..)) -import Bitcoin.Prim.Tx.Sighash (SighashType(..)) +import Bitcoin.Prim.Tx (OutPoint, Tx) +import Bitcoin.Prim.Tx.Sighash (SighashType) +import Control.DeepSeq (NFData(..)) import Data.Word (Word64) import GHC.Generics (Generic) -import Lightning.Protocol.BOLT3.Types -import Lightning.Protocol.BOLT3.Tx ( - CommitmentKeys(..) - , OutputType(..) - ) +import Lightning.Protocol.BOLT1 (Satoshi) +import qualified Lightning.Protocol.BOLT1 as BOLT1 +import Lightning.Protocol.BOLT3 -- close identification ----------------------------------------------- -- | What kind of close was detected on chain. +-- +-- @identify_close@ produces only 'LocalCommitClose' and +-- 'RemoteCommitClose'; recognising mutual and revoked closes is +-- left to the caller. data CloseType = MutualClose -- ^ Cooperative closure agreed by both parties. @@ -74,6 +77,8 @@ data CloseType -- ^ A revoked (outdated) commitment transaction was broadcast. deriving (Eq, Show, Generic) +instance NFData CloseType + -- output classification ---------------------------------------------- -- | An unresolved commitment transaction output. @@ -83,42 +88,46 @@ data UnresolvedOutput = UnresolvedOutput , uo_type :: !OutputResolution } deriving (Eq, Show, Generic) +instance NFData UnresolvedOutput + -- | How to resolve an output, per BOLT #5 rules. data OutputResolution = Resolved - -- ^ Already resolved (e.g. to_remote on local commit). + -- ^ Nothing to do (e.g. to_remote on our local commitment). | SpendToLocal !ToSelfDelay !RevocationPubkey !LocalDelayedPubkey - -- ^ Spend to_local after CSV delay. + -- ^ Spend to_local after the CSV delay. | SpendHTLCTimeout - !HTLC !CommitmentKeys !ChannelFeatures - -- ^ Spend via HTLC-timeout second-stage tx (local commit, - -- local offer). + !HTLC !CommitmentKeys !CommitmentFormat + -- ^ Spend via an HTLC-timeout transaction (our commitment, an + -- HTLC we offered). | SpendHTLCSuccess - !HTLC !CommitmentKeys !ChannelFeatures - -- ^ Spend via HTLC-success second-stage tx (local commit, - -- remote offer). + !HTLC !CommitmentKeys !CommitmentFormat + -- ^ Spend via an HTLC-success transaction (our commitment, an + -- HTLC we received). | SpendHTLCTimeoutDirect !HTLC - -- ^ Spend HTLC directly after timeout (remote commit, - -- local offer). + -- ^ Spend the HTLC directly after it times out (the remote + -- commitment, an HTLC we offered). | SpendHTLCPreimageDirect !HTLC - -- ^ Spend HTLC directly with preimage (remote commit, - -- remote offer). + -- ^ Spend the HTLC directly with the preimage (the remote + -- commitment, an HTLC we received). | Revoke !RevocationPubkey - -- ^ Spend revoked to_local with revocation key. + -- ^ Spend a revoked to_local output with the revocation key. | RevokeHTLC !RevocationPubkey !HTLCOutputType - -- ^ Spend revoked HTLC output with revocation key. + -- ^ Spend a revoked HTLC output with the revocation key. | AnchorSpend !FundingPubkey - -- ^ Spend anchor output. + -- ^ Spend our anchor output. deriving (Eq, Show, Generic) +instance NFData OutputResolution + -- HTLC output type ------------------------------------------------- --- | Type of HTLC output, restricted to only HTLC variants. +-- | The kind of an HTLC output, from the commitment owner's point of +-- view. -- --- Unlike bolt3's 'OutputType' which includes all six output --- types, this ADT makes it impossible to confuse HTLC outputs --- with to_local, to_remote, or anchor outputs. +-- Unlike bolt3's 'OutputType', this can't describe a to_local, +-- to_remote or anchor output. data HTLCOutputType = HTLCOfferedOutput {-# UNPACK #-} !CltvExpiry -- ^ Offered HTLC output with CLTV expiry. @@ -126,63 +135,74 @@ data HTLCOutputType -- ^ Received HTLC output with CLTV expiry. deriving (Eq, Show, Generic) --- | Extract an 'HTLCOutputType' from a bolt3 'OutputType'. +instance NFData HTLCOutputType + +-- | The 'HTLCOutputType' of a bolt3 'OutputType', or 'Nothing' if +-- it isn't an HTLC output. -- --- Returns 'Nothing' for non-HTLC output types. +-- >>> htlc_output_type OutputToLocal +-- Nothing +htlc_output_type :: OutputType -> Maybe HTLCOutputType +htlc_output_type t = case t of + OutputHTLC h -> Just $ case htlc_direction h of + HTLCOffered -> HTLCOfferedOutput (htlc_cltv_expiry h) + HTLCReceived -> HTLCReceivedOutput (htlc_cltv_expiry h) + OutputToLocal -> Nothing + OutputToRemote -> Nothing + OutputLocalAnchor -> Nothing + OutputRemoteAnchor -> Nothing +{-# INLINE htlc_output_type #-} + +-- | Penalty input weight for an HTLC output. -- --- >>> htlcOutputType (OutputOfferedHTLC (CltvExpiry 500)) --- Just (HTLCOfferedOutput (CltvExpiry ...)) --- >>> htlcOutputType OutputToLocal --- Nothing -htlcOutputType :: OutputType -> Maybe HTLCOutputType -htlcOutputType (OutputOfferedHTLC e) = - Just (HTLCOfferedOutput e) -htlcOutputType (OutputReceivedHTLC e) = - Just (HTLCReceivedOutput e) -htlcOutputType _ = Nothing -{-# INLINE htlcOutputType #-} - --- | Penalty input weight for an HTLC output type. -htlcOutputTypeWeight :: ChannelFeatures -> HTLCOutputType -> Word64 -htlcOutputTypeWeight !features (HTLCOfferedOutput _) - | has_anchors features = offered_htlc_penalty_input_weight_anchors - | otherwise = offered_htlc_penalty_input_weight -htlcOutputTypeWeight !features (HTLCReceivedOutput _) - | has_anchors features = accepted_htlc_penalty_input_weight_anchors - | otherwise = accepted_htlc_penalty_input_weight -{-# INLINE htlcOutputTypeWeight #-} +-- >>> htlc_output_type_weight Anchors (HTLCOfferedOutput (CltvExpiry 500)) +-- 410 +htlc_output_type_weight :: CommitmentFormat -> HTLCOutputType -> Word64 +htlc_output_type_weight !fmt !t = case (fmt, t) of + (StaticRemotekey, HTLCOfferedOutput _) -> + offered_htlc_penalty_input_weight + (StaticRemotekey, HTLCReceivedOutput _) -> + accepted_htlc_penalty_input_weight + (Anchors, HTLCOfferedOutput _) -> + offered_htlc_penalty_input_weight_anchors + (Anchors, HTLCReceivedOutput _) -> + accepted_htlc_penalty_input_weight_anchors +{-# INLINE htlc_output_type_weight #-} -- spending transactions ---------------------------------------------- --- | Unsigned spending transaction, ready for caller to sign. +-- | An unsigned spending transaction, with what the caller needs to +-- sign its (single) input. -- --- The caller uses bolt3 witness constructors to assemble the --- final witness after signing. +-- The caller signs the input's BIP143 sighash (ppad-tx's +-- @sighash_segwit@ over 'stx_tx', input 0, 'stx_input_script', +-- 'stx_input_value' and 'stx_sighash_type') and assembles the +-- witness with bolt3's witness functions. data SpendingTx = SpendingTx { stx_tx :: !Tx -- ^ The unsigned transaction. , stx_input_script :: !Script - -- ^ Witness script for the input being spent. + -- ^ Witness script of the output being spent. , stx_input_value :: {-# UNPACK #-} !Satoshi - -- ^ Value of the input being spent (for sighash). + -- ^ Value of the output being spent. , stx_sighash_type :: !SighashType - -- ^ Sighash type for the local signature. + -- ^ Sighash type of the local signature. } deriving (Eq, Show, Generic) +instance NFData SpendingTx where + rnf (SpendingTx tx s v h) = rnf tx `seq` rnf s `seq` rnf v `seq` h `seq` () + -- revoked outputs ---------------------------------------------------- -- | A revoked output that can be swept with a revocation key. --- --- This type restricts penalty transaction inputs to only --- valid revocation targets: to_local outputs and HTLC outputs. --- Other output types (to_remote, anchors) cannot appear in --- penalty transactions. data RevokedOutput = RevokedOutput { ro_outpoint :: !OutPoint , ro_value :: {-# UNPACK #-} !Satoshi , ro_type :: !RevokedOutputType } deriving (Eq, Show, Generic) +instance NFData RevokedOutput + -- | What kind of revoked output is being swept. data RevokedOutputType = RevokedToLocal @@ -191,93 +211,104 @@ data RevokedOutputType -- ^ Revoked HTLC output. deriving (Eq, Show, Generic) +instance NFData RevokedOutputType + -- | Penalty input weight for a revoked output. -revoked_output_weight :: ChannelFeatures -> RevokedOutput -> Word64 -revoked_output_weight !features !ro = case ro_type ro of +revoked_output_weight :: CommitmentFormat -> RevokedOutput -> Word64 +revoked_output_weight !fmt !ro = case ro_type ro of RevokedToLocal -> to_local_penalty_input_weight RevokedHTLC !htype -> - htlcOutputTypeWeight features htype + htlc_output_type_weight fmt htype {-# INLINE revoked_output_weight #-} -- weight constants (BOLT #5 Appendix A) ------------------------------ -- | Expected weight of the to_local penalty transaction witness --- (160 bytes). +-- (160 weight units). to_local_penalty_witness_weight :: Word64 to_local_penalty_witness_weight = 160 -- | Expected weight of the offered_htlc penalty transaction --- witness (243 bytes), for a 133-byte witness script (no --- option_anchors). +-- witness (243 weight units), for a 133-byte witness script +-- (no option_anchors). offered_htlc_penalty_witness_weight :: Word64 offered_htlc_penalty_witness_weight = 243 -- | Expected weight of the accepted_htlc penalty transaction --- witness (249 bytes), for a 139-byte witness script (no --- option_anchors). +-- witness (249 weight units), for a 139-byte witness script +-- (no option_anchors). accepted_htlc_penalty_witness_weight :: Word64 accepted_htlc_penalty_witness_weight = 249 -- | Expected weight of the offered_htlc penalty transaction --- witness with option_anchors (246 bytes), whose witness script --- is 136 bytes. +-- witness with option_anchors (246 weight units), whose witness +-- script is 136 bytes. offered_htlc_penalty_witness_weight_anchors :: Word64 offered_htlc_penalty_witness_weight_anchors = 246 -- | Expected weight of the accepted_htlc penalty transaction --- witness with option_anchors (252 bytes), whose witness script --- is 142 bytes. +-- witness with option_anchors (252 weight units), whose witness +-- script is 142 bytes. accepted_htlc_penalty_witness_weight_anchors :: Word64 accepted_htlc_penalty_witness_weight_anchors = 252 --- | Weight of a to_local penalty input (164 + 160 = 324 bytes). +-- | Weight of a to_local penalty input (164 + 160 = 324). to_local_penalty_input_weight :: Word64 to_local_penalty_input_weight = 324 --- | Weight of an offered_htlc penalty input --- (164 + 243 = 407 bytes). +-- | Weight of an offered_htlc penalty input (164 + 243 = 407). offered_htlc_penalty_input_weight :: Word64 offered_htlc_penalty_input_weight = 407 --- | Weight of an accepted_htlc penalty input --- (164 + 249 = 413 bytes). +-- | Weight of an accepted_htlc penalty input (164 + 249 = 413). accepted_htlc_penalty_input_weight :: Word64 accepted_htlc_penalty_input_weight = 413 -- | Weight of an offered_htlc penalty input with option_anchors --- (164 + 246 = 410 bytes). +-- (164 + 246 = 410). offered_htlc_penalty_input_weight_anchors :: Word64 offered_htlc_penalty_input_weight_anchors = 410 -- | Weight of an accepted_htlc penalty input with option_anchors --- (164 + 252 = 416 bytes). +-- (164 + 252 = 416). accepted_htlc_penalty_input_weight_anchors :: Word64 accepted_htlc_penalty_input_weight_anchors = 416 --- | Weight of a to_remote P2WPKH input --- (108 + 164 = 272 bytes). +-- | Weight of a to_remote P2WPKH input (108 + 164 = 272). to_remote_input_weight :: Word64 to_remote_input_weight = 272 --- | Base weight of a penalty transaction (4*53 + 2 = 214 bytes). +-- | Base weight of a penalty transaction (4 * 53 + 2 = 214). -- --- Non-witness: version(4) + input_count(1) + output_count(1) + --- value(8) + script_len(1) + p2wsh_script(34) + locktime(4) = 53 --- Witness header: 2 bytes. +-- Non-witness: version (4) + input count (1) + output count (1) + +-- value (8) + script length (1) + P2WSH script (34) + locktime (4) +-- = 53 bytes. Witness header: 2 bytes. +-- +-- The spending transactions here use it as their base weight +-- whatever their destination script. penalty_tx_base_weight :: Word64 penalty_tx_base_weight = 214 --- | Maximum standard transaction weight (400,000 bytes). +-- | Maximum standard transaction weight (400,000). max_standard_weight :: Word64 max_standard_weight = 400000 -- fee calculation ---------------------------------------------------- --- | Calculate the fee for a spending transaction given its weight. +-- | The fee for a transaction of the given weight: +-- +-- @fee = feerate_per_kw * weight / 1000@ +-- +-- rounded down, and saturating at 21 million BTC. -- --- @fee = feerate_per_kw * weight / 1000@ +-- >>> spending_fee (FeeratePerKw 253) 324 +-- Satoshi 81 spending_fee :: FeeratePerKw -> Word64 -> Satoshi spending_fee (FeeratePerKw !rate) !weight = - Satoshi ((fromIntegral rate * weight) `div` 1000) + let !f = toInteger rate * toInteger weight `quot` 1000 + !m = BOLT1.un_satoshi BOLT1.max_satoshi + in case BOLT1.satoshi (fromInteger (min f (toInteger m))) of + Just s -> s + Nothing -> BOLT1.max_satoshi {-# INLINE spending_fee #-} diff --git a/ppad-bolt5.cabal b/ppad-bolt5.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: ppad-bolt5 -version: 0.0.1 +version: 0.1.0 synopsis: On-chain transaction handling per BOLT #5 license: MIT license-file: LICENSE @@ -8,7 +8,7 @@ author: Jared Tobin maintainer: jared@ppad.tech category: Cryptography build-type: Simple -tested-with: GHC == 9.10.3 +tested-with: GHC == { 9.10.3 } extra-doc-files: CHANGELOG description: On-chain transaction handling, per @@ -25,16 +25,18 @@ library -Wall exposed-modules: Lightning.Protocol.BOLT5 + other-modules: Lightning.Protocol.BOLT5.Detect - Lightning.Protocol.BOLT5.Internal Lightning.Protocol.BOLT5.Spend Lightning.Protocol.BOLT5.Types build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13 - , ppad-bolt3 >= 0.0.1 && < 0.1 + , deepseq >= 1.4 && < 1.6 + , ppad-bolt1 >= 0.1 && < 0.2 + , ppad-bolt3 >= 0.1 && < 0.2 , ppad-sha256 >= 0.3.2 && < 0.4 - , ppad-tx >= 0.1 && < 0.2 + , ppad-tx >= 0.2 && < 0.3 test-suite bolt5-tests type: exitcode-stdio-1.0 @@ -43,11 +45,12 @@ test-suite bolt5-tests main-is: Main.hs ghc-options: - -rtsopts -Wall -O2 + -rtsopts -Wall build-depends: base , bytestring + , ppad-bolt1 , ppad-bolt3 , ppad-bolt5 , ppad-sha256 @@ -62,17 +65,20 @@ benchmark bolt5-bench default-language: Haskell2010 hs-source-dirs: bench main-is: Main.hs + other-modules: Fixtures ghc-options: - -rtsopts -O2 -Wall -fno-warn-orphans + -rtsopts -O2 -Wall build-depends: base , bytestring , criterion , deepseq + , ppad-bolt1 , ppad-bolt3 , ppad-bolt5 + , ppad-sha256 , ppad-tx benchmark bolt5-weigh @@ -80,15 +86,18 @@ benchmark bolt5-weigh default-language: Haskell2010 hs-source-dirs: bench main-is: Weight.hs + other-modules: Fixtures ghc-options: - -rtsopts -O2 -Wall -fno-warn-orphans + -rtsopts -O2 -Wall build-depends: base , bytestring , deepseq + , ppad-bolt1 , ppad-bolt3 , ppad-bolt5 + , ppad-sha256 , ppad-tx , weigh 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