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:
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
]