csecp256k1

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

commit 756d5f8e4d1d04e0b447578269e435483740169e
parent 2a8386e7d54bcdb1a53f28d893b8b0dac1a8fd16
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 22 Jun 2025 13:26:39 +0400

lib: vertical integration

Diffstat:
MREADME.md | 44++++++++++++++++++++------------------------
Mflake.lock | 32++++++++++++++++++++++++++++++++
Mflake.nix | 10++++++++++
Mppad-csecp256k1.cabal | 2+-
Mtest/BIP340.hs | 15++++++++++-----
Mtest/Wycheproof.hs | 12+++++++++---
6 files changed, 82 insertions(+), 33 deletions(-)

diff --git a/README.md b/README.md @@ -23,38 +23,34 @@ As we bind to libsecp256k1, the resulting functions are very fast: ``` benchmarking csecp256k1/ecdsa/sign - time 33.67 μs (33.43 μs .. 34.00 μs) - 1.000 R² (0.999 R² .. 1.000 R²) - mean 33.74 μs (33.64 μs .. 33.87 μs) - std dev 378.5 ns (259.2 ns .. 606.8 ns) + time 13.31 μs (13.30 μs .. 13.31 μs) + 1.000 R² (1.000 R² .. 1.000 R²) + mean 13.33 μs (13.32 μs .. 13.33 μs) + std dev 11.15 ns (8.932 ns .. 15.01 ns) benchmarking csecp256k1/ecdsa/verify - time 38.01 μs (37.44 μs .. 38.65 μs) - 0.999 R² (0.998 R² .. 1.000 R²) - mean 37.82 μs (37.56 μs .. 38.16 μs) - std dev 912.8 ns (657.5 ns .. 1.263 μs) - variance introduced by outliers: 22% (moderately inflated) + time 12.35 μs (12.34 μs .. 12.38 μs) + 1.000 R² (1.000 R² .. 1.000 R²) + mean 12.35 μs (12.35 μs .. 12.36 μs) + std dev 21.83 ns (9.273 ns .. 47.76 ns) benchmarking csecp256k1/schnorr/sign - time 49.97 μs (49.60 μs .. 50.41 μs) - 0.999 R² (0.999 R² .. 1.000 R²) - mean 49.95 μs (49.54 μs .. 50.54 μs) - std dev 1.618 μs (1.200 μs .. 2.399 μs) - variance introduced by outliers: 34% (moderately inflated) + time 18.35 μs (18.35 μs .. 18.36 μs) + 1.000 R² (1.000 R² .. 1.000 R²) + mean 18.35 μs (18.35 μs .. 18.35 μs) + std dev 5.990 ns (4.283 ns .. 9.131 ns) benchmarking csecp256k1/schnorr/verify - time 41.84 μs (41.32 μs .. 42.26 μs) - 0.999 R² (0.998 R² .. 0.999 R²) - mean 41.50 μs (41.06 μs .. 41.94 μs) - std dev 1.432 μs (1.167 μs .. 1.715 μs) - variance introduced by outliers: 37% (moderately inflated) + time 14.15 μs (14.14 μs .. 14.15 μs) + 1.000 R² (1.000 R² .. 1.000 R²) + mean 14.14 μs (14.13 μs .. 14.15 μs) + std dev 30.51 ns (14.54 ns .. 57.66 ns) benchmarking csecp256k1/ecdh/ecdh - time 47.43 μs (46.78 μs .. 48.19 μs) - 0.998 R² (0.997 R² .. 0.999 R²) - mean 46.86 μs (46.33 μs .. 47.58 μs) - std dev 2.075 μs (1.609 μs .. 2.747 μs) - variance introduced by outliers: 49% (moderately inflated) + time 15.02 μs (15.02 μs .. 15.03 μs) + 1.000 R² (1.000 R² .. 1.000 R²) + mean 15.02 μs (15.00 μs .. 15.03 μs) + std dev 34.78 ns (10.81 ns .. 71.53 ns) ``` ## Security diff --git a/flake.lock b/flake.lock @@ -34,6 +34,37 @@ "type": "github" } }, + "ppad-base16": { + "inputs": { + "flake-utils": [ + "ppad-base16", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-base16", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1741625558, + "narHash": "sha256-ZBDXRD5fsVqA5bGrAlcnhiu67Eo50q0M9614nR3NBwY=", + "ref": "master", + "rev": "fb63457f2e894eda28250dfe65d0fcd1d195ac2f", + "revCount": 24, + "type": "git", + "url": "git://git.ppad.tech/base16.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/base16.git" + } + }, "ppad-nixpkgs": { "inputs": { "flake-utils": "flake-utils", @@ -95,6 +126,7 @@ "ppad-nixpkgs", "nixpkgs" ], + "ppad-base16": "ppad-base16", "ppad-nixpkgs": "ppad-nixpkgs", "ppad-sha256": "ppad-sha256" } diff --git a/flake.nix b/flake.nix @@ -7,6 +7,12 @@ url = "git://git.ppad.tech/nixpkgs.git"; ref = "master"; }; + ppad-base16 = { + type = "git"; + url = "git://git.ppad.tech/base16.git"; + ref = "master"; + inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + }; ppad-sha256 = { type = "git"; url = "git://git.ppad.tech/sha256.git"; @@ -18,6 +24,7 @@ }; outputs = { self, nixpkgs, flake-utils, ppad-nixpkgs + , ppad-base16 , ppad-sha256 }: flake-utils.lib.eachDefaultSystem (system: @@ -27,11 +34,14 @@ pkgs = import nixpkgs { inherit system; }; hlib = pkgs.haskell.lib; + base16 = ppad-base16.packages.${system}.default; sha256 = ppad-sha256.packages.${system}.default; hpkgs = pkgs.haskell.packages.ghc981.extend (new: old: { + ppad-base16 = base16; ppad-sha256 = sha256; ${lib} = new.callCabal2nix lib ./. { + ppad-base16 = new.ppad-base16; ppad-sha256 = new.ppad-sha256; }; }); diff --git a/ppad-csecp256k1.cabal b/ppad-csecp256k1.cabal @@ -50,8 +50,8 @@ test-suite csecp256k1-tests aeson , attoparsec , base - , base16-bytestring , bytestring + , ppad-base16 , ppad-csecp256k1 , ppad-sha256 , tasty diff --git a/test/BIP340.hs b/test/BIP340.hs @@ -16,6 +16,11 @@ import qualified Data.ByteString.Base16 as B16 import Test.Tasty import Test.Tasty.HUnit +decodeLenient :: BS.ByteString -> BS.ByteString +decodeLenient bs = case B16.decode bs of + Nothing -> error "bang" + Just b -> b + data Case = Case { c_index :: !Int , c_sk :: !BS.ByteString @@ -29,7 +34,7 @@ data Case = Case { execute :: Context -> Case -> TestTree execute tex Case {..} = testCase ("bip0340 " <> show c_index) $ do - par <- try (parse_xonly tex (B16.decodeLenient c_pk)) + par <- try (parse_xonly tex (decodeLenient c_pk)) :: IO (Either Secp256k1Exception XOnlyPub) case par of Left _ -> assertBool mempty (not c_res) @@ -59,15 +64,15 @@ test_case :: AT.Parser Case test_case = do c_index <- AT.decimal AT.<?> "index" _ <- AT.char ',' - c_sk <- fmap B16.decodeLenient (AT.takeWhile (/= ',') AT.<?> "sk") + c_sk <- fmap decodeLenient (AT.takeWhile (/= ',') AT.<?> "sk") _ <- AT.char ',' c_pk <- AT.takeWhile1 (/= ',') AT.<?> "pk" _ <- AT.char ',' - c_aux <- fmap B16.decodeLenient (AT.takeWhile (/= ',') AT.<?> "aux") + c_aux <- fmap decodeLenient (AT.takeWhile (/= ',') AT.<?> "aux") _ <- AT.char ',' - c_msg <- fmap B16.decodeLenient (AT.takeWhile (/= ',') AT.<?> "msg") + c_msg <- fmap decodeLenient (AT.takeWhile (/= ',') AT.<?> "msg") _ <- AT.char ',' - c_sig <- fmap B16.decodeLenient (AT.takeWhile1 (/= ',') AT.<?> "sig") + c_sig <- fmap decodeLenient (AT.takeWhile1 (/= ',') AT.<?> "sig") _ <- AT.char ',' c_res <- (AT.string "TRUE" *> pure True) <|> (AT.string "FALSE" *> pure False) AT.<?> "res" diff --git a/test/Wycheproof.hs b/test/Wycheproof.hs @@ -13,15 +13,21 @@ import Crypto.Curve.Secp256k1 import qualified Crypto.Hash.SHA256 as SHA256 import Data.Aeson ((.:)) import qualified Data.Aeson as A +import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 import qualified Data.Text as T import qualified Data.Text.Encoding as TE import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, testCase) +decodeLenient :: BS.ByteString -> BS.ByteString +decodeLenient bs = case B16.decode bs of + Nothing -> error "bang" + Just b -> b + execute_group :: Context -> EcdsaTestGroup -> IO TestTree execute_group tex EcdsaTestGroup {..} = do - let raw = B16.decodeLenient (TE.encodeUtf8 pk_uncompressed) + let raw = decodeLenient (TE.encodeUtf8 pk_uncompressed) pub <- parse_pub tex raw let tests = fmap (execute tex pub) etg_tests pure (testGroup msg tests) @@ -31,8 +37,8 @@ execute_group tex EcdsaTestGroup {..} = do execute :: Context -> Pub -> EcdsaVerifyTest -> TestTree execute tex pub EcdsaVerifyTest {..} = testCase report $ do - let msg = SHA256.hash (B16.decodeLenient (TE.encodeUtf8 t_msg)) - sig = B16.decodeLenient (TE.encodeUtf8 t_sig) + let msg = SHA256.hash (decodeLenient (TE.encodeUtf8 t_msg)) + sig = decodeLenient (TE.encodeUtf8 t_sig) syg <- try (parse_der tex sig) :: IO (Either Secp256k1Exception Sig) case syg of Left _ -> assertBool mempty (t_result == "invalid")