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 f2031cfb310fda6bbe02150668e084cdabea0eca
parent acfdcfd23aa99407cfcf1f03aa3c637edd030a32
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 17 May 2026 18:46:15 -0230

test: assert wNAF derive_* variants match non-wNAF byte-for-byte

One vector test pinned against Appendix E for derive_pubkey', plus
seven QuickCheck properties (one per wNAF variant) randomly
generating basepoint/per-commit-point pairs from secret keys and
checking byte equivalence with the non-wNAF counterpart.

ppad-secp256k1 added to test-suite deps for S.precompute /
S.derive_pub used in the test fixture.

Diffstat:
Mppad-bolt3.cabal | 1+
Mtest/Main.hs | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 101 insertions(+), 0 deletions(-)

diff --git a/ppad-bolt3.cabal b/ppad-bolt3.cabal @@ -56,6 +56,7 @@ test-suite bolt3-tests , base16-bytestring , bytestring , ppad-bolt3 + , ppad-secp256k1 >= 0.5.4 && < 0.6 , QuickCheck , tasty , tasty-hunit diff --git a/test/Main.hs b/test/Main.hs @@ -2,6 +2,7 @@ module Main where +import qualified Crypto.Curve.Secp256k1 as S import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 import Data.Maybe (isJust, isNothing) @@ -13,8 +14,16 @@ import Lightning.Protocol.BOLT3 import Lightning.Protocol.BOLT3.Types ( Pubkey(..), Point(..) , PaymentHash(..), PerCommitmentPoint(..) + , PerCommitmentSecret(..) ) +-- Module-level wNAF context. Built once; reused across every +-- equivalence test below. Mirrors how downstream callers should use +-- it. +tex :: S.Context +tex = S.precompute +{-# NOINLINE tex #-} + main :: IO () main = defaultMain $ testGroup "ppad-bolt3" [ testGroup "Key derivation" [ @@ -61,6 +70,17 @@ keyDerivationTests = testGroup "BOLT #3 Appendix E" [ Nothing -> assertFailure "derive_pubkey returned Nothing" Just (Pubkey pk) -> pk @?= expected + , testCase "derive_pubkey' matches vector" $ do + let basepoint = Point $ hex + "036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2" + perCommitmentPoint = PerCommitmentPoint $ Point $ hex + "025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486" + expected = hex + "0235f2dbfaa89b57ec7b055afe29849ef7ddfeb1cefdb9ebdc43f5494984db29e5" + case derive_pubkey' tex basepoint perCommitmentPoint of + Nothing -> assertFailure "derive_pubkey' returned Nothing" + Just (Pubkey pk) -> pk @?= expected + , testCase "derive_revocationpubkey" $ do let revocationBasepoint = RevocationBasepoint $ Point $ hex "036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2" @@ -381,8 +401,88 @@ propertyTests = testGroup "invariants" [ propTrimPartition , testProperty "commitment_fee increases with HTLCs" propFeeMonotonic + , testProperty "derive_per_commitment_point' ≡ derive_per_commitment_point" + propDerivePcpEquiv + , testProperty "derive_pubkey' ≡ derive_pubkey" + propDerivePubkeyEquiv + , testProperty "derive_localpubkey' ≡ derive_localpubkey" + propDeriveLocalEquiv + , testProperty "derive_local_htlcpubkey' ≡ derive_local_htlcpubkey" + propDeriveLocalHtlcEquiv + , testProperty "derive_remote_htlcpubkey' ≡ derive_remote_htlcpubkey" + propDeriveRemoteHtlcEquiv + , testProperty "derive_local_delayedpubkey' ≡ derive_local_delayedpubkey" + propDeriveLocalDelEquiv + , testProperty "derive_remote_delayedpubkey' ≡ derive_remote_delayedpubkey" + propDeriveRemoteDelEquiv ] +-- | Random 32-byte secret, then derive a basepoint and per-commit +-- point from it. Returns (basepoint, pcp). Using derived points +-- keeps us on the curve without having to generate valid points +-- directly. +genPointPair :: Gen (Point, PerCommitmentPoint) +genPointPair = do + sk1 <- vectorOf 32 (choose (0, 255 :: Int)) + sk2 <- vectorOf 32 (choose (0, 255 :: Int)) + let bs1 = BS.pack (fmap fromIntegral sk1) + bs2 = BS.pack (fmap fromIntegral sk2) + mkPt b = case S.parse_int256 b of + Nothing -> Nothing + Just w -> S.serialize_point <$> S.derive_pub w + case (mkPt bs1, mkPt bs2) of + (Just p1, Just p2) -> + pure (Point p1, PerCommitmentPoint (Point p2)) + _ -> genPointPair -- ~negligible retry; sk=0 or sk>=q + +propDerivePcpEquiv :: Property +propDerivePcpEquiv = + forAll (vectorOf 32 (choose (0, 255 :: Int))) $ \sk -> + let bs = BS.pack (fmap fromIntegral sk) + sec = PerCommitmentSecret bs + in derive_per_commitment_point' tex sec + === derive_per_commitment_point sec + +propDerivePubkeyEquiv :: Property +propDerivePubkeyEquiv = + forAll genPointPair $ \(bp, pcp) -> + derive_pubkey' tex bp pcp === derive_pubkey bp pcp + +propDeriveLocalEquiv :: Property +propDeriveLocalEquiv = + forAll genPointPair $ \(bp, pcp) -> + let pbp = PaymentBasepoint bp + in derive_localpubkey' tex pbp pcp + === derive_localpubkey pbp pcp + +propDeriveLocalHtlcEquiv :: Property +propDeriveLocalHtlcEquiv = + forAll genPointPair $ \(bp, pcp) -> + let hbp = HtlcBasepoint bp + in derive_local_htlcpubkey' tex hbp pcp + === derive_local_htlcpubkey hbp pcp + +propDeriveRemoteHtlcEquiv :: Property +propDeriveRemoteHtlcEquiv = + forAll genPointPair $ \(bp, pcp) -> + let hbp = HtlcBasepoint bp + in derive_remote_htlcpubkey' tex hbp pcp + === derive_remote_htlcpubkey hbp pcp + +propDeriveLocalDelEquiv :: Property +propDeriveLocalDelEquiv = + forAll genPointPair $ \(bp, pcp) -> + let dbp = DelayedPaymentBasepoint bp + in derive_local_delayedpubkey' tex dbp pcp + === derive_local_delayedpubkey dbp pcp + +propDeriveRemoteDelEquiv :: Property +propDeriveRemoteDelEquiv = + forAll genPointPair $ \(bp, pcp) -> + let dbp = DelayedPaymentBasepoint bp + in derive_remote_delayedpubkey' tex dbp pcp + === derive_remote_delayedpubkey dbp pcp + -- | commitment_number accepts values in [0, 2^48-1] and -- rejects values >= 2^48. propCommitmentNumberRange :: Property