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