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 b930e58309f1afb27a72cb026c40991cfd9687c3
parent 2113539a278a3b1ab8484a5b205d9388b758ddb6
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon, 20 Apr 2026 15:18:54 +0800

type safety improvements: restrict newtypes, hide constructors

- Remove Num instances from CommitmentNumber, Sequence, Locktime,
  ToSelfDelay, CltvExpiry, and FeeratePerKw to prevent nonsensical
  arithmetic (e.g., multiplying commitment numbers or fee rates)

- Add next_commitment_number as the only checked arithmetic
  operation for commitment numbers, respecting the 48-bit range

- Create Lightning.Protocol.BOLT3.Internal module with unsafe
  constructors (unsafePubkey, unsafeSeckey, unsafeCommitmentNumber,
  unsafeSequence, unsafeLocktime) for test/benchmark use

- Hide constructors for validated types from the public BOLT3
  module: Pubkey, Seckey, Point, PaymentHash, PaymentPreimage,
  CommitmentNumber, PerCommitmentPoint, PerCommitmentSecret.
  Only smart constructors and accessors are publicly visible.
  Types.hs retains full exports for library-internal use.

- Re-export bolt1 accessors (unPoint, unPaymentHash,
  unPaymentPreimage, unPerCommitmentSecret) through Types.hs

- Add tests for next_commitment_number (increment, overflow)

- Update test and benchmark imports accordingly

- Update flake.lock for latest bolt1 type safety changes

Diffstat:
Mbench/Main.hs | 5+++++
Mbench/Weight.hs | 5+++++
Mflake.lock | 8++++----
Mlib/Lightning/Protocol/BOLT3.hs | 25+++++++++++++++++--------
Alib/Lightning/Protocol/BOLT3/Internal.hs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/Lightning/Protocol/BOLT3/Types.hs | 40++++++++++++++++++++++++++++++----------
Mppad-bolt3.cabal | 1+
Mtest/Main.hs | 24++++++++++++++++++++++++
8 files changed, 142 insertions(+), 22 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -8,6 +8,11 @@ import Criterion.Main import Data.Word (Word64) import qualified Data.ByteString as BS import Lightning.Protocol.BOLT3 +import Lightning.Protocol.BOLT3.Types + ( Pubkey(..), Point(..) + , PaymentHash(..), PerCommitmentPoint(..) + , CommitmentNumber(..) + ) -- NFData instances for benchmarking -- (Satoshi, MilliSatoshi, Point, PaymentHash, PerCommitmentSecret diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -7,6 +7,11 @@ import Control.DeepSeq (NFData(..)) import qualified Data.ByteString as BS import Data.Word (Word32, Word64) import Lightning.Protocol.BOLT3 +import Lightning.Protocol.BOLT3.Types + ( Pubkey(..), Point(..) + , PaymentHash(..), PerCommitmentPoint(..) + , CommitmentNumber(..) + ) import Weigh -- NFData instances for weigh diff --git a/flake.lock b/flake.lock @@ -222,11 +222,11 @@ ] }, "locked": { - "lastModified": 1776570879, - "narHash": "sha256-XsgGBvYWL+sD7pDZoPPi4l39DE7GH7maNnhm8iUeB/E=", + "lastModified": 1776668614, + "narHash": "sha256-ZckuUOZHrSya8kn7aRizWIQtqTZMrhSoi2NX7BE2s90=", "ref": "master", - "rev": "20ea43188d781368e5e64c7c646285a6b0aaeb94", - "revCount": 27, + "rev": "580036e8f5cb22d423a205abeb15fca33307267c", + "revCount": 29, "type": "git", "url": "git://git.ppad.tech/bolt1.git" }, diff --git a/lib/Lightning/Protocol/BOLT3.hs b/lib/Lightning/Protocol/BOLT3.hs @@ -62,17 +62,22 @@ module Lightning.Protocol.BOLT3 ( , satToMsat -- ** Keys and points - , Pubkey(..) + , Pubkey + , unPubkey , pubkey - , Seckey(..) + , Seckey + , unSeckey , seckey - , Point(..) + , Point + , unPoint , point -- ** Hashes - , PaymentHash(..) + , PaymentHash + , unPaymentHash , paymentHash - , PaymentPreimage(..) + , PaymentPreimage + , unPaymentPreimage , paymentPreimage -- ** Transaction primitives @@ -83,8 +88,10 @@ module Lightning.Protocol.BOLT3 ( , Locktime(..) -- ** Channel parameters - , CommitmentNumber(..) + , CommitmentNumber + , unCommitmentNumber , commitment_number + , next_commitment_number , ToSelfDelay(..) , CltvExpiry(..) , DustLimit(..) @@ -96,8 +103,10 @@ module Lightning.Protocol.BOLT3 ( -- ** Basepoints , Basepoints(..) - , PerCommitmentPoint(..) - , PerCommitmentSecret(..) + , PerCommitmentPoint + , unPerCommitmentPoint + , PerCommitmentSecret + , unPerCommitmentSecret , perCommitmentSecret , RevocationBasepoint(..) , PaymentBasepoint(..) diff --git a/lib/Lightning/Protocol/BOLT3/Internal.hs b/lib/Lightning/Protocol/BOLT3/Internal.hs @@ -0,0 +1,56 @@ +{-# 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/Types.hs b/lib/Lightning/Protocol/BOLT3/Types.hs @@ -1,7 +1,6 @@ {-# OPTIONS_HADDOCK prune #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | -- Module: Lightning.Protocol.BOLT3.Types @@ -25,12 +24,15 @@ module Lightning.Protocol.BOLT3.Types ( , seckey , Point(..) , point + , unPoint -- * Hashes (re-exported from BOLT1) , PaymentHash(..) , paymentHash + , unPaymentHash , PaymentPreimage(..) , paymentPreimage + , unPaymentPreimage -- * Transaction primitives , TxId(..) @@ -42,6 +44,7 @@ module Lightning.Protocol.BOLT3.Types ( -- * Channel parameters , CommitmentNumber(..) , commitment_number + , next_commitment_number , ToSelfDelay(..) , CltvExpiry(..) , DustLimit(..) @@ -56,6 +59,7 @@ module Lightning.Protocol.BOLT3.Types ( , PerCommitmentPoint(..) , PerCommitmentSecret(..) , perCommitmentSecret + , unPerCommitmentSecret , RevocationBasepoint(..) , PaymentBasepoint(..) , DelayedPaymentBasepoint(..) @@ -105,10 +109,11 @@ import GHC.Generics (Generic) import Lightning.Protocol.BOLT1.Prim ( Satoshi(..), MilliSatoshi(..) , satToMsat, msatToSat - , Point(..), point - , PaymentHash(..), paymentHash - , PaymentPreimage(..), paymentPreimage + , Point(..), point, unPoint + , PaymentHash(..), paymentHash, unPaymentHash + , PaymentPreimage(..), paymentPreimage, unPaymentPreimage , PerCommitmentSecret(..), perCommitmentSecret + , unPerCommitmentSecret ) -- keys and points ------------------------------------------------------------- @@ -152,18 +157,18 @@ seckey bs -- | Transaction input sequence number. newtype Sequence = Sequence { unSequence :: Word32 } - deriving (Eq, Ord, Show, Generic, Num) + deriving (Eq, Ord, Show, Generic) -- | Transaction locktime. newtype Locktime = Locktime { unLocktime :: Word32 } - deriving (Eq, Ord, Show, Generic, Num) + deriving (Eq, Ord, Show, Generic) -- channel parameters ---------------------------------------------------------- -- | 48-bit commitment number. newtype CommitmentNumber = CommitmentNumber { unCommitmentNumber :: Word64 } - deriving (Eq, Ord, Show, Generic, Num) + deriving (Eq, Ord, Show, Generic) -- | Parse a 48-bit commitment number. -- @@ -174,13 +179,28 @@ commitment_number n | otherwise = Nothing {-# INLINE commitment_number #-} +-- | 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 } - deriving (Eq, Ord, Show, Generic, Num) + deriving (Eq, Ord, Show, Generic) -- | CLTV expiry for HTLCs. newtype CltvExpiry = CltvExpiry { unCltvExpiry :: Word32 } - deriving (Eq, Ord, Show, Generic, Num) + deriving (Eq, Ord, Show, Generic) -- | Dust limit threshold. newtype DustLimit = DustLimit { unDustLimit :: Satoshi } @@ -188,7 +208,7 @@ newtype DustLimit = DustLimit { unDustLimit :: Satoshi } -- | Fee rate in satoshis per 1000 weight units. newtype FeeratePerKw = FeeratePerKw { unFeeratePerKw :: Word32 } - deriving (Eq, Ord, Show, Generic, Num) + deriving (Eq, Ord, Show, Generic) -- HTLC types ------------------------------------------------------------------ diff --git a/ppad-bolt3.cabal b/ppad-bolt3.cabal @@ -27,6 +27,7 @@ library Lightning.Protocol.BOLT3 Lightning.Protocol.BOLT3.Decode Lightning.Protocol.BOLT3.Encode + Lightning.Protocol.BOLT3.Internal Lightning.Protocol.BOLT3.Keys Lightning.Protocol.BOLT3.Scripts Lightning.Protocol.BOLT3.Tx diff --git a/test/Main.hs b/test/Main.hs @@ -8,6 +8,10 @@ import Data.Maybe (isJust, isNothing) import Test.Tasty import Test.Tasty.HUnit import Lightning.Protocol.BOLT3 +import Lightning.Protocol.BOLT3.Types + ( Pubkey(..), Point(..) + , PaymentHash(..), PerCommitmentPoint(..) + ) main :: IO () main = defaultMain $ testGroup "ppad-bolt3" [ @@ -334,4 +338,24 @@ smartConstructorTests = testGroup "validation" [ isNothing (commitment_number 281474976710656) @?= True , testCase "commitment_number rejects maxBound Word64" $ do isNothing (commitment_number maxBound) @?= True + + -- next_commitment_number + , testCase "next_commitment_number 0 -> 1" $ + case commitment_number 0 of + Nothing -> assertFailure "commitment_number 0" + Just cn0 -> case next_commitment_number cn0 of + Nothing -> assertFailure "next failed" + Just cn1 -> unCommitmentNumber cn1 @?= 1 + , testCase "next_commitment_number (2^48-2) -> (2^48-1)" $ + case commitment_number 281474976710654 of + Nothing -> assertFailure "commitment_number" + Just cn -> case next_commitment_number cn of + Nothing -> assertFailure "next failed" + Just cn' -> + unCommitmentNumber cn' @?= 281474976710655 + , testCase "next_commitment_number (2^48-1) -> Nothing" $ + case commitment_number 281474976710655 of + Nothing -> assertFailure "commitment_number" + Just cn -> + isNothing (next_commitment_number cn) @?= True ]