bolt3

Lightning transaction and script formats, per BOLT #3 (docs.ppad.tech/bolt3).
git clone git://git.ppad.tech/bolt3.git
Log | Files | Refs | README | LICENSE

commit 1709ab3ae333760b07fb2b90354fb9d03d33541e
parent 2fd8129b9ee18680bfb9b61d176286d575c90f55
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 14:53:33 +0400

lib: port to ppad-bolt1 0.1.0 and ppad-tx 0.2.0, redesign the API

Ports to bolt1's abstract types and bounded amounts (with checked
arithmetic) and to ppad-tx 0.2.0. bolt3's Pubkey is merged into
bolt1's Point, wrapped in the role newtypes. Only
Lightning.Protocol.BOLT3 is exposed; types with invariants are
abstract, and every public type has an NFData instance.

- CommitmentFormat (StaticRemotekey | Anchors) replaces
  ChannelFeatures.
- build_commitment_tx orders the payment basepoints for the obscured
  commitment number by cc_is_funder, so CommitmentContext is
  owner-relative throughout, and returns Nothing rather than an
  output-less transaction.
- OutputType carries the HTLC an output pays (OutputHTLC); outputs are
  CommitmentOutput values, whose field names no longer clash with
  ppad-tx's. build_htlc_tx replaces the timeout/success pair and fails
  if the fee exceeds the amount.
- Closing: build_legacy_closing_tx applies the signer's dust limit to
  both outputs and can omit the signer's own output;
  build_closing_tx (option_simple_close) charges the closer, takes an
  explicit choice of outputs and gives OP_RETURN outputs amount zero.
- to_remote_script is split into to_remote_witness_script and
  to_remote_script_pubkey. Witness functions append the witness script
  and take typed keys; funding_witness orders signatures by pubkey.
  Adds HTLC transaction input witnesses and remote_htlc_sighash.
- Per-commitment secrets: Seed and SecretIndex types,
  PerCommitmentSecret throughout, commitment_secret_index, and an
  abstract SecretStore with a redacted Show instance and
  secret_store/un_secret_store for persistence.
- Adds derive_commitment_keys, derive_privkey and
  derive_revocationprivkey (with constant-time arithmetic modulo the
  group order). Key derivation rejects the point at infinity.
- encode_tx becomes encode_commitment_tx; the encoders are documented
  as unsigned, witness-free serializations.
- Removes Validate, Decode, Encode and Internal, the role-specific
  derivation functions and their wNAF variants (derive_pubkey and the
  context variants of derive_per_commitment_point and
  derive_commitment_keys remain), encode_tx_for_signing, the witness
  encoders, has_anchors, sort_outputs, the trimming filters,
  derive_secret, the HTLC output aliases, weight constants and unused
  fields.

Diffstat:
Mlib/Lightning/Protocol/BOLT3.hs | 328+++++++++++++++++++++++++------------------------------------------------------
Dlib/Lightning/Protocol/BOLT3/Decode.hs | 27---------------------------
Dlib/Lightning/Protocol/BOLT3/Encode.hs | 102-------------------------------------------------------------------------------
Dlib/Lightning/Protocol/BOLT3/Internal.hs | 56--------------------------------------------------------
Mlib/Lightning/Protocol/BOLT3/Keys.hs | 1016+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mlib/Lightning/Protocol/BOLT3/Scripts.hs | 990+++++++++++++++++++++++++++++++++----------------------------------------------
Mlib/Lightning/Protocol/BOLT3/Tx.hs | 1071++++++++++++++++++++++++++++++++++++-------------------------------------------
Mlib/Lightning/Protocol/BOLT3/Types.hs | 566++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Dlib/Lightning/Protocol/BOLT3/Validate.hs | 359-------------------------------------------------------------------------------
Mppad-bolt3.cabal | 12+++++-------
10 files changed, 1855 insertions(+), 2672 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT3.hs b/lib/Lightning/Protocol/BOLT3.hs @@ -6,176 +6,135 @@ -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech> -- --- Bitcoin transaction formats for the Lightning Network, per --- [BOLT #3](https://github.com/lightning/bolts/blob/master/03-transactions.md). +-- Bitcoin transaction and script formats for the Lightning Network, per +-- [BOLT #3](https://github.com/lightning/bolts/blob/master/03-transactions.md) +-- (commitment, HTLC and closing transactions, their output scripts and +-- witnesses, per-commitment key derivation, and per-commitment secret +-- generation and storage). -- --- = Overview --- --- This library implements the transaction and script formats defined in --- BOLT #3, including: --- --- * Commitment transactions with to_local, to_remote, anchor, and HTLC --- outputs --- * HTLC-timeout and HTLC-success second-stage transactions --- * Closing transactions (legacy and option_simple_close) --- * Per-commitment key derivation and secret storage --- * Transaction serialization and parsing --- * Stateless validation --- --- = Quick Start --- --- @ --- import Lightning.Protocol.BOLT3 --- --- -- Build a commitment transaction --- let ctx = CommitmentContext { ... } --- tx = build_commitment_tx ctx --- --- -- Serialize for signing --- let bytes = encode_tx tx --- --- -- Validate the transaction --- case validate_commitment_tx dustLimit features tx of --- Right () -> putStrLn "Valid" --- Left err -> print err --- @ --- --- = Modules --- --- * "Lightning.Protocol.BOLT3.Types" - Core types (Satoshi, Pubkey, HTLC, --- etc.) --- * "Lightning.Protocol.BOLT3.Keys" - Per-commitment key derivation and --- secret storage --- * "Lightning.Protocol.BOLT3.Scripts" - Witness script templates (funding, --- to_local, HTLC, anchor) --- * "Lightning.Protocol.BOLT3.Tx" - Transaction assembly --- * "Lightning.Protocol.BOLT3.Encode" - Transaction serialization --- * "Lightning.Protocol.BOLT3.Decode" - Transaction parsing --- * "Lightning.Protocol.BOLT3.Validate" - Stateless validation +-- Amounts, points, hashes and per-commitment secrets are the types of +-- ppad-bolt1's "Lightning.Protocol.BOLT1"; transactions convert to +-- ppad-tx's "Bitcoin.Prim.Tx" for txids, sighashes and serialization. +-- The @option_static_remotekey@ and @option_anchors@ commitment formats +-- are supported ('CommitmentFormat'). module Lightning.Protocol.BOLT3 ( - -- * Types - -- ** Monetary amounts - Satoshi(..) - , MilliSatoshi(..) - , msatToSat - , satToMsat - - -- ** Keys and points - , Pubkey - , unPubkey - , pubkey - , Seckey - , unSeckey - , seckey - , Point - , unPoint - , point - - -- ** Hashes - , PaymentHash - , unPaymentHash - , paymentHash - , PaymentPreimage - , unPaymentPreimage - , paymentPreimage + -- * Commitment transactions + CommitmentContext(..) + , CommitmentFormat(..) + , CommitmentTx(..) + , CommitmentOutput(..) + , OutputType(..) + , build_commitment_tx - -- ** Transaction primitives - , TxId(..) - , mkTxId - , OutPoint(..) - , Sequence(..) - , Locktime(..) + -- ** HTLCs + , HTLC(..) + , HTLCDirection(..) -- ** Channel parameters , CommitmentNumber - , unCommitmentNumber , commitment_number + , un_commitment_number , next_commitment_number , ToSelfDelay(..) , CltvExpiry(..) , DustLimit(..) , FeeratePerKw(..) + , Locktime(..) + , Sequence(..) - -- ** HTLC types - , HTLC(..) - , HTLCDirection(..) + -- ** Fees and trimming + , commitment_fee + , htlc_timeout_fee + , htlc_success_fee + , htlc_trim_threshold + , is_trimmed + , anchor_output_value + + -- * HTLC transactions + , HTLCContext(..) + , HTLCTx(..) + , build_htlc_tx + + -- * Closing transactions + , ClosingTx(..) + , ClosingOutput(..) + , ClosingContext(..) + , ClosingOutputs(..) + , build_closing_tx + , LegacyClosingContext(..) + , build_legacy_closing_tx + + -- ** Dust thresholds + , dust_p2pkh + , dust_p2sh + , dust_p2wpkh + , dust_p2wsh + + -- * Serialization + , commitment_to_tx + , htlc_to_tx + , closing_to_tx + , encode_commitment_tx + , encode_htlc_tx + , encode_closing_tx - -- ** Basepoints + -- * Keys + -- ** Basepoints and per-commitment points , Basepoints(..) - , PerCommitmentPoint(..) - , PerCommitmentSecret - , unPerCommitmentSecret - , perCommitmentSecret , RevocationBasepoint(..) , PaymentBasepoint(..) , DelayedPaymentBasepoint(..) , HtlcBasepoint(..) + , PerCommitmentPoint(..) + , derive_per_commitment_point + , derive_per_commitment_point' - -- ** Derived keys - , LocalPubkey(..) - , RemotePubkey(..) + -- ** Public keys + , CommitmentKeys(..) + , derive_commitment_keys + , derive_commitment_keys' + , RevocationPubkey(..) , LocalDelayedPubkey(..) - , RemoteDelayedPubkey(..) , LocalHtlcPubkey(..) , RemoteHtlcPubkey(..) - , RevocationPubkey(..) + , RemotePubkey(..) , FundingPubkey(..) - - -- ** Script and witness - , Script(..) - , Witness(..) - - -- ** Channel features - , ChannelFeatures(..) - , has_anchors - - -- ** Constants - , commitment_weight_no_anchors - , commitment_weight_anchors - , htlc_timeout_weight_no_anchors - , htlc_timeout_weight_anchors - , htlc_success_weight_no_anchors - , htlc_success_weight_anchors - , htlc_output_weight - , dust_p2pkh - , dust_p2sh - , dust_p2wpkh - , dust_p2wsh - , anchor_output_value - - -- * Key derivation - , derive_per_commitment_point - , derive_per_commitment_point' , derive_pubkey - , derive_pubkey' - , derive_localpubkey - , derive_localpubkey' - , derive_local_htlcpubkey - , derive_local_htlcpubkey' - , derive_remote_htlcpubkey - , derive_remote_htlcpubkey' - , derive_local_delayedpubkey - , derive_local_delayedpubkey' - , derive_remote_delayedpubkey - , derive_remote_delayedpubkey' , derive_revocationpubkey - -- ** Secret generation + -- ** Private keys + , Seckey + , seckey + , un_seckey + , derive_privkey + , derive_revocationprivkey + + -- * Per-commitment secrets + , Seed + , seed + , un_seed + , SecretIndex + , secret_index + , un_secret_index + , commitment_secret_index , generate_from_seed - , derive_secret - -- ** Secret storage - , SecretStore(..) - , SecretEntry(..) - , empty_store + -- ** Storage + , SecretStore + , empty_secret_store , insert_secret , derive_old_secret + , secret_store + , un_secret_store - -- ** Commitment number + -- ** Commitment number obscuring , obscured_commitment_number - -- * Scripts + -- * Scripts and witnesses + , Script(..) + , to_p2wsh + -- ** Funding output , funding_script , funding_witness @@ -186,7 +145,8 @@ module Lightning.Protocol.BOLT3 ( , to_local_witness_revoke -- ** to_remote output - , to_remote_script + , to_remote_witness_script + , to_remote_script_pubkey , to_remote_witness -- ** Anchor outputs @@ -194,105 +154,23 @@ module Lightning.Protocol.BOLT3 ( , anchor_witness_owner , anchor_witness_anyone - -- ** Offered HTLC + -- ** Offered HTLC outputs , offered_htlc_script , offered_htlc_witness_preimage , offered_htlc_witness_revoke - -- ** Received HTLC + -- ** Received HTLC outputs , received_htlc_script , received_htlc_witness_timeout , received_htlc_witness_revoke - -- ** HTLC output (same as to_local) - , htlc_output_script - , htlc_output_witness_spend - , htlc_output_witness_revoke - - -- ** P2WSH helpers - , to_p2wsh - , witness_script_hash - - -- * Transaction assembly - -- ** Commitment transactions - , CommitmentTx(..) - , CommitmentContext(..) - , CommitmentKeys(..) - , build_commitment_tx - - -- ** HTLC transactions - , HTLCTx(..) - , HTLCContext(..) - , build_htlc_timeout_tx - , build_htlc_success_tx - - -- ** Closing transactions - , ClosingTx(..) - , ClosingContext(..) - , build_closing_tx - , build_legacy_closing_tx - - -- ** Transaction outputs - , TxOutput(..) - , OutputType(..) - - -- ** Fee calculation - , commitment_fee - , commitment_weight - , htlc_timeout_fee - , htlc_success_fee - - -- ** Trimming - , htlc_trim_threshold - , is_trimmed - , trimmed_htlcs - , untrimmed_htlcs - - -- ** Output ordering - , sort_outputs - - -- * Conversion to ppad-tx - , commitment_to_tx - , htlc_to_tx - , closing_to_tx - - -- * Serialization - , encode_tx - , encode_htlc_tx - , encode_closing_tx - , encode_tx_for_signing - , encode_witness - , encode_funding_witness - - -- * Parsing - , BT.Tx(..) - , BT.TxIn(..) - , BT.TxOut(BT.TxOut) - , BT.from_bytes - , decode_tx - - -- * Validation - , ValidationError(..) - , validate_commitment_tx - , validate_commitment_locktime - , validate_commitment_sequence - , validate_htlc_tx - , validate_htlc_timeout_tx - , validate_htlc_success_tx - , validate_closing_tx - , validate_legacy_closing_tx - , validate_output_ordering - , validate_dust_limits - , validate_anchor_outputs - , validate_commitment_fee - , validate_htlc_fee + -- ** HTLC transaction inputs + , htlc_success_witness + , htlc_timeout_witness + , remote_htlc_sighash ) where -import qualified Bitcoin.Prim.Tx as BT -import Lightning.Protocol.BOLT3.Types import Lightning.Protocol.BOLT3.Keys import Lightning.Protocol.BOLT3.Scripts import Lightning.Protocol.BOLT3.Tx -import Lightning.Protocol.BOLT3.Encode -import Lightning.Protocol.BOLT3.Decode -import Lightning.Protocol.BOLT3.Validate +import Lightning.Protocol.BOLT3.Types diff --git a/lib/Lightning/Protocol/BOLT3/Decode.hs b/lib/Lightning/Protocol/BOLT3/Decode.hs @@ -1,27 +0,0 @@ -{-# OPTIONS_HADDOCK prune #-} - --- | --- Module: Lightning.Protocol.BOLT3.Decode --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Parsing for BOLT #3 transactions. --- --- Delegates to ppad-tx for transaction decoding. - -module Lightning.Protocol.BOLT3.Decode ( - decode_tx - ) where - -import qualified Bitcoin.Prim.Tx as BT -import qualified Data.ByteString as BS - --- | Decode a raw Bitcoin transaction from bytes. --- --- Handles both legacy and SegWit transaction formats. --- --- >>> decode_tx rawTxBytes --- Just (Tx {...}) -decode_tx :: BS.ByteString -> Maybe BT.Tx -decode_tx = BT.from_bytes diff --git a/lib/Lightning/Protocol/BOLT3/Encode.hs b/lib/Lightning/Protocol/BOLT3/Encode.hs @@ -1,102 +0,0 @@ -{-# OPTIONS_HADDOCK prune #-} -{-# LANGUAGE BangPatterns #-} - --- | --- Module: Lightning.Protocol.BOLT3.Encode --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Serialization for BOLT #3 transactions and scripts. --- --- Delegates to ppad-tx for transaction encoding. - -module Lightning.Protocol.BOLT3.Encode ( - -- * Transaction serialization - encode_tx - , encode_htlc_tx - , encode_closing_tx - , encode_tx_for_signing - - -- * Witness serialization - , encode_witness - , encode_funding_witness - ) where - -import qualified Bitcoin.Prim.Tx as BT -import qualified Data.ByteString as BS -import qualified Data.ByteString.Builder as BSB -import Data.Word (Word64) -import Lightning.Protocol.BOLT3.Types -import Lightning.Protocol.BOLT3.Tx - --- transaction encoding -------------------------------------------------------- - --- | Encode a commitment transaction (SegWit format). --- --- Returns 'Nothing' if the transaction has no outputs. -encode_tx :: CommitmentTx -> Maybe BS.ByteString -encode_tx = fmap BT.to_bytes . commitment_to_tx - --- | Encode an HTLC transaction (SegWit format). -encode_htlc_tx :: HTLCTx -> BS.ByteString -encode_htlc_tx = BT.to_bytes . htlc_to_tx - --- | Encode a closing transaction (SegWit format). --- --- Returns 'Nothing' if the transaction has no outputs. -encode_closing_tx :: ClosingTx -> Maybe BS.ByteString -encode_closing_tx = fmap BT.to_bytes . closing_to_tx - --- | Encode a commitment transaction for signing (stripped --- format, no witness). --- --- Returns 'Nothing' if the transaction has no outputs. -encode_tx_for_signing - :: CommitmentTx -> Maybe BS.ByteString -encode_tx_for_signing = - fmap BT.to_bytes_legacy . commitment_to_tx - --- witness encoding ------------------------------------------------------------ - --- | Encode a witness stack. --- --- Format: varint item count, then for each item: --- varint length followed by item data. -encode_witness :: Witness -> BS.ByteString -encode_witness (Witness !items) = - BT.to_strict $ - put_varint (fromIntegral (length items)) - <> foldMap put_item items - where - put_item :: BS.ByteString -> BSB.Builder - put_item !bs = - put_varint (fromIntegral (BS.length bs)) - <> BSB.byteString bs - --- | Encode a varint to a 'BSB.Builder'. -put_varint :: Word64 -> BSB.Builder -put_varint !n - | n < 0xFD = BSB.word8 (fromIntegral n) - | n <= 0xFFFF = - BSB.word8 0xFD <> BSB.word16LE (fromIntegral n) - | n <= 0xFFFFFFFF = - BSB.word8 0xFE <> BSB.word32LE (fromIntegral n) - | otherwise = - BSB.word8 0xFF <> BSB.word64LE n -{-# INLINE put_varint #-} - --- | Encode a funding witness (2-of-2 multisig). --- --- The witness stack is: @0 <sig1> <sig2> <witnessScript>@ --- --- Signatures must be ordered to match pubkey order in the --- funding script. -encode_funding_witness - :: BS.ByteString -- ^ Signature for lesser pubkey - -> BS.ByteString -- ^ Signature for greater pubkey - -> Script -- ^ The funding witness script - -> BS.ByteString -encode_funding_witness !sig1 !sig2 (Script !ws) = - encode_witness - (Witness [BS.empty, sig1, sig2, ws]) diff --git a/lib/Lightning/Protocol/BOLT3/Internal.hs b/lib/Lightning/Protocol/BOLT3/Internal.hs @@ -1,56 +0,0 @@ -{-# OPTIONS_HADDOCK hide #-} - --- | --- Module: Lightning.Protocol.BOLT3.Internal --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Internal definitions for BOLT #3. --- --- This module exports unsafe constructors that bypass --- validation. Use only in tests or trusted internal code. - -module Lightning.Protocol.BOLT3.Internal ( - -- * Unsafe constructors (bypass validation) - unsafePubkey - , unsafeSeckey - , unsafeCommitmentNumber - , unsafeSequence - , unsafeLocktime - ) where - -import qualified Data.ByteString as BS -import Data.Word (Word32, Word64) -import Lightning.Protocol.BOLT3.Types - --- | Construct a 'Pubkey' without length validation. --- --- For test use only. -unsafePubkey :: BS.ByteString -> Pubkey -unsafePubkey = Pubkey - --- | Construct a 'Seckey' without length validation. --- --- For test use only. -unsafeSeckey :: BS.ByteString -> Seckey -unsafeSeckey = Seckey - --- | Construct a 'CommitmentNumber' without range --- validation. --- --- For test use only. -unsafeCommitmentNumber :: Word64 -> CommitmentNumber -unsafeCommitmentNumber = CommitmentNumber - --- | Construct a 'Sequence' directly. --- --- For test use only. -unsafeSequence :: Word32 -> Sequence -unsafeSequence = Sequence - --- | Construct a 'Locktime' directly. --- --- For test use only. -unsafeLocktime :: Word32 -> Locktime -unsafeLocktime = Locktime diff --git a/lib/Lightning/Protocol/BOLT3/Keys.hs b/lib/Lightning/Protocol/BOLT3/Keys.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} @@ -8,532 +8,624 @@ -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech> -- --- Per-commitment key derivation per BOLT #3. --- --- Implements key derivation formulas: --- --- @ --- pubkey = basepoint + SHA256(per_commitment_point || basepoint) * G --- revocationpubkey = revocation_basepoint * SHA256(revocation_basepoint --- || per_commitment_point) --- + per_commitment_point * SHA256(per_commitment_point --- || revocation_basepoint) --- @ +-- Key derivation, per-commitment secrets and commitment number +-- obscuring, per BOLT #3. module Lightning.Protocol.BOLT3.Keys ( - -- * Per-commitment point derivation + -- * Per-commitment points derive_per_commitment_point , derive_per_commitment_point' - -- * Key derivation + -- * Public keys , derive_pubkey - , derive_pubkey' - , derive_localpubkey - , derive_localpubkey' - , derive_local_htlcpubkey - , derive_local_htlcpubkey' - , derive_remote_htlcpubkey - , derive_remote_htlcpubkey' - , derive_local_delayedpubkey - , derive_local_delayedpubkey' - , derive_remote_delayedpubkey - , derive_remote_delayedpubkey' - - -- * Revocation key derivation , derive_revocationpubkey + , CommitmentKeys(..) + , derive_commitment_keys + , derive_commitment_keys' - -- * Per-commitment secret generation - , generate_from_seed - , derive_secret + -- * Private keys + , derive_privkey + , derive_revocationprivkey - -- * Per-commitment secret storage - , SecretStore(..) - , SecretEntry(..) - , empty_store + -- * Per-commitment secrets + , generate_from_seed + , SecretStore + , empty_secret_store , insert_secret , derive_old_secret + , secret_store + , un_secret_store -- * Commitment number obscuring , obscured_commitment_number ) where -import Data.Bits ((.&.), xor, shiftL, testBit, complementBit) +import Control.DeepSeq (NFData(..)) +import qualified Crypto.Curve.Secp256k1 as S +import qualified Crypto.Hash.SHA256 as SHA256 +import Data.Bits ((.&.), (.|.), complement, complementBit, shiftL, shiftR, + testBit, xor) import qualified Data.ByteString as BS +import qualified Data.ByteString.Builder as BSB +import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Unsafe as BU import Data.Word (Word64) import GHC.Generics (Generic) -import qualified Crypto.Curve.Secp256k1 as S -import qualified Crypto.Hash.SHA256 as SHA256 +import Lightning.Protocol.BOLT1 (Point, PerCommitmentSecret) +import qualified Lightning.Protocol.BOLT1 as BOLT1 import Lightning.Protocol.BOLT3.Types --- Per-commitment point derivation ---------------------------------------- +-- points --------------------------------------------------------------------- + +-- Serialize a secp256k1 point, failing on the point at infinity. +to_point :: S.Projective -> Maybe Point +to_point p + | p == S._CURVE_ZERO = Nothing + | otherwise = case BOLT1.point (S.serialize_point p) of + Just pt -> Just pt + Nothing -> internal_error +{-# INLINE to_point #-} + +-- per-commitment points ------------------------------------------------------ --- | Derive the per-commitment point from a per-commitment secret. +-- | Derive the per-commitment point from a per-commitment secret: -- --- @per_commitment_point = per_commitment_secret * G@ +-- @per_commitment_point = per_commitment_secret * G@ -- --- >>> let secret = PerCommitmentSecret (BS.replicate 32 0x01) --- >>> derive_per_commitment_point secret --- Just (PerCommitmentPoint ...) +-- Fails if the secret is not a valid secp256k1 secret key. +-- +-- >>> let Just s = BOLT1.per_commitment_secret (BS.pack [31, 30 .. 0]) +-- >>> let Just (PerCommitmentPoint p) = derive_per_commitment_point s +-- >>> B16.encode (BOLT1.un_point p) +-- "025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486" derive_per_commitment_point :: PerCommitmentSecret -> Maybe PerCommitmentPoint -derive_per_commitment_point (PerCommitmentSecret sec) = do - sk <- S.parse_int256 sec +derive_per_commitment_point s = do + sk <- S.parse_int256 (BOLT1.un_per_commitment_secret s) pk <- S.derive_pub sk - let !bs = S.serialize_point pk - pure $! PerCommitmentPoint (Point bs) -{-# INLINE derive_per_commitment_point #-} + PerCommitmentPoint <$> to_point pk --- | As 'derive_per_commitment_point', but takes a precomputed --- secp256k1 'S.Context' so the @G@-base scalar mult uses wNAF. --- Caller threads a single context across many calls. --- --- >>> let !tex = S.precompute --- >>> derive_per_commitment_point' tex secret --- Just (PerCommitmentPoint ...) +-- | As 'derive_per_commitment_point', but uses a precomputed +-- 'S.Context' to speed up the scalar multiplication. derive_per_commitment_point' :: S.Context -> PerCommitmentSecret -> Maybe PerCommitmentPoint -derive_per_commitment_point' tex (PerCommitmentSecret sec) = do - sk <- S.parse_int256 sec +derive_per_commitment_point' tex s = do + sk <- S.parse_int256 (BOLT1.un_per_commitment_secret s) pk <- S.derive_pub' tex sk - let !bs = S.serialize_point pk - pure $! PerCommitmentPoint (Point bs) -{-# INLINE derive_per_commitment_point' #-} + PerCommitmentPoint <$> to_point pk --- Key derivation --------------------------------------------------------- +-- public keys ---------------------------------------------------------------- --- | Derive a pubkey from a basepoint and per-commitment point. --- --- @pubkey = basepoint + SHA256(per_commitment_point || basepoint) * G@ +-- | Derive a per-commitment public key from a basepoint: -- --- This is the general derivation formula used for localpubkey, --- local_htlcpubkey, remote_htlcpubkey, local_delayedpubkey, and --- remote_delayedpubkey. +-- @pubkey = basepoint + SHA256(per_commitment_point || basepoint) * G@ -- --- >>> derive_pubkey basepoint per_commitment_point --- Just (Pubkey ...) -derive_pubkey - :: Point -- ^ basepoint - -> PerCommitmentPoint -- ^ per_commitment_point - -> Maybe Pubkey -derive_pubkey (Point basepointBs) (PerCommitmentPoint (Point pcpBs)) = do - basepoint <- S.parse_point basepointBs - -- SHA256(per_commitment_point || basepoint) - let !h = SHA256.hash (pcpBs <> basepointBs) - -- Treat hash as scalar and multiply by G - tweak <- S.parse_int256 h - tweakPoint <- S.derive_pub tweak - -- Add basepoint + tweak*G - let !result = S.add basepoint tweakPoint - !bs = S.serialize_point result - pure $! Pubkey bs -{-# INLINE derive_pubkey #-} - --- | As 'derive_pubkey', but takes a precomputed secp256k1 --- 'S.Context'. The @tweak * G@ multiplication uses wNAF; this is --- the highest-leverage variant since 'derive_pubkey' fires five --- times per commitment-key derivation. +-- This gives @local_htlcpubkey@, @remote_htlcpubkey@, +-- @local_delayedpubkey@ and the like, depending on the basepoint; +-- 'derive_commitment_keys' derives all of a commitment's keys at +-- once. -- --- >>> let !tex = S.precompute --- >>> derive_pubkey' tex basepoint per_commitment_point --- Just (Pubkey ...) -derive_pubkey' - :: S.Context - -> Point -- ^ basepoint - -> PerCommitmentPoint -- ^ per_commitment_point - -> Maybe Pubkey -derive_pubkey' - tex - (Point basepointBs) - (PerCommitmentPoint (Point pcpBs)) = do - basepoint <- S.parse_point basepointBs - let !h = SHA256.hash (pcpBs <> basepointBs) - tweak <- S.parse_int256 h - tweakPoint <- S.derive_pub' tex tweak - let !result = S.add basepoint tweakPoint - !bs = S.serialize_point result - pure $! Pubkey bs -{-# INLINE derive_pubkey' #-} - --- | Derive localpubkey from payment_basepoint and per_commitment_point. +-- Fails if the basepoint is not on the curve. -- --- >>> derive_localpubkey payment_basepoint per_commitment_point --- Just (LocalPubkey ...) -derive_localpubkey - :: PaymentBasepoint - -> PerCommitmentPoint - -> Maybe LocalPubkey -derive_localpubkey (PaymentBasepoint pt) pcp = - LocalPubkey <$> derive_pubkey pt pcp -{-# INLINE derive_localpubkey #-} - --- | As 'derive_localpubkey', but uses 'derive_pubkey'' internally --- (wNAF-accelerated). -derive_localpubkey' - :: S.Context - -> PaymentBasepoint - -> PerCommitmentPoint - -> Maybe LocalPubkey -derive_localpubkey' tex (PaymentBasepoint pt) pcp = - LocalPubkey <$> derive_pubkey' tex pt pcp -{-# INLINE derive_localpubkey' #-} - --- | Derive local_htlcpubkey from htlc_basepoint and per_commitment_point. --- --- >>> derive_local_htlcpubkey htlc_basepoint per_commitment_point --- Just (LocalHtlcPubkey ...) -derive_local_htlcpubkey - :: HtlcBasepoint - -> PerCommitmentPoint - -> Maybe LocalHtlcPubkey -derive_local_htlcpubkey (HtlcBasepoint pt) pcp = - LocalHtlcPubkey <$> derive_pubkey pt pcp -{-# INLINE derive_local_htlcpubkey #-} - --- | As 'derive_local_htlcpubkey', wNAF variant. -derive_local_htlcpubkey' - :: S.Context - -> HtlcBasepoint - -> PerCommitmentPoint - -> Maybe LocalHtlcPubkey -derive_local_htlcpubkey' tex (HtlcBasepoint pt) pcp = - LocalHtlcPubkey <$> derive_pubkey' tex pt pcp -{-# INLINE derive_local_htlcpubkey' #-} - --- | Derive remote_htlcpubkey from htlc_basepoint and per_commitment_point. --- --- >>> derive_remote_htlcpubkey htlc_basepoint per_commitment_point --- Just (RemoteHtlcPubkey ...) -derive_remote_htlcpubkey - :: HtlcBasepoint - -> PerCommitmentPoint - -> Maybe RemoteHtlcPubkey -derive_remote_htlcpubkey (HtlcBasepoint pt) pcp = - RemoteHtlcPubkey <$> derive_pubkey pt pcp -{-# INLINE derive_remote_htlcpubkey #-} - --- | As 'derive_remote_htlcpubkey', wNAF variant. -derive_remote_htlcpubkey' - :: S.Context - -> HtlcBasepoint - -> PerCommitmentPoint - -> Maybe RemoteHtlcPubkey -derive_remote_htlcpubkey' tex (HtlcBasepoint pt) pcp = - RemoteHtlcPubkey <$> derive_pubkey' tex pt pcp -{-# INLINE derive_remote_htlcpubkey' #-} +-- >>> let Just s = BOLT1.per_commitment_secret (BS.pack [31, 30 .. 0]) +-- >>> let Just pcp = derive_per_commitment_point s +-- >>> let Just b = BOLT1.per_commitment_secret (BS.pack [0 .. 31]) +-- >>> let Just (PerCommitmentPoint bp) = derive_per_commitment_point b +-- >>> fmap (B16.encode . BOLT1.un_point) (derive_pubkey bp pcp) +-- Just "0235f2dbfaa89b57ec7b055afe29849ef7ddfeb1cefdb9ebdc43f5494984db29e5" +derive_pubkey :: Point -> PerCommitmentPoint -> Maybe Point +derive_pubkey = derive_pubkey_with mul_g +{-# INLINE derive_pubkey #-} --- | Derive local_delayedpubkey from delayed_payment_basepoint and --- per_commitment_point. --- --- >>> derive_local_delayedpubkey delayed_payment_basepoint per_commitment_point --- Just (LocalDelayedPubkey ...) -derive_local_delayedpubkey - :: DelayedPaymentBasepoint - -> PerCommitmentPoint - -> Maybe LocalDelayedPubkey -derive_local_delayedpubkey (DelayedPaymentBasepoint pt) pcp = - LocalDelayedPubkey <$> derive_pubkey pt pcp -{-# INLINE derive_local_delayedpubkey #-} +-- Multiply the generator by a 32-byte scalar. +mul_g :: BS.ByteString -> Maybe S.Projective +mul_g bs = S.derive_pub =<< S.parse_int256 bs +{-# INLINE mul_g #-} --- | As 'derive_local_delayedpubkey', wNAF variant. -derive_local_delayedpubkey' - :: S.Context - -> DelayedPaymentBasepoint - -> PerCommitmentPoint - -> Maybe LocalDelayedPubkey -derive_local_delayedpubkey' tex (DelayedPaymentBasepoint pt) pcp = - LocalDelayedPubkey <$> derive_pubkey' tex pt pcp -{-# INLINE derive_local_delayedpubkey' #-} - --- | Derive remote_delayedpubkey from delayed_payment_basepoint and --- per_commitment_point. --- --- >>> derive_remote_delayedpubkey delayed_payment_basepoint pcp --- Just (RemoteDelayedPubkey ...) -derive_remote_delayedpubkey - :: DelayedPaymentBasepoint - -> PerCommitmentPoint - -> Maybe RemoteDelayedPubkey -derive_remote_delayedpubkey (DelayedPaymentBasepoint pt) pcp = - RemoteDelayedPubkey <$> derive_pubkey pt pcp -{-# INLINE derive_remote_delayedpubkey #-} +-- As 'mul_g', using a precomputed context. +mul_g' :: S.Context -> BS.ByteString -> Maybe S.Projective +mul_g' tex bs = S.derive_pub' tex =<< S.parse_int256 bs +{-# INLINE mul_g' #-} --- | As 'derive_remote_delayedpubkey', wNAF variant. -derive_remote_delayedpubkey' - :: S.Context - -> DelayedPaymentBasepoint +derive_pubkey_with + :: (BS.ByteString -> Maybe S.Projective) + -> Point -> PerCommitmentPoint - -> Maybe RemoteDelayedPubkey -derive_remote_delayedpubkey' tex (DelayedPaymentBasepoint pt) pcp = - RemoteDelayedPubkey <$> derive_pubkey' tex pt pcp -{-# INLINE derive_remote_delayedpubkey' #-} - --- Revocation key derivation ---------------------------------------------- - --- Note: no @derive_revocationpubkey'@ is provided. The two scalar --- mults inside this function are general (basepoint * scalar, not --- @G * scalar@), so the wNAF table built by 'S.precompute' — which --- stores multiples of the generator — doesn't apply. - --- | Derive revocationpubkey from revocation_basepoint and --- per_commitment_point. --- --- @ --- revocationpubkey = revocation_basepoint --- * SHA256(revocation_basepoint || per_commitment_point) --- + per_commitment_point --- * SHA256(per_commitment_point || revocation_basepoint) --- @ --- --- >>> derive_revocationpubkey revocation_basepoint per_commitment_point --- Just (RevocationPubkey ...) + -> Maybe Point +derive_pubkey_with mul bp (PerCommitmentPoint pcp) = do + let !bp_bs = BOLT1.un_point bp + base <- S.parse_point bp_bs + t <- mul (SHA256.hash (BOLT1.un_point pcp <> bp_bs)) + to_point (S.add base t) +{-# INLINE derive_pubkey_with #-} + +-- | Derive a commitment's @revocationpubkey@ from the revocation +-- basepoint of the party that will hold the revocation key, and the +-- commitment owner's per-commitment point: +-- +-- @ +-- revocationpubkey = +-- revocation_basepoint +-- * SHA256(revocation_basepoint || per_commitment_point) +-- + per_commitment_point +-- * SHA256(per_commitment_point || revocation_basepoint) +-- @ +-- +-- Fails if either point is not on the curve. +-- +-- >>> let Just s = BOLT1.per_commitment_secret (BS.pack [31, 30 .. 0]) +-- >>> let Just pcp = derive_per_commitment_point s +-- >>> let Just b = BOLT1.per_commitment_secret (BS.pack [0 .. 31]) +-- >>> let Just (PerCommitmentPoint p) = derive_per_commitment_point b +-- >>> let Just r = derive_revocationpubkey (RevocationBasepoint p) pcp +-- >>> (\(RevocationPubkey k) -> B16.encode (BOLT1.un_point k)) r +-- "02916e326636d19c33f13e8c0c3a03dd157f332f3e99c317c141dd865eb01f8ff0" derive_revocationpubkey :: RevocationBasepoint -> PerCommitmentPoint -> Maybe RevocationPubkey derive_revocationpubkey - (RevocationBasepoint (Point rbpBs)) - (PerCommitmentPoint (Point pcpBs)) = do - rbp <- S.parse_point rbpBs - pcp <- S.parse_point pcpBs - -- SHA256(revocation_basepoint || per_commitment_point) - let !h1 = SHA256.hash (rbpBs <> pcpBs) - -- SHA256(per_commitment_point || revocation_basepoint) - let !h2 = SHA256.hash (pcpBs <> rbpBs) - -- Multiply points by their respective scalars - s1 <- S.parse_int256 h1 - s2 <- S.parse_int256 h2 - p1 <- S.mul rbp s1 -- revocation_basepoint * h1 - p2 <- S.mul pcp s2 -- per_commitment_point * h2 - -- Add the two points - let !result = S.add p1 p2 - !bs = S.serialize_point result - pure $! RevocationPubkey (Pubkey bs) -{-# INLINE derive_revocationpubkey #-} - --- Per-commitment secret generation --------------------------------------- - --- | Generate the I'th per-commitment secret from a seed. --- --- Implements the generate_from_seed algorithm from BOLT #3: --- --- @ --- generate_from_seed(seed, I): --- P = seed --- for B in 47 down to 0: --- if B set in I: --- flip(B) in P --- P = SHA256(P) --- return P --- @ --- --- >>> generate_from_seed seed 281474976710655 --- <32-byte secret> -generate_from_seed - :: BS.ByteString -- ^ seed (32 bytes) - -> Word64 -- ^ index I (max 2^48 - 1) - -> BS.ByteString -- ^ per-commitment secret (32 bytes) -generate_from_seed seed idx = go 47 seed where - go :: Int -> BS.ByteString -> BS.ByteString - go !b !p - | b < 0 = p - | testBit idx b = - let !p' = flip_bit b p - !p'' = SHA256.hash p' - in go (b - 1) p'' - | otherwise = go (b - 1) p -{-# INLINE generate_from_seed #-} - --- | Derive a secret from a base secret. --- --- This is a generalization of generate_from_seed used for efficient --- secret storage. Given a base secret whose index has bits..47 the same --- as target index I, derive the I'th secret. --- --- @ --- derive_secret(base, bits, I): --- P = base --- for B in bits - 1 down to 0: --- if B set in I: --- flip(B) in P --- P = SHA256(P) --- return P --- @ -derive_secret - :: BS.ByteString -- ^ base secret - -> Int -- ^ bits (number of trailing bits to process) - -> Word64 -- ^ target index I - -> BS.ByteString -- ^ derived secret -derive_secret base bits idx = go (bits - 1) base where - go :: Int -> BS.ByteString -> BS.ByteString - go !b !p - | b < 0 = p - | testBit idx b = - let !p' = flip_bit b p - !p'' = SHA256.hash p' - in go (b - 1) p'' - | otherwise = go (b - 1) p -{-# INLINE derive_secret #-} - --- | Flip bit B in a 32-byte bytestring. --- --- "flip(B)" alternates the (B mod 8) bit of the (B div 8) byte. + (RevocationBasepoint rbp) + (PerCommitmentPoint pcp) = do + let !rbp_bs = BOLT1.un_point rbp + !pcp_bs = BOLT1.un_point pcp + r <- S.parse_point rbp_bs + p <- S.parse_point pcp_bs + s1 <- S.parse_int256 (SHA256.hash (rbp_bs <> pcp_bs)) + s2 <- S.parse_int256 (SHA256.hash (pcp_bs <> rbp_bs)) + p1 <- S.mul r s1 + p2 <- S.mul p s2 + RevocationPubkey <$> to_point (S.add p1 p2) + +-- | The keys a commitment transaction's scripts use. +data CommitmentKeys = CommitmentKeys + { ck_revocation_pubkey :: !RevocationPubkey + , ck_local_delayed :: !LocalDelayedPubkey + , ck_local_htlc :: !LocalHtlcPubkey + , ck_remote_htlc :: !RemoteHtlcPubkey + , ck_remote_payment :: !RemotePubkey + , ck_local_funding :: !FundingPubkey + , ck_remote_funding :: !FundingPubkey + } deriving (Eq, Show, Generic) + +instance NFData CommitmentKeys + +-- | Derive the keys for a commitment transaction from both parties' +-- basepoints and funding pubkeys, and the commitment owner's +-- per-commitment point. +-- +-- The owner is the "local" party: its delayed and HTLC basepoints +-- give @local_delayedpubkey@ and @local_htlcpubkey@, while the other +-- party's revocation and HTLC basepoints give @revocationpubkey@ and +-- @remote_htlcpubkey@. As every supported 'CommitmentFormat' uses +-- @option_static_remotekey@, @remotepubkey@ is the other party's +-- @payment_basepoint@. +-- +-- Fails if a basepoint it derives a key from, or the per-commitment +-- point, is not on the curve. +derive_commitment_keys + :: Basepoints -- ^ the owner's basepoints + -> FundingPubkey -- ^ the owner's funding pubkey + -> Basepoints -- ^ the other party's basepoints + -> FundingPubkey -- ^ the other party's funding pubkey + -> PerCommitmentPoint -- ^ the owner's per-commitment point + -> Maybe CommitmentKeys +derive_commitment_keys = derive_commitment_keys_with mul_g + +-- | As 'derive_commitment_keys', but uses a precomputed 'S.Context' +-- to speed up the scalar multiplications. +derive_commitment_keys' + :: S.Context + -> Basepoints + -> FundingPubkey + -> Basepoints + -> FundingPubkey + -> PerCommitmentPoint + -> Maybe CommitmentKeys +derive_commitment_keys' tex = derive_commitment_keys_with (mul_g' tex) + +derive_commitment_keys_with + :: (BS.ByteString -> Maybe S.Projective) + -> Basepoints + -> FundingPubkey + -> Basepoints + -> FundingPubkey + -> PerCommitmentPoint + -> Maybe CommitmentKeys +derive_commitment_keys_with mul local local_fund remote remote_fund pcp = do + let DelayedPaymentBasepoint local_delayed_bp = bp_delayed_payment local + HtlcBasepoint local_htlc_bp = bp_htlc local + HtlcBasepoint remote_htlc_bp = bp_htlc remote + PaymentBasepoint remote_payment_bp = bp_payment remote + revocation <- derive_revocationpubkey (bp_revocation remote) pcp + delayed <- derive_pubkey_with mul local_delayed_bp pcp + local_htlc <- derive_pubkey_with mul local_htlc_bp pcp + remote_htlc <- derive_pubkey_with mul remote_htlc_bp pcp + pure CommitmentKeys + { ck_revocation_pubkey = revocation + , ck_local_delayed = LocalDelayedPubkey delayed + , ck_local_htlc = LocalHtlcPubkey local_htlc + , ck_remote_htlc = RemoteHtlcPubkey remote_htlc + , ck_remote_payment = RemotePubkey remote_payment_bp + , ck_local_funding = local_fund + , ck_remote_funding = remote_fund + } + +-- private keys --------------------------------------------------------------- + +-- | Derive the private key for a per-commitment public key from its +-- basepoint secret: +-- +-- @privkey = basepoint_secret + SHA256(per_commitment_point || basepoint)@ +-- +-- where @basepoint = basepoint_secret * G@. The result is the secret +-- key for @'derive_pubkey' basepoint per_commitment_point@. +-- +-- The arithmetic on the secret is constant-time. Fails (with +-- negligible probability) if the result is zero. +-- +-- >>> let Just s = BOLT1.per_commitment_secret (BS.pack [31, 30 .. 0]) +-- >>> let Just pcp = derive_per_commitment_point s +-- >>> let Just k = seckey (BS.pack [0 .. 31]) +-- >>> fmap (B16.encode . un_seckey) (derive_privkey k pcp) +-- Just "cbced912d3b21bf196a766651e436aff192362621ce317704ea2f75d87e7be0f" +derive_privkey :: Seckey -> PerCommitmentPoint -> Maybe Seckey +derive_privkey (Seckey sk_bs) (PerCommitmentPoint pcp) = do + sk <- S.parse_int256 sk_bs + bp <- to_point =<< S.derive_pub sk + let !h = SHA256.hash (BOLT1.un_point pcp <> BOLT1.un_point bp) + !r = add_mod_n (w4 sk_bs) (reduce_n (w4 h)) + to_seckey r + +-- | Derive a commitment's @revocationprivkey@ from the revocation +-- basepoint secret and the commitment's (revealed) per-commitment +-- secret: +-- +-- @ +-- revocationprivkey = +-- revocation_basepoint_secret +-- * SHA256(revocation_basepoint || per_commitment_point) +-- + per_commitment_secret +-- * SHA256(per_commitment_point || revocation_basepoint) +-- @ +-- +-- The result is the secret key for the 'derive_revocationpubkey' +-- of the corresponding points. +-- +-- The arithmetic on the secrets is constant-time. Fails if the +-- per-commitment secret is not a valid secret key, or (with +-- negligible probability) if the result is zero. +-- +-- >>> let Just s = BOLT1.per_commitment_secret (BS.pack [31, 30 .. 0]) +-- >>> let Just k = seckey (BS.pack [0 .. 31]) +-- >>> fmap (B16.encode . un_seckey) (derive_revocationprivkey k s) +-- Just "d09ffff62ddb2297ab000cc85bcb4283fdeb6aa052affbc9dddcf33b61078110" +derive_revocationprivkey + :: Seckey -- ^ revocation basepoint secret + -> PerCommitmentSecret -- ^ per-commitment secret + -> Maybe Seckey +derive_revocationprivkey (Seckey rbs_bs) pcs = do + let !pcs_bs = BOLT1.un_per_commitment_secret pcs + rbs <- S.parse_int256 rbs_bs + ps <- S.parse_int256 pcs_bs + rbp <- to_point =<< S.derive_pub rbs + pcp <- to_point =<< S.derive_pub ps + let !rbp_bs = BOLT1.un_point rbp + !pcp_bs = BOLT1.un_point pcp + !h1 = reduce_n (w4 (SHA256.hash (rbp_bs <> pcp_bs))) + !h2 = reduce_n (w4 (SHA256.hash (pcp_bs <> rbp_bs))) + !r = add_mod_n (mul_mod_n (w4 rbs_bs) h1) (mul_mod_n (w4 pcs_bs) h2) + to_seckey r + +-- constant-time arithmetic modulo the secp256k1 group order ------------------ + +-- A 256-bit integer as four 64-bit limbs, least significant first. +data W4 = W4 + {-# UNPACK #-} !Word64 + {-# UNPACK #-} !Word64 + {-# UNPACK #-} !Word64 + {-# UNPACK #-} !Word64 + +-- The secp256k1 group order n. +_N :: W4 +_N = W4 0xBFD25E8CD0364141 0xBAAEDCE6AF48A03B + 0xFFFFFFFFFFFFFFFE 0xFFFFFFFFFFFFFFFF + +-- Read 32 big-endian bytes (the length is checked by the caller). +w4 :: BS.ByteString -> W4 +w4 bs = W4 (limb 24) (limb 16) (limb 8) (limb 0) where + limb :: Int -> Word64 + limb o = go 0 0 where + go !acc !i + | i == 8 = acc + | otherwise = + go ((acc `shiftL` 8) .|. fromIntegral (BU.unsafeIndex bs (o + i))) + (i + 1) + +-- Write 32 big-endian bytes. +unw4 :: W4 -> BS.ByteString +unw4 (W4 a b c d) = BSL.toStrict . BSB.toLazyByteString $ + BSB.word64BE d <> BSB.word64BE c <> BSB.word64BE b <> BSB.word64BE a + +-- A valid secret key from a result in [0, n), failing on zero. +to_seckey :: W4 -> Maybe Seckey +to_seckey r@(W4 a b c d) + | (a .|. b .|. c .|. d) == 0 = Nothing + | otherwise = Just (Seckey (unw4 r)) + +-- a + b + c, for a carry c in {0, 1}; returns the sum and carry out. +adc :: Word64 -> Word64 -> Word64 -> (Word64, Word64) +adc a b c = + let !s = a + b + c + !o = ((a .&. b) .|. ((a .|. b) .&. complement s)) `shiftR` 63 + in (s, o) +{-# INLINE adc #-} + +-- a - b - c, for a borrow c in {0, 1}; returns the difference and +-- borrow out. +sbb :: Word64 -> Word64 -> Word64 -> (Word64, Word64) +sbb a b c = + let !s = a - b - c + !o = ((complement a .&. b) .|. (complement (a `xor` b) .&. s)) + `shiftR` 63 + in (s, o) +{-# INLINE sbb #-} + +add4 :: W4 -> W4 -> (W4, Word64) +add4 (W4 a0 a1 a2 a3) (W4 b0 b1 b2 b3) = + let !(s0, c0) = adc a0 b0 0 + !(s1, c1) = adc a1 b1 c0 + !(s2, c2) = adc a2 b2 c1 + !(s3, c3) = adc a3 b3 c2 + in (W4 s0 s1 s2 s3, c3) +{-# INLINE add4 #-} + +sub4 :: W4 -> W4 -> (W4, Word64) +sub4 (W4 a0 a1 a2 a3) (W4 b0 b1 b2 b3) = + let !(s0, c0) = sbb a0 b0 0 + !(s1, c1) = sbb a1 b1 c0 + !(s2, c2) = sbb a2 b2 c1 + !(s3, c3) = sbb a3 b3 c2 + in (W4 s0 s1 s2 s3, c3) +{-# INLINE sub4 #-} + +-- x if the bit is 1, y if it is 0. +select :: Word64 -> W4 -> W4 -> W4 +select bit (W4 x0 x1 x2 x3) (W4 y0 y1 y2 y3) = + let !m = negate bit + f x y = (x .&. m) .|. (y .&. complement m) + in W4 (f x0 y0) (f x1 y1) (f x2 y2) (f x3 y3) +{-# INLINE select #-} + +-- x mod n, for any 256-bit x (one subtraction suffices, as 2^256 < 2n). +reduce_n :: W4 -> W4 +reduce_n x = + let !(t, borrow) = sub4 x _N + in select borrow x t + +-- a + b mod n, for a, b < n. +add_mod_n :: W4 -> W4 -> W4 +add_mod_n a b = + let !(s, carry) = add4 a b + !(t, borrow) = sub4 s _N + in select (carry .|. (borrow `xor` 1)) t s + +-- a * b mod n, for a, b < n, by double-and-add over all 256 bits of b. +mul_mod_n :: W4 -> W4 -> W4 +mul_mod_n a (W4 b0 b1 b2 b3) = go (255 :: Int) (W4 0 0 0 0) where + go !i !acc + | i < 0 = acc + | otherwise = + let !d = add_mod_n acc acc + !bit = (limb (i `shiftR` 6) `shiftR` (i .&. 63)) .&. 1 + in go (i - 1) (select bit (add_mod_n d a) d) + limb :: Int -> Word64 + limb j = case j of + 0 -> b0 + 1 -> b1 + 2 -> b2 + _ -> b3 + +-- per-commitment secrets ----------------------------------------------------- + +-- Wrap a 32-byte value as a per-commitment secret. +to_secret :: BS.ByteString -> PerCommitmentSecret +to_secret bs = case BOLT1.per_commitment_secret bs of + Just s -> s + Nothing -> internal_error +{-# INLINE to_secret #-} + +-- Flip bit b (< 256) of a 32-byte value: the (b mod 8) bit of the +-- (b div 8) byte. flip_bit :: Int -> BS.ByteString -> BS.ByteString flip_bit b bs = - let !byteIdx = b `div` 8 - !bitIdx = b `mod` 8 - !len = BS.length bs - in if byteIdx >= len - then bs - else - let !prefix = BS.take byteIdx bs - !byte = BS.index bs byteIdx - !byte' = complementBit byte bitIdx - !suffix = BS.drop (byteIdx + 1) bs - in prefix <> BS.singleton byte' <> suffix + let !(pre, post) = BS.splitAt (b `shiftR` 3) bs + in case BS.uncons post of + Just (h, t) -> pre <> BS.cons (complementBit h (b .&. 7)) t + Nothing -> bs {-# INLINE flip_bit #-} --- Per-commitment secret storage ------------------------------------------ - --- | Entry in the secret store: (bucket, index, secret). -data SecretEntry = SecretEntry - { se_bucket :: {-# UNPACK #-} !Int - , se_index :: {-# UNPACK #-} !Word64 - , se_secret :: !BS.ByteString - } deriving (Eq, Show, Generic) - --- | Compact storage for per-commitment secrets. --- --- Stores up to 49 (value, index) pairs, allowing efficient derivation --- of any previously-received secret. This is possible because for a --- given secret on a 2^X boundary, all secrets up to the next 2^X --- boundary can be derived from it. -newtype SecretStore = SecretStore { unSecretStore :: [SecretEntry] } - deriving (Eq, Show, Generic) - --- | Empty secret store. -empty_store :: SecretStore -empty_store = SecretStore [] -{-# INLINE empty_store #-} - --- | Determine which bucket to store a secret in based on its index. --- --- Counts trailing zeros in the index. Returns 0-47 for normal indices, --- or 48 if index is 0 (the seed). -where_to_put_secret :: Word64 -> Int -where_to_put_secret idx = go 0 where +-- The spec's derive_secret: the secret for index i, from a base +-- secret whose index agrees with i in bits 47 down to 'bits'. +derive_secret :: BS.ByteString -> Int -> Word64 -> BS.ByteString +derive_secret base bits i = go (bits - 1) base where + go !b !p + | b < 0 = p + | testBit i b = go (b - 1) (SHA256.hash (flip_bit b p)) + | otherwise = go (b - 1) p + +-- | Generate the per-commitment secret with a given index from a seed, +-- per BOLT #3's @generate_from_seed@. +-- +-- >>> let Just s = seed (BS.replicate 32 0xff) +-- >>> let Just i = secret_index 281474976710655 +-- >>> B16.encode (BOLT1.un_per_commitment_secret (generate_from_seed s i)) +-- "7cc854b54e3e0dcdb010d7a3fee464a9687be6e8db3be6854c475621e007a5dc" +generate_from_seed :: Seed -> SecretIndex -> PerCommitmentSecret +generate_from_seed (Seed s) (SecretIndex i) = to_secret (derive_secret s 48 i) + +-- | Compact storage for the per-commitment secrets received from a +-- peer, per BOLT #3's "Efficient Per-commitment Secret Storage". +-- +-- Holds at most 49 secrets, from which every secret received so far +-- can be derived. This is secret material: its 'Show' instance is +-- redacted, and it has no 'Eq' instance. +newtype SecretStore = SecretStore [Entry] + +-- A stored secret: its bucket (the number of trailing zeros of its +-- index), index, and value. Kept in ascending bucket order. +data Entry = Entry + {-# UNPACK #-} !Int + {-# UNPACK #-} !Word64 + !BS.ByteString + +instance Show SecretStore where + showsPrec d _ = showParen (d > 10) $ + showString "SecretStore <redacted>" + +instance NFData SecretStore where + rnf (SecretStore es) = rnf_entries es where + rnf_entries [] = () + rnf_entries (Entry _ _ s : rest) = rnf s `seq` rnf_entries rest + +-- | The empty 'SecretStore'. +empty_secret_store :: SecretStore +empty_secret_store = SecretStore [] + +-- The bucket for an index: its number of trailing zeros (48 for 0). +bucket_of :: Word64 -> Int +bucket_of i = go 0 where go !b - | b > 47 = 48 -- index 0, this is the seed - | testBit idx b = b - | otherwise = go (b + 1) -{-# INLINE where_to_put_secret #-} - --- | Insert a secret into the store, validating against existing secrets. --- --- Implements insert_secret from BOLT #3: the secret is checked against --- every known secret in a lower bucket, then replaces the entry in its --- own bucket. Entries in all other buckets are kept, so that every --- previously-inserted secret remains derivable via 'derive_old_secret'. --- Entries are kept in ascending bucket order. --- --- Returns Nothing if the secret is not 32 bytes, if the index is not --- below 2^48, or if a known secret doesn't derive correctly from the --- new one (indicating the secrets weren't generated from the same --- seed). --- --- >>> insert_secret secret 281474976710655 empty_store --- Just (SecretStore ...) + | b > 47 = 48 + | testBit i b = b + | otherwise = go (b + 1) + +-- Whether a secret at index j in bucket b can derive index i. +can_derive :: Int -> Word64 -> Word64 -> Bool +can_derive b j i = i .&. complement ((1 `shiftL` b) - 1) == j +{-# INLINE can_derive #-} + +-- | Add a newly received per-commitment secret to the store, per +-- BOLT #3's @insert_secret@. +-- +-- Fails if a stored secret cannot be derived from the new one, i.e. +-- if the secrets were not generated from the same seed (or were not +-- received in order). +-- +-- >>> let Just s = seed (BS.replicate 32 0xff) +-- >>> let Just i = secret_index 281474976710655 +-- >>> let secret = generate_from_seed s i +-- >>> let Just st = insert_secret secret i empty_secret_store +-- >>> fmap (== secret) (derive_old_secret i st) +-- Just True insert_secret - :: BS.ByteString -- ^ secret (32 bytes) - -> Word64 -- ^ index (max 2^48 - 1) - -> SecretStore -- ^ current store + :: PerCommitmentSecret + -> SecretIndex + -> SecretStore -> Maybe SecretStore -insert_secret secret idx (SecretStore known) - | BS.length secret /= 32 = Nothing - | idx > 0xFFFFFFFFFFFF = Nothing +insert_secret pcs (SecretIndex i) (SecretStore known) | all derives known = Just $! SecretStore (put known) - | otherwise = Nothing + | otherwise = Nothing where - !bucket = where_to_put_secret idx - !entry = SecretEntry bucket idx secret + !s = BOLT1.un_per_commitment_secret pcs + !b = bucket_of i + !e = Entry b i s - -- known secrets in lower buckets must derive from the new one - derives (SecretEntry b knownIdx knownSecret) - | b < bucket = derive_secret secret bucket knownIdx == knownSecret + -- every secret in a lower bucket must derive from the new one + derives (Entry kb ki ks) + | kb < b = to_secret (derive_secret s b ki) == to_secret ks | otherwise = True - -- replace this bucket's entry, preserving ascending bucket order - put [] = [entry] - put (e : es) = case compare (se_bucket e) bucket of - LT -> e : put es - EQ -> entry : es - GT -> entry : e : es -{-# INLINE insert_secret #-} + -- replace this bucket's entry, keeping ascending bucket order + put [] = [e] + put (k@(Entry kb _ _) : ks) = case compare kb b of + LT -> k : put ks + EQ -> e : ks + GT -> e : k : ks --- | Derive a previously-received secret from the store. --- --- Iterates over known secrets to find one whose index is a prefix of --- the target index, then derives the target secret from it. --- --- >>> derive_old_secret 281474976710654 store --- Just <32-byte secret> +-- | Derive a previously received per-commitment secret from the +-- store. Fails if the index has not been received. derive_old_secret - :: Word64 -- ^ target index - -> SecretStore -- ^ store - -> Maybe BS.ByteString -derive_old_secret targetIdx (SecretStore known) = go known where - go :: [SecretEntry] -> Maybe BS.ByteString + :: SecretIndex + -> SecretStore + -> Maybe PerCommitmentSecret +derive_old_secret (SecretIndex i) (SecretStore known) = go known where go [] = Nothing - go (SecretEntry bucket knownIdx knownSecret : rest) = - -- Mask off the non-zero prefix of the index using the entry's bucket - let !mask = complement ((1 `shiftL` bucket) - 1) - in if (targetIdx .&. mask) == knownIdx - then Just $! derive_secret knownSecret bucket targetIdx - else go rest - - complement :: Word64 -> Word64 - complement x = x `xor` 0xFFFFFFFFFFFFFFFF -{-# INLINE derive_old_secret #-} - --- Commitment number obscuring -------------------------------------------- - --- | Calculate the obscured commitment number. --- --- The 48-bit commitment number is obscured by XOR with the lower 48 bits --- of SHA256(payment_basepoint from open_channel --- || payment_basepoint from accept_channel). --- --- >>> obscured_commitment_number local_payment_bp remote_payment_bp cn --- <obscured value> + go (Entry b j s : rest) + | can_derive b j i = Just $! to_secret (derive_secret s b i) + | otherwise = go rest + +-- | Rebuild a 'SecretStore' from its entries, e.g. as produced by +-- 'un_secret_store'. +-- +-- Fails if two entries share a bucket (i.e. their indices have the +-- same number of trailing zeros), or if an entry contradicts another +-- from which it can be derived. +secret_store + :: [(SecretIndex, PerCommitmentSecret)] + -> Maybe SecretStore +secret_store = go [] where + go acc [] = Just (SecretStore acc) + go acc ((SecretIndex i, pcs) : rest) = do + let !e = Entry (bucket_of i) i (BOLT1.un_per_commitment_secret pcs) + acc' <- place e acc + go acc' rest + + -- insert in ascending bucket order, checking consistency with the + -- entries already placed + place e@(Entry b i s) es + | all (consistent e) es = put es + | otherwise = Nothing + where + put [] = Just [e] + put (k@(Entry kb _ _) : ks) = case compare kb b of + LT -> (k :) <$> put ks + EQ -> Nothing + GT -> Just (e : k : ks) + consistent _ (Entry kb ki ks) + | kb > b && can_derive kb ki i = + to_secret (derive_secret ks kb i) == to_secret s + | kb < b && can_derive b i ki = + to_secret (derive_secret s b ki) == to_secret ks + | otherwise = True + +-- | The entries of a 'SecretStore', for persistence; see +-- 'secret_store'. +un_secret_store :: SecretStore -> [(SecretIndex, PerCommitmentSecret)] +un_secret_store (SecretStore es) = + [ (SecretIndex i, to_secret s) | Entry _ i s <- es ] + +-- commitment number obscuring ------------------------------------------------ + +-- | Obscure a commitment number, by XOR with the lower 48 bits of +-- +-- @SHA256(payment_basepoint from open_channel +-- || payment_basepoint from accept_channel)@ +-- +-- As XOR is an involution, applying this to an obscured number +-- recovers the commitment number. ('build_commitment_tx' obscures +-- the commitment number itself.) +-- +-- >>> let pay h = fmap PaymentBasepoint (BOLT1.point =<< B16.decode h) +-- >>> let o1 = "034f355bdcb7cc0af728ef3cceb9615d9068" +-- >>> let Just o = pay (o1 <> "4bb5b2ca5f859ab0f0b704075871aa") +-- >>> let a1 = "032c0b7cf95324a07d05398b240174dc0c2b" +-- >>> let Just a = pay (a1 <> "e444d96b159aa6c7f7b1e668680991") +-- >>> fmap (obscured_commitment_number o a) (commitment_number 42) +-- Just 48035859142974 obscured_commitment_number :: PaymentBasepoint -- ^ opener's payment_basepoint -> PaymentBasepoint -- ^ accepter's payment_basepoint - -> CommitmentNumber -- ^ commitment number (48-bit) - -> Word64 -- ^ obscured commitment number + -> CommitmentNumber + -> Word64 obscured_commitment_number - (PaymentBasepoint (Point openerBs)) - (PaymentBasepoint (Point accepterBs)) - (CommitmentNumber cn) = - let !h = SHA256.hash (openerBs <> accepterBs) - -- Extract lower 48 bits (6 bytes) from the hash - !lower48 = extractLower48 h - -- Mask commitment number to 48 bits - !cn48 = cn .&. 0xFFFFFFFFFFFF - in cn48 `xor` lower48 -{-# INLINE obscured_commitment_number #-} - --- | Extract lower 48 bits from a 32-byte hash. --- --- Takes bytes 26-31 (last 6 bytes) and interprets as big-endian Word64. -extractLower48 :: BS.ByteString -> Word64 -extractLower48 h = - let !b0 = fromIntegral (BS.index h 26) `shiftL` 40 - !b1 = fromIntegral (BS.index h 27) `shiftL` 32 - !b2 = fromIntegral (BS.index h 28) `shiftL` 24 - !b3 = fromIntegral (BS.index h 29) `shiftL` 16 - !b4 = fromIntegral (BS.index h 30) `shiftL` 8 - !b5 = fromIntegral (BS.index h 31) - in b0 + b1 + b2 + b3 + b4 + b5 -{-# INLINE extractLower48 #-} + (PaymentBasepoint opener) + (PaymentBasepoint accepter) + cn = + let !h = SHA256.hash (BOLT1.un_point opener <> BOLT1.un_point accepter) + !mask = BS.foldl' (\acc x -> (acc `shiftL` 8) .|. fromIntegral x) 0 + (BS.drop 26 h) + in un_commitment_number cn `xor` mask diff --git a/lib/Lightning/Protocol/BOLT3/Scripts.hs b/lib/Lightning/Protocol/BOLT3/Scripts.hs @@ -1,6 +1,5 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} -{-# LANGUAGE OverloadedStrings #-} -- | -- Module: Lightning.Protocol.BOLT3.Scripts @@ -8,17 +7,7 @@ -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech> -- --- Script templates for BOLT #3 transaction outputs. --- --- Includes witness scripts for: --- --- * Funding output (2-of-2 multisig) --- * to_local output (revocable with CSV delay) --- * to_remote output (P2WPKH or anchored) --- * Anchor outputs --- * Offered HTLC outputs --- * Received HTLC outputs --- * HTLC-timeout/success output (revocable with delay) +-- Scripts and witnesses for BOLT #3 transaction outputs. module Lightning.Protocol.BOLT3.Scripts ( -- * Funding output @@ -31,7 +20,8 @@ module Lightning.Protocol.BOLT3.Scripts ( , to_local_witness_revoke -- * to_remote output - , to_remote_script + , to_remote_witness_script + , to_remote_script_pubkey , to_remote_witness -- * Anchor outputs @@ -39,639 +29,475 @@ module Lightning.Protocol.BOLT3.Scripts ( , anchor_witness_owner , anchor_witness_anyone - -- * Offered HTLC output + -- * Offered HTLC outputs , offered_htlc_script , offered_htlc_witness_preimage , offered_htlc_witness_revoke - -- * Received HTLC output + -- * Received HTLC outputs , received_htlc_script , received_htlc_witness_timeout , received_htlc_witness_revoke - -- * HTLC-timeout/success output (same as to_local) - , htlc_output_script - , htlc_output_witness_spend - , htlc_output_witness_revoke + -- * HTLC transaction inputs + , htlc_success_witness + , htlc_timeout_witness + , remote_htlc_sighash - -- * P2WSH helpers + -- * P2WSH , to_p2wsh - , witness_script_hash ) where +import Bitcoin.Prim.Tx (Witness(..)) +import Bitcoin.Prim.Tx.Sighash (SighashType(..)) +import qualified Crypto.Hash.RIPEMD160 as RIPEMD160 +import qualified Crypto.Hash.SHA256 as SHA256 import Data.Bits ((.&.), shiftR) -import Data.Word (Word8, Word16, Word32) import qualified Data.ByteString as BS import qualified Data.ByteString.Builder as BSB import qualified Data.ByteString.Lazy as BSL -import qualified Crypto.Hash.SHA256 as SHA256 -import qualified Crypto.Hash.RIPEMD160 as RIPEMD160 +import Data.Word (Word8, Word16, Word32) +import Lightning.Protocol.BOLT1 (Point, PaymentPreimage) +import qualified Lightning.Protocol.BOLT1 as BOLT1 import Lightning.Protocol.BOLT3.Types --- opcodes --------------------------------------------------------------------- - --- | OP_0 / OP_FALSE (0x00) -op_0 :: Word8 -op_0 = 0x00 +-- opcodes -------------------------------------------------------------------- --- | OP_PUSHDATA for 1-75 bytes just uses the length as opcode --- For data <=75 bytes, opcode is just the length - --- | OP_IF (0x63) -op_if :: Word8 -op_if = 0x63 +op_0, op_1, op_2, op_16 :: Word8 +op_0 = 0x00 +op_1 = 0x51 +op_2 = 0x52 +op_16 = 0x60 --- | OP_NOTIF (0x64) -op_notif :: Word8 +op_if, op_notif, op_else, op_endif, op_drop, op_dup, op_ifdup :: Word8 +op_if = 0x63 op_notif = 0x64 - --- | OP_ELSE (0x67) -op_else :: Word8 -op_else = 0x67 - --- | OP_ENDIF (0x68) -op_endif :: Word8 +op_else = 0x67 op_endif = 0x68 - --- | OP_DROP (0x75) -op_drop :: Word8 -op_drop = 0x75 - --- | OP_DUP (0x76) -op_dup :: Word8 -op_dup = 0x76 - --- | OP_SWAP (0x7c) -op_swap :: Word8 -op_swap = 0x7c - --- | OP_SIZE (0x82) -op_size :: Word8 -op_size = 0x82 - --- | OP_EQUAL (0x87) -op_equal :: Word8 -op_equal = 0x87 - --- | OP_EQUALVERIFY (0x88) -op_equalverify :: Word8 -op_equalverify = 0x88 - --- | OP_IFDUP (0x73) -op_ifdup :: Word8 op_ifdup = 0x73 +op_drop = 0x75 +op_dup = 0x76 --- | OP_HASH160 (0xa9) -op_hash160 :: Word8 -op_hash160 = 0xa9 - --- | OP_CHECKSIG (0xac) -op_checksig :: Word8 -op_checksig = 0xac +op_swap, op_size, op_equal, op_equalverify, op_hash160 :: Word8 +op_swap = 0x7c +op_size = 0x82 +op_equal = 0x87 +op_equalverify = 0x88 +op_hash160 = 0xa9 --- | OP_CHECKSIGVERIFY (0xad) -op_checksigverify :: Word8 +op_checksig, op_checksigverify, op_checkmultisig :: Word8 +op_checksig = 0xac op_checksigverify = 0xad +op_checkmultisig = 0xae --- | OP_CHECKMULTISIG (0xae) -op_checkmultisig :: Word8 -op_checkmultisig = 0xae - --- | OP_CHECKLOCKTIMEVERIFY (0xb1) -op_checklocktimeverify :: Word8 +op_checklocktimeverify, op_checksequenceverify :: Word8 op_checklocktimeverify = 0xb1 - --- | OP_CHECKSEQUENCEVERIFY (0xb2) -op_checksequenceverify :: Word8 op_checksequenceverify = 0xb2 --- | OP_1 (0x51) -op_1 :: Word8 -op_1 = 0x51 +-- helpers -------------------------------------------------------------------- --- | OP_2 (0x52) -op_2 :: Word8 -op_2 = 0x52 +op :: Word8 -> BSB.Builder +op = BSB.word8 +{-# INLINE op #-} --- | OP_16 (0x60) -op_16 :: Word8 -op_16 = 0x60 +-- Push data of at most 75 bytes (all pushes here are keys, hashes or +-- short numbers). +push :: BS.ByteString -> BSB.Builder +push bs = BSB.word8 (fromIntegral (BS.length bs)) <> BSB.byteString bs +{-# INLINE push #-} --- helpers --------------------------------------------------------------------- +push_point :: Point -> BSB.Builder +push_point = push . BOLT1.un_point +{-# INLINE push_point #-} --- | Push a bytestring onto the stack (handles length encoding). --- --- For data <= 75 bytes, the length itself is the opcode. -push_data :: BS.ByteString -> BSB.Builder -push_data !bs - | len <= 75 = BSB.word8 (fromIntegral len) <> BSB.byteString bs - | len <= 255 = BSB.word8 0x4c <> BSB.word8 (fromIntegral len) - <> BSB.byteString bs - | len <= 65535 = BSB.word8 0x4d <> BSB.word16LE (fromIntegral len) - <> BSB.byteString bs - | otherwise = BSB.word8 0x4e <> BSB.word32LE (fromIntegral len) - <> BSB.byteString bs +-- Push a non-negative number, minimally encoded. +push_num :: Word32 -> BSB.Builder +push_num n + | n == 0 = op op_0 + | n <= 16 = op (0x50 + fromIntegral n) + | otherwise = push (BS.pack (signed (le n))) where - !len = BS.length bs -{-# INLINE push_data #-} - --- | Encode a Word16 as minimal script number (for CSV delays). -push_csv_delay :: Word16 -> BSB.Builder -push_csv_delay !n - | n == 0 = BSB.word8 op_0 - | n <= 16 = BSB.word8 (0x50 + fromIntegral n) - | n <= 0x7f = push_data (BS.singleton (fromIntegral n)) - | n <= 0x7fff = push_data (BS.pack [lo, hi]) - | otherwise = push_data (BS.pack [lo, hi, 0x00]) -- need sign byte - where - !lo = fromIntegral (n .&. 0xff) - !hi = fromIntegral ((n `shiftR` 8) .&. 0xff) -{-# INLINE push_csv_delay #-} - --- | Encode a Word32 as minimal script number (for CLTV). -push_cltv :: Word32 -> BSB.Builder -push_cltv !n - | n == 0 = BSB.word8 op_0 - | n <= 16 = BSB.word8 (0x50 + fromIntegral n) - | otherwise = push_data (encode_scriptnum n) - where - encode_scriptnum :: Word32 -> BS.ByteString - encode_scriptnum 0 = BS.empty - encode_scriptnum !v = - let -- Build bytes little-endian (LSB first) - go :: Word32 -> [Word8] - go 0 = [] - go !x = fromIntegral (x .&. 0xff) : go (x `shiftR` 8) - !bytes = go v - -- If MSB has high bit set, need 0x00 suffix for positive numbers - !result = case reverse bytes of - [] -> bytes - (msb:_) | msb .&. 0x80 /= 0 -> bytes ++ [0x00] - _ -> bytes - in BS.pack result -{-# INLINE push_cltv #-} - --- | Build script from builder. -build_script :: BSB.Builder -> Script -build_script = Script . BSL.toStrict . BSB.toLazyByteString -{-# INLINE build_script #-} - --- | HASH160 = RIPEMD160(SHA256(x)) + -- little-endian magnitude + le :: Word32 -> [Word8] + le !x + | x == 0 = [] + | otherwise = fromIntegral (x .&. 0xff) : le (x `shiftR` 8) + + -- append a sign byte if the top bit is set + signed bs = case reverse bs of + (h : _) | h .&. 0x80 /= 0 -> bs <> [0x00] + _ -> bs + +push_delay :: Word16 -> BSB.Builder +push_delay = push_num . fromIntegral +{-# INLINE push_delay #-} + +script :: BSB.Builder -> Script +script = Script . BSL.toStrict . BSB.toLazyByteString +{-# INLINE script #-} + hash160 :: BS.ByteString -> BS.ByteString hash160 = RIPEMD160.hash . SHA256.hash {-# INLINE hash160 #-} --- P2WSH helpers --------------------------------------------------------------- +-- Append a witness script to a witness stack. +with_script :: [BS.ByteString] -> Script -> Witness +with_script items (Script s) = Witness (items <> [s]) +{-# INLINE with_script #-} --- | Compute SHA256 hash of a witness script. --- --- >>> witness_script_hash (Script "some_script") --- <32-byte SHA256 hash> -witness_script_hash :: Script -> BS.ByteString -witness_script_hash (Script !s) = SHA256.hash s -{-# INLINE witness_script_hash #-} +-- P2WSH ---------------------------------------------------------------------- --- | Convert a witness script to P2WSH scriptPubKey. --- --- P2WSH format: OP_0 <32-byte-hash> --- --- >>> to_p2wsh some_witness_script --- Script "\x00\x20<32-byte-hash>" +-- | The P2WSH scriptPubKey for a witness script: +-- @0 \<SHA256(script)\>@. to_p2wsh :: Script -> Script -to_p2wsh !script = - let !h = witness_script_hash script - in build_script (BSB.word8 op_0 <> push_data h) -{-# INLINE to_p2wsh #-} +to_p2wsh (Script s) = script (op op_0 <> push (SHA256.hash s)) --- funding output -------------------------------------------------------------- +-- funding output ------------------------------------------------------------- --- | Funding output witness script (2-of-2 multisig). --- --- Script: @2 <pubkey1> <pubkey2> 2 OP_CHECKMULTISIG@ +-- | The funding output's witness script: -- --- Where pubkey1 is lexicographically lesser. +-- @2 \<pubkey1\> \<pubkey2\> 2 OP_CHECKMULTISIG@ -- --- >>> funding_script pk1 pk2 --- Script "R!<pk_lesser>!<pk_greater>R\xae" +-- where @pubkey1@ is the lexicographically lesser of the two funding +-- pubkeys. The argument order doesn't matter. funding_script :: FundingPubkey -> FundingPubkey -> Script -funding_script (FundingPubkey (Pubkey !pk1)) (FundingPubkey (Pubkey !pk2)) = - let (!lesser, !greater) = if pk1 <= pk2 then (pk1, pk2) else (pk2, pk1) - in build_script $ - BSB.word8 op_2 - <> push_data lesser - <> push_data greater - <> BSB.word8 op_2 - <> BSB.word8 op_checkmultisig - --- | Witness for spending funding output. --- --- Witness: @0 <sig1> <sig2>@ --- --- Signatures ordered to match pubkey order in script. --- --- >>> funding_witness sig1 sig2 --- Witness ["", sig1, sig2] -funding_witness :: BS.ByteString -> BS.ByteString -> Witness -funding_witness !sig1 !sig2 = Witness [BS.empty, sig1, sig2] - --- to_local output ------------------------------------------------------------- - --- | to_local witness script (revocable with CSV delay). --- --- Script: --- --- @ --- OP_IF --- <revocationpubkey> --- OP_ELSE --- <to_self_delay> --- OP_CHECKSEQUENCEVERIFY --- OP_DROP --- <local_delayedpubkey> --- OP_ENDIF --- OP_CHECKSIG --- @ --- --- >>> to_local_script revpk delay localpk --- Script "c!<revpk>g<delay>\xb2u!<localpk>h\xac" +funding_script (FundingPubkey a) (FundingPubkey b) = + let (lo, hi) = if a <= b then (a, b) else (b, a) + in script $ + op op_2 <> push_point lo <> push_point hi + <> op op_2 <> op op_checkmultisig + +-- | The witness spending the funding output, given each party's +-- funding pubkey and signature (DER-encoded, with sighash byte): +-- +-- @0 \<signature_for_pubkey1\> \<signature_for_pubkey2\> \<script\>@ +-- +-- The signatures are ordered to match the pubkeys in +-- 'funding_script', so the argument order doesn't matter. +funding_witness + :: FundingPubkey -> BS.ByteString + -> FundingPubkey -> BS.ByteString + -> Witness +funding_witness pa@(FundingPubkey a) sa pb@(FundingPubkey b) sb = + let (s1, s2) = if a <= b then (sa, sb) else (sb, sa) + in with_script [BS.empty, s1, s2] (funding_script pa pb) + +-- to_local output ------------------------------------------------------------ + +-- | The @to_local@ output's witness script: +-- +-- @ +-- OP_IF +-- \<revocationpubkey\> +-- OP_ELSE +-- \<to_self_delay\> OP_CHECKSEQUENCEVERIFY OP_DROP +-- \<local_delayedpubkey\> +-- OP_ENDIF +-- OP_CHECKSIG +-- @ +-- +-- HTLC-success and HTLC-timeout transaction outputs use the same +-- script. to_local_script :: RevocationPubkey -> ToSelfDelay -> LocalDelayedPubkey -> Script to_local_script - (RevocationPubkey (Pubkey !revpk)) - (ToSelfDelay !delay) - (LocalDelayedPubkey (Pubkey !localpk)) = - build_script $ - BSB.word8 op_if - <> push_data revpk - <> BSB.word8 op_else - <> push_csv_delay delay - <> BSB.word8 op_checksequenceverify - <> BSB.word8 op_drop - <> push_data localpk - <> BSB.word8 op_endif - <> BSB.word8 op_checksig - --- | Witness for delayed spend of to_local output. --- --- Input nSequence must be set to to_self_delay. --- --- Witness: @<local_delayedsig> <>@ --- --- >>> to_local_witness_spend sig --- Witness [sig, ""] -to_local_witness_spend :: BS.ByteString -> Witness -to_local_witness_spend !sig = Witness [sig, BS.empty] - --- | Witness for revocation spend of to_local output. --- --- Witness: @<revocation_sig> 1@ --- --- >>> to_local_witness_revoke sig --- Witness [sig, "\x01"] -to_local_witness_revoke :: BS.ByteString -> Witness -to_local_witness_revoke !sig = Witness [sig, BS.singleton 0x01] - --- to_remote output ------------------------------------------------------------ - --- | to_remote witness script. --- --- With option_anchors: --- --- @ --- <remotepubkey> OP_CHECKSIGVERIFY 1 OP_CHECKSEQUENCEVERIFY --- @ --- --- Without option_anchors: P2WPKH (just the pubkey hash). --- --- >>> to_remote_script pk (ChannelFeatures True) --- Script "!<pk>\xadQ\xb2" -to_remote_script :: RemotePubkey -> ChannelFeatures -> Script -to_remote_script (RemotePubkey (Pubkey !pk)) !features - | has_anchors features = - -- Anchors: script with 1-block CSV - build_script $ - push_data pk - <> BSB.word8 op_checksigverify - <> BSB.word8 op_1 - <> BSB.word8 op_checksequenceverify - | otherwise = - -- No anchors: P2WPKH (OP_0 <20-byte-hash>) - let !h = hash160 pk - in build_script (BSB.word8 op_0 <> push_data h) - --- | Witness for spending to_remote output. --- --- With option_anchors (P2WSH), input nSequence must be 1. --- Witness: @<remote_sig>@ (witness script appended by caller) --- --- Without option_anchors (P2WPKH): --- Witness: @<remote_sig> <remotepubkey>@ --- --- >>> to_remote_witness sig pk (ChannelFeatures False) --- Witness [sig, pk] -to_remote_witness :: BS.ByteString -> RemotePubkey -> ChannelFeatures -> Witness -to_remote_witness !sig (RemotePubkey (Pubkey !pk)) !features - | has_anchors features = Witness [sig] - | otherwise = Witness [sig, pk] - --- anchor outputs -------------------------------------------------------------- - --- | Anchor output witness script. --- --- Script: --- --- @ --- <funding_pubkey> OP_CHECKSIG OP_IFDUP --- OP_NOTIF --- OP_16 OP_CHECKSEQUENCEVERIFY --- OP_ENDIF --- @ --- --- >>> anchor_script fundpk --- Script "!<fundpk>\xac\x73d`\xb2h" + (RevocationPubkey rev) + (ToSelfDelay delay) + (LocalDelayedPubkey delayed) = script $ + op op_if + <> push_point rev + <> op op_else + <> push_delay delay + <> op op_checksequenceverify + <> op op_drop + <> push_point delayed + <> op op_endif + <> op op_checksig + +-- | The witness for the owner's delayed spend of a @to_local@ (or +-- HTLC transaction) output, given its witness script: +-- +-- @\<local_delayedsig\> \<\> \<script\>@ +-- +-- The spending input's @nSequence@ must be @to_self_delay@. +to_local_witness_spend :: BS.ByteString -> Script -> Witness +to_local_witness_spend sig = with_script [sig, BS.empty] + +-- | The witness for the revocation spend of a @to_local@ (or HTLC +-- transaction) output, given its witness script: +-- +-- @\<revocation_sig\> 1 \<script\>@ +to_local_witness_revoke :: BS.ByteString -> Script -> Witness +to_local_witness_revoke sig = with_script [sig, BS.singleton 0x01] + +-- to_remote output ----------------------------------------------------------- + +-- | The @to_remote@ output's witness script under 'Anchors': +-- +-- @\<remotepubkey\> OP_CHECKSIGVERIFY 1 OP_CHECKSEQUENCEVERIFY@ +-- +-- (Under 'StaticRemotekey' the output is P2WPKH, with no witness +-- script; see 'to_remote_script_pubkey'.) +to_remote_witness_script :: RemotePubkey -> Script +to_remote_witness_script (RemotePubkey pk) = script $ + push_point pk + <> op op_checksigverify + <> op op_1 + <> op op_checksequenceverify + +-- | The @to_remote@ output's scriptPubKey: P2WPKH to @remotepubkey@ +-- under 'StaticRemotekey', and P2WSH of 'to_remote_witness_script' +-- under 'Anchors'. +to_remote_script_pubkey :: RemotePubkey -> CommitmentFormat -> Script +to_remote_script_pubkey pk@(RemotePubkey p) fmt = case fmt of + StaticRemotekey -> script (op op_0 <> push (hash160 (BOLT1.un_point p))) + Anchors -> to_p2wsh (to_remote_witness_script pk) + +-- | The witness spending a @to_remote@ output: @\<remote_sig\> +-- \<remotepubkey\>@ (P2WPKH) under 'StaticRemotekey', and +-- @\<remote_sig\> \<script\>@ under 'Anchors', where the spending +-- input's @nSequence@ must be 1. +to_remote_witness + :: BS.ByteString -> RemotePubkey -> CommitmentFormat -> Witness +to_remote_witness sig pk@(RemotePubkey p) fmt = case fmt of + StaticRemotekey -> Witness [sig, BOLT1.un_point p] + Anchors -> with_script [sig] (to_remote_witness_script pk) + +-- anchor outputs ------------------------------------------------------------- + +-- | An anchor output's witness script, for the funding pubkey of the +-- party it belongs to: +-- +-- @ +-- \<funding_pubkey\> OP_CHECKSIG OP_IFDUP +-- OP_NOTIF +-- OP_16 OP_CHECKSEQUENCEVERIFY +-- OP_ENDIF +-- @ anchor_script :: FundingPubkey -> Script -anchor_script (FundingPubkey (Pubkey !pk)) = - build_script $ - push_data pk - <> BSB.word8 op_checksig - <> BSB.word8 op_ifdup - <> BSB.word8 op_notif - <> BSB.word8 op_16 - <> BSB.word8 op_checksequenceverify - <> BSB.word8 op_endif - --- | Witness for owner to spend anchor output. --- --- Witness: @<sig>@ --- --- >>> anchor_witness_owner sig --- Witness [sig] -anchor_witness_owner :: BS.ByteString -> Witness -anchor_witness_owner !sig = Witness [sig] - --- | Witness for anyone to sweep anchor output after 16 blocks. --- --- Witness: @<>@ --- --- >>> anchor_witness_anyone --- Witness [""] -anchor_witness_anyone :: Witness -anchor_witness_anyone = Witness [BS.empty] - --- offered HTLC output --------------------------------------------------------- - --- | Offered HTLC witness script. --- --- Without option_anchors: --- --- @ --- OP_DUP OP_HASH160 <RIPEMD160(SHA256(revocationpubkey))> OP_EQUAL --- OP_IF --- OP_CHECKSIG --- OP_ELSE --- <remote_htlcpubkey> OP_SWAP OP_SIZE 32 OP_EQUAL --- OP_NOTIF --- OP_DROP 2 OP_SWAP <local_htlcpubkey> 2 OP_CHECKMULTISIG --- OP_ELSE --- OP_HASH160 <RIPEMD160(payment_hash)> OP_EQUALVERIFY --- OP_CHECKSIG --- OP_ENDIF --- OP_ENDIF --- @ --- --- With option_anchors, adds @1 OP_CHECKSEQUENCEVERIFY OP_DROP@ before --- final OP_ENDIF. +anchor_script (FundingPubkey pk) = script $ + push_point pk + <> op op_checksig + <> op op_ifdup + <> op op_notif + <> op op_16 + <> op op_checksequenceverify + <> op op_endif + +-- | The witness for an anchor's owner to spend it: +-- @\<sig\> \<script\>@. +anchor_witness_owner :: BS.ByteString -> FundingPubkey -> Witness +anchor_witness_owner sig pk = with_script [sig] (anchor_script pk) + +-- | The witness for anyone to sweep an anchor, 16 blocks after the +-- commitment transaction confirms: @\<\> \<script\>@. +anchor_witness_anyone :: FundingPubkey -> Witness +anchor_witness_anyone pk = with_script [BS.empty] (anchor_script pk) + +-- offered HTLC outputs ------------------------------------------------------- + +-- csv suffix for anchors HTLC scripts +htlc_csv :: CommitmentFormat -> BSB.Builder +htlc_csv fmt = case fmt of + StaticRemotekey -> mempty + Anchors -> op op_1 <> op op_checksequenceverify <> op op_drop +{-# INLINE htlc_csv #-} + +-- | An offered HTLC output's witness script: +-- +-- @ +-- OP_DUP OP_HASH160 \<RIPEMD160(SHA256(revocationpubkey))\> OP_EQUAL +-- OP_IF +-- OP_CHECKSIG +-- OP_ELSE +-- \<remote_htlcpubkey\> OP_SWAP OP_SIZE 32 OP_EQUAL +-- OP_NOTIF +-- OP_DROP 2 OP_SWAP \<local_htlcpubkey\> 2 OP_CHECKMULTISIG +-- OP_ELSE +-- OP_HASH160 \<RIPEMD160(payment_hash)\> OP_EQUALVERIFY +-- OP_CHECKSIG +-- OP_ENDIF +-- OP_ENDIF +-- @ +-- +-- Under 'Anchors', @1 OP_CHECKSEQUENCEVERIFY OP_DROP@ precedes the +-- final @OP_ENDIF@. offered_htlc_script :: RevocationPubkey -> RemoteHtlcPubkey -> LocalHtlcPubkey - -> PaymentHash - -> ChannelFeatures + -> BOLT1.PaymentHash + -> CommitmentFormat -> Script offered_htlc_script - (RevocationPubkey (Pubkey !revpk)) - (RemoteHtlcPubkey (Pubkey !remotepk)) - (LocalHtlcPubkey (Pubkey !localpk)) - (PaymentHash !ph) - !features = - let !revpk_hash = hash160 revpk - !payment_hash160 = RIPEMD160.hash ph - !csv_suffix = if has_anchors features - then BSB.word8 op_1 - <> BSB.word8 op_checksequenceverify - <> BSB.word8 op_drop - else mempty - in build_script $ - -- OP_DUP OP_HASH160 <revpk_hash> OP_EQUAL - BSB.word8 op_dup - <> BSB.word8 op_hash160 - <> push_data revpk_hash - <> BSB.word8 op_equal - -- OP_IF OP_CHECKSIG - <> BSB.word8 op_if - <> BSB.word8 op_checksig - -- OP_ELSE - <> BSB.word8 op_else - -- <remote_htlcpubkey> OP_SWAP OP_SIZE 32 OP_EQUAL - <> push_data remotepk - <> BSB.word8 op_swap - <> BSB.word8 op_size - <> push_data (BS.singleton 32) - <> BSB.word8 op_equal - -- OP_NOTIF - <> BSB.word8 op_notif - -- OP_DROP 2 OP_SWAP <local_htlcpubkey> 2 OP_CHECKMULTISIG - <> BSB.word8 op_drop - <> BSB.word8 op_2 - <> BSB.word8 op_swap - <> push_data localpk - <> BSB.word8 op_2 - <> BSB.word8 op_checkmultisig - -- OP_ELSE - <> BSB.word8 op_else - -- OP_HASH160 <payment_hash160> OP_EQUALVERIFY OP_CHECKSIG - <> BSB.word8 op_hash160 - <> push_data payment_hash160 - <> BSB.word8 op_equalverify - <> BSB.word8 op_checksig - -- OP_ENDIF - <> BSB.word8 op_endif - -- CSV suffix for anchors - <> csv_suffix - -- OP_ENDIF - <> BSB.word8 op_endif - --- | Witness for remote node to claim offered HTLC with preimage. --- --- With option_anchors, input nSequence must be 1. --- --- Witness: @<remotehtlcsig> <payment_preimage>@ --- --- >>> offered_htlc_witness_preimage sig preimage --- Witness [sig, preimage] + (RevocationPubkey rev) + (RemoteHtlcPubkey remote) + (LocalHtlcPubkey local) + ph + fmt = script $ + op op_dup <> op op_hash160 + <> push (hash160 (BOLT1.un_point rev)) + <> op op_equal + <> op op_if + <> op op_checksig + <> op op_else + <> push_point remote <> op op_swap <> op op_size + <> push (BS.singleton 32) <> op op_equal + <> op op_notif + <> op op_drop <> op op_2 <> op op_swap + <> push_point local <> op op_2 <> op op_checkmultisig + <> op op_else + <> op op_hash160 + <> push (RIPEMD160.hash (BOLT1.un_payment_hash ph)) + <> op op_equalverify <> op op_checksig + <> op op_endif + <> htlc_csv fmt + <> op op_endif + +-- | The witness for the remote party to claim an offered HTLC output +-- with the payment preimage, given its witness script: +-- +-- @\<remotehtlcsig\> \<payment_preimage\> \<script\>@ +-- +-- Under 'Anchors' the spending input's @nSequence@ must be 1. offered_htlc_witness_preimage - :: BS.ByteString -> PaymentPreimage -> Witness -offered_htlc_witness_preimage !sig (PaymentPreimage !preimage) = - Witness [sig, preimage] - --- | Witness for revocation spend of offered HTLC. --- --- Witness: @<revocation_sig> <revocationpubkey>@ --- --- >>> offered_htlc_witness_revoke sig revpk --- Witness [sig, revpk] -offered_htlc_witness_revoke :: BS.ByteString -> Pubkey -> Witness -offered_htlc_witness_revoke !sig (Pubkey !revpk) = Witness [sig, revpk] - --- received HTLC output -------------------------------------------------------- - --- | Received HTLC witness script. --- --- Without option_anchors: --- --- @ --- OP_DUP OP_HASH160 <RIPEMD160(SHA256(revocationpubkey))> OP_EQUAL --- OP_IF --- OP_CHECKSIG --- OP_ELSE --- <remote_htlcpubkey> OP_SWAP OP_SIZE 32 OP_EQUAL --- OP_IF --- OP_HASH160 <RIPEMD160(payment_hash)> OP_EQUALVERIFY --- 2 OP_SWAP <local_htlcpubkey> 2 OP_CHECKMULTISIG --- OP_ELSE --- OP_DROP <cltv_expiry> OP_CHECKLOCKTIMEVERIFY OP_DROP --- OP_CHECKSIG --- OP_ENDIF --- OP_ENDIF --- @ --- --- With option_anchors, adds @1 OP_CHECKSEQUENCEVERIFY OP_DROP@ before --- final OP_ENDIF. + :: BS.ByteString -> PaymentPreimage -> Script -> Witness +offered_htlc_witness_preimage sig pre = + with_script [sig, BOLT1.un_payment_preimage pre] + +-- | The witness for the revocation spend of an offered HTLC output, +-- given its witness script: +-- +-- @\<revocation_sig\> \<revocationpubkey\> \<script\>@ +offered_htlc_witness_revoke + :: BS.ByteString -> RevocationPubkey -> Script -> Witness +offered_htlc_witness_revoke sig (RevocationPubkey rev) = + with_script [sig, BOLT1.un_point rev] + +-- received HTLC outputs ------------------------------------------------------ + +-- | A received HTLC output's witness script: +-- +-- @ +-- OP_DUP OP_HASH160 \<RIPEMD160(SHA256(revocationpubkey))\> OP_EQUAL +-- OP_IF +-- OP_CHECKSIG +-- OP_ELSE +-- \<remote_htlcpubkey\> OP_SWAP OP_SIZE 32 OP_EQUAL +-- OP_IF +-- OP_HASH160 \<RIPEMD160(payment_hash)\> OP_EQUALVERIFY +-- 2 OP_SWAP \<local_htlcpubkey\> 2 OP_CHECKMULTISIG +-- OP_ELSE +-- OP_DROP \<cltv_expiry\> OP_CHECKLOCKTIMEVERIFY OP_DROP +-- OP_CHECKSIG +-- OP_ENDIF +-- OP_ENDIF +-- @ +-- +-- Under 'Anchors', @1 OP_CHECKSEQUENCEVERIFY OP_DROP@ precedes the +-- final @OP_ENDIF@. received_htlc_script :: RevocationPubkey -> RemoteHtlcPubkey -> LocalHtlcPubkey - -> PaymentHash + -> BOLT1.PaymentHash -> CltvExpiry - -> ChannelFeatures + -> CommitmentFormat -> Script received_htlc_script - (RevocationPubkey (Pubkey !revpk)) - (RemoteHtlcPubkey (Pubkey !remotepk)) - (LocalHtlcPubkey (Pubkey !localpk)) - (PaymentHash !ph) - (CltvExpiry !expiry) - !features = - let !revpk_hash = hash160 revpk - !payment_hash160 = RIPEMD160.hash ph - !csv_suffix = if has_anchors features - then BSB.word8 op_1 - <> BSB.word8 op_checksequenceverify - <> BSB.word8 op_drop - else mempty - in build_script $ - -- OP_DUP OP_HASH160 <revpk_hash> OP_EQUAL - BSB.word8 op_dup - <> BSB.word8 op_hash160 - <> push_data revpk_hash - <> BSB.word8 op_equal - -- OP_IF OP_CHECKSIG - <> BSB.word8 op_if - <> BSB.word8 op_checksig - -- OP_ELSE - <> BSB.word8 op_else - -- <remote_htlcpubkey> OP_SWAP OP_SIZE 32 OP_EQUAL - <> push_data remotepk - <> BSB.word8 op_swap - <> BSB.word8 op_size - <> push_data (BS.singleton 32) - <> BSB.word8 op_equal - -- OP_IF - <> BSB.word8 op_if - -- OP_HASH160 <payment_hash160> OP_EQUALVERIFY - <> BSB.word8 op_hash160 - <> push_data payment_hash160 - <> BSB.word8 op_equalverify - -- 2 OP_SWAP <local_htlcpubkey> 2 OP_CHECKMULTISIG - <> BSB.word8 op_2 - <> BSB.word8 op_swap - <> push_data localpk - <> BSB.word8 op_2 - <> BSB.word8 op_checkmultisig - -- OP_ELSE - <> BSB.word8 op_else - -- OP_DROP <cltv_expiry> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG - <> BSB.word8 op_drop - <> push_cltv expiry - <> BSB.word8 op_checklocktimeverify - <> BSB.word8 op_drop - <> BSB.word8 op_checksig - -- OP_ENDIF - <> BSB.word8 op_endif - -- CSV suffix for anchors - <> csv_suffix - -- OP_ENDIF - <> BSB.word8 op_endif - --- | Witness for remote node to timeout received HTLC. --- --- With option_anchors, input nSequence must be 1. --- --- Witness: @<remotehtlcsig> <>@ --- --- >>> received_htlc_witness_timeout sig --- Witness [sig, ""] -received_htlc_witness_timeout :: BS.ByteString -> Witness -received_htlc_witness_timeout !sig = Witness [sig, BS.empty] - --- | Witness for revocation spend of received HTLC. --- --- Witness: @<revocation_sig> <revocationpubkey>@ --- --- >>> received_htlc_witness_revoke sig revpk --- Witness [sig, revpk] -received_htlc_witness_revoke :: BS.ByteString -> Pubkey -> Witness -received_htlc_witness_revoke !sig (Pubkey !revpk) = Witness [sig, revpk] - --- HTLC-timeout/success output ------------------------------------------------- - --- | HTLC output witness script (same structure as to_local). --- --- Used for HTLC-timeout and HTLC-success transaction outputs. --- --- Script: --- --- @ --- OP_IF --- <revocationpubkey> --- OP_ELSE --- <to_self_delay> --- OP_CHECKSEQUENCEVERIFY --- OP_DROP --- <local_delayedpubkey> --- OP_ENDIF --- OP_CHECKSIG --- @ -htlc_output_script - :: RevocationPubkey - -> ToSelfDelay - -> LocalDelayedPubkey - -> Script -htlc_output_script = to_local_script - --- | Witness for delayed spend of HTLC output. --- --- Input nSequence must be set to to_self_delay. --- --- Witness: @<local_delayedsig> 0@ -htlc_output_witness_spend :: BS.ByteString -> Witness -htlc_output_witness_spend = to_local_witness_spend - --- | Witness for revocation spend of HTLC output. --- --- Witness: @<revocationsig> 1@ -htlc_output_witness_revoke :: BS.ByteString -> Witness -htlc_output_witness_revoke = to_local_witness_revoke + (RevocationPubkey rev) + (RemoteHtlcPubkey remote) + (LocalHtlcPubkey local) + ph + (CltvExpiry expiry) + fmt = script $ + op op_dup <> op op_hash160 + <> push (hash160 (BOLT1.un_point rev)) + <> op op_equal + <> op op_if + <> op op_checksig + <> op op_else + <> push_point remote <> op op_swap <> op op_size + <> push (BS.singleton 32) <> op op_equal + <> op op_if + <> op op_hash160 + <> push (RIPEMD160.hash (BOLT1.un_payment_hash ph)) + <> op op_equalverify + <> op op_2 <> op op_swap <> push_point local + <> op op_2 <> op op_checkmultisig + <> op op_else + <> op op_drop <> push_num expiry + <> op op_checklocktimeverify <> op op_drop + <> op op_checksig + <> op op_endif + <> htlc_csv fmt + <> op op_endif + +-- | The witness for the remote party to time out a received HTLC +-- output, given its witness script: +-- +-- @\<remotehtlcsig\> \<\> \<script\>@ +-- +-- Under 'Anchors' the spending input's @nSequence@ must be 1. +received_htlc_witness_timeout :: BS.ByteString -> Script -> Witness +received_htlc_witness_timeout sig = with_script [sig, BS.empty] + +-- | The witness for the revocation spend of a received HTLC output, +-- given its witness script: +-- +-- @\<revocation_sig\> \<revocationpubkey\> \<script\>@ +received_htlc_witness_revoke + :: BS.ByteString -> RevocationPubkey -> Script -> Witness +received_htlc_witness_revoke sig (RevocationPubkey rev) = + with_script [sig, BOLT1.un_point rev] + +-- HTLC transaction inputs ---------------------------------------------------- + +-- | The witness for an HTLC-success transaction's input, given both +-- HTLC signatures, the payment preimage and the received HTLC +-- output's witness script: +-- +-- @0 \<remotehtlcsig\> \<localhtlcsig\> \<payment_preimage\> \<script\>@ +-- +-- See 'remote_htlc_sighash' for the signatures' sighash types. +htlc_success_witness + :: BS.ByteString -- ^ remotehtlcsig + -> BS.ByteString -- ^ localhtlcsig + -> PaymentPreimage + -> Script -- ^ the received HTLC output's witness script + -> Witness +htlc_success_witness remote local pre = + with_script [BS.empty, remote, local, BOLT1.un_payment_preimage pre] + +-- | The witness for an HTLC-timeout transaction's input, given both +-- HTLC signatures and the offered HTLC output's witness script: +-- +-- @0 \<remotehtlcsig\> \<localhtlcsig\> \<\> \<script\>@ +-- +-- See 'remote_htlc_sighash' for the signatures' sighash types. +htlc_timeout_witness + :: BS.ByteString -- ^ remotehtlcsig + -> BS.ByteString -- ^ localhtlcsig + -> Script -- ^ the offered HTLC output's witness script + -> Witness +htlc_timeout_witness remote local = + with_script [BS.empty, remote, local, BS.empty] + +-- | The sighash type of @remotehtlcsig@: the signature on an +-- HTLC-success or HTLC-timeout transaction that the commitment's +-- non-owner sends in @commitment_signed@. +-- +-- Under 'Anchors' it is @SIGHASH_SINGLE|SIGHASH_ANYONECANPAY@, so +-- that the owner can add inputs and outputs to pay the (otherwise +-- zero) fee; under 'StaticRemotekey' it is @SIGHASH_ALL@. The +-- owner's own signature, @localhtlcsig@, always uses @SIGHASH_ALL@. +-- +-- >>> remote_htlc_sighash Anchors +-- SIGHASH_SINGLE_ANYONECANPAY +remote_htlc_sighash :: CommitmentFormat -> SighashType +remote_htlc_sighash fmt = case fmt of + StaticRemotekey -> SIGHASH_ALL + Anchors -> SIGHASH_SINGLE_ANYONECANPAY diff --git a/lib/Lightning/Protocol/BOLT3/Tx.hs b/lib/Lightning/Protocol/BOLT3/Tx.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} @@ -8,667 +8,562 @@ -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech> -- --- Transaction assembly for BOLT #3. --- --- Constructs: --- --- * Commitment transactions --- * HTLC-timeout transactions --- * HTLC-success transactions --- * Closing transactions +-- Commitment, HTLC and closing transactions, per BOLT #3. module Lightning.Protocol.BOLT3.Tx ( - -- * Commitment transaction - CommitmentTx(..) - , CommitmentContext(..) - , CommitmentKeys(..) + -- * Commitment transactions + CommitmentContext(..) + , CommitmentTx(..) + , CommitmentOutput(..) + , OutputType(..) , build_commitment_tx -- * HTLC transactions - , HTLCTx(..) , HTLCContext(..) - , build_htlc_timeout_tx - , build_htlc_success_tx + , HTLCTx(..) + , build_htlc_tx - -- * Closing transaction + -- * Closing transactions , ClosingTx(..) + , ClosingOutput(..) , ClosingContext(..) + , ClosingOutputs(..) , build_closing_tx + , LegacyClosingContext(..) , build_legacy_closing_tx - -- * Conversion to ppad-tx - , commitment_to_tx - , htlc_to_tx - , closing_to_tx - - -- * Transaction outputs - , TxOutput(..) - , OutputType(..) - - -- * Fee calculation + -- * Fees , commitment_fee , htlc_timeout_fee , htlc_success_fee - , commitment_weight -- * Trimming - , is_trimmed - , trimmed_htlcs - , untrimmed_htlcs , htlc_trim_threshold + , is_trimmed - -- * Output ordering - , sort_outputs + -- * Serialization + , commitment_to_tx + , htlc_to_tx + , closing_to_tx + , encode_commitment_tx + , encode_htlc_tx + , encode_closing_tx ) where +import Bitcoin.Prim.Tx (OutPoint(..), TxId) import qualified Bitcoin.Prim.Tx as BT -import Data.Bits ((.&.), (.|.), shiftL, shiftR) +import Control.DeepSeq (NFData) +import Data.Bits ((.&.), (.|.), shiftR) import qualified Data.ByteString as BS import Data.List (sortBy) -import Data.List.NonEmpty (NonEmpty(..), nonEmpty) +import Data.List.NonEmpty (NonEmpty(..)) +import qualified Data.List.NonEmpty as NE +import Data.Maybe (fromMaybe) import Data.Word (Word32, Word64) import GHC.Generics (Generic) +import Lightning.Protocol.BOLT1 (Satoshi, MilliSatoshi) +import qualified Lightning.Protocol.BOLT1 as BOLT1 import Lightning.Protocol.BOLT3.Keys import Lightning.Protocol.BOLT3.Scripts import Lightning.Protocol.BOLT3.Types --- transaction outputs --------------------------------------------------------- +-- commitment transactions ---------------------------------------------------- --- | Type of output in a commitment transaction. +-- | What a commitment transaction output pays. data OutputType = OutputToLocal | OutputToRemote | OutputLocalAnchor | OutputRemoteAnchor - | OutputOfferedHTLC {-# UNPACK #-} !CltvExpiry - | OutputReceivedHTLC {-# UNPACK #-} !CltvExpiry + | OutputHTLC !HTLC + -- ^ an offered or received HTLC (per its 'htlc_direction') deriving (Eq, Show, Generic) --- | A transaction output with value, script, and type information. -data TxOutput = TxOutput - { txout_value :: {-# UNPACK #-} !Satoshi - , txout_script :: !Script - , txout_type :: !OutputType - } deriving (Eq, Show, Generic) +instance NFData OutputType --- commitment transaction ------------------------------------------------------ - --- | Derived keys needed for commitment transaction outputs. -data CommitmentKeys = CommitmentKeys - { ck_revocation_pubkey :: !RevocationPubkey - , ck_local_delayed :: !LocalDelayedPubkey - , ck_local_htlc :: !LocalHtlcPubkey - , ck_remote_htlc :: !RemoteHtlcPubkey - , ck_local_payment :: !LocalPubkey - , ck_remote_payment :: !RemotePubkey - , ck_local_funding :: !FundingPubkey - , ck_remote_funding :: !FundingPubkey +-- | A commitment transaction output. +data CommitmentOutput = CommitmentOutput + { co_value :: {-# UNPACK #-} !Satoshi + , co_script :: !Script -- ^ scriptPubKey + , co_type :: !OutputType } deriving (Eq, Show, Generic) --- | Context for building a commitment transaction. +instance NFData CommitmentOutput + +-- | The parameters of a commitment transaction, from the point of view +-- of its owner (the "local" party, whose outputs are delayed). data CommitmentContext = CommitmentContext - { cc_funding_outpoint :: !OutPoint - , cc_commitment_number :: !CommitmentNumber - , cc_local_payment_bp :: !PaymentBasepoint - , cc_remote_payment_bp :: !PaymentBasepoint - , cc_to_self_delay :: !ToSelfDelay - , cc_dust_limit :: !DustLimit - , cc_feerate :: !FeeratePerKw - , cc_features :: !ChannelFeatures - , cc_is_funder :: !Bool - , cc_to_local_msat :: !MilliSatoshi - , cc_to_remote_msat :: !MilliSatoshi - , cc_htlcs :: ![HTLC] - , cc_keys :: !CommitmentKeys + { cc_funding_outpoint :: {-# UNPACK #-} !OutPoint + , cc_commitment_number :: {-# UNPACK #-} !CommitmentNumber + , cc_local_payment_bp :: !PaymentBasepoint + -- ^ the owner's @payment_basepoint@ + , cc_remote_payment_bp :: !PaymentBasepoint + -- ^ the other party's @payment_basepoint@ + , cc_to_self_delay :: {-# UNPACK #-} !ToSelfDelay + -- ^ the delay on the owner's outputs (set by the other party) + , cc_dust_limit :: {-# UNPACK #-} !DustLimit + -- ^ the owner's @dust_limit_satoshis@ + , cc_feerate :: {-# UNPACK #-} !FeeratePerKw + , cc_format :: !CommitmentFormat + , cc_is_funder :: !Bool + -- ^ whether the owner opened the channel, and so pays the fee + , cc_to_local_msat :: {-# UNPACK #-} !MilliSatoshi + , cc_to_remote_msat :: {-# UNPACK #-} !MilliSatoshi + , cc_htlcs :: ![HTLC] + , cc_keys :: !CommitmentKeys } deriving (Eq, Show, Generic) --- | A commitment transaction. +instance NFData CommitmentContext + +-- | An unsigned commitment transaction. data CommitmentTx = CommitmentTx - { ctx_version :: {-# UNPACK #-} !Word32 - , ctx_locktime :: !Locktime - , ctx_input_outpoint :: !OutPoint - , ctx_input_sequence :: !Sequence - , ctx_outputs :: ![TxOutput] - , ctx_funding_script :: !Script + { ctx_version :: {-# UNPACK #-} !Word32 + , ctx_locktime :: {-# UNPACK #-} !Locktime + , ctx_input_outpoint :: {-# UNPACK #-} !OutPoint + , ctx_input_sequence :: {-# UNPACK #-} !Sequence + , ctx_outputs :: !(NonEmpty CommitmentOutput) + -- ^ in BIP69+CLTV order + , ctx_funding_script :: !Script + -- ^ the funding output's witness script, which signatures commit + -- to } deriving (Eq, Show, Generic) --- | Build a commitment transaction. +instance NFData CommitmentTx + +-- | Build a commitment transaction, per BOLT #3's "Commitment +-- Transaction Construction": -- --- Follows the algorithm from BOLT #3: +-- * the locktime and input sequence encode the obscured commitment +-- number (the payment basepoints are ordered by 'cc_is_funder'); +-- * HTLCs whose amount, less the HTLC transaction fee, is below the +-- dust limit are trimmed; +-- * the base fee (and under 'Anchors', both anchors) are taken from +-- the funder's output, which is left at zero if it can't cover +-- them; +-- * @to_local@ and @to_remote@ outputs below the dust limit are +-- omitted, and under 'Anchors' each party's anchor is added if its +-- output, or any HTLC output, exists; +-- * outputs are sorted by value, scriptPubKey and CLTV expiry. -- --- 1. Initialize input and locktime with obscured commitment number --- 2. Calculate which HTLCs are trimmed --- 3. Calculate base fee and subtract from funder --- 4. Add untrimmed HTLC outputs --- 5. Add to_local output if above dust --- 6. Add to_remote output if above dust --- 7. Add anchor outputs if option_anchors --- 8. Sort outputs per BIP69+CLTV -build_commitment_tx :: CommitmentContext -> CommitmentTx -build_commitment_tx ctx = - let !obscured = obscured_commitment_number - (cc_local_payment_bp ctx) - (cc_remote_payment_bp ctx) - (cc_commitment_number ctx) - - -- Locktime: upper 8 bits are 0x20, lower 24 bits are lower 24 of obscured - !locktime = Locktime $ - (0x20 `shiftL` 24) .|. (fromIntegral obscured .&. 0x00FFFFFF) - - -- Sequence: upper 8 bits are 0x80, lower 24 bits are upper 24 of obscured - !inputSeq = Sequence $ - (0x80 `shiftL` 24) .|. - (fromIntegral (obscured `shiftR` 24) .&. 0x00FFFFFF) - - -- Funding script for witness - !fundingScript = funding_script - (ck_local_funding $ cc_keys ctx) - (ck_remote_funding $ cc_keys ctx) - - -- Calculate untrimmed HTLCs - !untrimmedHtlcs = untrimmed_htlcs - (cc_dust_limit ctx) - (cc_feerate ctx) - (cc_features ctx) - (cc_htlcs ctx) - - -- Calculate base fee - !baseFee = commitment_fee - (cc_feerate ctx) - (cc_features ctx) - (fromIntegral $ length untrimmedHtlcs) - - -- Anchor cost if applicable - !anchorCost = if has_anchors (cc_features ctx) - then 2 * anchor_output_value - else Satoshi 0 - - -- Subtract fees and anchors from funder - !totalDeduction = baseFee + anchorCost - !(toLocalSat, toRemoteSat) = if cc_is_funder ctx - then - let !local = msatToSat (cc_to_local_msat ctx) - !deducted = if unSatoshi local >= unSatoshi totalDeduction - then Satoshi (unSatoshi local - unSatoshi totalDeduction) - else Satoshi 0 - in (deducted, msatToSat (cc_to_remote_msat ctx)) - else - let !remote = msatToSat (cc_to_remote_msat ctx) - !deducted = if unSatoshi remote >= unSatoshi totalDeduction - then Satoshi (unSatoshi remote - unSatoshi totalDeduction) - else Satoshi 0 - in (msatToSat (cc_to_local_msat ctx), deducted) - - !dustLimit = unDustLimit (cc_dust_limit ctx) - - -- Build HTLC outputs - !htlcOutputs = map (htlcOutput ctx) untrimmedHtlcs - - -- Build to_local output if above dust - !toLocalOutput = - if unSatoshi toLocalSat >= unSatoshi dustLimit - then - let !script = to_p2wsh $ to_local_script - (ck_revocation_pubkey $ cc_keys ctx) - (cc_to_self_delay ctx) - (ck_local_delayed $ cc_keys ctx) - in [TxOutput toLocalSat script OutputToLocal] - else [] - - -- Build to_remote output if above dust - !toRemoteOutput = - if unSatoshi toRemoteSat >= unSatoshi dustLimit - then - let !script = if has_anchors (cc_features ctx) - then to_p2wsh $ to_remote_script - (ck_remote_payment $ cc_keys ctx) - (cc_features ctx) - else to_remote_script - (ck_remote_payment $ cc_keys ctx) - (cc_features ctx) - in [TxOutput toRemoteSat script OutputToRemote] - else [] - - -- Build anchor outputs if option_anchors - !hasUntrimmedHtlcs = not (null untrimmedHtlcs) - !toLocalExists = not (null toLocalOutput) - !toRemoteExists = not (null toRemoteOutput) - - !localAnchorOutput = - if has_anchors (cc_features ctx) && - (toLocalExists || hasUntrimmedHtlcs) - then - let !script = to_p2wsh $ anchor_script - (ck_local_funding $ cc_keys ctx) - in [TxOutput anchor_output_value script OutputLocalAnchor] - else [] - - !remoteAnchorOutput = - if has_anchors (cc_features ctx) && - (toRemoteExists || hasUntrimmedHtlcs) - then - let !script = to_p2wsh $ anchor_script - (ck_remote_funding $ cc_keys ctx) - in [TxOutput anchor_output_value script OutputRemoteAnchor] - else [] - - -- Combine and sort all outputs - !allOutputs = toLocalOutput ++ toRemoteOutput ++ - localAnchorOutput ++ remoteAnchorOutput ++ - htlcOutputs - !sortedOutputs = sort_outputs allOutputs - - in CommitmentTx - { ctx_version = 2 - , ctx_locktime = locktime - , ctx_input_outpoint = cc_funding_outpoint ctx - , ctx_input_sequence = inputSeq - , ctx_outputs = sortedOutputs - , ctx_funding_script = fundingScript - } -{-# INLINE build_commitment_tx #-} - --- | Build an HTLC output for commitment transaction. -htlcOutput :: CommitmentContext -> HTLC -> TxOutput -htlcOutput ctx htlc = - let !amountSat = msatToSat (htlc_amount_msat htlc) - !keys = cc_keys ctx - !features = cc_features ctx - !expiry = htlc_cltv_expiry htlc - in case htlc_direction htlc of - HTLCOffered -> - let !script = to_p2wsh $ offered_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - features - in TxOutput amountSat script (OutputOfferedHTLC expiry) - HTLCReceived -> - let !script = to_p2wsh $ received_htlc_script - (ck_revocation_pubkey keys) - (ck_remote_htlc keys) - (ck_local_htlc keys) - (htlc_payment_hash htlc) - expiry - features - in TxOutput amountSat script (OutputReceivedHTLC expiry) -{-# INLINE htlcOutput #-} - --- HTLC transactions ----------------------------------------------------------- - --- | Context for building HTLC transactions. +-- Fails if the transaction would have no outputs. +build_commitment_tx :: CommitmentContext -> Maybe CommitmentTx +build_commitment_tx ctx = do + outputs <- NE.nonEmpty . sort_outputs $ + to_local <> to_remote <> local_anchor <> remote_anchor <> htlcs + pure CommitmentTx + { ctx_version = 2 + , ctx_locktime = + Locktime (0x20000000 .|. (fromIntegral obscured .&. 0xFFFFFF)) + , ctx_input_outpoint = cc_funding_outpoint ctx + , ctx_input_sequence = + Sequence + (0x80000000 .|. (fromIntegral (obscured `shiftR` 24) .&. 0xFFFFFF)) + , ctx_outputs = outputs + , ctx_funding_script = + funding_script (ck_local_funding keys) (ck_remote_funding keys) + } + where + !keys = cc_keys ctx + !fmt = cc_format ctx + !rate = cc_feerate ctx + !dust = cc_dust_limit ctx + DustLimit dust_sat = dust + + (!opener, !accepter) + | cc_is_funder ctx = (cc_local_payment_bp ctx, cc_remote_payment_bp ctx) + | otherwise = (cc_remote_payment_bp ctx, cc_local_payment_bp ctx) + !obscured = + obscured_commitment_number opener accepter (cc_commitment_number ctx) + + untrimmed = filter (not . is_trimmed dust rate fmt) (cc_htlcs ctx) + + !fee = commitment_fee rate fmt (fromIntegral (length untrimmed)) + !deduction = case fmt of + StaticRemotekey -> fee + Anchors -> + fromMaybe BOLT1.max_satoshi (BOLT1.add_sat fee (sat 660)) + deduct s = fromMaybe (sat 0) (BOLT1.sub_sat s deduction) + + !local_sat = BOLT1.msat_to_sat (cc_to_local_msat ctx) + !remote_sat = BOLT1.msat_to_sat (cc_to_remote_msat ctx) + (!to_local_sat, !to_remote_sat) + | cc_is_funder ctx = (deduct local_sat, remote_sat) + | otherwise = (local_sat, deduct remote_sat) + + to_local = + [ CommitmentOutput to_local_sat + (to_p2wsh (to_local_script (ck_revocation_pubkey keys) + (cc_to_self_delay ctx) (ck_local_delayed keys))) + OutputToLocal + | to_local_sat >= dust_sat ] + + to_remote = + [ CommitmentOutput to_remote_sat + (to_remote_script_pubkey (ck_remote_payment keys) fmt) + OutputToRemote + | to_remote_sat >= dust_sat ] + + has_htlcs = not (null untrimmed) + anchor pk ty = CommitmentOutput anchor_output_value + (to_p2wsh (anchor_script pk)) ty + + local_anchor = case fmt of + Anchors | not (null to_local) || has_htlcs -> + [anchor (ck_local_funding keys) OutputLocalAnchor] + _ -> [] + + remote_anchor = case fmt of + Anchors | not (null to_remote) || has_htlcs -> + [anchor (ck_remote_funding keys) OutputRemoteAnchor] + _ -> [] + + htlcs = fmap htlc_output untrimmed + + htlc_output h = + let !script = 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 + in CommitmentOutput (BOLT1.msat_to_sat (htlc_amount_msat h)) + (to_p2wsh script) (OutputHTLC h) + +-- BIP69+CLTV order: value, then scriptPubKey, then (for HTLCs) CLTV +-- expiry. +sort_outputs :: [CommitmentOutput] -> [CommitmentOutput] +sort_outputs = sortBy cmp where + cmp a b = compare (co_value a) (co_value b) + <> compare (co_script a) (co_script b) + <> compare (cltv (co_type a)) (cltv (co_type b)) + cltv t = case t of + OutputHTLC h -> Just (htlc_cltv_expiry h) + _ -> Nothing + +-- HTLC transactions ---------------------------------------------------------- + +-- | The parameters of an HTLC-success or HTLC-timeout transaction, +-- spending an HTLC output of the owner's commitment transaction. data HTLCContext = HTLCContext - { hc_commitment_txid :: !TxId - , hc_output_index :: {-# UNPACK #-} !Word32 - , hc_htlc :: !HTLC - , hc_to_self_delay :: !ToSelfDelay - , hc_feerate :: !FeeratePerKw - , hc_features :: !ChannelFeatures - , hc_revocation_pubkey :: !RevocationPubkey - , hc_local_delayed :: !LocalDelayedPubkey + { hc_commitment_txid :: !TxId + , hc_output_index :: {-# UNPACK #-} !Word32 + , hc_htlc :: !HTLC + , hc_to_self_delay :: {-# UNPACK #-} !ToSelfDelay + , hc_feerate :: {-# UNPACK #-} !FeeratePerKw + , hc_format :: !CommitmentFormat + , hc_revocation_pubkey :: !RevocationPubkey + , hc_local_delayed :: !LocalDelayedPubkey } deriving (Eq, Show, Generic) --- | An HTLC transaction (timeout or success). +instance NFData HTLCContext + +-- | An unsigned HTLC-success or HTLC-timeout transaction. data HTLCTx = HTLCTx - { htx_version :: {-# UNPACK #-} !Word32 - , htx_locktime :: !Locktime - , htx_input_outpoint :: !OutPoint - , htx_input_sequence :: !Sequence - , htx_output_value :: !Satoshi - , htx_output_script :: !Script + { htx_version :: {-# UNPACK #-} !Word32 + , htx_locktime :: {-# UNPACK #-} !Locktime + , htx_input_outpoint :: {-# UNPACK #-} !OutPoint + , htx_input_sequence :: {-# UNPACK #-} !Sequence + , htx_output_value :: {-# UNPACK #-} !Satoshi + , htx_output_script :: !Script } deriving (Eq, Show, Generic) --- | Internal helper for HTLC transaction construction. --- --- Both HTLC-timeout and HTLC-success transactions share the same --- structure, differing only in locktime and fee calculation. -build_htlc_tx_common - :: HTLCContext - -> Locktime -- ^ Transaction locktime - -> Satoshi -- ^ Fee to subtract from output - -> HTLCTx -build_htlc_tx_common ctx locktime fee = - let !amountSat = msatToSat (htlc_amount_msat $ hc_htlc ctx) - !outputValue = if unSatoshi amountSat >= unSatoshi fee - then Satoshi (unSatoshi amountSat - unSatoshi fee) - else Satoshi 0 - !inputSeq = if has_anchors (hc_features ctx) - then Sequence 1 - else Sequence 0 - !outpoint = OutPoint (hc_commitment_txid ctx) (hc_output_index ctx) - !outputScript = to_p2wsh $ htlc_output_script - (hc_revocation_pubkey ctx) - (hc_to_self_delay ctx) - (hc_local_delayed ctx) - in HTLCTx - { htx_version = 2 - , htx_locktime = locktime - , htx_input_outpoint = outpoint - , htx_input_sequence = inputSeq - , htx_output_value = outputValue - , htx_output_script = outputScript - } -{-# INLINE build_htlc_tx_common #-} - --- | Build an HTLC-timeout transaction. --- --- * locktime: cltv_expiry --- * sequence: 0 (or 1 with option_anchors) --- * output: to_local style script with revocation and delayed paths -build_htlc_timeout_tx :: HTLCContext -> HTLCTx -build_htlc_timeout_tx ctx = - let !fee = htlc_timeout_fee (hc_feerate ctx) (hc_features ctx) - !locktime = Locktime (unCltvExpiry $ htlc_cltv_expiry $ hc_htlc ctx) - in build_htlc_tx_common ctx locktime fee -{-# INLINE build_htlc_timeout_tx #-} - --- | Build an HTLC-success transaction. +instance NFData HTLCTx + +-- | Build the second-stage transaction for an HTLC output of the +-- owner's commitment transaction: HTLC-timeout (locktime +-- @cltv_expiry@) for an offered HTLC, and HTLC-success (locktime 0) +-- for a received one. -- --- * locktime: 0 --- * sequence: 0 (or 1 with option_anchors) --- * output: to_local style script with revocation and delayed paths -build_htlc_success_tx :: HTLCContext -> HTLCTx -build_htlc_success_tx ctx = - let !fee = htlc_success_fee (hc_feerate ctx) (hc_features ctx) - in build_htlc_tx_common ctx (Locktime 0) fee -{-# INLINE build_htlc_success_tx #-} - --- closing transaction --------------------------------------------------------- - --- | Context for building closing transactions. -data ClosingContext = ClosingContext - { clc_funding_outpoint :: !OutPoint - , clc_local_amount :: !Satoshi - , clc_remote_amount :: !Satoshi - , clc_local_script :: !Script - , clc_remote_script :: !Script - , clc_local_dust_limit :: !DustLimit - , clc_remote_dust_limit :: !DustLimit - , clc_fee :: !Satoshi - , clc_is_funder :: !Bool - , clc_locktime :: !Locktime - , clc_funding_script :: !Script +-- The output pays the HTLC amount, less the HTLC transaction fee +-- (zero under 'Anchors'), to the 'to_local_script' of the owner's +-- keys. Fails if the fee exceeds the amount (such an HTLC is always +-- trimmed). +build_htlc_tx :: HTLCContext -> Maybe HTLCTx +build_htlc_tx ctx = do + value <- BOLT1.sub_sat (BOLT1.msat_to_sat (htlc_amount_msat h)) fee + pure HTLCTx + { htx_version = 2 + , htx_locktime = locktime + , htx_input_outpoint = + OutPoint (hc_commitment_txid ctx) (hc_output_index ctx) + , htx_input_sequence = case fmt of + StaticRemotekey -> Sequence 0 + Anchors -> Sequence 1 + , htx_output_value = value + , htx_output_script = to_p2wsh $ to_local_script + (hc_revocation_pubkey ctx) (hc_to_self_delay ctx) + (hc_local_delayed ctx) + } + where + !h = hc_htlc ctx + !fmt = hc_format ctx + (!fee, !locktime) = case htlc_direction h of + HTLCOffered -> + let CltvExpiry e = htlc_cltv_expiry h + in (htlc_timeout_fee (hc_feerate ctx) fmt, Locktime e) + HTLCReceived -> + (htlc_success_fee (hc_feerate ctx) fmt, Locktime 0) + +-- closing transactions ------------------------------------------------------- + +-- | A closing transaction output. +data ClosingOutput = ClosingOutput + { clo_value :: {-# UNPACK #-} !Satoshi + , clo_script :: !Script -- ^ scriptPubKey } deriving (Eq, Show, Generic) --- | A closing transaction. +instance NFData ClosingOutput + +-- | An unsigned closing transaction. data ClosingTx = ClosingTx - { cltx_version :: {-# UNPACK #-} !Word32 - , cltx_locktime :: !Locktime - , cltx_input_outpoint :: !OutPoint - , cltx_input_sequence :: !Sequence - , cltx_outputs :: ![TxOutput] - , cltx_funding_script :: !Script + { cltx_version :: {-# UNPACK #-} !Word32 + , cltx_locktime :: {-# UNPACK #-} !Locktime + , cltx_input_outpoint :: {-# UNPACK #-} !OutPoint + , cltx_input_sequence :: {-# UNPACK #-} !Sequence + , cltx_outputs :: !(NonEmpty ClosingOutput) + -- ^ in BIP69 order + } deriving (Eq, Show, Generic) + +instance NFData ClosingTx + +-- | Which outputs an @option_simple_close@ closing transaction has, +-- matching the signature fields of @closing_complete@. +data ClosingOutputs + = CloserOutputOnly + | CloseeOutputOnly + | CloserAndCloseeOutputs + deriving (Eq, Show, Generic) + +instance NFData ClosingOutputs + +-- | The parameters of an @option_simple_close@ closing transaction, +-- as given by a @closing_complete@ message. +data ClosingContext = ClosingContext + { clc_funding_outpoint :: {-# UNPACK #-} !OutPoint + , clc_closer_msat :: {-# UNPACK #-} !MilliSatoshi + -- ^ the closer's final balance + , clc_closee_msat :: {-# UNPACK #-} !MilliSatoshi + -- ^ the closee's final balance + , clc_closer_script :: !Script + , clc_closee_script :: !Script + , clc_fee :: {-# UNPACK #-} !Satoshi + , clc_locktime :: {-# UNPACK #-} !Locktime + , clc_outputs :: !ClosingOutputs } deriving (Eq, Show, Generic) --- | Build a closing transaction (option_simple_close). +instance NFData ClosingContext + +-- | Build an @option_simple_close@ closing transaction (BOLT #3's +-- "Closing Transaction"), for @closing_complete@ and @closing_sig@. -- --- * locktime: from closing_complete message --- * sequence: 0xFFFFFFFD --- * outputs: sorted per BIP69 -build_closing_tx :: ClosingContext -> ClosingTx -build_closing_tx ctx = - let -- Subtract fee from closer - !(localAmt, remoteAmt) = if clc_is_funder ctx - then - let !deducted = if unSatoshi (clc_local_amount ctx) >= - unSatoshi (clc_fee ctx) - then Satoshi (unSatoshi (clc_local_amount ctx) - - unSatoshi (clc_fee ctx)) - else Satoshi 0 - in (deducted, clc_remote_amount ctx) - else - let !deducted = if unSatoshi (clc_remote_amount ctx) >= - unSatoshi (clc_fee ctx) - then Satoshi (unSatoshi (clc_remote_amount ctx) - - unSatoshi (clc_fee ctx)) - else Satoshi 0 - in (clc_local_amount ctx, deducted) - - -- Build outputs, omitting dust - !localOutput = - if unSatoshi localAmt >= unSatoshi (unDustLimit $ clc_local_dust_limit ctx) - then [TxOutput localAmt (clc_local_script ctx) OutputToLocal] - else [] - - !remoteOutput = - if unSatoshi remoteAmt >= unSatoshi (unDustLimit $ clc_remote_dust_limit ctx) - then [TxOutput remoteAmt (clc_remote_script ctx) OutputToRemote] - else [] - - !allOutputs = localOutput ++ remoteOutput - !sortedOutputs = sort_outputs allOutputs - - in ClosingTx - { cltx_version = 2 - , cltx_locktime = clc_locktime ctx - , cltx_input_outpoint = clc_funding_outpoint ctx - , cltx_input_sequence = Sequence 0xFFFFFFFD - , cltx_outputs = sortedOutputs - , cltx_funding_script = clc_funding_script ctx - } -{-# INLINE build_closing_tx #-} - --- | Build a legacy closing transaction (closing_signed). +-- The closer pays the fee: its output is its balance, rounded down +-- to whole satoshis, less the fee. An output whose scriptPubKey +-- starts with @OP_RETURN@ has amount 0. Only the chosen outputs are +-- included, and none is trimmed. Fails if the fee exceeds the +-- closer's balance. +build_closing_tx :: ClosingContext -> Maybe ClosingTx +build_closing_tx ctx = do + closer_sat <- + BOLT1.sub_sat (BOLT1.msat_to_sat (clc_closer_msat ctx)) (clc_fee ctx) + let !closer = output closer_sat (clc_closer_script ctx) + !closee = output (BOLT1.msat_to_sat (clc_closee_msat ctx)) + (clc_closee_script ctx) + pure ClosingTx + { cltx_version = 2 + , cltx_locktime = clc_locktime ctx + , cltx_input_outpoint = clc_funding_outpoint ctx + , cltx_input_sequence = Sequence 0xFFFFFFFD + , cltx_outputs = case clc_outputs ctx of + CloserOutputOnly -> closer :| [] + CloseeOutputOnly -> closee :| [] + CloserAndCloseeOutputs -> sort_closing (closer :| [closee]) + } + where + output amt spk@(Script s) = case BS.uncons s of + Just (0x6a, _) -> ClosingOutput (sat 0) spk + _ -> ClosingOutput amt spk + +-- | The parameters of a legacy closing transaction (for +-- @closing_signed@), from the point of view of the node producing +-- the signature (the "local" party). +data LegacyClosingContext = LegacyClosingContext + { lcc_funding_outpoint :: {-# UNPACK #-} !OutPoint + , lcc_local_msat :: {-# UNPACK #-} !MilliSatoshi + , lcc_remote_msat :: {-# UNPACK #-} !MilliSatoshi + , lcc_local_script :: !Script + , lcc_remote_script :: !Script + , lcc_dust_limit :: {-# UNPACK #-} !DustLimit + -- ^ the signer's @dust_limit_satoshis@ + , lcc_fee :: {-# UNPACK #-} !Satoshi + , lcc_is_funder :: !Bool + -- ^ whether the signer funded the channel, and so pays the fee + , lcc_omit_local :: !Bool + -- ^ whether the signer eliminates its own output + } deriving (Eq, Show, Generic) + +instance NFData LegacyClosingContext + +-- | Build a legacy closing transaction (BOLT #3's "Legacy Closing +-- Transaction"), for @closing_signed@. -- --- * locktime: 0 --- * sequence: 0xFFFFFFFF --- * outputs: sorted per BIP69 -build_legacy_closing_tx :: ClosingContext -> ClosingTx -build_legacy_closing_tx ctx = - let !result = build_closing_tx ctx - { clc_locktime = Locktime 0 } - in result { cltx_input_sequence = Sequence 0xFFFFFFFF } -{-# INLINE build_legacy_closing_tx #-} - --- fee calculation ------------------------------------------------------------- - --- | Calculate the base commitment transaction fee. +-- Balances are rounded down to whole satoshis and the fee is taken +-- from the funder's output. Outputs below the signer's dust limit +-- are removed, as is the signer's own output if 'lcc_omit_local' is +-- set. Fails if the fee exceeds the funder's balance, or if no +-- output remains. +build_legacy_closing_tx :: LegacyClosingContext -> Maybe ClosingTx +build_legacy_closing_tx ctx = do + let !local_sat = BOLT1.msat_to_sat (lcc_local_msat ctx) + !remote_sat = BOLT1.msat_to_sat (lcc_remote_msat ctx) + DustLimit dust = lcc_dust_limit ctx + (l, r) <- + if lcc_is_funder ctx + then fmap (\x -> (x, remote_sat)) (BOLT1.sub_sat local_sat fee) + else fmap (\x -> (local_sat, x)) (BOLT1.sub_sat remote_sat fee) + outputs <- NE.nonEmpty $ + [ ClosingOutput l (lcc_local_script ctx) + | not (lcc_omit_local ctx), l >= dust ] + <> [ ClosingOutput r (lcc_remote_script ctx) | r >= dust ] + pure ClosingTx + { cltx_version = 2 + , cltx_locktime = Locktime 0 + , cltx_input_outpoint = lcc_funding_outpoint ctx + , cltx_input_sequence = Sequence 0xFFFFFFFF + , cltx_outputs = sort_closing outputs + } + where + !fee = lcc_fee ctx + +-- BIP69 order: value, then scriptPubKey. +sort_closing :: NonEmpty ClosingOutput -> NonEmpty ClosingOutput +sort_closing = NE.sortBy $ \a b -> + compare (clo_value a) (clo_value b) <> compare (clo_script a) (clo_script b) + +-- fees ----------------------------------------------------------------------- + +-- feerate * weight / 1000, saturating at 21 million BTC. +weight_fee :: FeeratePerKw -> Integer -> Satoshi +weight_fee (FeeratePerKw rate) weight = + let !f = toInteger rate * weight `quot` 1000 + in if f > toInteger (BOLT1.un_satoshi BOLT1.max_satoshi) + then BOLT1.max_satoshi + else sat (fromInteger f) + +-- | The base fee of a commitment transaction with a given number of +-- untrimmed HTLC outputs: -- --- @fee = feerate_per_kw * weight / 1000@ +-- @feerate_per_kw * (724 + 172 * num_htlcs) / 1000@ -- --- where @weight = base_weight + 172 * num_htlcs@ -commitment_fee :: FeeratePerKw -> ChannelFeatures -> Word64 -> Satoshi -commitment_fee feerate features numHtlcs = - let !weight = commitment_weight features numHtlcs - !fee = (fromIntegral (unFeeratePerKw feerate) * weight) `div` 1000 - in Satoshi fee -{-# INLINE commitment_fee #-} - --- | Calculate commitment transaction weight. +-- with a base weight of 1124 rather than 724 under 'Anchors'. -- --- @weight = base + 172 * num_htlcs@ -commitment_weight :: ChannelFeatures -> Word64 -> Word64 -commitment_weight features numHtlcs = - let !base = if has_anchors features - then commitment_weight_anchors - else commitment_weight_no_anchors - in base + htlc_output_weight * numHtlcs -{-# INLINE commitment_weight #-} - --- | Calculate HTLC-timeout transaction fee. +-- >>> commitment_fee (FeeratePerKw 5000) StaticRemotekey 2 +-- Satoshi 5340 +commitment_fee :: FeeratePerKw -> CommitmentFormat -> Word64 -> Satoshi +commitment_fee rate fmt n = weight_fee rate (base + 172 * toInteger n) where + base = case fmt of + StaticRemotekey -> 724 + Anchors -> 1124 + +-- | The fee of an HTLC-timeout transaction: @feerate_per_kw * 663 / +-- 1000@, or zero under 'Anchors'. -- --- With option_anchors, fee is 0 (CPFP). --- Otherwise, @fee = feerate_per_kw * 663 / 1000@ -htlc_timeout_fee :: FeeratePerKw -> ChannelFeatures -> Satoshi -htlc_timeout_fee feerate features - | has_anchors features = Satoshi 0 - | otherwise = - let !weight = htlc_timeout_weight_no_anchors - !fee = (fromIntegral (unFeeratePerKw feerate) * weight) `div` 1000 - in Satoshi fee -{-# INLINE htlc_timeout_fee #-} - --- | Calculate HTLC-success transaction fee. +-- >>> htlc_timeout_fee (FeeratePerKw 5000) StaticRemotekey +-- Satoshi 3315 +htlc_timeout_fee :: FeeratePerKw -> CommitmentFormat -> Satoshi +htlc_timeout_fee rate fmt = case fmt of + StaticRemotekey -> weight_fee rate 663 + Anchors -> sat 0 + +-- | The fee of an HTLC-success transaction: @feerate_per_kw * 703 / +-- 1000@, or zero under 'Anchors'. -- --- With option_anchors, fee is 0 (CPFP). --- Otherwise, @fee = feerate_per_kw * 703 / 1000@ -htlc_success_fee :: FeeratePerKw -> ChannelFeatures -> Satoshi -htlc_success_fee feerate features - | has_anchors features = Satoshi 0 - | otherwise = - let !weight = htlc_success_weight_no_anchors - !fee = (fromIntegral (unFeeratePerKw feerate) * weight) `div` 1000 - in Satoshi fee -{-# INLINE htlc_success_fee #-} - --- trimming -------------------------------------------------------------------- - --- | Calculate the trim threshold for an HTLC. +-- >>> htlc_success_fee (FeeratePerKw 5000) StaticRemotekey +-- Satoshi 3515 +htlc_success_fee :: FeeratePerKw -> CommitmentFormat -> Satoshi +htlc_success_fee rate fmt = case fmt of + StaticRemotekey -> weight_fee rate 703 + Anchors -> sat 0 + +-- trimming ------------------------------------------------------------------- + +-- | The amount below which an HTLC in a given direction is trimmed: +-- the dust limit plus the fee of its HTLC transaction. -- --- An HTLC is trimmed if: --- @amount < dust_limit + htlc_tx_fee@ +-- >>> let Just d = fmap DustLimit (BOLT1.satoshi 546) +-- >>> htlc_trim_threshold d (FeeratePerKw 5000) StaticRemotekey HTLCOffered +-- Satoshi 3861 htlc_trim_threshold :: DustLimit -> FeeratePerKw - -> ChannelFeatures + -> CommitmentFormat -> HTLCDirection -> Satoshi -htlc_trim_threshold dust feerate features direction = - let !dustVal = unDustLimit dust - !htlcFee = case direction of - HTLCOffered -> htlc_timeout_fee feerate features - HTLCReceived -> htlc_success_fee feerate features - in Satoshi (unSatoshi dustVal + unSatoshi htlcFee) -{-# INLINE htlc_trim_threshold #-} - --- | Check if an HTLC should be trimmed. --- --- An HTLC is trimmed if its amount minus the HTLC tx fee is below --- the dust limit. -is_trimmed :: DustLimit -> FeeratePerKw -> ChannelFeatures -> HTLC -> Bool -is_trimmed dust feerate features htlc = - let !threshold = htlc_trim_threshold dust feerate features - (htlc_direction htlc) - !amountSat = msatToSat (htlc_amount_msat htlc) - in unSatoshi amountSat < unSatoshi threshold -{-# INLINE is_trimmed #-} - --- | Filter HTLCs that are trimmed. -trimmed_htlcs - :: DustLimit - -> FeeratePerKw - -> ChannelFeatures - -> [HTLC] - -> [HTLC] -trimmed_htlcs dust feerate features = - filter (is_trimmed dust feerate features) -{-# INLINE trimmed_htlcs #-} - --- | Filter HTLCs that are not trimmed. -untrimmed_htlcs - :: DustLimit - -> FeeratePerKw - -> ChannelFeatures - -> [HTLC] - -> [HTLC] -untrimmed_htlcs dust feerate features = - filter (not . is_trimmed dust feerate features) -{-# INLINE untrimmed_htlcs #-} - --- conversion to ppad-tx ------------------------------------------------------- - --- | Convert a 'TxOutput' to a ppad-tx 'BT.TxOut'. -toTxOut :: TxOutput -> BT.TxOut -toTxOut o = BT.TxOut - { BT.txout_value = - unSatoshi (txout_value o) - , BT.txout_script_pubkey = - unScript (txout_script o) +htlc_trim_threshold (DustLimit dust) rate fmt dir = + let !fee = case dir of + HTLCOffered -> htlc_timeout_fee rate fmt + HTLCReceived -> htlc_success_fee rate fmt + in fromMaybe BOLT1.max_satoshi (BOLT1.add_sat dust fee) + +-- | Whether an HTLC is trimmed from a commitment transaction: whether +-- its amount, rounded down to whole satoshis, is below +-- 'htlc_trim_threshold'. +is_trimmed :: DustLimit -> FeeratePerKw -> CommitmentFormat -> HTLC -> Bool +is_trimmed dust rate fmt h = + BOLT1.msat_to_sat (htlc_amount_msat h) + < htlc_trim_threshold dust rate fmt (htlc_direction h) + +-- serialization -------------------------------------------------------------- + +unsigned_tx + :: Word32 -> Locktime -> OutPoint -> Sequence -> NonEmpty BT.TxOut + -> BT.Tx +unsigned_tx version (Locktime lt) prevout (Sequence sq) outs = BT.Tx + { BT.tx_version = version + , BT.tx_inputs = BT.TxIn prevout BS.empty sq :| [] + , BT.tx_outputs = outs + , BT.tx_witnesses = [] + , BT.tx_locktime = lt } -{-# INLINE toTxOut #-} --- | Convert a commitment transaction to a ppad-tx 'BT.Tx'. --- --- Returns 'Nothing' if the transaction has no outputs. -commitment_to_tx :: CommitmentTx -> Maybe BT.Tx -commitment_to_tx ctx = do - outs <- nonEmpty (map toTxOut (ctx_outputs ctx)) - let !input = BT.TxIn - { BT.txin_prevout = ctx_input_outpoint ctx - , BT.txin_script_sig = BS.empty - , BT.txin_sequence = - unSequence (ctx_input_sequence ctx) - } - pure $! BT.Tx - { BT.tx_version = ctx_version ctx - , BT.tx_inputs = input :| [] - , BT.tx_outputs = outs - , BT.tx_witnesses = [] - , BT.tx_locktime = unLocktime (ctx_locktime ctx) - } - --- | Convert an HTLC transaction to a ppad-tx 'BT.Tx'. +txout :: Satoshi -> Script -> BT.TxOut +txout v (Script s) = BT.TxOut (BOLT1.un_satoshi v) s +{-# INLINE txout #-} + +-- | The unsigned commitment transaction as a ppad-tx 'BT.Tx' (with no +-- witnesses), e.g. for computing its txid or sighashes. +commitment_to_tx :: CommitmentTx -> BT.Tx +commitment_to_tx c = unsigned_tx (ctx_version c) (ctx_locktime c) + (ctx_input_outpoint c) (ctx_input_sequence c) + (fmap (\o -> txout (co_value o) (co_script o)) (ctx_outputs c)) + +-- | The unsigned HTLC transaction as a ppad-tx 'BT.Tx'. htlc_to_tx :: HTLCTx -> BT.Tx -htlc_to_tx htx = - let !input = BT.TxIn - { BT.txin_prevout = htx_input_outpoint htx - , BT.txin_script_sig = BS.empty - , BT.txin_sequence = - unSequence (htx_input_sequence htx) - } - !output = BT.TxOut - { BT.txout_value = - unSatoshi (htx_output_value htx) - , BT.txout_script_pubkey = - unScript (htx_output_script htx) - } - in BT.Tx - { BT.tx_version = htx_version htx - , BT.tx_inputs = input :| [] - , BT.tx_outputs = output :| [] - , BT.tx_witnesses = [] - , BT.tx_locktime = unLocktime (htx_locktime htx) - } - --- | Convert a closing transaction to a ppad-tx 'BT.Tx'. --- --- Returns 'Nothing' if the transaction has no outputs. -closing_to_tx :: ClosingTx -> Maybe BT.Tx -closing_to_tx ctx = do - outs <- nonEmpty (map toTxOut (cltx_outputs ctx)) - let !input = BT.TxIn - { BT.txin_prevout = cltx_input_outpoint ctx - , BT.txin_script_sig = BS.empty - , BT.txin_sequence = - unSequence (cltx_input_sequence ctx) - } - pure $! BT.Tx - { BT.tx_version = cltx_version ctx - , BT.tx_inputs = input :| [] - , BT.tx_outputs = outs - , BT.tx_witnesses = [] - , BT.tx_locktime = unLocktime (cltx_locktime ctx) - } - --- output ordering ------------------------------------------------------------- - --- | Sort outputs per BOLT #3 ordering. --- --- Outputs are sorted by: --- 1. Value (smallest first) --- 2. ScriptPubKey (lexicographic) --- 3. CLTV expiry (for HTLCs) -sort_outputs :: [TxOutput] -> [TxOutput] -sort_outputs = sortBy compareOutputs -{-# INLINE sort_outputs #-} - --- | Compare two outputs for ordering. -compareOutputs :: TxOutput -> TxOutput -> Ordering -compareOutputs o1 o2 = - case compare (txout_value o1) (txout_value o2) of - EQ -> case compare (unScript $ txout_script o1) - (unScript $ txout_script o2) of - EQ -> compareCltvExpiry (txout_type o1) (txout_type o2) - other -> other - other -> other -{-# INLINE compareOutputs #-} - --- | Compare CLTV expiry for HTLC outputs. -compareCltvExpiry :: OutputType -> OutputType -> Ordering -compareCltvExpiry (OutputOfferedHTLC e1) (OutputOfferedHTLC e2) = compare e1 e2 -compareCltvExpiry (OutputReceivedHTLC e1) (OutputReceivedHTLC e2) = compare e1 e2 -compareCltvExpiry (OutputOfferedHTLC e1) (OutputReceivedHTLC e2) = compare e1 e2 -compareCltvExpiry (OutputReceivedHTLC e1) (OutputOfferedHTLC e2) = compare e1 e2 -compareCltvExpiry _ _ = EQ -{-# INLINE compareCltvExpiry #-} +htlc_to_tx h = unsigned_tx (htx_version h) (htx_locktime h) + (htx_input_outpoint h) (htx_input_sequence h) + (txout (htx_output_value h) (htx_output_script h) :| []) + +-- | The unsigned closing transaction as a ppad-tx 'BT.Tx'. +closing_to_tx :: ClosingTx -> BT.Tx +closing_to_tx c = unsigned_tx (cltx_version c) (cltx_locktime c) + (cltx_input_outpoint c) (cltx_input_sequence c) + (fmap (\o -> txout (clo_value o) (clo_script o)) (cltx_outputs c)) + +-- | Serialize an unsigned commitment transaction, without witness +-- data. This is the form whose double-SHA256 is the txid; it is not +-- what segwit signatures commit to (see ppad-tx's +-- @Bitcoin.Prim.Tx.Sighash@). To broadcast, attach the witness to +-- 'commitment_to_tx' and serialize with ppad-tx. +encode_commitment_tx :: CommitmentTx -> BS.ByteString +encode_commitment_tx = BT.to_bytes_legacy . commitment_to_tx + +-- | Serialize an unsigned HTLC transaction, without witness data (cf. +-- 'encode_commitment_tx'). +encode_htlc_tx :: HTLCTx -> BS.ByteString +encode_htlc_tx = BT.to_bytes_legacy . htlc_to_tx + +-- | Serialize an unsigned closing transaction, without witness data +-- (cf. 'encode_commitment_tx'). +encode_closing_tx :: ClosingTx -> BS.ByteString +encode_closing_tx = BT.to_bytes_legacy . closing_to_tx diff --git a/lib/Lightning/Protocol/BOLT3/Types.hs b/lib/Lightning/Protocol/BOLT3/Types.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_HADDOCK prune #-} +{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} @@ -8,366 +8,404 @@ -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech> -- --- Core types for BOLT #3 transaction and script formats. +-- Types for BOLT #3 transaction and script formats. module Lightning.Protocol.BOLT3.Types ( - -- * Monetary amounts (re-exported from BOLT1) - Satoshi(..) - , MilliSatoshi(..) - , msatToSat - , satToMsat - - -- * Keys and points - , Pubkey(..) - , pubkey + -- * Points and keys + PerCommitmentPoint(..) + , RevocationBasepoint(..) + , PaymentBasepoint(..) + , DelayedPaymentBasepoint(..) + , HtlcBasepoint(..) + , Basepoints(..) + , RevocationPubkey(..) + , LocalDelayedPubkey(..) + , LocalHtlcPubkey(..) + , RemoteHtlcPubkey(..) + , RemotePubkey(..) + , FundingPubkey(..) , Seckey(..) , seckey - , Point(..) - , point - , unPoint - - -- * Hashes (re-exported from BOLT1) - , PaymentHash(..) - , paymentHash - , unPaymentHash - , PaymentPreimage(..) - , paymentPreimage - , unPaymentPreimage - - -- * Transaction primitives - , TxId(..) - , mkTxId - , OutPoint(..) - , Sequence(..) - , Locktime(..) + , un_seckey + + -- * Per-commitment secrets + , Seed(..) + , seed + , un_seed + , SecretIndex(..) + , secret_index + , un_secret_index + , commitment_secret_index -- * Channel parameters , CommitmentNumber(..) , commitment_number + , un_commitment_number , next_commitment_number , ToSelfDelay(..) , CltvExpiry(..) , DustLimit(..) , FeeratePerKw(..) + , Locktime(..) + , Sequence(..) + , CommitmentFormat(..) - -- * HTLC types + -- * HTLCs , HTLC(..) , HTLCDirection(..) - -- * Basepoints - , Basepoints(..) - , PerCommitmentPoint(..) - , PerCommitmentSecret(..) - , perCommitmentSecret - , unPerCommitmentSecret - , RevocationBasepoint(..) - , PaymentBasepoint(..) - , DelayedPaymentBasepoint(..) - , HtlcBasepoint(..) - - -- * Derived keys - , LocalPubkey(..) - , RemotePubkey(..) - , LocalDelayedPubkey(..) - , RemoteDelayedPubkey(..) - , LocalHtlcPubkey(..) - , RemoteHtlcPubkey(..) - , RevocationPubkey(..) - , FundingPubkey(..) - - -- * Script + -- * Scripts , Script(..) - -- * Witness (re-exported from ppad-tx) - , Witness(..) - - -- * Channel options - , ChannelFeatures(..) - , has_anchors - - -- * Transaction weights (constants) - , commitment_weight_no_anchors - , commitment_weight_anchors - , htlc_timeout_weight_no_anchors - , htlc_timeout_weight_anchors - , htlc_success_weight_no_anchors - , htlc_success_weight_anchors - , htlc_output_weight - - -- * Dust thresholds (constants) + -- * Dust thresholds , dust_p2pkh , dust_p2sh , dust_p2wpkh , dust_p2wsh , anchor_output_value + + -- * Internal + , sat + , internal_error ) where -import Bitcoin.Prim.Tx (TxId(..), mkTxId, OutPoint(..), Witness(..)) -import Data.Word (Word16, Word32, Word64) +import Control.DeepSeq (NFData(..)) +import qualified Crypto.Curve.Secp256k1 as S import qualified Data.ByteString as BS +import Data.Word (Word16, Word32, Word64) import GHC.Generics (Generic) -import Lightning.Protocol.BOLT1.Prim - ( Satoshi(..), MilliSatoshi(..) - , satToMsat, msatToSat - , Point(..), point, unPoint - , PaymentHash(..), paymentHash, unPaymentHash - , PaymentPreimage(..), paymentPreimage, unPaymentPreimage - , PerCommitmentSecret(..), perCommitmentSecret - , unPerCommitmentSecret - ) - --- keys and points ------------------------------------------------------------- - --- | Compressed public key (33 bytes). -newtype Pubkey = Pubkey { unPubkey :: BS.ByteString } - deriving (Eq, Ord, Show, Generic) +import Lightning.Protocol.BOLT1 (Point, Satoshi, MilliSatoshi, PaymentHash) +import qualified Lightning.Protocol.BOLT1 as BOLT1 --- | Parse a 33-byte compressed public key. --- --- Returns Nothing if the input is not exactly 33 bytes. --- --- >>> pubkey (BS.replicate 33 0x02) --- Just (Pubkey ...) --- >>> pubkey (BS.replicate 32 0x02) --- Nothing -pubkey :: BS.ByteString -> Maybe Pubkey -pubkey bs - | BS.length bs == 33 = Just (Pubkey bs) - | otherwise = Nothing -{-# INLINE pubkey #-} - --- | Secret key (32 bytes). -newtype Seckey = Seckey { unSeckey :: BS.ByteString } - deriving (Eq, Generic) - --- Don't show secret keys -instance Show Seckey where - show _ = "Seckey <redacted>" +-- internal ------------------------------------------------------------------ --- | Parse a 32-byte secret key. --- --- Returns Nothing if the input is not exactly 32 bytes. -seckey :: BS.ByteString -> Maybe Seckey -seckey bs - | BS.length bs == 32 = Just (Seckey bs) - | otherwise = Nothing -{-# INLINE seckey #-} +-- Abort on a state that the surrounding code rules out. +internal_error :: a +internal_error = error "ppad-bolt3: internal error, please report a bug!" +{-# NOINLINE internal_error #-} --- transaction primitives ------------------------------------------------------ +-- A 'Satoshi' amount known to be at most 21 million BTC (a constant, +-- or a value bounded by construction). +sat :: Word64 -> Satoshi +sat w = case BOLT1.satoshi w of + Just s -> s + Nothing -> internal_error +{-# INLINE sat #-} --- | Transaction input sequence number. -newtype Sequence = Sequence { unSequence :: Word32 } - deriving (Eq, Ord, Show, Generic) +-- points and keys ------------------------------------------------------------ --- | Transaction locktime. -newtype Locktime = Locktime { unLocktime :: Word32 } +-- | A per-commitment point, from which a commitment's keys are +-- derived. +newtype PerCommitmentPoint = PerCommitmentPoint Point deriving (Eq, Ord, Show, Generic) --- channel parameters ---------------------------------------------------------- +instance NFData PerCommitmentPoint --- | 48-bit commitment number. -newtype CommitmentNumber = CommitmentNumber - { unCommitmentNumber :: Word64 } +-- | A @revocation_basepoint@. +newtype RevocationBasepoint = RevocationBasepoint Point deriving (Eq, Ord, Show, Generic) --- | Parse a 48-bit commitment number. --- --- Returns Nothing if the value exceeds 2^48 - 1. -commitment_number :: Word64 -> Maybe CommitmentNumber -commitment_number n - | n <= 281474976710655 = Just (CommitmentNumber n) - | otherwise = Nothing -{-# INLINE commitment_number #-} +instance NFData RevocationBasepoint --- | Increment a commitment number by one. --- --- Returns Nothing if the result would exceed 2^48 - 1. --- --- >>> fmap next_commitment_number (commitment_number 0) --- Just (Just (CommitmentNumber {unCommitmentNumber = 1})) --- >>> fmap next_commitment_number (commitment_number 281474976710655) --- Just Nothing -next_commitment_number - :: CommitmentNumber -> Maybe CommitmentNumber -next_commitment_number (CommitmentNumber n) - | n < 281474976710655 = Just (CommitmentNumber (n + 1)) - | otherwise = Nothing -{-# INLINE next_commitment_number #-} - --- | CSV delay for to_local outputs. -newtype ToSelfDelay = ToSelfDelay { unToSelfDelay :: Word16 } +-- | A @payment_basepoint@. +newtype PaymentBasepoint = PaymentBasepoint Point deriving (Eq, Ord, Show, Generic) --- | CLTV expiry for HTLCs. -newtype CltvExpiry = CltvExpiry { unCltvExpiry :: Word32 } - deriving (Eq, Ord, Show, Generic) - --- | Dust limit threshold. -newtype DustLimit = DustLimit { unDustLimit :: Satoshi } - deriving (Eq, Ord, Show, Generic) +instance NFData PaymentBasepoint --- | Fee rate in satoshis per 1000 weight units. -newtype FeeratePerKw = FeeratePerKw { unFeeratePerKw :: Word32 } +-- | A @delayed_payment_basepoint@. +newtype DelayedPaymentBasepoint = DelayedPaymentBasepoint Point deriving (Eq, Ord, Show, Generic) --- HTLC types ------------------------------------------------------------------ +instance NFData DelayedPaymentBasepoint --- | Direction of an HTLC from the commitment tx owner's perspective. -data HTLCDirection - = HTLCOffered -- ^ We offered this HTLC (outgoing) - | HTLCReceived -- ^ We received this HTLC (incoming) +-- | An @htlc_basepoint@. +newtype HtlcBasepoint = HtlcBasepoint Point deriving (Eq, Ord, Show, Generic) --- | HTLC output details. -data HTLC = HTLC - { htlc_direction :: !HTLCDirection - , htlc_amount_msat :: {-# UNPACK #-} !MilliSatoshi - , htlc_payment_hash :: {-# UNPACK #-} !PaymentHash - , htlc_cltv_expiry :: {-# UNPACK #-} !CltvExpiry +instance NFData HtlcBasepoint + +-- | The four basepoints a party announces in @open_channel@ or +-- @accept_channel@. +data Basepoints = Basepoints + { bp_revocation :: !RevocationBasepoint + , bp_payment :: !PaymentBasepoint + , bp_delayed_payment :: !DelayedPaymentBasepoint + , bp_htlc :: !HtlcBasepoint } deriving (Eq, Show, Generic) --- basepoints ------------------------------------------------------------------ +instance NFData Basepoints --- | Per-commitment point (used to derive keys). -newtype PerCommitmentPoint = PerCommitmentPoint - { unPerCommitmentPoint :: Point } +-- | A commitment's @revocationpubkey@. +newtype RevocationPubkey = RevocationPubkey Point deriving (Eq, Ord, Show, Generic) --- | Revocation basepoint. -newtype RevocationBasepoint = RevocationBasepoint - { unRevocationBasepoint :: Point } - deriving (Eq, Ord, Show, Generic) +instance NFData RevocationPubkey --- | Payment basepoint. -newtype PaymentBasepoint = PaymentBasepoint - { unPaymentBasepoint :: Point } +-- | A commitment's @local_delayedpubkey@: the owner's key for its +-- delayed outputs. +newtype LocalDelayedPubkey = LocalDelayedPubkey Point deriving (Eq, Ord, Show, Generic) --- | Delayed payment basepoint. -newtype DelayedPaymentBasepoint = DelayedPaymentBasepoint - { unDelayedPaymentBasepoint :: Point } - deriving (Eq, Ord, Show, Generic) +instance NFData LocalDelayedPubkey --- | HTLC basepoint. -newtype HtlcBasepoint = HtlcBasepoint - { unHtlcBasepoint :: Point } +-- | A commitment's @local_htlcpubkey@: the owner's HTLC key. +newtype LocalHtlcPubkey = LocalHtlcPubkey Point deriving (Eq, Ord, Show, Generic) --- | Collection of all basepoints for one party. -data Basepoints = Basepoints - { bp_revocation :: !RevocationBasepoint - , bp_payment :: !PaymentBasepoint - , bp_delayed_payment :: !DelayedPaymentBasepoint - , bp_htlc :: !HtlcBasepoint - } deriving (Eq, Show, Generic) +instance NFData LocalHtlcPubkey + +-- | A commitment's @remote_htlcpubkey@: the other party's HTLC key. +newtype RemoteHtlcPubkey = RemoteHtlcPubkey Point + deriving (Eq, Ord, Show, Generic) --- derived keys ---------------------------------------------------------------- +instance NFData RemoteHtlcPubkey --- | Local pubkey. -newtype LocalPubkey = LocalPubkey { unLocalPubkey :: Pubkey } +-- | A commitment's @remotepubkey@: the other party's +-- @payment_basepoint@, which its @to_remote@ output pays. +newtype RemotePubkey = RemotePubkey Point deriving (Eq, Ord, Show, Generic) --- | Remote pubkey. -newtype RemotePubkey = RemotePubkey - { unRemotePubkey :: Pubkey } +instance NFData RemotePubkey + +-- | A @funding_pubkey@. +newtype FundingPubkey = FundingPubkey Point deriving (Eq, Ord, Show, Generic) --- | Local delayed pubkey. -newtype LocalDelayedPubkey = LocalDelayedPubkey - { unLocalDelayedPubkey :: Pubkey } +instance NFData FundingPubkey + +-- | A secp256k1 secret key: 32 bytes encoding an integer in +-- [1, n), for n the group order. +-- +-- This is secret material: its 'Show' instance is redacted, and it +-- has no 'Eq' instance. +newtype Seckey = Seckey BS.ByteString + +instance Show Seckey where + showsPrec d _ = showParen (d > 10) $ showString "Seckey <redacted>" + +instance NFData Seckey where + rnf (Seckey bs) = rnf bs + +-- | Construct a 'Seckey' from 32 big-endian bytes. Fails unless the +-- bytes encode an integer in [1, n). +-- +-- >>> fmap (BS.length . un_seckey) (seckey (BS.replicate 32 0x01)) +-- Just 32 +-- >>> seckey (BS.replicate 32 0x00) +-- Nothing +-- >>> seckey (BS.replicate 32 0xff) +-- Nothing +seckey :: BS.ByteString -> Maybe Seckey +seckey bs = do + w <- S.parse_int256 bs + if S.ge w + then Just (Seckey bs) + else Nothing + +-- | The bytes of a 'Seckey'. +un_seckey :: Seckey -> BS.ByteString +un_seckey (Seckey bs) = bs +{-# INLINE un_seckey #-} + +-- per-commitment secrets ----------------------------------------------------- + +-- | The 32-byte seed from which a party generates its per-commitment +-- secrets. +-- +-- This is secret material: its 'Show' instance is redacted, and it +-- has no 'Eq' instance. +newtype Seed = Seed BS.ByteString + +instance Show Seed where + showsPrec d _ = showParen (d > 10) $ showString "Seed <redacted>" + +instance NFData Seed where + rnf (Seed bs) = rnf bs + +-- | Construct a 'Seed' from exactly 32 bytes. +-- +-- >>> fmap (BS.length . un_seed) (seed (BS.replicate 32 0xff)) +-- Just 32 +-- >>> seed (BS.replicate 31 0xff) +-- Nothing +seed :: BS.ByteString -> Maybe Seed +seed bs + | BS.length bs == 32 = Just (Seed bs) + | otherwise = Nothing +{-# INLINE seed #-} + +-- | The bytes of a 'Seed'. +un_seed :: Seed -> BS.ByteString +un_seed (Seed bs) = bs +{-# INLINE un_seed #-} + +-- | The 48-bit index /I/ of a per-commitment secret. Secrets are +-- used from index 2^48 - 1 downwards. +newtype SecretIndex = SecretIndex Word64 deriving (Eq, Ord, Show, Generic) --- | Remote delayed pubkey. -newtype RemoteDelayedPubkey = RemoteDelayedPubkey - { unRemoteDelayedPubkey :: Pubkey } +instance NFData SecretIndex + +-- | Construct a 'SecretIndex'. Fails at or above 2^48. +-- +-- >>> secret_index 281474976710655 +-- Just (SecretIndex 281474976710655) +-- >>> secret_index 281474976710656 +-- Nothing +secret_index :: Word64 -> Maybe SecretIndex +secret_index w + | w <= 0xFFFFFFFFFFFF = Just (SecretIndex w) + | otherwise = Nothing +{-# INLINE secret_index #-} + +-- | The value of a 'SecretIndex'. +un_secret_index :: SecretIndex -> Word64 +un_secret_index (SecretIndex w) = w +{-# INLINE un_secret_index #-} + +-- | The index of the per-commitment secret for a commitment number: +-- 2^48 - 1 - n. +-- +-- >>> fmap commitment_secret_index (commitment_number 0) +-- Just (SecretIndex 281474976710655) +commitment_secret_index :: CommitmentNumber -> SecretIndex +commitment_secret_index (CommitmentNumber n) = SecretIndex (0xFFFFFFFFFFFF - n) +{-# INLINE commitment_secret_index #-} + +-- channel parameters --------------------------------------------------------- + +-- | A 48-bit commitment number. +newtype CommitmentNumber = CommitmentNumber Word64 deriving (Eq, Ord, Show, Generic) --- | Local HTLC pubkey. -newtype LocalHtlcPubkey = LocalHtlcPubkey - { unLocalHtlcPubkey :: Pubkey } +instance NFData CommitmentNumber + +-- | Construct a 'CommitmentNumber'. Fails at or above 2^48. +-- +-- >>> commitment_number 42 +-- Just (CommitmentNumber 42) +-- >>> commitment_number 281474976710656 +-- Nothing +commitment_number :: Word64 -> Maybe CommitmentNumber +commitment_number n + | n <= 0xFFFFFFFFFFFF = Just (CommitmentNumber n) + | otherwise = Nothing +{-# INLINE commitment_number #-} + +-- | The value of a 'CommitmentNumber'. +un_commitment_number :: CommitmentNumber -> Word64 +un_commitment_number (CommitmentNumber n) = n +{-# INLINE un_commitment_number #-} + +-- | The next commitment number. Fails past 2^48 - 1. +-- +-- >>> commitment_number 0 >>= next_commitment_number +-- Just (CommitmentNumber 1) +-- >>> commitment_number 281474976710655 >>= next_commitment_number +-- Nothing +next_commitment_number :: CommitmentNumber -> Maybe CommitmentNumber +next_commitment_number (CommitmentNumber n) + | n < 0xFFFFFFFFFFFF = Just (CommitmentNumber (n + 1)) + | otherwise = Nothing +{-# INLINE next_commitment_number #-} + +-- | The CSV delay (@to_self_delay@) on a commitment owner's outputs. +newtype ToSelfDelay = ToSelfDelay Word16 deriving (Eq, Ord, Show, Generic) --- | Remote HTLC pubkey. -newtype RemoteHtlcPubkey = RemoteHtlcPubkey - { unRemoteHtlcPubkey :: Pubkey } +instance NFData ToSelfDelay + +-- | An HTLC's absolute CLTV expiry. +newtype CltvExpiry = CltvExpiry Word32 deriving (Eq, Ord, Show, Generic) --- | Revocation pubkey. -newtype RevocationPubkey = RevocationPubkey - { unRevocationPubkey :: Pubkey } +instance NFData CltvExpiry + +-- | A @dust_limit_satoshis@ threshold. +newtype DustLimit = DustLimit Satoshi deriving (Eq, Ord, Show, Generic) --- | Funding pubkey (used in 2-of-2 multisig). -newtype FundingPubkey = FundingPubkey - { unFundingPubkey :: Pubkey } +instance NFData DustLimit + +-- | A fee rate, in satoshis per 1000 weight units. +newtype FeeratePerKw = FeeratePerKw Word32 deriving (Eq, Ord, Show, Generic) --- script ---------------------------------------------------------------------- +instance NFData FeeratePerKw --- | Bitcoin script (serialized). -newtype Script = Script { unScript :: BS.ByteString } +-- | A transaction locktime. +newtype Locktime = Locktime Word32 deriving (Eq, Ord, Show, Generic) --- channel options ------------------------------------------------------------- +instance NFData Locktime --- | Channel feature flags relevant to BOLT #3. -data ChannelFeatures = ChannelFeatures - { cf_option_anchors :: !Bool - } deriving (Eq, Show, Generic) +-- | A transaction input's sequence number. +newtype Sequence = Sequence Word32 + deriving (Eq, Ord, Show, Generic) --- | Check if option_anchors is enabled. -has_anchors :: ChannelFeatures -> Bool -has_anchors = cf_option_anchors -{-# INLINE has_anchors #-} +instance NFData Sequence + +-- | The commitment format a channel uses, as fixed by its +-- @channel_type@. +data CommitmentFormat + = StaticRemotekey + -- ^ @option_static_remotekey@ without anchors: @to_remote@ is + -- P2WPKH and HTLC transactions pay their own fees. + | Anchors + -- ^ @option_anchors@: two anchor outputs, a CSV-locked + -- @to_remote@, and zero-fee HTLC transactions. + deriving (Eq, Ord, Show, Generic) --- transaction weights (constants from spec) ----------------------------------- +instance NFData CommitmentFormat --- | Base commitment tx weight without option_anchors. -commitment_weight_no_anchors :: Word64 -commitment_weight_no_anchors = 724 +-- HTLCs ---------------------------------------------------------------------- --- | Base commitment tx weight with option_anchors. -commitment_weight_anchors :: Word64 -commitment_weight_anchors = 1124 +-- | The direction of an HTLC, from the commitment owner's point of +-- view. +data HTLCDirection + = HTLCOffered -- ^ offered by the owner + | HTLCReceived -- ^ received by the owner + deriving (Eq, Ord, Show, Generic) --- | HTLC-timeout tx weight without option_anchors. -htlc_timeout_weight_no_anchors :: Word64 -htlc_timeout_weight_no_anchors = 663 +instance NFData HTLCDirection --- | HTLC-timeout tx weight with option_anchors. -htlc_timeout_weight_anchors :: Word64 -htlc_timeout_weight_anchors = 666 +-- | An HTLC committed to a commitment transaction. +data HTLC = HTLC + { htlc_direction :: !HTLCDirection + , htlc_amount_msat :: {-# UNPACK #-} !MilliSatoshi + , htlc_payment_hash :: !PaymentHash + , htlc_cltv_expiry :: {-# UNPACK #-} !CltvExpiry + } deriving (Eq, Show, Generic) + +instance NFData HTLC --- | HTLC-success tx weight without option_anchors. -htlc_success_weight_no_anchors :: Word64 -htlc_success_weight_no_anchors = 703 +-- scripts -------------------------------------------------------------------- --- | HTLC-success tx weight with option_anchors. -htlc_success_weight_anchors :: Word64 -htlc_success_weight_anchors = 706 +-- | A serialized Bitcoin script. +newtype Script = Script BS.ByteString + deriving (Eq, Ord, Show, Generic) --- | Weight added per HTLC output in commitment tx. -htlc_output_weight :: Word64 -htlc_output_weight = 172 +instance NFData Script --- dust thresholds (constants from Bitcoin Core) ------------------------------- +-- dust thresholds ------------------------------------------------------------ --- | P2PKH dust threshold (546 satoshis). +-- | Bitcoin Core's P2PKH dust threshold (546 satoshis). dust_p2pkh :: Satoshi -dust_p2pkh = Satoshi 546 +dust_p2pkh = sat 546 --- | P2SH dust threshold (540 satoshis). +-- | Bitcoin Core's P2SH dust threshold (540 satoshis). dust_p2sh :: Satoshi -dust_p2sh = Satoshi 540 +dust_p2sh = sat 540 --- | P2WPKH dust threshold (294 satoshis). +-- | Bitcoin Core's P2WPKH dust threshold (294 satoshis). dust_p2wpkh :: Satoshi -dust_p2wpkh = Satoshi 294 +dust_p2wpkh = sat 294 --- | P2WSH dust threshold (330 satoshis). +-- | Bitcoin Core's P2WSH dust threshold (330 satoshis). dust_p2wsh :: Satoshi -dust_p2wsh = Satoshi 330 +dust_p2wsh = sat 330 --- | Fixed anchor output value (330 satoshis). +-- | The value of each anchor output (330 satoshis). anchor_output_value :: Satoshi -anchor_output_value = Satoshi 330 +anchor_output_value = sat 330 diff --git a/lib/Lightning/Protocol/BOLT3/Validate.hs b/lib/Lightning/Protocol/BOLT3/Validate.hs @@ -1,359 +0,0 @@ -{-# OPTIONS_HADDOCK prune #-} -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveGeneric #-} - --- | --- Module: Lightning.Protocol.BOLT3.Validate --- Copyright: (c) 2025 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- Stateless validation for BOLT #3 transactions. --- --- Provides validation for: --- --- * Commitment transaction structure and outputs --- * HTLC transaction structure --- * Closing transaction structure --- * Output ordering per BIP69+CLTV --- * Dust limit compliance - -module Lightning.Protocol.BOLT3.Validate ( - -- * Validation errors - ValidationError(..) - - -- * Commitment transaction validation - , validate_commitment_tx - , validate_commitment_locktime - , validate_commitment_sequence - - -- * HTLC transaction validation - , validate_htlc_tx - , validate_htlc_timeout_tx - , validate_htlc_success_tx - - -- * Closing transaction validation - , validate_closing_tx - , validate_legacy_closing_tx - - -- * Output validation - , validate_output_ordering - , validate_dust_limits - , validate_anchor_outputs - - -- * Fee validation - , validate_commitment_fee - , validate_htlc_fee - ) where - -import Data.Bits ((.&.), shiftR) -import Data.Word (Word32, Word64) -import GHC.Generics (Generic) -import Lightning.Protocol.BOLT3.Types -import Lightning.Protocol.BOLT3.Tx - --- validation errors ----------------------------------------------------------- - --- | Errors that can occur during validation. -data ValidationError - = InvalidVersion {-# UNPACK #-} !Word32 {-# UNPACK #-} !Word32 - -- ^ Expected version, actual version - | InvalidLocktime {-# UNPACK #-} !Word32 - -- ^ Invalid locktime format - | InvalidSequence {-# UNPACK #-} !Word32 - -- ^ Invalid sequence format - | InvalidOutputOrdering - -- ^ Outputs not in BIP69+CLTV order - | DustLimitViolation {-# UNPACK #-} !Int !Satoshi !Satoshi - -- ^ Output index, actual value, dust limit - | MissingAnchorOutput - -- ^ Expected anchor output not present - | InvalidAnchorValue {-# UNPACK #-} !Satoshi - -- ^ Anchor value not 330 satoshis - | InvalidFee {-# UNPACK #-} !Satoshi {-# UNPACK #-} !Satoshi - -- ^ Expected fee, actual fee - | InvalidHTLCLocktime {-# UNPACK #-} !Word32 {-# UNPACK #-} !Word32 - -- ^ Expected locktime, actual locktime - | InvalidHTLCSequence {-# UNPACK #-} !Word32 {-# UNPACK #-} !Word32 - -- ^ Expected sequence, actual sequence - | NoOutputs - -- ^ Transaction has no outputs - | TooManyOutputs {-# UNPACK #-} !Int - -- ^ More outputs than expected - deriving (Eq, Show, Generic) - --- commitment transaction validation ------------------------------------------- - --- | Validate a commitment transaction. --- --- Checks: --- --- * Version is 2 --- * Locktime format (upper 8 bits = 0x20) --- * Sequence format (upper 8 bits = 0x80) --- * Output ordering per BIP69+CLTV --- * Dust limit compliance --- * Anchor outputs if option_anchors -validate_commitment_tx - :: DustLimit - -> ChannelFeatures - -> CommitmentTx - -> Either ValidationError () -validate_commitment_tx dust features tx = do - -- Version must be 2 - validateVersion 2 (ctx_version tx) - -- Locktime format - validate_commitment_locktime (ctx_locktime tx) - -- Sequence format - validate_commitment_sequence (ctx_input_sequence tx) - -- Output ordering - validate_output_ordering (ctx_outputs tx) - -- Dust limits - validate_dust_limits dust (ctx_outputs tx) - -- Anchors if applicable - if has_anchors features - then validate_anchor_outputs (ctx_outputs tx) - else pure () -{-# INLINE validate_commitment_tx #-} - --- | Validate commitment transaction locktime format. --- --- Upper 8 bits must be 0x20. -validate_commitment_locktime :: Locktime -> Either ValidationError () -validate_commitment_locktime (Locktime lt) = - let !upper = (lt `shiftR` 24) .&. 0xFF - in if upper == 0x20 - then Right () - else Left (InvalidLocktime lt) -{-# INLINE validate_commitment_locktime #-} - --- | Validate commitment transaction sequence format. --- --- Upper 8 bits must be 0x80. -validate_commitment_sequence :: Sequence -> Either ValidationError () -validate_commitment_sequence (Sequence sq) = - let !upper = (sq `shiftR` 24) .&. 0xFF - in if upper == 0x80 - then Right () - else Left (InvalidSequence sq) -{-# INLINE validate_commitment_sequence #-} - --- HTLC transaction validation ------------------------------------------------- - --- | Validate an HTLC transaction (timeout or success). --- --- Checks: --- --- * Version is 2 --- * Single output -validate_htlc_tx :: HTLCTx -> Either ValidationError () -validate_htlc_tx tx = do - validateVersion 2 (htx_version tx) - pure () -{-# INLINE validate_htlc_tx #-} - --- | Validate an HTLC-timeout transaction. --- --- Checks: --- --- * Base HTLC validation --- * Locktime equals HTLC cltv_expiry --- * Sequence is 0 (or 1 with option_anchors) -validate_htlc_timeout_tx - :: ChannelFeatures - -> CltvExpiry - -> HTLCTx - -> Either ValidationError () -validate_htlc_timeout_tx features expiry tx = do - validate_htlc_tx tx - -- Locktime must be cltv_expiry - let !expectedLt = unCltvExpiry expiry - !actualLt = unLocktime (htx_locktime tx) - if expectedLt == actualLt - then pure () - else Left (InvalidHTLCLocktime expectedLt actualLt) - -- Sequence - let !expectedSeq = if has_anchors features then 1 else 0 - !actualSeq = unSequence (htx_input_sequence tx) - if expectedSeq == actualSeq - then pure () - else Left (InvalidHTLCSequence expectedSeq actualSeq) -{-# INLINE validate_htlc_timeout_tx #-} - --- | Validate an HTLC-success transaction. --- --- Checks: --- --- * Base HTLC validation --- * Locktime is 0 --- * Sequence is 0 (or 1 with option_anchors) -validate_htlc_success_tx - :: ChannelFeatures - -> HTLCTx - -> Either ValidationError () -validate_htlc_success_tx features tx = do - validate_htlc_tx tx - -- Locktime must be 0 - let !actualLt = unLocktime (htx_locktime tx) - if actualLt == 0 - then pure () - else Left (InvalidHTLCLocktime 0 actualLt) - -- Sequence - let !expectedSeq = if has_anchors features then 1 else 0 - !actualSeq = unSequence (htx_input_sequence tx) - if expectedSeq == actualSeq - then pure () - else Left (InvalidHTLCSequence expectedSeq actualSeq) -{-# INLINE validate_htlc_success_tx #-} - --- closing transaction validation ---------------------------------------------- - --- | Validate a closing transaction (option_simple_close). --- --- Checks: --- --- * Version is 2 --- * Sequence is 0xFFFFFFFD --- * At least one output --- * Output ordering per BIP69 -validate_closing_tx :: ClosingTx -> Either ValidationError () -validate_closing_tx tx = do - validateVersion 2 (cltx_version tx) - let !actualSeq = unSequence (cltx_input_sequence tx) - if actualSeq == 0xFFFFFFFD - then pure () - else Left (InvalidSequence actualSeq) - validateOutputCount (cltx_outputs tx) - validate_output_ordering (cltx_outputs tx) -{-# INLINE validate_closing_tx #-} - --- | Validate a legacy closing transaction (closing_signed). --- --- Checks: --- --- * Version is 2 --- * Locktime is 0 --- * Sequence is 0xFFFFFFFF --- * At least one output --- * Output ordering per BIP69 -validate_legacy_closing_tx :: ClosingTx -> Either ValidationError () -validate_legacy_closing_tx tx = do - validateVersion 2 (cltx_version tx) - let !actualLt = unLocktime (cltx_locktime tx) - if actualLt == 0 - then pure () - else Left (InvalidLocktime actualLt) - let !actualSeq = unSequence (cltx_input_sequence tx) - if actualSeq == 0xFFFFFFFF - then pure () - else Left (InvalidSequence actualSeq) - validateOutputCount (cltx_outputs tx) - validate_output_ordering (cltx_outputs tx) -{-# INLINE validate_legacy_closing_tx #-} - --- output validation ----------------------------------------------------------- - --- | Validate output ordering per BIP69+CLTV. --- --- Outputs must be sorted by: --- 1. Value (smallest first) --- 2. ScriptPubKey (lexicographic) --- 3. CLTV expiry (for HTLC outputs) -validate_output_ordering :: [TxOutput] -> Either ValidationError () -validate_output_ordering outputs = - let !sorted = sort_outputs outputs - in if outputs == sorted - then Right () - else Left InvalidOutputOrdering -{-# INLINE validate_output_ordering #-} - --- | Validate that no output violates dust limits. -validate_dust_limits - :: DustLimit - -> [TxOutput] - -> Either ValidationError () -validate_dust_limits dust = go 0 where - !limit = unDustLimit dust - go !_ [] = Right () - go !idx (out:rest) = - let !val = txout_value out - in case txout_type out of - -- Anchors have fixed value, don't check against dust limit - OutputLocalAnchor -> go (idx + 1) rest - OutputRemoteAnchor -> go (idx + 1) rest - -- All other outputs must be above dust - _ -> if unSatoshi val >= unSatoshi limit - then go (idx + 1) rest - else Left (DustLimitViolation idx val limit) -{-# INLINE validate_dust_limits #-} - --- | Validate anchor outputs are present and correctly valued. -validate_anchor_outputs :: [TxOutput] -> Either ValidationError () -validate_anchor_outputs outputs = - let !anchors = filter isAnchor outputs - in if null anchors - then Left MissingAnchorOutput - else validateAnchorValues anchors - where - isAnchor out = case txout_type out of - OutputLocalAnchor -> True - OutputRemoteAnchor -> True - _ -> False - - validateAnchorValues [] = Right () - validateAnchorValues (a:as) = - let !val = txout_value a - in if val == anchor_output_value - then validateAnchorValues as - else Left (InvalidAnchorValue val) -{-# INLINE validate_anchor_outputs #-} - --- fee validation -------------------------------------------------------------- - --- | Validate commitment transaction fee. --- --- Checks that the fee matches the expected calculation. -validate_commitment_fee - :: FeeratePerKw - -> ChannelFeatures - -> Word64 -- ^ Number of untrimmed HTLCs - -> Satoshi -- ^ Actual fee - -> Either ValidationError () -validate_commitment_fee feerate features numHtlcs actualFee = - let !expectedFee = commitment_fee feerate features numHtlcs - in if actualFee == expectedFee - then Right () - else Left (InvalidFee expectedFee actualFee) -{-# INLINE validate_commitment_fee #-} - --- | Validate HTLC transaction fee. -validate_htlc_fee - :: FeeratePerKw - -> ChannelFeatures - -> HTLCDirection - -> Satoshi -- ^ Actual fee - -> Either ValidationError () -validate_htlc_fee feerate features direction actualFee = - let !expectedFee = case direction of - HTLCOffered -> htlc_timeout_fee feerate features - HTLCReceived -> htlc_success_fee feerate features - in if actualFee == expectedFee - then Right () - else Left (InvalidFee expectedFee actualFee) -{-# INLINE validate_htlc_fee #-} - --- helpers --------------------------------------------------------------------- - --- | Validate transaction version. -validateVersion :: Word32 -> Word32 -> Either ValidationError () -validateVersion expected actual = - if expected == actual - then Right () - else Left (InvalidVersion expected actual) -{-# INLINE validateVersion #-} - --- | Validate that transaction has at least one output. -validateOutputCount :: [TxOutput] -> Either ValidationError () -validateOutputCount [] = Left NoOutputs -validateOutputCount _ = Right () -{-# INLINE validateOutputCount #-} diff --git a/ppad-bolt3.cabal b/ppad-bolt3.cabal @@ -25,22 +25,20 @@ library -Wall exposed-modules: Lightning.Protocol.BOLT3 - Lightning.Protocol.BOLT3.Decode - Lightning.Protocol.BOLT3.Encode - Lightning.Protocol.BOLT3.Internal + other-modules: Lightning.Protocol.BOLT3.Keys Lightning.Protocol.BOLT3.Scripts Lightning.Protocol.BOLT3.Tx Lightning.Protocol.BOLT3.Types - Lightning.Protocol.BOLT3.Validate build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13 - , ppad-bolt1 >= 0.0.1 && < 0.1 + , deepseq >= 1.4 && < 1.6 + , ppad-bolt1 >= 0.1 && < 0.2 , ppad-ripemd160 >= 0.1.4 && < 0.2 - , ppad-secp256k1 >= 0.5.4 && < 0.6 + , ppad-secp256k1 >= 0.5.7 && < 0.6 , ppad-sha256 >= 0.3.2 && < 0.4 - , ppad-tx >= 0.1 && < 0.2 + , ppad-tx >= 0.2 && < 0.3 test-suite bolt3-tests type: exitcode-stdio-1.0