bolt4

Onion routing protocol, per BOLT #4.
git clone git://git.ppad.tech/bolt4.git
Log | Files | Refs | README | LICENSE

commit b6e03182d3417e71de26b50d3d8cb38082594b02
parent f6c4a17746f0c33df65ee8cc0f24f29de3afcbff
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 16:13:15 +0400

ppad-bolt4: address review comments for IMPL6

- Fix deriveBlindingRho to use "rho" key per BOLT4 spec
- Export helper functions from Codec, remove duplicates in Blinding
- Replace manual Integer modular arithmetic with Montgomery
  multiplication from ppad-fixed (Blinding.mulSecKey, Prim.blindSecKey)
- Use error instead of silent empty return in encryptHopData

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mflake.lock | 24++++++++++--------------
Mflake.nix | 14+++++++++++++-
Mlib/Lightning/Protocol/BOLT4/Blinding.hs | 112++++++++++++++-----------------------------------------------------------------
Mlib/Lightning/Protocol/BOLT4/Codec.hs | 11++++++++++-
Mlib/Lightning/Protocol/BOLT4/Prim.hs | 31+++++++++----------------------
Mppad-bolt4.cabal | 1+
6 files changed, 62 insertions(+), 131 deletions(-)

diff --git a/flake.lock b/flake.lock @@ -297,35 +297,28 @@ "ppad-fixed_2": { "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": 1767278248, - "narHash": "sha256-ynF6Tyew83dDr3dFWdTdgK3N5WEkLSCQ/uHHTxb5J1s=", - "ref": "master", - "rev": "ae6f5d732d69e6e2bb70ea9da18e2a8060ca9aeb", - "revCount": 290, - "type": "git", - "url": "git://git.ppad.tech/fixed.git" + "lastModified": 1768121142, + "narHash": "sha256-Pqc0bQLTrWSSy/9AdIpDTqppbxKemFP8Msc9pFnt0Vs=", + "path": "/Users/jtobin/src/ppad/fixed", + "type": "path" }, "original": { - "ref": "master", - "type": "git", - "url": "git://git.ppad.tech/fixed.git" + "path": "/Users/jtobin/src/ppad/fixed", + "type": "path" } }, "ppad-hmac-drbg": { @@ -441,7 +434,9 @@ "nixpkgs" ], "ppad-base16": "ppad-base16_4", - "ppad-fixed": "ppad-fixed_2", + "ppad-fixed": [ + "ppad-fixed" + ], "ppad-hmac-drbg": "ppad-hmac-drbg", "ppad-nixpkgs": [ "ppad-nixpkgs" @@ -541,6 +536,7 @@ "ppad-aead": "ppad-aead", "ppad-base16": "ppad-base16_2", "ppad-chacha": "ppad-chacha", + "ppad-fixed": "ppad-fixed_2", "ppad-nixpkgs": "ppad-nixpkgs", "ppad-secp256k1": "ppad-secp256k1", "ppad-sha256": "ppad-sha256" diff --git a/flake.nix b/flake.nix @@ -12,9 +12,13 @@ ppad-chacha.url = "path:/Users/jtobin/src/ppad/chacha"; ppad-chacha.inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + ppad-fixed.url = "path:/Users/jtobin/src/ppad/fixed"; + ppad-fixed.inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + ppad-secp256k1.url = "path:/Users/jtobin/src/ppad/secp256k1"; ppad-secp256k1.inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; ppad-secp256k1.inputs.ppad-sha256.follows = "ppad-sha256"; + ppad-secp256k1.inputs.ppad-fixed.follows = "ppad-fixed"; ppad-sha256.url = "path:/Users/jtobin/src/ppad/sha256"; ppad-sha256.inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; @@ -26,7 +30,7 @@ }; outputs = { self, nixpkgs, flake-utils, ppad-nixpkgs - , ppad-aead, ppad-base16, ppad-chacha + , ppad-aead, ppad-base16, ppad-chacha, ppad-fixed , ppad-secp256k1, ppad-sha256 }: flake-utils.lib.eachDefaultSystem (system: @@ -56,6 +60,12 @@ (hlib.enableCabalFlag chacha "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 @@ -72,12 +82,14 @@ ppad-aead = aead-llvm; ppad-base16 = base16-llvm; ppad-chacha = chacha-llvm; + ppad-fixed = fixed-llvm; ppad-secp256k1 = secp256k1-llvm; ppad-sha256 = sha256-llvm; ${lib} = new.callCabal2nix lib ./. { ppad-aead = new.ppad-aead; ppad-base16 = new.ppad-base16; ppad-chacha = new.ppad-chacha; + ppad-fixed = new.ppad-fixed; ppad-secp256k1 = new.ppad-secp256k1; ppad-sha256 = new.ppad-sha256; }; diff --git a/lib/Lightning/Protocol/BOLT4/Blinding.hs b/lib/Lightning/Protocol/BOLT4/Blinding.hs @@ -42,14 +42,16 @@ module Lightning.Protocol.BOLT4.Blinding ( import qualified Crypto.AEAD.ChaCha20Poly1305 as AEAD import qualified Crypto.Curve.Secp256k1 as Secp256k1 import qualified Crypto.Hash.SHA256 as SHA256 -import Data.Bits (shiftL) import qualified Data.ByteString as BS import qualified Data.ByteString.Builder as B -import qualified Data.ByteString.Lazy as BL -import Data.Word (Word8, Word16, Word32, Word64) +import Data.Word (Word16, Word32, Word64) +import qualified Numeric.Montgomery.Secp256k1.Scalar as S import Lightning.Protocol.BOLT4.Codec ( encodeShortChannelId, decodeShortChannelId , encodeTlvStream, decodeTlvStream + , toStrict, word16BE, word32BE + , encodeWord64TU, decodeWord64TU + , encodeWord32TU, decodeWord32TU ) import Lightning.Protocol.BOLT4.Prim (SharedSecret(..), DerivedKey(..)) import Lightning.Protocol.BOLT4.Types (ShortChannelId(..), TlvRecord(..)) @@ -107,10 +109,10 @@ data BlindingError -- | Derive rho key for encrypting hop data. -- --- @rho = HMAC-SHA256(key="blinded_node_id", data=shared_secret)@ +-- @rho = HMAC-SHA256(key="rho", data=shared_secret)@ deriveBlindingRho :: SharedSecret -> DerivedKey deriveBlindingRho (SharedSecret !ss) = - let SHA256.MAC !result = SHA256.hmac "blinded_node_id" ss + let SHA256.MAC !result = SHA256.hmac "rho" ss in DerivedKey result {-# INLINE deriveBlindingRho #-} @@ -170,7 +172,7 @@ encryptHopData (DerivedKey !rho) !hopData = let !plaintext = encodeBlindedHopData hopData !nonce = BS.replicate 12 0 in case AEAD.encrypt BS.empty rho nonce plaintext of - Left _ -> BS.empty -- Should not happen with valid key + Left e -> error $ "encryptHopData: unexpected AEAD error: " ++ show e Right (!ciphertext, !mac) -> ciphertext <> mac {-# INLINE encryptHopData #-} @@ -372,94 +374,18 @@ processBlindedHop !nodeSecKey !pathKey !encData = do maybe (Left InvalidPathKey) Right (nextPathKey pathKey ss) Right (hopData, nextKey) --- Helper functions ---------------------------------------------------------- - --- | Convert Builder to strict ByteString. -toStrict :: B.Builder -> BS.ByteString -toStrict = BL.toStrict . B.toLazyByteString -{-# INLINE toStrict #-} - --- | Decode big-endian Word16. -word16BE :: BS.ByteString -> Word16 -word16BE !bs = - let !b0 = fromIntegral (BS.index bs 0) :: Word16 - !b1 = fromIntegral (BS.index bs 1) :: Word16 - in (b0 `shiftL` 8) + b1 -{-# INLINE word16BE #-} - --- | Decode big-endian Word32. -word32BE :: BS.ByteString -> Word32 -word32BE !bs = - let !b0 = fromIntegral (BS.index bs 0) :: Word32 - !b1 = fromIntegral (BS.index bs 1) :: Word32 - !b2 = fromIntegral (BS.index bs 2) :: Word32 - !b3 = fromIntegral (BS.index bs 3) :: Word32 - in (b0 `shiftL` 24) + (b1 `shiftL` 16) + (b2 `shiftL` 8) + b3 -{-# INLINE word32BE #-} - --- | Encode Word64 as truncated unsigned (minimal bytes). -encodeWord64TU :: Word64 -> BS.ByteString -encodeWord64TU !n - | n == 0 = BS.empty - | otherwise = BS.dropWhile (== 0) (toStrict (B.word64BE n)) -{-# INLINE encodeWord64TU #-} - --- | Decode truncated unsigned to Word64. -decodeWord64TU :: BS.ByteString -> Maybe Word64 -decodeWord64TU !bs - | BS.null bs = Just 0 - | BS.length bs > 8 = Nothing - | not (BS.null bs) && BS.index bs 0 == 0 = Nothing -- Non-canonical - | otherwise = Just (go 0 bs) - where - go :: Word64 -> BS.ByteString -> Word64 - go !acc !b = case BS.uncons b of - Nothing -> acc - Just (x, rest) -> go ((acc `shiftL` 8) + fromIntegral x) rest -{-# INLINE decodeWord64TU #-} - --- | Encode Word32 as truncated unsigned. -encodeWord32TU :: Word32 -> BS.ByteString -encodeWord32TU !n - | n == 0 = BS.empty - | otherwise = BS.dropWhile (== 0) (toStrict (B.word32BE n)) -{-# INLINE encodeWord32TU #-} - --- | Decode truncated unsigned to Word32. -decodeWord32TU :: BS.ByteString -> Maybe Word32 -decodeWord32TU !bs - | BS.null bs = Just 0 - | BS.length bs > 4 = Nothing - | not (BS.null bs) && BS.index bs 0 == 0 = Nothing -- Non-canonical - | otherwise = Just (go 0 bs) - where - go :: Word32 -> BS.ByteString -> Word32 - go !acc !b = case BS.uncons b of - Nothing -> acc - Just (x, rest) -> go ((acc `shiftL` 8) + fromIntegral x) rest -{-# INLINE decodeWord32TU #-} +-- Scalar multiplication ----------------------------------------------------- --- | Multiply two secret keys mod curve order q. +-- | Multiply two 32-byte scalars mod curve order q. +-- +-- Uses Montgomery multiplication from ppad-fixed for efficiency. mulSecKey :: BS.ByteString -> BS.ByteString -> BS.ByteString mulSecKey !a !b = - let !aInt = bsToInteger a - !bInt = bsToInteger b - -- secp256k1 curve order - !qInt = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - !resultInt = (aInt * bInt) `mod` qInt - in integerToBS32 resultInt + let !aW = Secp256k1.unsafe_roll32 a + !bW = Secp256k1.unsafe_roll32 b + !aM = S.to aW + !bM = S.to bW + !resultM = S.mul aM bM + !resultW = S.retr resultM + in Secp256k1.unroll32 resultW {-# INLINE mulSecKey #-} - --- Convert big-endian ByteString to Integer. -bsToInteger :: BS.ByteString -> Integer -bsToInteger = BS.foldl' (\acc b -> acc * 256 + fromIntegral b) 0 -{-# INLINE bsToInteger #-} - --- Convert Integer to 32-byte big-endian ByteString. -integerToBS32 :: Integer -> BS.ByteString -integerToBS32 n = BS.pack (go 32 n []) - where - go :: Int -> Integer -> [Word8] -> [Word8] - go 0 _ acc = acc - go i x acc = go (i - 1) (x `div` 256) (fromIntegral (x `mod` 256) : acc) -{-# INLINE integerToBS32 #-} diff --git a/lib/Lightning/Protocol/BOLT4/Codec.hs b/lib/Lightning/Protocol/BOLT4/Codec.hs @@ -35,13 +35,22 @@ module Lightning.Protocol.BOLT4.Codec ( -- * Failure messages , encodeFailureMessage , decodeFailureMessage + + -- * Internal helpers (for Blinding) + , toStrict + , word16BE + , word32BE + , encodeWord64TU + , decodeWord64TU + , encodeWord32TU + , decodeWord32TU ) where import Data.Bits (shiftL, shiftR, (.&.)) import qualified Data.ByteString as BS import qualified Data.ByteString.Builder as B import qualified Data.ByteString.Lazy as BL -import Data.Word (Word8, Word16, Word32, Word64) +import Data.Word (Word16, Word32, Word64) import Lightning.Protocol.BOLT4.Types -- BigSize encoding --------------------------------------------------------- diff --git a/lib/Lightning/Protocol/BOLT4/Prim.hs b/lib/Lightning/Protocol/BOLT4/Prim.hs @@ -48,6 +48,7 @@ import Data.Bits (xor) import qualified Data.ByteString as BS import qualified Data.List as L import Data.Word (Word8, Word32) +import qualified Numeric.Montgomery.Secp256k1.Scalar as S -- | 32-byte shared secret derived from ECDH. newtype SharedSecret = SharedSecret BS.ByteString @@ -157,6 +158,7 @@ blindPubKey !pub (BlindingFactor !bf) = do -- -- @new_seckey = seckey * blinding_factor (mod q)@ -- +-- Uses Montgomery multiplication from ppad-fixed for efficiency. -- Takes a 32-byte secret key and returns a 32-byte blinded secret key. blindSecKey :: BS.ByteString -- ^ 32-byte secret key @@ -166,30 +168,15 @@ blindSecKey !secBs (BlindingFactor !bf) | BS.length secBs /= 32 = Nothing | BS.length bf /= 32 = Nothing | otherwise = - -- Convert to Integer, multiply, reduce mod q, convert back - let !secInt = bsToInteger secBs - !bfInt = bsToInteger bf - -- secp256k1 curve order - !qInt = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - !resultInt = (secInt * bfInt) `mod` qInt - !resultBs = integerToBS32 resultInt - in Just resultBs + let !secW = Secp256k1.unsafe_roll32 secBs + !bfW = Secp256k1.unsafe_roll32 bf + !secM = S.to secW + !bfM = S.to bfW + !resultM = S.mul secM bfM + !resultW = S.retr resultM + in Just $! Secp256k1.unroll32 resultW {-# INLINE blindSecKey #-} --- Convert big-endian ByteString to Integer. -bsToInteger :: BS.ByteString -> Integer -bsToInteger = BS.foldl' (\acc b -> acc * 256 + fromIntegral b) 0 -{-# INLINE bsToInteger #-} - --- Convert Integer to 32-byte big-endian ByteString. -integerToBS32 :: Integer -> BS.ByteString -integerToBS32 n = BS.pack (go 32 n []) - where - go :: Int -> Integer -> [Word8] -> [Word8] - go 0 _ acc = acc - go i x acc = go (i - 1) (x `div` 256) (fromIntegral (x `mod` 256) : acc) -{-# INLINE integerToBS32 #-} - -- Stream generation --------------------------------------------------------- -- | Generate pseudo-random byte stream using ChaCha20. diff --git a/ppad-bolt4.cabal b/ppad-bolt4.cabal @@ -36,6 +36,7 @@ library , bytestring >= 0.9 && < 0.13 , ppad-aead >= 0.3 && < 0.4 , ppad-chacha >= 0.2 && < 0.3 + , ppad-fixed >= 0.1 && < 0.2 , ppad-secp256k1 >= 0.5 && < 0.6 , ppad-sha256 >= 0.3 && < 0.4