csecp256k1

Haskell FFI bindings to bitcoin-core/secp256k1.
git clone git://git.ppad.tech/csecp256k1.git
Log | Files | Refs | README | LICENSE

commit 4863b35572d4694cc209ff36f8d6c27b4e6ab55b
parent 35e5fcdb70e9319a81ac9da9e7625db869702f08
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat,  9 Mar 2024 12:44:43 +0400

bench: flesh out benchmark suite

Current benchmarks:

benchmarking csecp256k1/ecdsa/sign
time                 34.56 μs   (34.20 μs .. 34.86 μs)
                     0.999 R²   (0.999 R² .. 0.999 R²)
mean                 34.37 μs   (34.09 μs .. 34.67 μs)
std dev              1.014 μs   (880.4 ns .. 1.230 μs)
variance introduced by outliers: 31% (moderately inflated)

benchmarking csecp256k1/ecdsa/verify
time                 39.23 μs   (38.71 μs .. 39.73 μs)
                     0.998 R²   (0.998 R² .. 0.999 R²)
mean                 39.28 μs   (38.74 μs .. 40.04 μs)
std dev              2.042 μs   (1.546 μs .. 2.583 μs)
variance introduced by outliers: 58% (severely inflated)

benchmarking csecp256k1/schnorr/sign
time                 53.14 μs   (51.68 μs .. 54.89 μs)
                     0.995 R²   (0.992 R² .. 0.999 R²)
mean                 52.08 μs   (51.42 μs .. 52.96 μs)
std dev              2.686 μs   (2.187 μs .. 3.910 μs)
variance introduced by outliers: 56% (severely inflated)

benchmarking csecp256k1/schnorr/verify
time                 42.69 μs   (42.01 μs .. 43.62 μs)
                     0.997 R²   (0.996 R² .. 0.999 R²)
mean                 42.83 μs   (42.28 μs .. 43.47 μs)
std dev              2.025 μs   (1.721 μs .. 2.439 μs)
variance introduced by outliers: 53% (severely inflated)

benchmarking csecp256k1/ecdh/ecdh
time                 49.23 μs   (48.04 μs .. 50.44 μs)
                     0.997 R²   (0.996 R² .. 0.998 R²)
mean                 48.31 μs   (47.70 μs .. 49.01 μs)
std dev              2.425 μs   (1.959 μs .. 3.020 μs)
variance introduced by outliers: 55% (severely inflated)

Worth noting that I've benchmarked a few variations here:

* replacing various Data.ByteString internals (packCString,
  useAsCString, etc.) with unsafe variants,

* replacing the use of allocaBytes with
  Data.ByteString.Internal.mallocByteString &
  Foreign.ForeignPtr.withForeignPtr, as is done in aseipp's 'ed25519'
  package, and

* using the 'ccall unsafe' FFI calling convention instead of 'capi', as
  recommend by bgamari as a potential performance improvement in GHC's
  'capi' docs.

None yield any measurable performance improvement.

Diffstat:
Mbench/Main.hs | 58+++++++++++++++++++++++++++++++++++++++++++++++-----------
Mlib/Crypto/Secp256k1.hs | 7++++++-
Mppad-csecp256k1.cabal | 65+++++++++++++++++++++++++++++++++--------------------------------
3 files changed, 86 insertions(+), 44 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -5,8 +5,10 @@ module Main where import Control.DeepSeq import Criterion.Main import qualified Crypto.Secp256k1 as S +import qualified Crypto.Secp256k1.Internal as SI import qualified Data.ByteString as BS +instance NFData S.Context instance NFData S.KeyPair instance NFData S.Pub instance NFData S.Sig @@ -14,21 +16,32 @@ instance NFData S.XOnlyPub main :: IO () main = defaultMain [ - sign + suite ] -sign :: Benchmark -sign = bgroup "sign" [ - bench "sign" . nfIO $ sign_bench _HAS _SEC - , bench "sign_schnorr" . nfIO $ - sign_schnorr_bench _HAS _SEC (BS.replicate 32 0) - ] +suite :: Benchmark +suite = envWithCleanup setup destroy $ \ ~(tex, fen, pub, sig) -> + bgroup "csecp256k1" [ + bgroup "ecdsa" [ + bench "sign" . nfIO $ S.sign tex _SEC _HAS + , bench "verify" . nfIO $ S.verify tex pub _HAS sig + ] + , bgroup "schnorr" [ + bench "sign" . nfIO $ S.sign_schnorr tex _HAS _SEC fen + , bench "verify" . nfIO $ S.verify_schnorr tex pub _HAS _SIG_SCHNORR + ] + , bgroup "ecdh" [ + bench "ecdh" . nfIO $ S.ecdh tex pub _SEC + ] + ] where - sign_bench has sec = S.wcontext $ \tex -> - S.sign tex sec has + setup = do + ptr <- SI.secp256k1_context_create SI._SECP256K1_CONTEXT_NONE + pub <- S.wcontext $ \tex -> S.parse_pub tex _PUB_COMPRESSED + sig <- S.wcontext $ \tex -> S.parse_der tex _DER + pure (S.Context ptr, BS.replicate 32 0, pub, sig) - sign_schnorr_bench has sec enn = S.wcontext $ \tex -> - S.sign_schnorr tex has sec enn + destroy (S.Context tex, _, _, _) = SI.secp256k1_context_destroy tex -- inputs @@ -46,3 +59,26 @@ _SEC = mconcat [ , "+\ETX\FS\230\147>\ETX\154" ] +-- 33-byte (compressed) public key +_PUB_COMPRESSED :: BS.ByteString +_PUB_COMPRESSED = mconcat [ + "\ETX\221\237B\ETX\218\201j~\133\242\195t\163|\227\233\201\161U" + , "\167+d\180U\ESC\v\254w\157\212G\ENQ" + ] + +-- DER-encoded signature +_DER :: BS.ByteString +_DER = mconcat [ + "0E\STX!\NUL\245\STX\191\160z\244>~\242ea\139\r\146\154v\EM\238\SOH\214" + , "\NAK\SO7\235n\170\242\200\189\&7\251\"\STX o\EOT\NAK\171\SO\154\151z" + , "\253x\178\194n\243\155\&9R\tm1\159\212\177\SOH\199h\173l\DC3.0E" + ] + +-- 64-byte schnorr signature +_SIG_SCHNORR :: BS.ByteString +_SIG_SCHNORR = mconcat [ + "\214\185AtJ\189\250Gp\NAK2\221\DC2[\182\209\192j{\140^\222R\NUL~" + , "\139d@<\138\163rh\247\152\r\228\175\236\219\156\151\214~\135\&7" + , "\225\&6\234\220;\164R\191\170\186\243\NAK\147\f\144\156ez" + ] + diff --git a/lib/Crypto/Secp256k1.hs b/lib/Crypto/Secp256k1.hs @@ -17,7 +17,7 @@ -- supporting ECDSA/Schnorr signatures and ECDH secret computation. module Crypto.Secp256k1 ( - Context + Context(..) , wcontext , wrcontext @@ -74,7 +74,12 @@ import qualified Foreign.Storable as S (poke, peek) -- -- You should create and use values of this type via 'wrcontext' or -- 'wcontext'. +-- +-- The data constructor is exported only to make the implementation +-- easier to benchmark. You should /not/ pattern match on or +-- manipulate context values. newtype Context = Context (Ptr I.Context) + deriving stock Generic instance Show Context where show (Context tex) = "<bitcoin-core/secp256k1 context " <> show tex <> ">" diff --git a/ppad-csecp256k1.cabal b/ppad-csecp256k1.cabal @@ -33,6 +33,39 @@ library , bytestring , secp256k1-sys +test-suite csecp256k1-tests + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: test + main-is: Main.hs + + ghc-options: + -rtsopts -Wall + + build-depends: + base + , bytestring + , ppad-csecp256k1 + , tasty + , tasty-hunit + +benchmark csecp256k1-bench + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: bench + main-is: Main.hs + + ghc-options: + -rtsopts -O2 -Wall -fno-warn-orphans + + build-depends: + base + , bytestring + , criterion + , deepseq + , ppad-csecp256k1 + , secp256k1-sys + library secp256k1-sys default-language: Haskell2010 hs-source-dirs: secp256k1-sys/lib @@ -83,35 +116,3 @@ test-suite secp256k1-sys-tests , tasty , tasty-hunit -test-suite csecp256k1-tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - - ghc-options: - -rtsopts -Wall - - build-depends: - base - , bytestring - , ppad-csecp256k1 - , tasty - , tasty-hunit - -benchmark csecp256k1-bench - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: bench - main-is: Main.hs - - ghc-options: - -rtsopts -O2 -Wall -fno-warn-orphans - - build-depends: - base - , bytestring - , criterion - , deepseq - , ppad-csecp256k1 -