diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,123 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+## 0.6.1
+### Changed
+- Bump version and LTS Haskell.
+- Update dependencies.
+
+## 0.6.0
+### Changed
+- Remove all use of CPP.
+- Depend on base16 instead of the old base16-bytestring package.
+- Target ghc-9.0.1 compiler.
+
+### Fixed
+- Do not crash on bytestrings that are backed by null C pointers.
+
+## 0.5.0
+### Changed
+- Include version boundaries and other changes submitted by Emily Pillmore.
+
+## 0.4.0
+### Changed
+- Remove fragile ForeignPtr implementation in favor of just storing ByteStrings.
+- Reuse memory instead of copying when possible.
+
+## 0.3.1
+### Fixed
+- Use unsafe calls in FFI.
+
+## 0.3.0
+### Fixed
+- Compiles with all flags now.
+
+### Added
+- Script to compile with all flags.
+
+### Removed
+- Remove ECDH support.
+- Remove Schnorr support.
+- Remove Recovery support.
+
+## 0.2.5
+### Changed
+- Reuse context aggressively.
+- Generate context in a single thread.
+
+### Fixed
+- Memory deallocation bug.
+
+## 0.2.4
+### Changed
+- Update Cabal and package version.
+
+## 0.2.3
+### Changed
+- Return meaningful error upon encountering weird ret status from upstream code.
+
+### Added
+- Test parallel signature creation and verification.
+
+## 0.2.2
+### Removed
+- Hide tweak negation behind a flag for compatibilidy with Debian 9.
+
+### Fixed
+- Correct code that was not compiling with some flags enabled.
+
+## 0.2.1
+### Changed
+- Do not depend on hardcoded DER signatures in tests.
+
+## 0.2.0
+### Added
+- Support for ECDH APIs.
+- Support for Schnorr APIs.
+
+### Removed
+- Enabling key recovery APIs need a flag.
+
+## 0.1.8
+### Added
+- Add missing `NFData` instances for some types.
+
+## 0.1.7
+### Added
+- Add `NFData` instances for all types.
+
+## 0.1.6
+### Added
+- Use `pkgconfig` for C library dependency.
+
+## 0.1.5
+### Added
+- Flag for ECDH bindings.
+
+## 0.1.4
+### Changed
+- Constrain imports to avoid clashes with a QuickCheck function.
+
+## 0.1.3
+### Added
+- Hashable instances for various types.
+
+## 0.1.2
+### Changed
+- Separate dependencies between library and tests.
+- Remove `hspec` default to prevent problems with Nix.
+
+### Removed
+- Dependency to `cryptohash` not needed.
+
+## 0.1.1
+### Changed
+- Update changelog to reflect name and version change.
+- Update to LTS Haskell 12.9.
+
+## 0.1.0
+### Changed
+- Name of package change from `secp256k1` to `secp256k1-haskell` to avoid Nix conflicts.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2020 Haskoin Developers
+Copyright 2022 Keagan McClelland
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+[![Build Status](https://travis-ci.org/haskoin/secp256k1-haskell.svg?branch=master)](https://travis-ci.org/haskoin/secp256k1-haskell)
+
+# Haskell bindings for secp256k1
+
+This project contains Haskell bindings for the
+[secp256k1](https://github.com/bitcoin-core/secp256k1) library.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/libsecp256k1.cabal b/libsecp256k1.cabal
new file mode 100644
--- /dev/null
+++ b/libsecp256k1.cabal
@@ -0,0 +1,79 @@
+cabal-version: 2.0
+
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+
+name:           libsecp256k1
+version:        0.0.0
+synopsis:       Bindings for secp256k1
+description:    Sign and verify signatures using the secp256k1 library.
+category:       Crypto
+homepage:       http://github.com/ProofOfKeags/secp256k1-haskell#readme
+bug-reports:    https://github.com/ProofOfKeags/libsecp256k1-haskell.git/issues
+author:         Keagan McClelland
+maintainer:     keagan.mcclelland@gmail.com
+copyright:      (c) 2017 Jean-Pierre Rupp; (c) 2020 Haskoin Developers; (c) 2022 Keagan McClelland
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    CHANGELOG.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/ProofOfKeags/libsecp256k1-haskell.git
+
+library
+  exposed-modules:
+      Crypto.Secp256k1
+      Crypto.Secp256k1.Prim
+      Paths_libsecp256k1
+  autogen-modules:
+      Paths_libsecp256k1
+  hs-source-dirs:
+      src
+  pkgconfig-depends:
+      libsecp256k1
+  build-depends:
+      QuickCheck >=2.9.2 && <2.15
+    , base >=4.9 && <5
+    , base16 >=0.3.0.1
+    , bytestring >=0.10.8 && <0.12
+    , cereal >=0.5.4 && <0.6
+    , deepseq >=1.4.2 && <1.5
+    , entropy >=0.3.8 && <0.5
+    , hashable >=1.2.6 && <1.5
+    , string-conversions ==0.4.*
+    , unliftio-core >=0.1.0 && <0.3
+  default-language: Haskell2010
+
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Crypto.Secp256k1.PrimSpec
+      Crypto.Secp256k1Spec
+      Paths_libsecp256k1
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      HUnit
+    , QuickCheck >=2.9.2 && <2.15
+    , base >=4.9 && <5
+    , base16 >=0.3.0.1
+    , bytestring >=0.10.8 && <0.12
+    , cereal >=0.5.4 && <0.6
+    , deepseq >=1.4.2 && <1.5
+    , entropy >=0.3.8 && <0.5
+    , hashable >=1.2.6 && <1.5
+    , hspec
+    , monad-par
+    , mtl
+    , secp256k1-haskell
+    , string-conversions ==0.4.*
+    , unliftio-core >=0.1.0 && <0.3
+  default-language: Haskell2010
+  build-tool-depends: hspec-discover:hspec-discover
diff --git a/src/Crypto/Secp256k1.hs b/src/Crypto/Secp256k1.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Secp256k1.hs
@@ -0,0 +1,510 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+-- |
+-- Module      : Crypto.Secp256k1
+-- License     : UNLICENSE
+-- Maintainer  : Keagan McClelland <keagan.mcclelland@gmail.com>
+-- Stability   : experimental
+-- Portability : POSIX
+--
+-- Crytpographic functions from Bitcoin’s secp256k1 library.
+module Crypto.Secp256k1 (
+    -- * Messages
+    Msg,
+    msg,
+    getMsg,
+
+    -- * Secret Keys
+    SecKey,
+    secKey,
+    getSecKey,
+    derivePubKey,
+
+    -- * Public Keys
+    PubKey,
+    importPubKey,
+    exportPubKey,
+
+    -- * Signatures
+    Sig,
+    signMsg,
+    verifySig,
+    normalizeSig,
+
+    -- ** DER
+    importSig,
+    exportSig,
+
+    -- ** Compact
+    CompactSig,
+    getCompactSig,
+    compactSig,
+    exportCompactSig,
+    importCompactSig,
+
+    -- * Addition & Multiplication
+    Tweak,
+    tweak,
+    getTweak,
+    tweakAddSecKey,
+    tweakMulSecKey,
+    tweakAddPubKey,
+    tweakMulPubKey,
+    combinePubKeys,
+) where
+
+import Control.DeepSeq (NFData)
+import Control.Monad (replicateM, unless, (<=<))
+import qualified Crypto.Secp256k1.Prim as Prim
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Base16 as B16
+import Data.Hashable (Hashable (..))
+import Data.Maybe (fromJust, fromMaybe, isJust)
+import Data.Serialize (
+    Serialize (..),
+    getByteString,
+    putByteString,
+ )
+import Data.String (IsString (..))
+import Data.String.Conversions (ConvertibleStrings, cs)
+import Foreign (
+    alloca,
+    allocaArray,
+    allocaBytes,
+    free,
+    mallocBytes,
+    nullFunPtr,
+    nullPtr,
+    peek,
+    poke,
+    pokeArray,
+ )
+import GHC.Generics (Generic)
+import System.IO.Unsafe (unsafePerformIO)
+import Test.QuickCheck (
+    Arbitrary (..),
+    arbitraryBoundedRandom,
+    suchThat,
+ )
+import Text.Read (
+    Lexeme (String),
+    lexP,
+    parens,
+    pfail,
+    readPrec,
+ )
+
+
+newtype PubKey = PubKey {getPubKey :: ByteString}
+    deriving (Eq, Generic, NFData)
+newtype Msg = Msg {getMsg :: ByteString}
+    deriving (Eq, Generic, NFData)
+newtype Sig = Sig {getSig :: ByteString}
+    deriving (Eq, Generic, NFData)
+newtype SecKey = SecKey {getSecKey :: ByteString}
+    deriving (Eq, Generic, NFData)
+newtype Tweak = Tweak {getTweak :: ByteString}
+    deriving (Eq, Generic, NFData)
+newtype CompactSig = CompactSig {getCompactSig :: ByteString}
+    deriving (Eq, Generic, NFData)
+
+
+instance Serialize PubKey where
+    put (PubKey bs) = putByteString bs
+    get = PubKey <$> getByteString 64
+
+
+instance Serialize Msg where
+    put (Msg m) = putByteString m
+    get = Msg <$> getByteString 32
+
+
+instance Serialize Sig where
+    put (Sig bs) = putByteString bs
+    get = Sig <$> getByteString 64
+
+
+instance Serialize SecKey where
+    put (SecKey bs) = putByteString bs
+    get = SecKey <$> getByteString 32
+
+
+instance Serialize Tweak where
+    put (Tweak bs) = putByteString bs
+    get = Tweak <$> getByteString 32
+
+
+instance Serialize CompactSig where
+    put (CompactSig bs) = putByteString bs
+    get = CompactSig <$> getByteString 64
+
+
+decodeHex :: ConvertibleStrings a ByteString => a -> Maybe ByteString
+decodeHex str = case B16.decodeBase16 $ cs str of
+    Right bs -> Just bs
+    Left _ -> Nothing
+
+
+instance Read PubKey where
+    readPrec = do
+        String str <- lexP
+        maybe pfail return $ importPubKey =<< decodeHex str
+
+
+instance Hashable PubKey where
+    i `hashWithSalt` k = i `hashWithSalt` exportPubKey True k
+
+
+instance IsString PubKey where
+    fromString = fromMaybe e . (importPubKey <=< decodeHex)
+        where
+            e = error "Could not decode public key from hex string"
+
+
+instance Show PubKey where
+    showsPrec _ = shows . B16.encodeBase16 . exportPubKey True
+
+
+instance Read Msg where
+    readPrec = parens $ do
+        String str <- lexP
+        maybe pfail return $ msg =<< decodeHex str
+
+
+instance Hashable Msg where
+    i `hashWithSalt` m = i `hashWithSalt` getMsg m
+
+
+instance IsString Msg where
+    fromString = fromMaybe e . (msg <=< decodeHex)
+        where
+            e = error "Could not decode message from hex string"
+
+
+instance Show Msg where
+    showsPrec _ = shows . B16.encodeBase16 . getMsg
+
+
+instance Read Sig where
+    readPrec = parens $ do
+        String str <- lexP
+        maybe pfail return $ importSig =<< decodeHex str
+
+
+instance IsString Sig where
+    fromString = fromMaybe e . (importSig <=< decodeHex)
+        where
+            e = error "Could not decode signature from hex string"
+
+
+instance Hashable Sig where
+    i `hashWithSalt` s = i `hashWithSalt` exportSig s
+
+
+instance Show Sig where
+    showsPrec _ = shows . B16.encodeBase16 . exportSig
+
+
+instance Read SecKey where
+    readPrec = parens $ do
+        String str <- lexP
+        maybe pfail return $ secKey =<< decodeHex str
+
+
+instance Hashable SecKey where
+    i `hashWithSalt` k = i `hashWithSalt` getSecKey k
+
+
+instance IsString SecKey where
+    fromString = fromMaybe e . (secKey <=< decodeHex)
+        where
+            e = error "Colud not decode secret key from hex string"
+
+
+instance Show SecKey where
+    showsPrec _ = shows . B16.encodeBase16 . getSecKey
+
+
+instance Hashable Tweak where
+    i `hashWithSalt` t = i `hashWithSalt` getTweak t
+
+
+instance Read Tweak where
+    readPrec = parens $ do
+        String str <- lexP
+        maybe pfail return $ tweak =<< decodeHex str
+
+
+instance IsString Tweak where
+    fromString = fromMaybe e . (tweak <=< decodeHex)
+        where
+            e = error "Could not decode tweak from hex string"
+
+
+instance Show Tweak where
+    showsPrec _ = shows . B16.encodeBase16 . getTweak
+
+
+-- | Import 32-byte 'ByteString' as 'Msg'.
+msg :: ByteString -> Maybe Msg
+msg bs
+    | BS.length bs == 32 = Just (Msg bs)
+    | otherwise = Nothing
+
+
+-- | Import 32-byte 'ByteString' as 'SecKey'.
+secKey :: ByteString -> Maybe SecKey
+secKey bs
+    | BS.length bs == 32 = Just (SecKey bs)
+    | otherwise = Nothing
+
+
+compactSig :: ByteString -> Maybe CompactSig
+compactSig bs
+    | BS.length bs == 64 = Just (CompactSig bs)
+    | otherwise = Nothing
+
+
+-- | Convert signature to a normalized lower-S form. 'Nothing' indicates that it
+-- was already normal.
+normalizeSig :: Sig -> Maybe Sig
+normalizeSig (Sig sig) = unsafePerformIO $
+    Prim.unsafeUseByteString sig $ \(sig_in, _) -> do
+        sig_out <- mallocBytes 64
+        ret <- Prim.ecdsaSignatureNormalize Prim.ctx sig_out sig_in
+        if Prim.isSuccess ret
+            then do
+                bs <- Prim.unsafePackByteString (sig_out, 64)
+                return (Just (Sig bs))
+            else do
+                free sig_out
+                return Nothing
+
+
+-- | 32-Byte 'ByteString' as 'Tweak'.
+tweak :: ByteString -> Maybe Tweak
+tweak bs
+    | BS.length bs == 32 = Just (Tweak bs)
+    | otherwise = Nothing
+
+
+-- | Import DER-encoded public key.
+importPubKey :: ByteString -> Maybe PubKey
+importPubKey bs
+    | BS.null bs = Nothing
+    | otherwise = unsafePerformIO $
+        Prim.unsafeUseByteString bs $ \(input, len) -> do
+            pub_key <- mallocBytes 64
+            ret <- Prim.ecPubkeyParse Prim.ctx pub_key input len
+            if Prim.isSuccess ret
+                then do
+                    out <- Prim.unsafePackByteString (pub_key, 64)
+                    return (Just (PubKey out))
+                else do
+                    free pub_key
+                    return Nothing
+
+
+-- | Encode public key as DER. First argument 'True' for compressed output.
+exportPubKey :: Bool -> PubKey -> ByteString
+exportPubKey compress (PubKey in_bs) = unsafePerformIO $
+    Prim.unsafeUseByteString in_bs $ \(in_ptr, _) ->
+        alloca $ \len_ptr ->
+            allocaBytes len $ \out_ptr -> do
+                poke len_ptr $ fromIntegral len
+                ret <- Prim.ecPubKeySerialize Prim.ctx out_ptr len_ptr in_ptr flags
+                unless (Prim.isSuccess ret) $ error "could not serialize public key"
+                final_len <- peek len_ptr
+                Prim.packByteString (out_ptr, final_len)
+    where
+        len = if compress then 33 else 65
+        flags = if compress then Prim.compressed else Prim.uncompressed
+
+
+exportCompactSig :: Sig -> CompactSig
+exportCompactSig (Sig sig_bs) = unsafePerformIO $
+    Prim.unsafeUseByteString sig_bs $ \(sig_ptr, _) -> do
+        out_ptr <- mallocBytes 64
+        ret <- Prim.ecdsaSignatureSerializeCompact Prim.ctx out_ptr sig_ptr
+        unless (Prim.isSuccess ret) $ do
+            free out_ptr
+            error "Could not obtain compact signature"
+        out_bs <- Prim.unsafePackByteString (out_ptr, 64)
+        return $ CompactSig out_bs
+
+
+importCompactSig :: CompactSig -> Maybe Sig
+importCompactSig (CompactSig compact_sig) = unsafePerformIO $
+    Prim.unsafeUseByteString compact_sig $ \(compact_ptr, _) -> do
+        out_sig <- mallocBytes 64
+        ret <- Prim.ecdsaSignatureParseCompact Prim.ctx out_sig compact_ptr
+        if Prim.isSuccess ret
+            then do
+                out_bs <- Prim.unsafePackByteString (out_sig, 64)
+                return (Just (Sig out_bs))
+            else do
+                free out_sig
+                return Nothing
+
+
+-- | Import DER-encoded signature.
+importSig :: ByteString -> Maybe Sig
+importSig bs
+    | BS.null bs = Nothing
+    | otherwise = unsafePerformIO $
+        Prim.unsafeUseByteString bs $ \(in_ptr, in_len) -> do
+            out_sig <- mallocBytes 64
+            ret <- Prim.ecdsaSignatureParseDer Prim.ctx out_sig in_ptr in_len
+            if Prim.isSuccess ret
+                then do
+                    out_bs <- Prim.unsafePackByteString (out_sig, 64)
+                    return (Just (Sig out_bs))
+                else do
+                    free out_sig
+                    return Nothing
+
+
+-- | Encode signature as strict DER.
+exportSig :: Sig -> ByteString
+exportSig (Sig in_sig) = unsafePerformIO $
+    Prim.unsafeUseByteString in_sig $ \(in_ptr, _) ->
+        alloca $ \out_len ->
+            allocaBytes 72 $ \out_ptr -> do
+                poke out_len 72
+                ret <- Prim.ecdsaSignatureSerializeDer Prim.ctx out_ptr out_len in_ptr
+                unless (Prim.isSuccess ret) $ error "could not serialize signature"
+                final_len <- peek out_len
+                Prim.packByteString (out_ptr, final_len)
+
+
+-- | Verify message signature. 'True' means that the signature is correct.
+verifySig :: PubKey -> Sig -> Msg -> Bool
+verifySig (PubKey pub_key) (Sig sig) (Msg m) = unsafePerformIO $
+    Prim.unsafeUseByteString pub_key $ \(pub_key_ptr, _) ->
+        Prim.unsafeUseByteString sig $ \(sig_ptr, _) ->
+            Prim.unsafeUseByteString m $ \(msg_ptr, _) ->
+                Prim.isSuccess <$> Prim.ecdsaVerify Prim.ctx sig_ptr msg_ptr pub_key_ptr
+
+
+signMsg :: SecKey -> Msg -> Sig
+signMsg (SecKey sec_key) (Msg m) = unsafePerformIO $
+    Prim.unsafeUseByteString sec_key $ \(sec_key_ptr, _) ->
+        Prim.unsafeUseByteString m $ \(msg_ptr, _) -> do
+            sig_ptr <- mallocBytes 64
+            ret <- Prim.ecdsaSign Prim.ctx sig_ptr msg_ptr sec_key_ptr nullFunPtr nullPtr
+            unless (Prim.isSuccess ret) $ do
+                free sig_ptr
+                error "could not sign message"
+            Sig <$> Prim.unsafePackByteString (sig_ptr, 64)
+
+
+derivePubKey :: SecKey -> PubKey
+derivePubKey (SecKey sec_key) = unsafePerformIO $
+    Prim.unsafeUseByteString sec_key $ \(sec_key_ptr, _) -> do
+        pub_key_ptr <- mallocBytes 64
+        ret <- Prim.ecPubKeyCreate Prim.ctx pub_key_ptr sec_key_ptr
+        unless (Prim.isSuccess ret) $ do
+            free pub_key_ptr
+            error "could not compute public key"
+        PubKey <$> Prim.unsafePackByteString (pub_key_ptr, 64)
+
+
+-- | Add tweak to secret key.
+tweakAddSecKey :: SecKey -> Tweak -> Maybe SecKey
+tweakAddSecKey (SecKey sec_key) (Tweak t) = unsafePerformIO $
+    Prim.unsafeUseByteString new_bs $ \(sec_key_ptr, _) ->
+        Prim.unsafeUseByteString t $ \(tweak_ptr, _) -> do
+            ret <- Prim.ecSeckeyTweakAdd Prim.ctx sec_key_ptr tweak_ptr
+            if Prim.isSuccess ret
+                then return (Just (SecKey new_bs))
+                else return Nothing
+    where
+        new_bs = BS.copy sec_key
+
+
+-- | Multiply secret key by tweak.
+tweakMulSecKey :: SecKey -> Tweak -> Maybe SecKey
+tweakMulSecKey (SecKey sec_key) (Tweak t) = unsafePerformIO $
+    Prim.unsafeUseByteString new_bs $ \(sec_key_ptr, _) ->
+        Prim.unsafeUseByteString t $ \(tweak_ptr, _) -> do
+            ret <- Prim.ecSeckeyTweakMul Prim.ctx sec_key_ptr tweak_ptr
+            if Prim.isSuccess ret
+                then return (Just (SecKey new_bs))
+                else return Nothing
+    where
+        new_bs = BS.copy sec_key
+
+
+-- | Add tweak to public key. Tweak is multiplied first by G to obtain a point.
+tweakAddPubKey :: PubKey -> Tweak -> Maybe PubKey
+tweakAddPubKey (PubKey pub_key) (Tweak t) = unsafePerformIO $
+    Prim.unsafeUseByteString new_bs $ \(pub_key_ptr, _) ->
+        Prim.unsafeUseByteString t $ \(tweak_ptr, _) -> do
+            ret <- Prim.ecPubKeyTweakAdd Prim.ctx pub_key_ptr tweak_ptr
+            if Prim.isSuccess ret
+                then return (Just (PubKey new_bs))
+                else return Nothing
+    where
+        new_bs = BS.copy pub_key
+
+
+-- | Multiply public key by tweak. Tweak is multiplied first by G to obtain a
+-- point.
+tweakMulPubKey :: PubKey -> Tweak -> Maybe PubKey
+tweakMulPubKey (PubKey pub_key) (Tweak t) = unsafePerformIO $
+    Prim.unsafeUseByteString new_bs $ \(pub_key_ptr, _) ->
+        Prim.unsafeUseByteString t $ \(tweak_ptr, _) -> do
+            ret <- Prim.ecPubKeyTweakMul Prim.ctx pub_key_ptr tweak_ptr
+            if Prim.isSuccess ret
+                then return (Just (PubKey new_bs))
+                else return Nothing
+    where
+        new_bs = BS.copy pub_key
+
+
+-- | Add multiple public keys together.
+combinePubKeys :: [PubKey] -> Maybe PubKey
+combinePubKeys [] = Nothing
+combinePubKeys pubs = unsafePerformIO $
+    pointers [] pubs $ \ps ->
+        allocaArray (length ps) $ \a -> do
+            out <- mallocBytes 64
+            pokeArray a ps
+            ret <- Prim.ecPubKeyCombine Prim.ctx out a (fromIntegral $ length ps)
+            if Prim.isSuccess ret
+                then do
+                    bs <- Prim.unsafePackByteString (out, 64)
+                    return (Just (PubKey bs))
+                else do
+                    free out
+                    return Nothing
+    where
+        pointers ps [] f = f ps
+        pointers ps (PubKey pub_key : pub_keys) f =
+            Prim.unsafeUseByteString pub_key $ \(p, _) ->
+                pointers (p : ps) pub_keys f
+
+
+instance Arbitrary Msg where
+    arbitrary = gen_msg
+        where
+            valid_bs = bs_gen `suchThat` isJust
+            bs_gen = msg . BS.pack <$> replicateM 32 arbitraryBoundedRandom
+            gen_msg = fromJust <$> valid_bs
+
+
+instance Arbitrary SecKey where
+    arbitrary = gen_key
+        where
+            valid_bs = bs_gen `suchThat` isJust
+            bs_gen = secKey . BS.pack <$> replicateM 32 arbitraryBoundedRandom
+            gen_key = fromJust <$> valid_bs
+
+
+instance Arbitrary PubKey where
+    arbitrary = derivePubKey <$> arbitrary
diff --git a/src/Crypto/Secp256k1/Prim.hs b/src/Crypto/Secp256k1/Prim.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Secp256k1/Prim.hs
@@ -0,0 +1,554 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE RankNTypes #-}
+
+-- |
+-- Module      : Crypto.Secp256k1.Prim
+-- License     : UNLICENSE
+-- Maintainer  : Keagan McClelland <keagan.mcclelland@gmail.com>
+-- Stability   : experimental
+-- Portability : POSIX
+--
+-- The API for this module may change at any time. This is an internal module only
+-- exposed for hacking and experimentation.
+module Crypto.Secp256k1.Prim where
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Unsafe as BU
+import Foreign (FunPtr, Ptr, castPtr)
+import Foreign.C (
+    CInt (..),
+    CSize (..),
+    CString,
+    CUChar,
+    CUInt (..),
+ )
+import GHC.TypeNats (Nat)
+import System.IO.Unsafe (unsafePerformIO)
+
+
+data LCtx
+data Pubkey64
+data XonlyPubkey64
+data Keypair96
+data Msg32
+data RecSig65
+data Sig64
+data Compact64
+data Seed32
+data Seckey32
+data Tweak32
+data SchnorrExtra
+data Scratch
+data Bytes (n :: Nat)
+
+
+type CtxFlags = CUInt
+type SerFlags = CUInt
+type Ret = CInt
+
+
+type NonceFun a =
+    Ptr CUChar ->
+    Ptr CUChar ->
+    Ptr CUChar ->
+    Ptr CUChar ->
+    Ptr a ->
+    CInt ->
+    IO CInt
+
+
+type NonceFunHardened a =
+    Ptr CUChar ->
+    Ptr CUChar ->
+    CSize ->
+    Ptr CUChar ->
+    Ptr CUChar ->
+    Ptr CUChar ->
+    CSize ->
+    Ptr a ->
+    IO CInt
+
+
+type EcdhHashFun a =
+    Ptr CUChar ->
+    Ptr CUChar ->
+    Ptr CUChar ->
+    Ptr a ->
+    IO CInt
+
+
+type Ctx = Ptr LCtx
+
+
+verify :: CtxFlags
+verify = 0x0101
+
+
+sign :: CtxFlags
+sign = 0x0201
+
+
+signVerify :: CtxFlags
+signVerify = 0x0301
+
+
+compressed :: SerFlags
+compressed = 0x0102
+
+
+uncompressed :: SerFlags
+uncompressed = 0x0002
+
+
+isSuccess :: Ret -> Bool
+isSuccess 0 = False
+isSuccess 1 = True
+isSuccess n = error $ "isSuccess expected 0 or 1 but got " ++ show n
+
+
+unsafeUseByteString :: ByteString -> ((Ptr a, CSize) -> IO b) -> IO b
+unsafeUseByteString bs f =
+    BU.unsafeUseAsCStringLen bs $ \(b, l) ->
+        f (castPtr b, fromIntegral l)
+
+
+useByteString :: ByteString -> ((Ptr a, CSize) -> IO b) -> IO b
+useByteString bs f =
+    BS.useAsCStringLen bs $ \(b, l) ->
+        f (castPtr b, fromIntegral l)
+
+
+unsafePackByteString :: (Ptr a, CSize) -> IO ByteString
+unsafePackByteString (b, l) =
+    BU.unsafePackMallocCStringLen (castPtr b, fromIntegral l)
+
+
+packByteString :: (Ptr a, CSize) -> IO ByteString
+packByteString (b, l) =
+    BS.packCStringLen (castPtr b, fromIntegral l)
+
+
+ctx :: Ctx
+ctx = unsafePerformIO $ contextCreate signVerify
+{-# NOINLINE ctx #-}
+
+
+-- secp256k1_context_clone
+foreign import ccall safe "secp256k1.h secp256k1_context_clone"
+    contextClone :: Ctx -> IO Ctx
+
+
+-- secp256k1_context_create
+foreign import ccall safe "secp256k1.h secp256k1_context_create"
+    contextCreate :: CtxFlags -> IO Ctx
+
+
+-- secp256k1_context_destroy
+foreign import ccall safe "secp256k1.h &secp256k1_context_destroy"
+    contextDestroy :: FunPtr (Ctx -> IO ())
+
+
+-- secp256k1_context_no_precomp
+foreign import ccall safe "secp256k1.h secp256k1_context_no_precomp"
+    contextNoPrecomp :: Ctx
+
+
+-- secp256k1_context_preallocated_clone
+foreign import ccall safe "secp256k1.h secp256k1_context_preallocated_clone"
+    contextPreallocatedClone :: Ctx -> Ptr (Bytes n) -> IO Ctx
+
+
+-- secp256k1_context_preallocated_clone_size
+foreign import ccall safe "secp256k1.h secp256k1_context_preallocated_clone_size"
+    contextPreallocatedCloneSize :: Ctx -> IO CSize
+
+
+-- secp256k1_context_preallocated_create
+foreign import ccall safe "secp256k1.h secp256k1_context_preallocated_create"
+    contextPreallocatedCreate :: Ptr (Bytes n) -> CUInt -> IO Ctx
+
+
+-- secp256k1_context_preallocated_destroy
+foreign import ccall safe "secp256k1.h secp256k1_context_preallocated_destroy"
+    contextPreallocatedDestroy :: Ctx -> IO ()
+
+
+-- secp256k1_context_preallocated_size
+foreign import ccall safe "secp256k1.h secp256k1_context_preallocated_size"
+    contextPreallocatedSize :: CUInt -> IO CSize
+
+
+-- secp256k1_context_randomize
+foreign import ccall safe "secp256k1.h secp256k1_context_randomize"
+    contextRandomize :: Ctx -> Ptr Seed32 -> IO Ret
+
+
+-- secp256k1_context_set_error_callback
+foreign import ccall safe "secp256k1.h secp256k1_context_set_error_callback"
+    setErrorCallback ::
+        Ctx ->
+        -- | message, data
+        FunPtr (CString -> Ptr a -> IO ()) ->
+        -- | data
+        Ptr a ->
+        IO ()
+
+
+-- secp256k1_context_set_illegal_callback
+foreign import ccall safe "secp256k1.h secp256k1_context_set_illegal_callback"
+    setIllegalCallback ::
+        Ctx ->
+        -- | message, data
+        FunPtr (CString -> Ptr a -> IO ()) ->
+        -- | data
+        Ptr a ->
+        IO ()
+
+
+-- secp256k1_ecdh
+foreign import ccall safe "secp256k1.h secp256k1_ecdh"
+    ecdh :: Ctx -> Ptr (Bytes n) -> Ptr Pubkey64 -> Ptr Seckey32 -> FunPtr (EcdhHashFun a) -> Ptr a -> IO Ret
+
+
+-- secp256k1_ecdh_hash_function_default
+foreign import ccall safe "secp256k1.h &secp256k1_ecdh_hash_function_default"
+    ecdhHashFunctionDefault :: FunPtr (EcdhHashFun a)
+
+
+-- secp256k1_ecdh_hash_function_sha256
+foreign import ccall safe "secp256k1.h &secp256k1_ecdh_hash_sha256"
+    ecdhHashSha256 :: FunPtr (EcdhHashFun a)
+
+
+-- secp256k1_ecdsa_recover
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_recover"
+    ecdsaRecover :: Ctx -> Ptr Pubkey64 -> Ptr RecSig65 -> Ptr Msg32 -> IO Ret
+
+
+-- secp256k1_ecdsa_recoverable_signature_convert
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_recoverable_signature_convert"
+    ecdsaRecoverableSignatureConvert :: Ctx -> Ptr Sig64 -> Ptr RecSig65 -> IO Ret
+
+
+-- secp256k1_ecdsa_recoverable_signature_parse_compact
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_recoverable_signature_parse_compact"
+    ecdsaRecoverableSignatureParseCompact :: Ctx -> Ptr RecSig65 -> Ptr (Bytes 64) -> CInt -> IO Ret
+
+
+-- secp256k1_ecdsa_recoverable_signature_serialize_compact
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_recoverable_signature_serialize_compact"
+    ecdsaRecoverableSignatureSerializeCompact :: Ctx -> Ptr (Bytes 64) -> Ptr CInt -> Ptr RecSig65 -> IO Ret
+
+
+-- secp256k1_ecdsa_sign
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_sign"
+    ecdsaSign ::
+        Ctx ->
+        Ptr Sig64 ->
+        Ptr Msg32 ->
+        Ptr Seckey32 ->
+        FunPtr (NonceFun a) ->
+        -- | nonce data
+        Ptr a ->
+        IO Ret
+
+
+-- secp256k1_ecdsa_signature_normalize
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_normalize"
+    ecdsaSignatureNormalize ::
+        Ctx ->
+        -- | output
+        Ptr Sig64 ->
+        -- | input
+        Ptr Sig64 ->
+        IO Ret
+
+
+-- secp256k1_ecdsa_signature_parse_compact
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_parse_compact"
+    ecdsaSignatureParseCompact ::
+        Ctx ->
+        Ptr Sig64 ->
+        Ptr Compact64 ->
+        IO Ret
+
+
+-- secp256k1_ecdsa_signature_parse_der
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_parse_der"
+    ecdsaSignatureParseDer ::
+        Ctx ->
+        Ptr Sig64 ->
+        -- | encoded DER signature
+        Ptr (Bytes n) ->
+        -- | size of encoded signature
+        CSize ->
+        IO Ret
+
+
+-- secp256k1_ecdsa_signature_serialize_compact
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_serialize_compact"
+    ecdsaSignatureSerializeCompact ::
+        Ctx ->
+        Ptr Compact64 ->
+        Ptr Sig64 ->
+        IO Ret
+
+
+-- secp256k1_ecdsa_signature_serialize_der
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_serialize_der"
+    ecdsaSignatureSerializeDer ::
+        Ctx ->
+        -- | array for encoded signature, must be large enough
+        Ptr (Bytes n) ->
+        -- | size of encoded signature, will be updated
+        Ptr CSize ->
+        Ptr Sig64 ->
+        IO Ret
+
+
+-- secp256k1_ecdsa_sign_recoverable
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_sign_recoverable"
+    ecdsaSignRecoverable ::
+        Ctx -> Ptr RecSig65 -> Ptr Msg32 -> Ptr Seckey32 -> FunPtr (NonceFun a) -> Ptr a -> IO Ret
+
+
+-- secp256k1_ecdsa_verify
+foreign import ccall safe "secp256k1.h secp256k1_ecdsa_verify"
+    ecdsaVerify ::
+        Ctx ->
+        Ptr Sig64 ->
+        Ptr Msg32 ->
+        Ptr Pubkey64 ->
+        IO Ret
+
+
+-- secp256k1_ec_privkey_negate
+{-# DEPRECATED ecPrivkeyNegate "use ecSeckeyNegate instead" #-}
+foreign import ccall safe "secp256k1.h secp256k1_ec_privkey_negate"
+    ecPrivkeyNegate ::
+        Ctx ->
+        Ptr Tweak32 ->
+        IO Ret
+
+
+-- secp256k1_ec_privkey_tweak_add
+{-# DEPRECATED ecPrivkeyTweakAdd "use ecSeckeyTweakAdd instead" #-}
+foreign import ccall safe "secp256k1.h secp256k1_ec_privkey_tweak_add"
+    ecPrivkeyTweakAdd ::
+        Ctx ->
+        Ptr Seckey32 ->
+        Ptr Tweak32 ->
+        IO Ret
+
+
+-- secp256k1_ec_privkey_tweak_mul
+{-# DEPRECATED ecPrivkeyTweakMul "use ecSeckeyTweakMul instead" #-}
+foreign import ccall safe "secp256k1.h secp256k1_ec_privkey_tweak_mul"
+    ecPrivkeyTweakMul ::
+        Ctx ->
+        Ptr Seckey32 ->
+        Ptr Tweak32 ->
+        IO Ret
+
+
+-- secp256k1_ec_pubkey_cmp
+foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_cmp"
+    ecPubkeyCmp ::
+        Ctx ->
+        Ptr Pubkey64 ->
+        Ptr Pubkey64
+
+
+-- secp256k1_ec_pubkey_combine
+foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_combine"
+    ecPubKeyCombine ::
+        Ctx ->
+        -- | pointer to public key storage
+        Ptr Pubkey64 ->
+        -- | pointer to array of public keys
+        Ptr (Ptr Pubkey64) ->
+        -- | number of public keys
+        CInt ->
+        IO Ret
+
+
+-- secp256k1_ec_pubkey_create
+foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_create"
+    ecPubKeyCreate ::
+        Ctx ->
+        Ptr Pubkey64 ->
+        Ptr Seckey32 ->
+        IO Ret
+
+
+-- secp256k1_ec_pubkey_negate
+foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_negate"
+    ecPubkeyNegate :: Ctx -> Ptr Pubkey64 -> IO Ret
+
+
+-- secp256k1_ec_pubkey_parse
+foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_parse"
+    ecPubkeyParse ::
+        Ctx ->
+        Ptr Pubkey64 ->
+        -- | encoded public key array
+        Ptr (Bytes n) ->
+        -- | size of encoded public key array
+        CSize ->
+        IO Ret
+
+
+-- secp256k1_ec_pubkey_serialize
+foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_serialize"
+    ecPubKeySerialize ::
+        Ctx ->
+        -- | array for encoded public key, must be large enough
+        Ptr (Bytes n) ->
+        -- | size of encoded public key, will be updated
+        Ptr CSize ->
+        Ptr Pubkey64 ->
+        SerFlags ->
+        IO Ret
+
+
+-- secp256k1_ec_pubkey_tweak_add
+foreign import ccall unsafe "secp256k1.h secp256k1_ec_pubkey_tweak_add"
+    ecPubKeyTweakAdd ::
+        Ctx ->
+        Ptr Pubkey64 ->
+        Ptr Tweak32 ->
+        IO Ret
+
+
+-- secp256k1_ec_pubkey_tweak_mul
+foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_tweak_mul"
+    ecPubKeyTweakMul ::
+        Ctx ->
+        Ptr Pubkey64 ->
+        Ptr Tweak32 ->
+        IO Ret
+
+
+-- secp256k1_ec_seckey_negate
+foreign import ccall safe "secp256k1.h secp256k1_ec_seckey_negate"
+    ecSeckeyNegate :: Ctx -> Ptr Seckey32 -> IO Ret
+
+
+-- secp256k1_ec_seckey_tweak_add
+foreign import ccall safe "secp256k1.h secp256k1_ec_seckey_tweak_add"
+    ecSeckeyTweakAdd :: Ctx -> Ptr Seckey32 -> Ptr Tweak32 -> IO Ret
+
+
+-- secp256k1_ec_seckey_tweak_mul
+foreign import ccall safe "secp256k1.h secp256k1_ec_seckey_tweak_mul"
+    ecSeckeyTweakMul :: Ctx -> Ptr Seckey32 -> Ptr Tweak32 -> IO Ret
+
+
+-- secp256k1_ec_seckey_verify
+foreign import ccall safe "secp256k1.h secp256k1_ec_seckey_verify"
+    ecSecKeyVerify ::
+        Ctx ->
+        Ptr Seckey32 ->
+        IO Ret
+
+
+-- secp256k1_keypair_create
+foreign import ccall safe "secp256k1.h secp256k1_keypair_create"
+    keypairCreate :: Ctx -> Ptr Keypair96 -> Ptr Seckey32 -> IO Ret
+
+
+-- secp256k1_keypair_pub
+foreign import ccall safe "secp256k1.h secp256k1_keypair_pub"
+    keypairPub :: Ctx -> Ptr Pubkey64 -> Ptr Keypair96 -> IO Ret
+
+
+-- secp256k1_keypair_sec
+foreign import ccall safe "secp256k1.h secp256k1_keypair_sec"
+    keypairSec :: Ctx -> Ptr Seckey32 -> Ptr Keypair96 -> IO Ret
+
+
+-- secp256k1_keypair_xonly_pub
+foreign import ccall safe "secp256k1.h secp256k1_keypair_xonly_pub"
+    keypairXonlyPub :: Ctx -> Ptr XonlyPubkey64 -> Ptr CInt -> Ptr Keypair96 -> IO Ret
+
+
+-- secp256k1_keypair_xonly_tweak_add
+foreign import ccall safe "secp256k1.h secp256k1_keypair_xonly_tweak_add"
+    keypairXonlyTweakAdd :: Ctx -> Ptr Keypair96 -> Ptr Tweak32 -> IO Ret
+
+
+-- secp256k1_nonce_function_bip340
+foreign import ccall safe "secp256k1.h &secp256k1_nonce_function_bip340"
+    nonceFunctionBip340 :: FunPtr (NonceFunHardened a)
+
+
+-- secp256k1_nonce_function_default
+foreign import ccall safe "secp256k1.h &secp256k1_nonce_function_default"
+    nonceFunctionDefault :: FunPtr (NonceFun a)
+
+
+-- secp256k1_nonce_function_rfc6979
+foreign import ccall safe "secp256k1.h &secp256k1_nonce_function_rfc6979"
+    nonceFunctionRfc6979 :: FunPtr (NonceFun a)
+
+
+-- secp256k1_schnorrsig_sign
+foreign import ccall safe "secp256k1.h secp256k1_schnorrsig_sign"
+    schnorrsigSign :: Ctx -> Ptr Sig64 -> Ptr Msg32 -> Ptr Keypair96 -> Ptr (Bytes 32) -> IO Ret
+
+
+-- secp256k1_schnorrsig_sign_custom
+foreign import ccall safe "secp256k1.h secp256k1_schnorrsig_sign_custom"
+    schnorrsigSignCustom :: Ctx -> Ptr Sig64 -> Ptr (Bytes n) -> CSize -> Ptr Keypair96 -> Ptr SchnorrExtra -> IO Ret
+
+
+-- secp256k1_schnorrsig_verify
+foreign import ccall safe "secp256k1.h secp256k1_schnorrsig_verify"
+    schnorrSigSignVerify :: Ctx -> Ptr Sig64 -> Ptr (Bytes n) -> CSize -> Ptr XonlyPubkey64 -> IO Ret
+
+
+-- secp256k1_scratch_space_create
+foreign import ccall safe "secp256k1.h secp256k1_scratch_space_create"
+    scratchSpaceCreate :: Ctx -> CSize -> IO (Ptr Scratch)
+
+
+-- secp256k1_scratch_space_destroy
+foreign import ccall safe "secp256k1.h secp256k1_scratch_space_destroy"
+    scratchSpaceDestroy :: Ctx -> Ptr Scratch -> IO ()
+
+
+-- secp256k1_tagged_sha256
+foreign import ccall safe "secp256k1.h secp256k1_tagged_sha256"
+    taggedSha256 :: Ctx -> Ptr (Bytes 32) -> Ptr (Bytes n) -> CSize -> Ptr (Bytes n) -> CSize -> IO Ret
+
+
+-- secp256k1_xonly_pubkey_cmp
+foreign import ccall safe "secp256k1.h secp256k1_xonly_pubkey_cmp"
+    xonlyPubkeyCmp :: Ctx -> Ptr XonlyPubkey64 -> Ptr XonlyPubkey64 -> IO Ret
+
+
+-- secp256k1_xonly_pubkey_from_pubkey
+foreign import ccall safe "secp256k1.h secp256k1_xonly_pubkey_from_pubkey"
+    xonlyPubkeyFromPubkey :: Ctx -> Ptr XonlyPubkey64 -> Ptr CInt -> Ptr Pubkey64 -> IO Ret
+
+
+-- secp256k1_xonly_pubkey_parse
+foreign import ccall safe "secp256k1.h secp256k1_xonly_pubkey_parse"
+    xonlyPubkeyParse :: Ctx -> Ptr XonlyPubkey64 -> Ptr (Bytes 32) -> IO Ret
+
+
+-- secp256k1_xonly_pubkey_serialize
+foreign import ccall safe "secp256k1.h secp256k1_xonly_pubkey_serialize"
+    xonlyPubkeySerialize :: Ctx -> Ptr (Bytes 32) -> Ptr XonlyPubkey64 -> IO Ret
+
+
+-- secp256k1_xonly_pubkey_tweak_add
+foreign import ccall safe "secp256k1.h secp256k1_xonly_pubkey_tweak_add"
+    xonlyPubkeyTweakAdd :: Ctx -> Ptr Pubkey64 -> Ptr XonlyPubkey64 -> Ptr Tweak32 -> IO Ret
+
+
+-- secp256k1_xonly_pubkey_tweak_add_check
+foreign import ccall safe "secp256k1.h secp256k1_xonly_pubkey_tweak_add_check"
+    xonlyPubkeyTweakAddCheck :: Ctx -> Ptr (Bytes 32) -> CInt -> Ptr XonlyPubkey64 -> Ptr Tweak32 -> IO Ret
diff --git a/test/Crypto/Secp256k1/PrimSpec.hs b/test/Crypto/Secp256k1/PrimSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Crypto/Secp256k1/PrimSpec.hs
@@ -0,0 +1,466 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Crypto.Secp256k1.PrimSpec (spec) where
+
+import Control.Monad
+import Control.Monad.Trans
+import Crypto.Secp256k1.Internal
+import Data.ByteString (
+    ByteString,
+    copy,
+    packCStringLen,
+    useAsCStringLen,
+ )
+import qualified Data.ByteString.Base16 as B16
+import Data.Either (fromRight)
+import Foreign
+import System.Entropy
+import Test.HUnit (Assertion, assertBool, assertEqual)
+import Test.Hspec
+
+
+spec :: Spec
+spec = do
+    describe "housekeeping" $ do
+        it "creates context" createContextTest
+        it "randomizes context" randomizeContextTest
+        it "clones context" cloneContextTest
+    describe "serialization" $ do
+        it "parses public key" ecPubkeyParseTest
+        it "serializes public key" ecPubKeySerializeTest
+        it "parses DER signature" ecdsaSignatureParseDerTest
+        it "serializes DER signature" ecdsaSignatureSerializeDerTest
+    describe "signatures" $ do
+        it "verifies signature" ecdsaVerifyTest
+        it "signs message" ecdsaSignTest
+    describe "secret keys" $ do
+        it "verifies secret key" ecSecKeyVerifyTest
+        it "creates public key" ecPubkeyCreateTest
+        it "adds secret key" ecSecKeyTweakAddTest
+        it "multiplies secret key" ecSecKeyTweakMulTest
+    describe "public keys" $ do
+        it "adds public key" ecPubKeyTweakAddTest
+        it "multiplies public key" ecPubKeyTweakMulTest
+        it "combines public keys" ecPubKeyCombineTest
+
+
+withEntropy :: (Ptr Seed32 -> IO a) -> IO a
+withEntropy f =
+    getEntropy 32 >>= \e ->
+        useByteString e $ \(s, _) -> f s
+
+
+createContextTest :: Assertion
+createContextTest = do
+    context_ptr <- liftIO $ contextCreate signVerify
+    assertBool "context not null" $ context_ptr /= nullPtr
+
+
+randomizeContextTest :: Assertion
+randomizeContextTest = do
+    ret <-
+        liftIO $
+            contextCreate sign >>= \x ->
+                withEntropy (contextRandomize x)
+    assertBool "context randomized" $ isSuccess ret
+
+
+cloneContextTest :: Assertion
+cloneContextTest = do
+    (x1, x2) <- liftIO $ do
+        x1 <- contextCreate signVerify
+        ret <- withEntropy $ contextRandomize x1
+        unless (isSuccess ret) $ error "failed to randomize context"
+        x2 <- contextClone x1
+        return (x1, x2)
+    assertBool "original context not null" $ x1 /= nullPtr
+    assertBool "cloned context not null" $ x2 /= nullPtr
+    assertBool "context ptrs different" $ x1 /= x2
+
+
+ecPubkeyParseTest :: Assertion
+ecPubkeyParseTest = do
+    ret <- liftIO $
+        useAsCStringLen der $ \(i, il) -> do
+            x <- contextCreate verify
+            allocaBytes 64 $ \pubkey ->
+                ecPubKeyParse x pubkey (castPtr i) (fromIntegral il)
+    assertBool "parsed public key" (isSuccess ret)
+    where
+        der =
+            fromRight undefined $
+                B16.decodeBase16
+                    "03dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705"
+
+
+ecPubKeySerializeTest :: Assertion
+ecPubKeySerializeTest = do
+    (ret, dec) <- liftIO $
+        useByteString der $ \(i, il) ->
+            allocaBytes 64 $ \k ->
+                alloca $ \ol ->
+                    allocaBytes 72 $ \o -> do
+                        poke ol 72
+                        x <- contextCreate verify
+                        ret1 <- ecPubKeyParse x k i il
+                        unless (isSuccess ret1) $ error "failed to parse pubkey"
+                        ret2 <- ecPubKeySerialize x o ol k compressed
+                        len <- fromIntegral <$> peek ol
+                        decoded <- packCStringLen (castPtr o, len)
+                        return (ret2, decoded)
+    assertBool "serialized public key successfully" $ isSuccess ret
+    assertEqual "public key matches" der dec
+    where
+        der =
+            fromRight undefined $
+                B16.decodeBase16
+                    "03dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705"
+
+
+ecdsaSignatureParseDerTest :: Assertion
+ecdsaSignatureParseDerTest = do
+    ret <- liftIO $
+        useAsCStringLen der $ \(d, dl) -> allocaBytes 64 $ \s -> do
+            x <- contextCreate verify
+            ecdsaSignatureParseDer x s (castPtr d) (fromIntegral dl)
+    assertBool "parsed signature successfully" $ isSuccess ret
+    where
+        der =
+            fromRight undefined $
+                B16.decodeBase16
+                    "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+                    \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+                    \45"
+
+
+parseDer :: Ctx -> ByteString -> IO ByteString
+parseDer x bs =
+    useAsCStringLen bs $ \(d, dl) ->
+        allocaBytes 64 $ \s -> do
+            ret <- ecdsaSignatureParseDer x s (castPtr d) (fromIntegral dl)
+            unless (isSuccess ret) $ error "could not parse DER"
+            packByteString (s, 64)
+
+
+ecdsaSignatureSerializeDerTest :: Assertion
+ecdsaSignatureSerializeDerTest = do
+    (ret, enc) <- liftIO $ do
+        x <- contextCreate verify
+        sig <- parseDer x der
+        alloca $ \ol ->
+            allocaBytes 72 $ \o ->
+                useByteString sig $ \(s, _) -> do
+                    poke ol 72
+                    ret <- ecdsaSignatureSerializeDer x o ol s
+                    len <- fromIntegral <$> peek ol
+                    enc <- packCStringLen (castPtr o, len)
+                    return (ret, enc)
+    assertBool "serialization successful" $ isSuccess ret
+    assertEqual "signatures match" der enc
+    where
+        der =
+            fromRight undefined $
+                B16.decodeBase16
+                    "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+                    \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+                    \45"
+
+
+ecdsaVerifyTest :: Assertion
+ecdsaVerifyTest = do
+    ret <- liftIO $ do
+        x <- contextCreate verify
+        sig <- parseDer x der
+        pk <- useByteString pub $ \(p, pl) ->
+            allocaBytes 64 $ \k -> do
+                ret <- ecPubKeyParse x k p (fromIntegral pl)
+                unless (isSuccess ret) $ error "could not parse public key"
+                packByteString (k, 64)
+        useByteString msg $ \(m, _) ->
+            useByteString pk $ \(k, _) ->
+                useByteString sig $ \(s, _) ->
+                    ecdsaVerify x s m k
+    assertBool "signature valid" $ isSuccess ret
+    where
+        der =
+            fromRight undefined $
+                B16.decodeBase16
+                    "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+                    \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+                    \45"
+        pub =
+            fromRight undefined $
+                B16.decodeBase16
+                    "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+                    \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        msg =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+
+
+signCtx :: IO Ctx
+signCtx =
+    contextCreate sign >>= \c ->
+        withEntropy (contextRandomize c) >>= \r ->
+            unless (isSuccess r) (error "failed to randomize context") >> return c
+
+
+createPubKey :: Ctx -> Ptr SecKey32 -> Ptr PubKey64 -> IO ()
+createPubKey x k p = do
+    ret <- ecPubKeyCreate x p k
+    unless (isSuccess ret) $ error "failed to create public key"
+
+
+ecdsaSignTest :: Assertion
+ecdsaSignTest = do
+    der <- liftIO $ do
+        x <- signCtx
+        allocaBytes 64 $ \s ->
+            useByteString msg $ \(m, _) ->
+                useByteString key $ \(k, _) ->
+                    alloca $ \ol ->
+                        allocaBytes 72 $ \o -> do
+                            poke ol 72
+                            ret1 <- ecdsaSign x s m k nullFunPtr nullPtr
+                            unless (isSuccess ret1) $ error "could not sign message"
+                            ret2 <- ecdsaSignatureSerializeDer x o ol s
+                            unless (isSuccess ret2) $ error "could not serialize signature"
+                            len <- peek ol
+                            packCStringLen (castPtr o, fromIntegral len)
+    ret <- liftIO $ do
+        pub <- allocaBytes 64 $ \p ->
+            useByteString key $ \(s, _) -> do
+                x <- signCtx
+                createPubKey x s p
+                packByteString (p, 64)
+        useByteString msg $ \(m, _) ->
+            useByteString pub $ \(p, _) -> do
+                x <- contextCreate verify
+                s' <- parseDer x der
+                useByteString s' $ \(s, _) -> ecdsaVerify x s m p
+    assertBool "signature matches" (isSuccess ret)
+    where
+        msg =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        key =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+
+
+ecSecKeyVerifyTest :: Assertion
+ecSecKeyVerifyTest = do
+    ret <- liftIO $
+        useByteString key $ \(k, _) -> do
+            x <- signCtx
+            ecSecKeyVerify x k
+    assertBool "valid secret key" $ isSuccess ret
+    where
+        key =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+
+
+ecPubkeyCreateTest :: Assertion
+ecPubkeyCreateTest = do
+    pk <- liftIO $
+        useByteString key $ \(s, _) ->
+            allocaBytes 64 $ \k -> do
+                x <- signCtx
+                createPubKey x s k
+                allocaBytes 65 $ \o ->
+                    alloca $ \ol -> do
+                        poke ol 65
+                        rets <- ecPubKeySerialize x o ol k uncompressed
+                        unless (isSuccess rets) $ error "failed to serialize public key"
+                        len <- fromIntegral <$> peek ol
+                        packCStringLen (castPtr o, len)
+    assertEqual "public key matches" pub pk
+    where
+        key =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        pub =
+            fromRight undefined $
+                B16.decodeBase16
+                    "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+                    \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+
+
+ecSecKeyTweakAddTest :: Assertion
+ecSecKeyTweakAddTest = do
+    (ret, tweaked) <-
+        liftIO $
+            signCtx >>= \x ->
+                useByteString tweak $ \(w, _) ->
+                    useByteString key $ \(k, _) -> do
+                        ret <- ecSecKeyTweakAdd x k w
+                        tweaked <- packByteString (k, 32)
+                        return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+    where
+        key =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        tweak =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        expected =
+            fromRight undefined $
+                B16.decodeBase16
+                    "ec1e3ce1cefa18a671d51125e2b249688d934b0e28f5d1665384d9b02f929059"
+
+
+ecSecKeyTweakMulTest :: Assertion
+ecSecKeyTweakMulTest = do
+    (ret, tweaked) <- liftIO $ do
+        x <- contextCreate sign
+        retr <- withEntropy $ contextRandomize x
+        unless (isSuccess retr) $ error "failed to randomize context"
+        useByteString tweak $ \(w, _) -> useByteString key $ \(k, _) -> do
+            ret <- ecSecKeyTweakMul x k w
+            tweaked <- packByteString (k, 32)
+            return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+    where
+        key =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        tweak =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        expected =
+            fromRight undefined $
+                B16.decodeBase16
+                    "a96f5962493acb179f60a86a9785fc7a30e0c39b64c09d24fe064d9aef15e4c0"
+
+
+serializeKey :: Ctx -> Ptr PubKey64 -> IO ByteString
+serializeKey x p = allocaBytes 72 $ \d -> alloca $ \dl -> do
+    poke dl 72
+    ret <- ecPubKeySerialize x d dl p uncompressed
+    unless (isSuccess ret) $ error "could not serialize public key"
+    len <- peek dl
+    packCStringLen (castPtr d, fromIntegral len)
+
+
+parseKey :: Ctx -> ByteString -> IO ByteString
+parseKey x bs =
+    allocaBytes 64 $ \p ->
+        useByteString bs $ \(d, dl) -> do
+            ret <- ecPubKeyParse x p d dl
+            unless (isSuccess ret) $ error "could not parse public key"
+            packByteString (p, 64)
+
+
+ecPubKeyTweakAddTest :: Assertion
+ecPubKeyTweakAddTest = do
+    (ret, tweaked) <- liftIO $ do
+        x <- contextCreate verify
+        pk <- copy <$> parseKey x pub
+        useByteString tweak $ \(w, _) ->
+            useByteString pk $ \(p, _) -> do
+                ret <- ecPubKeyTweakAdd x p w
+                tweaked <- serializeKey x p
+                return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+    where
+        pub =
+            fromRight undefined $
+                B16.decodeBase16
+                    "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+                    \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        tweak =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        expected =
+            fromRight undefined $
+                B16.decodeBase16
+                    "04441c3982b97576646e0df0c96736063df6b42f2ee566d13b9f6424302d1379e518fd\
+                    \c87a14c5435bff7a5db4552042cb4120c6b86a4bbd3d0643f3c14ad01368"
+
+
+ecPubKeyTweakMulTest :: Assertion
+ecPubKeyTweakMulTest = do
+    (ret, tweaked) <- liftIO $ do
+        x <- contextCreate verify
+        pk <- copy <$> parseKey x pub
+        useByteString tweak $ \(w, _) ->
+            useByteString pk $ \(p, _) -> do
+                ret <- ecPubKeyTweakMul x p w
+                tweaked <- serializeKey x p
+                return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+    where
+        pub =
+            fromRight undefined $
+                B16.decodeBase16
+                    "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+                    \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        tweak =
+            fromRight undefined $
+                B16.decodeBase16
+                    "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        expected =
+            fromRight undefined $
+                B16.decodeBase16
+                    "04f379dc99cdf5c83e433defa267fbb3377d61d6b779c06a0e4ce29ae3ff5353b12ae4\
+                    \9c9d07e7368f2ba5a446c203255ce912322991a2d6a9d5d5761c61ed1845"
+
+
+ecPubKeyCombineTest :: Assertion
+ecPubKeyCombineTest = do
+    (ret, com) <- liftIO $
+        allocaBytes 64 $ \p1 ->
+            allocaBytes 64 $ \p2 ->
+                allocaBytes 64 $ \p3 ->
+                    allocaArray 3 $ \a ->
+                        allocaBytes 64 $ \p -> do
+                            x <- contextCreate verify
+                            parse x pub1 p1
+                            parse x pub2 p2
+                            parse x pub3 p3
+                            pokeArray a [p1, p2, p3]
+                            ret <- ecPubKeyCombine x p a 3
+                            com <- serializeKey x p
+                            return (ret, com)
+    assertBool "successful key combination" $ isSuccess ret
+    assertEqual "combined keys match" expected com
+    where
+        parse x pub p = useByteString pub $ \(d, dl) -> do
+            ret <- ecPubKeyParse x p d dl
+            unless (isSuccess ret) $ error "could not parse public key"
+        pub1 =
+            fromRight undefined $
+                B16.decodeBase16
+                    "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+                    \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        pub2 =
+            fromRight undefined $
+                B16.decodeBase16
+                    "0487d82042d93447008dfe2af762068a1e53ff394a5bf8f68a045fa642b99ea5d153f5\
+                    \77dd2dba6c7ae4cfd7b6622409d7edd2d76dd13a8092cd3af97b77bd2c77"
+        pub3 =
+            fromRight undefined $
+                B16.decodeBase16
+                    "049b101edcbe1ee37ff6b2318526a425b629e823d7d8d9154417880595a28000ee3feb\
+                    \d908754b8ce4e491aa6fe488b41fb5d4bb3788e33c9ff95a7a9229166d59"
+        expected =
+            fromRight undefined $
+                B16.decodeBase16
+                    "043d9a7ec70011efc23c33a7e62d2ea73cca87797e3b659d93bea6aa871aebde56c3bc\
+                    \6134ca82e324b0ab9c0e601a6d2933afe7fb5d9f3aae900f5c5dc6e362c8"
diff --git a/test/Crypto/Secp256k1Spec.hs b/test/Crypto/Secp256k1Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Crypto/Secp256k1Spec.hs
@@ -0,0 +1,248 @@
+module Crypto.Secp256k1Spec (spec) where
+
+import           Control.Monad.Par
+import qualified Control.Monad.Par       as P
+import           Crypto.Secp256k1
+import qualified Data.ByteString         as BS
+import qualified Data.ByteString.Base16  as B16
+import qualified Data.ByteString.Char8   as B8
+import           Data.Either             (fromRight)
+import           Data.Maybe              (fromMaybe, isNothing)
+import           Data.String             (fromString)
+import           Data.String.Conversions (cs)
+import           Test.HUnit              (Assertion, assertEqual)
+import           Test.Hspec
+import           Test.QuickCheck
+
+spec :: Spec
+spec = do
+    describe "signatures" $ do
+        it "signs message" $
+            property signMsgTest
+        it "signs messages in parallel" $
+            property signMsgParTest
+        it "detects bad signature" $
+            property badSignatureTest
+        it "normalizes signatures" $
+            property normalizeSigTest
+    describe "serialization" $ do
+        it "serializes public key" $
+            property serializePubKeyTest
+        it "serializes public keys in parallel" $
+            property parSerializePubKeyTest
+        it "serializes DER signature" $
+            property serializeSigTest
+        it "serializes DER signatures in parallel" $
+            property parSerializeSigTest
+        it "serializes compact signature" $
+            property serializeCompactSigTest
+        it "serialize secret key" $
+            property serializeSecKeyTest
+        it "shows and reads public key" $
+            property (showRead :: PubKey -> Bool)
+        it "shows and reads secret key" $
+            property (showRead :: SecKey -> Bool)
+        it "shows and reads tweak" $
+            property (showReadTweak :: SecKey -> Bool)
+        it "shows and reads signature" $
+            property (showReadSig :: (SecKey, Msg) -> Bool)
+        it "shows and reads message" $
+            property (showRead :: Msg -> Bool)
+        it "reads public key from string" $
+            property isStringPubKey
+        it "reads secret key from string" $
+            property isStringSecKey
+        it "reads signature from string" $
+            property isStringSig
+        it "reads message from string" $
+            property isStringMsg
+        it "reads tweak from string" $
+            property isStringTweak
+    describe "tweaks" $ do
+        it "add secret key" $ property tweakAddSecKeyTest
+        it "multiply secret key" $ property tweakMulSecKeyTest
+        it "add public key" $ property tweakAddPubKeyTest
+        it "multiply public key" $ property tweakMulPubKeyTest
+        it "combine public keys" $ property combinePubKeyTest
+        it "can't combine 0 public keys" $ property combinePubKeyEmptyListTest
+        it "negates tweak" $ property negateTweakTest
+
+hexToBytes :: String -> BS.ByteString
+hexToBytes = fromRight undefined . B16.decodeBase16 . B8.pack
+
+isStringPubKey :: (PubKey, Bool) -> Bool
+isStringPubKey (k, c) = k == fromString (cs hex) where
+    hex = B16.encodeBase16 $ exportPubKey c k
+
+isStringSig :: (SecKey, Msg) -> Bool
+isStringSig (k, m) = g == fromString (cs hex) where
+    g = signMsg k m
+    hex = B16.encodeBase16 $ exportSig g
+
+isStringMsg :: Msg -> Bool
+isStringMsg m = m == fromString (cs m') where
+    m' = B16.encodeBase16 $ getMsg m
+
+isStringSecKey :: SecKey -> Bool
+isStringSecKey k = k == fromString (cs hex) where
+    hex = B16.encodeBase16 $ getSecKey k
+
+isStringTweak :: SecKey -> Bool
+isStringTweak k = t == fromString (cs hex) where
+    t = fromMaybe e . tweak $ getSecKey k
+    hex = B16.encodeBase16 $ getTweak t
+    e = error "Could not extract tweak from secret key"
+
+showReadTweak :: SecKey -> Bool
+showReadTweak k = showRead t where
+    t = tweak $ getSecKey k
+
+showReadSig :: (SecKey, Msg) -> Bool
+showReadSig (k, m) = showRead sig where
+    sig = signMsg k m
+
+showRead :: (Show a, Read a, Eq a) => a -> Bool
+showRead x = read (show x) == x
+
+signMsgTest :: (Msg, SecKey) -> Bool
+signMsgTest (fm, fk) = verifySig fp fg fm where
+    fp = derivePubKey fk
+    fg = signMsg fk fm
+
+signMsgParTest :: [(Msg, SecKey)] -> Bool
+signMsgParTest xs = P.runPar $ do
+    ys <- mapM (P.spawnP . signMsgTest) xs
+    and <$> mapM P.get ys
+
+badSignatureTest :: (Msg, SecKey, PubKey) -> Bool
+badSignatureTest (fm, fk, fp) = not $ verifySig fp fg fm where
+    fg = signMsg fk fm
+
+normalizeSigTest :: (Msg, SecKey) -> Bool
+normalizeSigTest (fm, fk) = isNothing sig where
+    fg = signMsg fk fm
+    sig = normalizeSig fg
+
+serializePubKeyTest :: (PubKey, Bool) -> Bool
+serializePubKeyTest (fp, b) =
+    case importPubKey $ exportPubKey b fp of
+        Just fp' -> fp == fp'
+        Nothing  -> False
+
+parSerializePubKeyTest :: [(PubKey, Bool)] -> Bool
+parSerializePubKeyTest ps = runPar $ do
+    as <- mapM (spawnP . serializePubKeyTest) ps
+    and <$> mapM get as
+
+serializeSigTest :: (Msg, SecKey) -> Bool
+serializeSigTest (fm, fk) =
+    case importSig $ exportSig fg of
+        Just fg' -> fg == fg'
+        Nothing  -> False
+  where
+    fg = signMsg fk fm
+
+parSerializeSigTest :: [(Msg, SecKey)] -> Bool
+parSerializeSigTest ms = runPar $ do
+    as <- mapM (spawnP . serializeSigTest) ms
+    and <$> mapM get as
+
+serializeCompactSigTest :: (Msg, SecKey) -> Bool
+serializeCompactSigTest (fm, fk) =
+    case importCompactSig $ exportCompactSig fg of
+        Just fg' -> fg == fg'
+        Nothing  -> False
+  where
+    fg = signMsg fk fm
+
+serializeSecKeyTest :: SecKey -> Bool
+serializeSecKeyTest fk =
+    case secKey $ getSecKey fk of
+        Just fk' -> fk == fk'
+        Nothing  -> False
+
+tweakAddSecKeyTest :: Assertion
+tweakAddSecKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        key <- secKey $ hexToBytes
+            "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        twk <- tweak $ hexToBytes
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakAddSecKey key twk
+    expected = secKey $ hexToBytes
+        "ec1e3ce1cefa18a671d51125e2b249688d934b0e28f5d1665384d9b02f929059"
+
+tweakMulSecKeyTest :: Assertion
+tweakMulSecKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        key <- secKey $ hexToBytes
+            "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        twk <- tweak $ hexToBytes
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakMulSecKey key twk
+    expected = secKey $ hexToBytes
+        "a96f5962493acb179f60a86a9785fc7a30e0c39b64c09d24fe064d9aef15e4c0"
+
+tweakAddPubKeyTest :: Assertion
+tweakAddPubKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        pub <- importPubKey $ hexToBytes
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd4470512213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        twk <- tweak $ hexToBytes
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakAddPubKey pub twk
+    expected = importPubKey $ hexToBytes
+        "04441c3982b97576646e0df0c96736063df6b42f2ee566d13b9f6424302d1379e518fdc87a14c5435bff7a5db4552042cb4120c6b86a4bbd3d0643f3c14ad01368"
+
+tweakMulPubKeyTest :: Assertion
+tweakMulPubKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        pub <- importPubKey $ hexToBytes
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd4470512213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        twk <- tweak $ hexToBytes
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakMulPubKey pub twk
+    expected = importPubKey $ hexToBytes
+        "04f379dc99cdf5c83e433defa267fbb3377d61d6b779c06a0e4ce29ae3ff5353b12ae49c9d07e7368f2ba5a446c203255ce912322991a2d6a9d5d5761c61ed1845"
+
+combinePubKeyTest :: Assertion
+combinePubKeyTest =
+    assertEqual "combined keys match" expected combined
+  where
+    combined = do
+        pub1 <- importPubKey $ hexToBytes
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd4470512213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        pub2 <- importPubKey $ hexToBytes
+            "0487d82042d93447008dfe2af762068a1e53ff394a5bf8f68a045fa642b99ea5d153f577dd2dba6c7ae4cfd7b6622409d7edd2d76dd13a8092cd3af97b77bd2c77"
+        pub3 <- importPubKey $ hexToBytes
+            "049b101edcbe1ee37ff6b2318526a425b629e823d7d8d9154417880595a28000ee3febd908754b8ce4e491aa6fe488b41fb5d4bb3788e33c9ff95a7a9229166d59"
+        combinePubKeys [pub1, pub2, pub3]
+    expected = importPubKey $ hexToBytes
+        "043d9a7ec70011efc23c33a7e62d2ea73cca87797e3b659d93bea6aa871aebde56c3bc6134ca82e324b0ab9c0e601a6d2933afe7fb5d9f3aae900f5c5dc6e362c8"
+
+combinePubKeyEmptyListTest :: Assertion
+combinePubKeyEmptyListTest =
+    assertEqual "empty pubkey list must return Nothing" expected combined
+  where
+    expected = Nothing
+    combined = combinePubKeys []
+
+negateTweakTest :: Assertion
+negateTweakTest =
+    assertEqual "can recover secret key 1 after adding tweak 1" oneKey subtracted
+  where
+    Just oneKey = secKey $ fromRight undefined $ B16.decodeBase16 $ B8.pack
+        "0000000000000000000000000000000000000000000000000000000000000001"
+    Just oneTwk = tweak $ fromRight undefined $ B16.decodeBase16 $ B8.pack
+        "0000000000000000000000000000000000000000000000000000000000000001"
+    Just minusOneTwk = tweakNegate oneTwk
+    Just twoKey = tweakAddSecKey oneKey oneTwk
+    Just subtracted = tweakAddSecKey twoKey minusOneTwk
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
