fixed

Pure Haskell large fixed-width integers and Montgomery arithmetic.
git clone git://git.ppad.tech/fixed.git
Log | Files | Refs | README | LICENSE

commit bbe5cf300c2e9ad9238cab2ab243a081f8923bc9
parent 43ca6ac31f3d98bc6c6ab0591a0ca70c1e17ea0f
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 12 Dec 2025 09:09:22 +0400

lib: add exp for montgomery (and sqrt for curve)

Diffstat:
Mbench/Main.hs | 17+++++++++++++++++
Mlib/Numeric/Montgomery/Secp256k1/Curve.hs | 39++++++++++++++++++++++++---------------
Mlib/Numeric/Montgomery/Secp256k1/Scalar.hs | 20+++++++++++++++++++-
Mtest/Montgomery/Curve.hs | 20++++++++++++++++++++
Mtest/Montgomery/Scalar.hs | 20++++++++++++++++++++
5 files changed, 100 insertions(+), 16 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -7,6 +7,7 @@ module Main where import qualified Numeric.Montgomery.Secp256k1.Curve as C import qualified Numeric.Montgomery.Secp256k1.Scalar as S import Criterion.Main +import Prelude hiding (exp, sqrt) main :: IO () main = defaultMain [ @@ -15,6 +16,8 @@ main = defaultMain [ , mul , sqr , inv + , exp + , sqrt , redc , retr ] @@ -67,6 +70,20 @@ inv = bgroup "inv" [ , bench "scalar: M(2 ^ 255 - 19) ^ -1" $ nf S.inv (2 ^ 255 - 19) ] +sqrt :: Benchmark +sqrt = bgroup "sqrt" [ + bench "curve: sqrt M(2)" $ nf C.sqrt 2 + , bench "curve: sqrt M(2 ^ 255 - 19)" $ nf C.sqrt (2 ^ 255 - 19) + ] + +exp :: Benchmark +exp = bgroup "exp" [ + bench "curve: M(2) ^ 2" $ nf C.exp 2 + , bench "curve: M(2 ^ 255 - 19) ^ 2" $ nf C.exp (2 ^ 255 - 19) + , bench "scalar: M(2) ^ 2" $ nf S.exp 2 + , bench "scalar: M(2 ^ 255 - 19) ^ 2" $ nf S.exp (2 ^ 255 - 19) + ] + redc :: Benchmark redc = bgroup "redc" [ bench "curve: REDC(M(2), M(2))" $ nf (C.redc 2) 2 diff --git a/lib/Numeric/Montgomery/Secp256k1/Curve.hs b/lib/Numeric/Montgomery/Secp256k1/Curve.hs @@ -48,6 +48,7 @@ module Numeric.Montgomery.Secp256k1.Curve ( , inv , inv# , sqrt + , exp ) where import Control.DeepSeq @@ -59,7 +60,7 @@ import qualified Data.Word.Wide as W import Data.Word.Wider (Wider(..)) import qualified Data.Word.Wider as WW import GHC.Exts (Word(..)) -import Prelude hiding (div, mod, or, and, not, quot, rem, recip, sqrt) +import Prelude hiding (div, mod, or, and, not, quot, rem, recip, sqrt, exp) -- montgomery arithmetic, specialized to the secp256k1 field prime modulus -- 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F @@ -991,17 +992,25 @@ inv (Montgomery w) = Montgomery (inv# w) -- Just 15 sqrt :: Montgomery -> Maybe Montgomery sqrt n = - let !e0 = 0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c - !rv = loop 1 n e0 - in if C.decide (eq (rv * rv) n) - then Just $! rv - else Nothing - where - loop !r !m !e@(Wider (# Limb (W# -> w), _, _, _ #)) = case WW.cmp e 0 of - GT -> - let !nm = m * m - !ne = WW.shr1 e - !nr | B.testBit w 0 = r * m - | otherwise = r - in loop nr nm ne - _ -> r + let !e0 = 0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c + !rv = exp n e0 + in if C.decide (eq (sqr rv) n) + then Just $! rv + else Nothing + +-- | Exponentiation in the Montgomery domain. +-- +-- >>> exp 2 3 +-- 8 +-- >>> exp 2 10 +-- 1024 +exp :: Montgomery -> Wider -> Montgomery +exp b = loop 1 b where + loop !r !m !e@(Wider (# Limb (W# -> w), _, _, _ #)) = case WW.cmp e 0 of + GT -> + let !nm = sqr m + !ne = WW.shr1 e + !nr | B.testBit w 0 = r * m + | otherwise = r + in loop nr nm ne + _ -> r diff --git a/lib/Numeric/Montgomery/Secp256k1/Scalar.hs b/lib/Numeric/Montgomery/Secp256k1/Scalar.hs @@ -47,9 +47,11 @@ module Numeric.Montgomery.Secp256k1.Scalar ( , neg# , inv , inv# + , exp ) where import Control.DeepSeq +import qualified Data.Bits as B import qualified Data.Choice as C import Data.Word.Limb (Limb(..)) import qualified Data.Word.Limb as L @@ -57,7 +59,7 @@ import qualified Data.Word.Wide as W import Data.Word.Wider (Wider(..)) import qualified Data.Word.Wider as WW import GHC.Exts (Word(..)) -import Prelude hiding (div, mod, or, and, not, quot, rem, recip) +import Prelude hiding (div, mod, or, and, not, quot, rem, recip, exp) -- montgomery arithmetic, specialized to the secp256k1 scalar group order -- 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 @@ -934,3 +936,19 @@ inv -> Montgomery -- ^ inverse inv (Montgomery w) = Montgomery (inv# w) +-- | Exponentiation in the Montgomery domain. +-- +-- >>> exp 2 3 +-- 8 +-- >>> exp 2 10 +-- 1024 +exp :: Montgomery -> Wider -> Montgomery +exp b = loop 1 b where + loop !r !m !e@(Wider (# Limb (W# -> w), _, _, _ #)) = case WW.cmp e 0 of + GT -> + let !nm = sqr m + !ne = WW.shr1 e + !nr | B.testBit w 0 = r * m + | otherwise = r + in loop nr nm ne + _ -> r diff --git a/test/Montgomery/Curve.hs b/test/Montgomery/Curve.hs @@ -3,18 +3,30 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE UnboxedSums #-} {-# LANGUAGE UnboxedTuples #-} +{-# LANGUAGE ViewPatterns #-} module Montgomery.Curve ( tests ) where import qualified Data.Word.Wider as W +import qualified GHC.Num.Integer as I +import GHC.Natural import qualified Numeric.Montgomery.Secp256k1.Curve as C import Test.Tasty import qualified Test.Tasty.HUnit as H import qualified Test.Tasty.QuickCheck as Q +-- generic modular exponentiation +-- b ^ e mod m +modexp :: Integer -> Natural -> Natural -> Integer +modexp b (fromIntegral -> e) p = case I.integerPowMod# b e p of + (# fromIntegral -> n | #) -> n + (# | _ #) -> error "bang" +{-# INLINE modexp #-} + -- modulus m :: W.Wider m = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F @@ -116,6 +128,13 @@ mul_matches a b = im = W.from m in W.eq_vartime (W.to ((ia * ib) `mod` im)) (C.from (ma * mb)) +exp_matches :: C.Montgomery -> W.Wider -> Bool +exp_matches a b = + let ia = W.from (C.from a) + nb = fromIntegral (W.from b) + nm = fromIntegral (W.from m) + in W.eq_vartime (W.to (modexp ia nb nm)) (C.from (C.exp a b)) + inv_valid :: Q.NonZero C.Montgomery -> Bool inv_valid (Q.NonZero s) = C.eq_vartime (C.inv s * s) 1 @@ -127,6 +146,7 @@ tests = testGroup "montgomery tests (curve)" [ , H.testCase "mul" mul , Q.testProperty "a + b mod m ~ ma + mb" $ Q.withMaxSuccess 500 add_matches , Q.testProperty "a * b mod m ~ ma * mb" $ Q.withMaxSuccess 500 mul_matches + , Q.testProperty "a ^ b mod m ~ ma ^ mb" $ Q.withMaxSuccess 500 exp_matches , Q.testProperty "n ^ -1 mod m * n ~ 1" $ Q.withMaxSuccess 500 inv_valid ] diff --git a/test/Montgomery/Scalar.hs b/test/Montgomery/Scalar.hs @@ -3,18 +3,30 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE UnboxedSums #-} {-# LANGUAGE UnboxedTuples #-} +{-# LANGUAGE ViewPatterns #-} module Montgomery.Scalar ( tests ) where import qualified Data.Word.Wider as W +import qualified GHC.Num.Integer as I +import GHC.Natural import qualified Numeric.Montgomery.Secp256k1.Scalar as S import Test.Tasty import qualified Test.Tasty.HUnit as H import qualified Test.Tasty.QuickCheck as Q +-- generic modular exponentiation +-- b ^ e mod m +modexp :: Integer -> Natural -> Natural -> Integer +modexp b (fromIntegral -> e) q = case I.integerPowMod# b e q of + (# fromIntegral -> n | #) -> n + (# | _ #) -> error "bang" +{-# INLINE modexp #-} + -- modulus m :: W.Wider m = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 @@ -116,6 +128,13 @@ mul_matches a b = im = W.from m in W.eq_vartime (W.to ((ia * ib) `mod` im)) (S.from (ma * mb)) +exp_matches :: S.Montgomery -> W.Wider -> Bool +exp_matches a b = + let ia = W.from (S.from a) + nb = fromIntegral (W.from b) + nm = fromIntegral (W.from m) + in W.eq_vartime (W.to (modexp ia nb nm)) (S.from (S.exp a b)) + inv_valid :: Q.NonZero S.Montgomery -> Bool inv_valid (Q.NonZero s) = S.eq_vartime (S.inv s * s) 1 @@ -127,6 +146,7 @@ tests = testGroup "montgomery tests (scalar)" [ , H.testCase "mul" mul , Q.testProperty "a + b mod m ~ ma + mb" $ Q.withMaxSuccess 500 add_matches , Q.testProperty "a * b mod m ~ ma * mb" $ Q.withMaxSuccess 500 mul_matches + , Q.testProperty "a ^ b mod m ~ ma ^ mb" $ Q.withMaxSuccess 500 exp_matches , Q.testProperty "n ^ -1 mod m * n ~ 1" $ Q.withMaxSuccess 500 inv_valid ]