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 b6b3c35e24b99b2538475f27cff76d0556993160
parent 0b9fe954c950237b4d9fd3df076ca08282442572
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 15:01:06 +0400

Merge impl/fixed-scalars: ppad-fixed scalar arithmetic

Replace bolt3's hand-rolled mod-n arithmetic in private key derivation
with ppad-fixed's constant-time Montgomery scalars, add ppad-fixed to
the flake (with ppad-secp256k1 following it), and bump the
ppad-secp256k1 lower bound to 0.5.8.

Diffstat:
Mflake.lock | 24++++++++++++------------
Mflake.nix | 18++++++++++++++++--
Mlib/Lightning/Protocol/BOLT3/Keys.hs | 131++++++++-----------------------------------------------------------------------
Mppad-bolt3.cabal | 3++-
4 files changed, 43 insertions(+), 133 deletions(-)

diff --git a/flake.lock b/flake.lock @@ -273,28 +273,25 @@ "ppad-fixed": { "inputs": { "flake-utils": [ - "ppad-secp256k1", "ppad-fixed", "ppad-nixpkgs", "flake-utils" ], "nixpkgs": [ - "ppad-secp256k1", "ppad-fixed", "ppad-nixpkgs", "nixpkgs" ], "ppad-nixpkgs": [ - "ppad-secp256k1", "ppad-nixpkgs" ] }, "locked": { - "lastModified": 1780868194, - "narHash": "sha256-Yd3J3gCNLE2vZVMMg1fD7aVpPySIbFqxtwskU6nD/mc=", + "lastModified": 1785603497, + "narHash": "sha256-rF8KdVJ+VwUT2bEfCLZMPc05rUclr0wN2uOTYvd3z7g=", "ref": "master", - "rev": "74ebe44f818353129c5ec973a55c8bc487e2fd7b", - "revCount": 297, + "rev": "e303bda7578385f488c2f23baa2e1a6a2b0b2aaf", + "revCount": 305, "type": "git", "url": "git://git.ppad.tech/fixed.git" }, @@ -414,7 +411,9 @@ "nixpkgs" ], "ppad-base16": "ppad-base16_3", - "ppad-fixed": "ppad-fixed", + "ppad-fixed": [ + "ppad-fixed" + ], "ppad-hmac-drbg": "ppad-hmac-drbg", "ppad-nixpkgs": [ "ppad-nixpkgs" @@ -425,11 +424,11 @@ "ppad-sha512": "ppad-sha512" }, "locked": { - "lastModified": 1783709454, - "narHash": "sha256-6BknM38fe2vFwWWRsBeebTt+hKwaZL0ib89XqPO6Y6U=", + "lastModified": 1785605654, + "narHash": "sha256-VDNnKXmJIZZ/3u7Tf1rK93GR4x7OiAadqykhqCwkHb4=", "ref": "master", - "rev": "bc0d3792bcea0475ed2e4879747dbd0e0e6db3e7", - "revCount": 266, + "rev": "fdb80d54b86c61bd1a15e2e8db351091d02c89ee", + "revCount": 280, "type": "git", "url": "git://git.ppad.tech/secp256k1.git" }, @@ -556,6 +555,7 @@ ], "ppad-bolt1": "ppad-bolt1", "ppad-bolt9": "ppad-bolt9", + "ppad-fixed": "ppad-fixed", "ppad-nixpkgs": "ppad-nixpkgs", "ppad-ripemd160": "ppad-ripemd160", "ppad-secp256k1": "ppad-secp256k1", diff --git a/flake.nix b/flake.nix @@ -26,12 +26,19 @@ ref = "master"; inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; }; + ppad-fixed = { + type = "git"; + url = "git://git.ppad.tech/fixed.git"; + ref = "master"; + inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + }; ppad-secp256k1 = { type = "git"; url = "git://git.ppad.tech/secp256k1.git"; ref = "master"; inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; inputs.ppad-sha256.follows = "ppad-sha256"; + inputs.ppad-fixed.follows = "ppad-fixed"; }; ppad-ripemd160 = { type = "git"; @@ -51,8 +58,8 @@ }; outputs = { self, nixpkgs, flake-utils, ppad-nixpkgs - , ppad-bolt1, ppad-bolt9, ppad-sha256, ppad-secp256k1 - , ppad-ripemd160, ppad-tx }: + , ppad-bolt1, ppad-bolt9, ppad-sha256, ppad-fixed + , ppad-secp256k1, ppad-ripemd160, ppad-tx }: flake-utils.lib.eachDefaultSystem (system: let lib = "ppad-bolt3"; @@ -68,6 +75,12 @@ (hlib.enableCabalFlag sha256 "llvm") [ llvm clang ]; + fixed = ppad-fixed.packages.${system}.default; + fixed-llvm = + hlib.addBuildTools + (hlib.enableCabalFlag fixed "llvm") + [ llvm clang ]; + secp256k1 = ppad-secp256k1.packages.${system}.default; secp256k1-llvm = hlib.addBuildTools @@ -93,6 +106,7 @@ ppad-bolt1 = bolt1; ppad-bolt9 = bolt9; ppad-sha256 = sha256-llvm; + ppad-fixed = fixed-llvm; ppad-secp256k1 = secp256k1-llvm; ppad-ripemd160 = ripemd160-llvm; ppad-tx = tx-llvm; diff --git a/lib/Lightning/Protocol/BOLT3/Keys.hs b/lib/Lightning/Protocol/BOLT3/Keys.hs @@ -43,17 +43,16 @@ module Lightning.Protocol.BOLT3.Keys ( import Control.DeepSeq (NFData(..)) import qualified Crypto.Curve.Secp256k1 as S import qualified Crypto.Hash.SHA256 as SHA256 +import qualified Data.Choice as C 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 Lightning.Protocol.BOLT1 (Point, PerCommitmentSecret) import qualified Lightning.Protocol.BOLT1 as BOLT1 import Lightning.Protocol.BOLT3.Types +import qualified Numeric.Montgomery.Secp256k1.Scalar as SC -- points --------------------------------------------------------------------- @@ -277,8 +276,7 @@ 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 + to_seckey (SC.to sk + SC.to (S.unsafe_roll32 h)) -- | Derive a commitment's @revocationprivkey@ from the revocation -- basepoint secret and the commitment's (revealed) per-commitment @@ -315,119 +313,16 @@ derive_revocationprivkey (Seckey rbs_bs) pcs = do 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 + !h1 = SC.to (S.unsafe_roll32 (SHA256.hash (rbp_bs <> pcp_bs))) + !h2 = SC.to (S.unsafe_roll32 (SHA256.hash (pcp_bs <> rbp_bs))) + to_seckey (SC.to rbs * h1 + SC.to ps * h2) + +-- The secret key for a nonzero scalar. Reduction, arithmetic and the +-- zero check are constant-time (ppad-fixed). +to_seckey :: SC.Montgomery -> Maybe Seckey +to_seckey r + | C.decide (SC.eq r 0) = Nothing + | otherwise = Just (Seckey (S.unroll32 (SC.retr r))) -- per-commitment secrets ----------------------------------------------------- diff --git a/ppad-bolt3.cabal b/ppad-bolt3.cabal @@ -35,8 +35,9 @@ library , bytestring >= 0.9 && < 0.13 , deepseq >= 1.4 && < 1.6 , ppad-bolt1 >= 0.1 && < 0.2 + , ppad-fixed >= 0.2 && < 0.3 , ppad-ripemd160 >= 0.1.4 && < 0.2 - , ppad-secp256k1 >= 0.5.7 && < 0.6 + , ppad-secp256k1 >= 0.5.8 && < 0.6 , ppad-sha256 >= 0.3.2 && < 0.4 , ppad-tx >= 0.2 && < 0.3