diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,31 @@
+# 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).
+
+## 1.1.2
+### Changed
+- Simplify `Show` and `Read` instances.
+
+## 1.1.1
+### Removed
+- Old obvious installation instructions from `README.md` file.
+
+### Changed
+- Fix arbitrary instances such that unwanted colissions are less likely.
+
+## 1.1.0
+### Removed
+- Lax parsing for DER.
+- DER parsing of secret keys.
+- Embedded code for library.
+
+## 1.0.0 [YANKED]
+### Added
+- Changelog.
+
+### Changed
+- `Show`/`Read` instances now isomorphic to Haskell code.
+- Semantic versioning.
+- `Hspec` tests.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Haskell bindings for secp256k1
+
+This project contains Haskell bindings for the [secp256k1](https://github.com/bitcoin-core/secp256k1) library from the Bitcoin Core project.
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/UNLICENSE b/UNLICENSE
new file mode 100644
--- /dev/null
+++ b/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+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 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.
+
+For more information, please refer to <http://unlicense.org/>
diff --git a/secp256k1-haskell.cabal b/secp256k1-haskell.cabal
new file mode 100644
--- /dev/null
+++ b/secp256k1-haskell.cabal
@@ -0,0 +1,76 @@
+-- This file has been generated from package.yaml by hpack version 0.28.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 5fa20b19c25e16beba80f187b074b53c7d0bef071771bc3875c6de4afba11a33
+
+name:           secp256k1-haskell
+version:        0.1.0
+synopsis:       Bindings for secp256k1 library from Bitcoin Core
+description:    Sign and verify signatures using the very fast C secp256k1 library from Pieter Wuille. Has Haskell types and abstractions for keys and signatures.
+category:       Crypto
+homepage:       http://github.com/haskoin/secp256k1-haskell#readme
+bug-reports:    https://github.com/haskoin/secp256k1-haskell.git/issues
+author:         Jean-Pierre Rupp
+maintainer:     xenog@protonmail.com
+copyright:      (c) 2017 Jean-Pierre Rupp
+license:        PublicDomain
+license-file:   UNLICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+extra-source-files:
+    CHANGELOG.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/haskoin/secp256k1-haskell.git
+
+library
+  exposed-modules:
+      Crypto.Secp256k1
+      Crypto.Secp256k1.Internal
+  other-modules:
+      Paths_secp256k1_haskell
+  hs-source-dirs:
+      src
+  extra-libraries:
+      secp256k1
+  build-depends:
+      HUnit
+    , QuickCheck
+    , base >=4.8 && <5
+    , base16-bytestring
+    , bytestring
+    , cereal
+    , cryptohash
+    , entropy
+    , hspec
+    , mtl
+    , string-conversions
+  default-language: Haskell2010
+
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Crypto.Secp256k1.InternalSpec
+      Crypto.Secp256k1Spec
+      Paths_secp256k1_haskell
+  hs-source-dirs:
+      test
+  build-depends:
+      HUnit
+    , QuickCheck
+    , base >=4.8 && <5
+    , base16-bytestring
+    , bytestring
+    , cereal
+    , cryptohash
+    , entropy
+    , hspec ==2.*
+    , mtl
+    , secp256k1-haskell
+    , string-conversions
+  default-language: Haskell2010
+  build-tool-depends: hspec-discover:hspec-discover == 2.*
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,449 @@
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-|
+Module      : Crypto.Secp256k1
+License     : MIT
+Maintainer  : Jean-Pierre Rupp <root@haskoin.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(..)
+    , exportCompactSig
+    , importCompactSig
+    -- * Recoverable
+    , RecSig
+    , CompactRecSig(..)
+    , importCompactRecSig
+    , exportCompactRecSig
+    , convertRecSig
+    , signRecMsg
+    , recover
+
+    -- * Addition & Multiplication
+    , Tweak
+    , tweak
+    , getTweak
+    , tweakAddSecKey
+    , tweakMulSecKey
+    , tweakAddPubKey
+    , tweakMulPubKey
+    , combinePubKeys
+    ) where
+
+import           Control.Monad
+import           Crypto.Secp256k1.Internal
+import           Data.Serialize
+import           Data.ByteString           (ByteString)
+import qualified Data.ByteString           as BS
+import qualified Data.ByteString.Base16    as B16
+import           Data.ByteString.Short     (fromShort, toShort)
+import           Data.Maybe
+import           Data.String
+import           Data.String.Conversions
+import           Foreign
+import           System.IO.Unsafe
+import           Test.QuickCheck
+import           Text.Read
+
+newtype PubKey = PubKey (ForeignPtr PubKey64)
+newtype Msg = Msg (ForeignPtr Msg32)
+newtype Sig = Sig (ForeignPtr Sig64)
+newtype SecKey = SecKey (ForeignPtr SecKey32)
+newtype Tweak = Tweak (ForeignPtr Tweak32)
+newtype RecSig = RecSig (ForeignPtr RecSig65)
+
+decodeHex :: ConvertibleStrings a ByteString => a -> Maybe ByteString
+decodeHex str = if BS.null r then Just bs else Nothing where
+    (bs, r) = B16.decode $ cs str
+
+instance Read PubKey where
+    readPrec = do
+        String str <- lexP
+        maybe pfail return $ importPubKey =<< decodeHex str
+
+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.encode . exportPubKey True
+
+instance Read Msg where
+    readPrec = parens $ do
+        String str <- lexP
+        maybe pfail return $ msg =<< decodeHex str
+
+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.encode . 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 Show Sig where
+    showsPrec _ = shows . B16.encode . exportSig
+
+recSigFromString :: String -> Maybe RecSig
+recSigFromString str = do
+    bs <- decodeHex str
+    rs <- either (const Nothing) Just $ decode bs
+    importCompactRecSig rs
+
+instance Read RecSig where
+    readPrec = parens $ do
+        String str <- lexP
+        maybe pfail return $ recSigFromString str
+
+instance IsString RecSig where
+    fromString = fromMaybe e . recSigFromString
+      where
+        e = error "Could not decode signature from hex string"
+
+instance Show RecSig where
+    showsPrec _ = shows . B16.encode . encode . exportCompactRecSig
+
+instance Read SecKey where
+    readPrec = parens $ do
+        String str <- lexP
+        maybe pfail return $ secKey =<< decodeHex str
+
+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.encode . getSecKey
+
+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.encode . getTweak
+
+instance Eq PubKey where
+    fp1 == fp2 = getPubKey fp1 == getPubKey fp2
+
+instance Eq Msg where
+    fm1 == fm2 = getMsg fm1 == getMsg fm2
+
+instance Eq Sig where
+    fg1 == fg2 = exportCompactSig fg1 == exportCompactSig fg2
+
+instance Eq RecSig where
+    fg1 == fg2 = exportCompactRecSig fg1 == exportCompactRecSig fg2
+
+instance Eq SecKey where
+    fk1 == fk2 = getSecKey fk1 == getSecKey fk2
+
+instance Eq Tweak where
+    ft1 == ft2 = getTweak ft1 == getTweak ft2
+
+-- | Import 32-byte 'ByteString' as 'Msg'.
+msg :: ByteString -> Maybe Msg
+msg bs
+    | BS.length bs == 32 = unsafePerformIO $ do
+        fp <- mallocForeignPtr
+        withForeignPtr fp $ flip poke (Msg32 (toShort bs))
+        return $ Just $ Msg fp
+    | otherwise = Nothing
+
+-- | Import 32-byte 'ByteString' as 'SecKey'.
+secKey :: ByteString -> Maybe SecKey
+secKey bs
+    | BS.length bs == 32 = withContext $ \ctx -> do
+        fp <- mallocForeignPtr
+        ret <- withForeignPtr fp $ \p -> do
+            poke p (SecKey32 (toShort bs))
+            ecSecKeyVerify ctx p
+        if isSuccess ret
+            then return $ Just $ SecKey fp
+            else return Nothing
+    | otherwise = Nothing
+
+-- | Convert signature to a normalized lower-S form. Boolean value 'True'
+-- indicates that the signature changed, 'False' indicates that it was already
+-- normal.
+normalizeSig :: Sig -> (Sig, Bool)
+normalizeSig (Sig fg) = withContext $ \ctx -> do
+    fg' <- mallocForeignPtr
+    ret <- withForeignPtr fg $ \pg -> withForeignPtr fg' $ \pg' ->
+        ecdsaSignatureNormalize ctx pg' pg
+    return (Sig fg', isSuccess ret)
+
+-- | 32-Byte 'ByteString' as 'Tweak'.
+tweak :: ByteString -> Maybe Tweak
+tweak bs
+    | BS.length bs == 32 = unsafePerformIO $ do
+        fp <- mallocForeignPtr
+        withForeignPtr fp $ flip poke (Tweak32 (toShort bs))
+        return $ Just $ Tweak fp
+    | otherwise = Nothing
+
+-- | Get 32-byte secret key.
+getSecKey :: SecKey -> ByteString
+getSecKey (SecKey fk) =
+    fromShort $ getSecKey32 $ unsafePerformIO $ withForeignPtr fk peek
+
+-- Get 64-byte public key.
+getPubKey :: PubKey -> ByteString
+getPubKey (PubKey fp) =
+    fromShort $ getPubKey64 $ unsafePerformIO $ withForeignPtr fp peek
+
+-- | Get 32-byte message.
+getMsg :: Msg -> ByteString
+getMsg (Msg fm) =
+    fromShort $ getMsg32 $ unsafePerformIO $ withForeignPtr fm peek
+
+-- | Get 32-byte tweak.
+getTweak :: Tweak -> ByteString
+getTweak (Tweak ft) =
+    fromShort $ getTweak32 $ unsafePerformIO $ withForeignPtr ft peek
+
+-- | Import DER-encoded public key.
+importPubKey :: ByteString -> Maybe PubKey
+importPubKey bs =  withContext $ \ctx -> useByteString bs $ \(b, l) -> do
+    fp <- mallocForeignPtr
+    ret <- withForeignPtr fp $ \p -> ecPubKeyParse ctx p b l
+    if isSuccess ret then return $ Just $ PubKey fp else return Nothing
+
+-- | Encode public key as DER. First argument 'True' for compressed output.
+exportPubKey :: Bool -> PubKey -> ByteString
+exportPubKey compress (PubKey pub) = withContext $ \ctx ->
+    withForeignPtr pub $ \p -> alloca $ \l -> allocaBytes z $ \o -> do
+        poke l (fromIntegral z)
+        ret <- ecPubKeySerialize ctx o l p c
+        unless (isSuccess ret) $ error "could not serialize public key"
+        n <- peek l
+        packByteString (o, n)
+  where
+    c = if compress then compressed else uncompressed
+    z = if compress then 33 else 65
+
+exportCompactSig :: Sig -> CompactSig
+exportCompactSig (Sig fg) = withContext $ \ctx ->
+    withForeignPtr fg $ \pg -> alloca $ \pc -> do
+        ret <- ecdsaSignatureSerializeCompact ctx pc pg
+        unless (isSuccess ret) $ error "Could not obtain compact signature"
+        peek pc
+
+importCompactSig :: CompactSig -> Maybe Sig
+importCompactSig c = withContext $ \ctx -> alloca $ \pc -> do
+    poke pc c
+    fg <- mallocForeignPtr
+    ret <- withForeignPtr fg $ \pg -> ecdsaSignatureParseCompact ctx pg pc
+    if isSuccess ret then return $ Just $ Sig fg else return Nothing
+
+-- | Import DER-encoded signature.
+importSig :: ByteString -> Maybe Sig
+importSig bs = withContext $ \ctx ->
+    useByteString bs $ \(b, l) -> do
+        fg <- mallocForeignPtr
+        ret <- withForeignPtr fg $ \g -> ecdsaSignatureParseDer ctx g b l
+        if isSuccess ret then return $ Just $ Sig fg else return Nothing
+
+-- | Encode signature as strict DER.
+exportSig :: Sig -> ByteString
+exportSig (Sig fg) = withContext $ \ctx ->
+    withForeignPtr fg $ \g -> alloca $ \l -> allocaBytes 72 $ \o -> do
+        poke l 72
+        ret <- ecdsaSignatureSerializeDer ctx o l g
+        unless (isSuccess ret) $ error "could not serialize signature"
+        n <- peek l
+        packByteString (o, n)
+
+-- | Verify message signature. 'True' means that the signature is correct.
+verifySig :: PubKey -> Sig -> Msg -> Bool
+verifySig (PubKey fp) (Sig fg) (Msg fm) = withContext $ \ctx ->
+    withForeignPtr fp $ \p -> withForeignPtr fg $ \g ->
+        withForeignPtr fm $ \m -> isSuccess <$> ecdsaVerify ctx g m p
+
+signMsg :: SecKey -> Msg -> Sig
+signMsg (SecKey fk) (Msg fm) = withContext $ \ctx ->
+    withForeignPtr fk $ \k -> withForeignPtr fm $ \m -> do
+        fg <- mallocForeignPtr
+        ret <- withForeignPtr fg $ \g -> ecdsaSign ctx g m k nullFunPtr nullPtr
+        unless (isSuccess ret) $ error "could not sign message"
+        return $ Sig fg
+
+derivePubKey :: SecKey -> PubKey
+derivePubKey (SecKey fk) = withContext $ \ctx -> withForeignPtr fk $ \k -> do
+    fp <- mallocForeignPtr
+    ret <- withForeignPtr fp $ \p -> ecPubKeyCreate ctx p k
+    unless (isSuccess ret) $ error "could not compute public key"
+    return $ PubKey fp
+
+-- | Add tweak to secret key.
+tweakAddSecKey :: SecKey -> Tweak -> Maybe SecKey
+tweakAddSecKey (SecKey fk) (Tweak ft) = withContext $ \ctx ->
+    withForeignPtr fk $ \k -> withForeignPtr ft $ \t -> do
+        fk' <- mallocForeignPtr
+        ret <- withForeignPtr fk' $ \k' ->  do
+            key <- peek k
+            poke k' key
+            ecSecKeyTweakAdd ctx k' t
+        if isSuccess ret then return $ Just $ SecKey fk' else return Nothing
+
+-- | Multiply secret key by tweak.
+tweakMulSecKey :: SecKey -> Tweak -> Maybe SecKey
+tweakMulSecKey (SecKey fk) (Tweak ft) = withContext $ \ctx ->
+    withForeignPtr fk $ \k -> withForeignPtr ft $ \t -> do
+        fk' <- mallocForeignPtr
+        ret <- withForeignPtr fk' $ \k' ->  do
+            key <- peek k
+            poke k' key
+            ecSecKeyTweakMul ctx k' t
+        if isSuccess ret then return $ Just $ SecKey fk' else return Nothing
+
+-- | Add tweak to public key. Tweak is multiplied first by G to obtain a point.
+tweakAddPubKey :: PubKey -> Tweak -> Maybe PubKey
+tweakAddPubKey (PubKey fp) (Tweak ft) = withContext $ \ctx ->
+    withForeignPtr fp $ \p -> withForeignPtr ft $ \t -> do
+        fp' <- mallocForeignPtr
+        ret <- withForeignPtr fp' $ \p' ->  do
+            pub <- peek p
+            poke p' pub
+            ecPubKeyTweakAdd ctx p' t
+        if isSuccess ret then return $ Just $ PubKey fp' else return Nothing
+
+-- | Multiply public key by tweak. Tweak is multiplied first by G to obtain a
+-- point.
+tweakMulPubKey :: PubKey -> Tweak -> Maybe PubKey
+tweakMulPubKey (PubKey fp) (Tweak ft) = withContext $ \ctx ->
+    withForeignPtr fp $ \p -> withForeignPtr ft $ \t -> do
+        fp' <- mallocForeignPtr
+        ret <- withForeignPtr fp' $ \p' ->  do
+            pub <- peek p
+            poke p' pub
+            ecPubKeyTweakMul ctx p' t
+        if isSuccess ret then return $ Just $ PubKey fp' else return Nothing
+
+-- | Add multiple public keys together.
+combinePubKeys :: [PubKey] -> Maybe PubKey
+combinePubKeys pubs = withContext $ \ctx -> pointers [] pubs $ \ps ->
+    allocaArray (length ps) $ \a -> do
+        pokeArray a ps
+        fp <- mallocForeignPtr
+        ret <- withForeignPtr fp $ \p ->
+            ecPubKeyCombine ctx p a (fromIntegral $ length ps)
+        if isSuccess ret
+            then return $ Just $ PubKey fp
+            else return Nothing
+  where
+    pointers ps [] f = f ps
+    pointers ps (PubKey fp : pubs') f =
+        withForeignPtr fp $ \p -> pointers (p:ps) pubs' f
+
+-- | Parse a compact ECDSA signature (64 bytes + recovery id).
+importCompactRecSig :: CompactRecSig -> Maybe RecSig
+importCompactRecSig cr =
+  if getCompactRecSigV cr `notElem` [0,1,2,3]
+  then Nothing
+  else withContext $ \ctx -> alloca $ \pc -> do
+    let
+      c = CompactSig (getCompactRecSigR cr) (getCompactRecSigS cr)
+      recid = fromIntegral $ getCompactRecSigV cr
+    poke pc c
+    fg <- mallocForeignPtr
+    ret <- withForeignPtr fg $ \pg ->
+        ecdsaRecoverableSignatureParseCompact ctx pg pc recid
+    if isSuccess ret then return $ Just $ RecSig fg else return Nothing
+
+-- | Serialize an ECDSA signature in compact format (64 bytes + recovery id).
+exportCompactRecSig :: RecSig -> CompactRecSig
+exportCompactRecSig (RecSig fg) = withContext $ \ctx ->
+    withForeignPtr fg $ \pg -> alloca $ \pc -> alloca $ \pr -> do
+        ret <- ecdsaRecoverableSignatureSerializeCompact ctx pc pr pg
+        unless (isSuccess ret) $ error "Could not obtain compact signature"
+        CompactSig r s <- peek pc
+        v <- fromIntegral <$> peek pr
+        return $ CompactRecSig r s v
+
+-- | Convert a recoverable signature into a normal signature.
+convertRecSig :: RecSig -> Sig
+convertRecSig (RecSig frg) = withContext $ \ctx ->
+    withForeignPtr frg $ \prg -> do
+        fg <- mallocForeignPtr
+        ret <- withForeignPtr fg $ \pg ->
+            ecdsaRecoverableSignatureConvert ctx pg prg
+        unless (isSuccess ret) $
+            error "Could not convert a recoverable signature"
+        return $ Sig fg
+
+-- | Create a recoverable ECDSA signature.
+signRecMsg :: SecKey -> Msg -> RecSig
+signRecMsg (SecKey fk) (Msg fm) = withContext $ \ctx ->
+    withForeignPtr fk $ \k -> withForeignPtr fm $ \m -> do
+        fg <- mallocForeignPtr
+        ret <- withForeignPtr fg $ \g ->
+            ecdsaSignRecoverable ctx g m k nullFunPtr nullPtr
+        unless (isSuccess ret) $ error "could not sign message"
+        return $ RecSig fg
+
+-- | Recover an ECDSA public key from a signature.
+recover :: RecSig -> Msg -> Maybe PubKey
+recover (RecSig frg) (Msg fm) = withContext $ \ctx ->
+    withForeignPtr frg $ \prg -> withForeignPtr fm $ \pm -> do
+        fp <- mallocForeignPtr
+        ret <- withForeignPtr fp $ \pp -> ecdsaRecover ctx pp prg pm
+        if isSuccess ret then return $ Just $ PubKey fp else return Nothing
+
+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 = do
+        key <- arbitrary
+        return $ derivePubKey key
diff --git a/src/Crypto/Secp256k1/Internal.hs b/src/Crypto/Secp256k1/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Secp256k1/Internal.hs
@@ -0,0 +1,473 @@
+{-# LANGUAGE RecordWildCards #-}
+{-|
+Module      : Crypto.Secp256k1.Internal
+License     : MIT
+Maintainer  : Jean-Pierre Rupp <root@haskoin.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.Internal where
+
+import           Control.Monad
+import           Data.ByteString       (ByteString)
+import qualified Data.ByteString       as BS
+import           Data.ByteString.Short (ShortByteString, fromShort, toShort)
+import           Data.Serialize        (Serialize (..))
+import qualified Data.Serialize.Get    as Get
+import qualified Data.Serialize.Put    as Put
+import           Foreign
+import           Foreign.C
+import           System.Entropy
+import           System.IO.Unsafe
+
+data Ctx = Ctx
+
+newtype PubKey64 = PubKey64 { getPubKey64 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Msg32 = Msg32 { getMsg32 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Sig64 = Sig64 { getSig64 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+data CompactSig =
+    CompactSig
+        { getCompactSigR :: !ShortByteString
+        , getCompactSigS :: !ShortByteString
+        }
+    deriving (Show, Eq, Ord)
+
+newtype RecSig65 = RecSig65 { getRecSig65 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+data CompactRecSig =
+    CompactRecSig
+        { getCompactRecSigR :: !ShortByteString
+        , getCompactRecSigS :: !ShortByteString
+        , getCompactRecSigV :: !Word8
+        }
+    deriving (Show, Eq, Ord)
+
+newtype Seed32 = Seed32 { getSeed32 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype SecKey32 = SecKey32 { getSecKey32 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Tweak32 = Tweak32 { getTweak32 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Nonce32 = Nonce32 { getNonce32 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Algo16 = Algo16 { getAlgo16 :: ShortByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype CtxFlags = CtxFlags { getCtxFlags :: CUInt }
+    deriving (Read, Show, Eq, Ord)
+
+newtype SerFlags = SerFlags { getSerFlags :: CUInt }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Ret = Ret { getRet :: CInt }
+    deriving (Read, Show, Eq, Ord)
+
+-- | Nonce32-generating function
+type NonceFunction a
+    =  Ptr Nonce32
+    -> Ptr Msg32
+    -> Ptr SecKey32
+    -> Ptr Algo16
+    -> Ptr a       -- ^ extra data
+    -> CUInt       -- ^ attempt
+    -> Ret
+
+verify :: CtxFlags
+verify = CtxFlags 0x0101
+
+sign :: CtxFlags
+sign = CtxFlags 0x0201
+
+signVerify :: CtxFlags
+signVerify = CtxFlags 0x0301
+
+compressed :: SerFlags
+compressed = SerFlags 0x0102
+
+uncompressed :: SerFlags
+uncompressed = SerFlags 0x0002
+
+useByteString :: ByteString -> ((Ptr CUChar, CSize) -> IO a) -> IO a
+useByteString bs f =
+    BS.useAsCStringLen bs $ \(b, l) -> f (castPtr b, fromIntegral l)
+
+packByteString :: (Ptr CUChar, CSize) -> IO ByteString
+packByteString (b, l) = BS.packCStringLen (castPtr b, fromIntegral l)
+
+instance Storable PubKey64 where
+    sizeOf _ = 64
+    alignment _ = 1
+    peek p = PubKey64 . toShort <$> packByteString (castPtr p, 64)
+    poke p (PubKey64 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 64
+
+instance Storable Sig64 where
+    sizeOf _ = 64
+    alignment _ = 1
+    peek p = Sig64 . toShort <$> packByteString (castPtr p, 64)
+    poke p (Sig64 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 64
+
+instance Storable CompactSig where
+    sizeOf _ = 64
+    alignment _ = 1
+    peek p = do
+        bs <- BS.packCStringLen (castPtr p, 64)
+        let (s, r) = BS.splitAt 32 bs
+        guard $ BS.length s == 32
+        guard $ BS.length r == 32
+        return CompactSig { getCompactSigR = toShort r
+                          , getCompactSigS = toShort s
+                          }
+    poke p CompactSig{..} =
+        useByteString bs $ \(b, _) -> copyArray (castPtr p) b 64
+      where
+        bs = fromShort getCompactSigS `BS.append` fromShort getCompactSigR
+
+instance Serialize CompactSig where
+    get = do
+        s <- Get.getByteString 32
+        r <- Get.getByteString 32
+        return CompactSig { getCompactSigR = toShort r
+                          , getCompactSigS = toShort s
+                          }
+    put (CompactSig r s) = do
+        Put.putShortByteString s
+        Put.putShortByteString r
+
+instance Storable RecSig65 where
+    sizeOf _ = 65
+    alignment _ = 1
+    peek p = RecSig65 . toShort <$> packByteString (castPtr p, 65)
+    poke p (RecSig65 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 65
+
+instance Storable CompactRecSig where
+    sizeOf _ = 65
+    alignment _ = 1
+    peek p = do
+        bs <- BS.packCStringLen (castPtr p, 65)
+        let (s, r) = BS.splitAt 32 $ BS.take 64 bs
+            v = BS.last bs
+        return CompactRecSig { getCompactRecSigR = toShort r
+                             , getCompactRecSigS = toShort s
+                             , getCompactRecSigV = v
+                             }
+    poke p CompactRecSig{..} =
+        useByteString bs $ \(b, _) -> copyArray (castPtr p) b 65
+      where
+        bs = fromShort getCompactRecSigS `BS.append`
+             fromShort getCompactRecSigR `BS.snoc`
+             getCompactRecSigV
+
+instance Serialize CompactRecSig where
+    get = do
+        s <- Get.getByteString 32
+        r <- Get.getByteString 32
+        v <- Get.getWord8
+        return CompactRecSig { getCompactRecSigR = toShort r
+                             , getCompactRecSigS = toShort s
+                             , getCompactRecSigV = v
+                             }
+    put (CompactRecSig r s v) = do
+        Put.putShortByteString s
+        Put.putShortByteString r
+        Put.putWord8 v
+
+instance Storable Msg32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = Msg32 . toShort <$> packByteString (castPtr p, 32)
+    poke p (Msg32 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable Seed32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = Seed32 . toShort <$> packByteString (castPtr p, 32)
+    poke p (Seed32 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable SecKey32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = SecKey32 . toShort <$> packByteString (castPtr p, 32)
+    poke p (SecKey32 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable Tweak32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = Tweak32 . toShort <$> packByteString (castPtr p, 32)
+    poke p (Tweak32 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable Nonce32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = Nonce32 . toShort <$> packByteString (castPtr p, 32)
+    poke p (Nonce32 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable Algo16 where
+    sizeOf _ = 16
+    alignment _ = 1
+    peek p = Algo16 . toShort <$> packByteString (castPtr p, 16)
+    poke p (Algo16 k) = useByteString (fromShort k) $
+        \(b, _) -> copyArray (castPtr p) b 16
+
+isSuccess :: Ret -> Bool
+isSuccess (Ret 0) = False
+isSuccess (Ret 1) = True
+isSuccess _       = undefined
+
+{-# NOINLINE fctx #-}
+fctx :: ForeignPtr Ctx
+fctx = unsafePerformIO $ do
+    x <- contextCreate signVerify
+    e <- getEntropy 32
+    ret <- alloca $ \s -> poke s (Seed32 (toShort e)) >> contextRandomize x s
+    unless (isSuccess ret) $ error "failed to randomize context"
+    newForeignPtr contextDestroy x
+
+{-# INLINE withContext #-}
+withContext :: (Ptr Ctx -> IO a) -> a
+withContext f = unsafeDupablePerformIO (withForeignPtr fctx f)
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_create"
+    contextCreate
+    :: CtxFlags
+    -> IO (Ptr Ctx)
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_clone"
+    contextClone
+    :: Ptr Ctx
+    -> IO (Ptr Ctx)
+
+foreign import ccall
+    "secp256k1.h &secp256k1_context_destroy"
+    contextDestroy
+    :: FunPtr (Ptr Ctx -> IO ())
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_set_illegal_callback"
+    setIllegalCallback
+    :: Ptr Ctx
+    -> FunPtr (CString -> Ptr a -> IO ()) -- ^ message, data
+    -> Ptr a                              -- ^ data
+    -> IO ()
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_set_error_callback"
+    setErrorCallback
+    :: Ptr Ctx
+    -> FunPtr (CString -> Ptr a -> IO ()) -- ^ message, data
+    -> Ptr a                              -- ^ data
+    -> IO ()
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_parse"
+    ecPubKeyParse
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr CUChar -- ^ encoded public key array
+    -> CSize      -- ^ size of encoded public key array
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_serialize"
+    ecPubKeySerialize
+    :: Ptr Ctx
+    -> Ptr CUChar -- ^ array for encoded public key, must be large enough
+    -> Ptr CSize  -- ^ size of encoded public key, will be updated
+    -> Ptr PubKey64
+    -> SerFlags
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_signature_parse_compact"
+    ecdsaSignatureParseCompact
+    :: Ptr Ctx
+    -> Ptr Sig64
+    -> Ptr CompactSig
+    -> IO Ret
+
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_signature_parse_der"
+    ecdsaSignatureParseDer
+    :: Ptr Ctx
+    -> Ptr Sig64
+    -> Ptr CUChar -- ^ encoded DER signature
+    -> CSize      -- ^ size of encoded signature
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_signature_serialize_der"
+    ecdsaSignatureSerializeDer
+    :: Ptr Ctx
+    -> Ptr CUChar -- ^ array for encoded signature, must be large enough
+    -> Ptr CSize  -- ^ size of encoded signature, will be updated
+    -> Ptr Sig64
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_signature_serialize_compact"
+    ecdsaSignatureSerializeCompact
+    :: Ptr Ctx
+    -> Ptr CompactSig
+    -> Ptr Sig64
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_verify"
+    ecdsaVerify
+    :: Ptr Ctx
+    -> Ptr Sig64
+    -> Ptr Msg32
+    -> Ptr PubKey64
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_signature_normalize"
+    ecdsaSignatureNormalize
+    :: Ptr Ctx
+    -> Ptr Sig64 -- ^ output
+    -> Ptr Sig64 -- ^ input
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_sign"
+    ecdsaSign
+    :: Ptr Ctx
+    -> Ptr Sig64
+    -> Ptr Msg32
+    -> Ptr SecKey32
+    -> FunPtr (NonceFunction a)
+    -> Ptr a -- ^ nonce data
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_seckey_verify"
+    ecSecKeyVerify
+    :: Ptr Ctx
+    -> Ptr SecKey32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_create"
+    ecPubKeyCreate
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr SecKey32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_privkey_tweak_add"
+    ecSecKeyTweakAdd
+    :: Ptr Ctx
+    -> Ptr SecKey32
+    -> Ptr Tweak32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_tweak_add"
+    ecPubKeyTweakAdd
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr Tweak32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_privkey_tweak_mul"
+    ecSecKeyTweakMul
+    :: Ptr Ctx
+    -> Ptr SecKey32
+    -> Ptr Tweak32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_tweak_mul"
+    ecPubKeyTweakMul
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr Tweak32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_randomize"
+    contextRandomize
+    :: Ptr Ctx
+    -> Ptr Seed32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_combine"
+    ecPubKeyCombine
+    :: Ptr Ctx
+    -> Ptr PubKey64 -- ^ pointer to public key storage
+    -> Ptr (Ptr PubKey64) -- ^ pointer to array of public keys
+    -> CInt -- ^ number of public keys
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_parse_compact"
+    ecdsaRecoverableSignatureParseCompact
+    :: Ptr Ctx
+    -> Ptr RecSig65
+    -> Ptr CompactSig
+    -> CInt
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_convert"
+    ecdsaRecoverableSignatureConvert
+    :: Ptr Ctx
+    -> Ptr Sig64
+    -> Ptr RecSig65
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_serialize_compact"
+    ecdsaRecoverableSignatureSerializeCompact
+    :: Ptr Ctx
+    -> Ptr CompactSig
+    -> Ptr CInt
+    -> Ptr RecSig65
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1_recovery.h secp256k1_ecdsa_sign_recoverable"
+    ecdsaSignRecoverable
+    :: Ptr Ctx
+    -> Ptr RecSig65
+    -> Ptr Msg32
+    -> Ptr SecKey32
+    -> FunPtr (NonceFunction a)
+    -> Ptr a -- ^ nonce data
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1_recovery.h secp256k1_ecdsa_recover"
+    ecdsaRecover
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr RecSig65
+    -> Ptr Msg32
+    -> IO Ret
diff --git a/test/Crypto/Secp256k1/InternalSpec.hs b/test/Crypto/Secp256k1/InternalSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Crypto/Secp256k1/InternalSpec.hs
@@ -0,0 +1,417 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Crypto.Secp256k1.InternalSpec (spec) where
+
+import           Control.Monad
+import           Control.Monad.Trans
+import           Crypto.Secp256k1.Internal
+import           Data.ByteString           (ByteString, packCStringLen,
+                                            useAsCStringLen)
+import qualified Data.ByteString.Base16    as B16
+import           Data.ByteString.Short     (toShort)
+import           Foreign
+import           System.Entropy
+import           Test.Hspec
+import           Test.HUnit                (Assertion, assertBool, assertEqual)
+
+spec :: Spec
+spec = do
+    describe "housekeeping" $ do
+        it "creates context" createContextTest
+        it "randomizez context" randomizeContextTest
+        it "clones context" cloneContextTest
+    describe "serialization" $ do
+        it "parses public key" ecPubkeyParseTest
+        it "serializes public key" ecPubKeySerializeTest
+        it "handles storable public key" pubkeyStorableTest
+        it "handles storable signature" signatureStorableTest
+        it "parses DER signature" ecdsaSignatureParseDerTest
+        it "serializes DER signature" ecdsaSignatureSerializeDerTest
+    describe "signatures" $ do
+        it "verifies signature" ecdsaVerifyTest
+        -- TODO:
+        -- , testCase "RFC6979 nonce function"   nonce_function_rfc6979_test
+        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 ->
+    alloca $ \s ->
+    poke s (Seed32 (toShort e)) >> 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
+        alloca $ \pubkey ->
+            ecPubKeyParse x pubkey (castPtr i) (fromIntegral il)
+    assertBool "parsed public key" (isSuccess ret)
+  where
+    der = fst $ B16.decode
+        "03dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705"
+
+ecPubKeySerializeTest :: Assertion
+ecPubKeySerializeTest = do
+    (ret, dec) <- liftIO $ useAsCStringLen der $ \(i, il) ->
+        alloca $ \k -> alloca $ \ol -> allocaBytes 72 $ \o -> do
+        poke ol 72
+        x <- contextCreate verify
+        ret1 <-
+            ecPubKeyParse x k (castPtr i) (fromIntegral 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 = fst $ B16.decode
+        "03dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705"
+
+pubkeyStorableTest :: Assertion
+pubkeyStorableTest = do
+    (pk1, pk2, dec) <- liftIO $ useAsCStringLen der $ \(i, il) -> do
+        x <- contextCreate verify
+        pk1 <- alloca $ \pk -> do
+            ret <-
+                ecPubKeyParse x pk (castPtr i) (fromIntegral il)
+            unless (isSuccess ret) $ error "failed to parse pubkey"
+            peek pk
+        (pk2, dec) <- alloca $ \pk -> alloca $ \ol -> allocaBytes 72 $ \o -> do
+            poke ol 72
+            poke pk pk1
+            ret <-
+                ecPubKeySerialize x o ol pk compressed
+            unless (isSuccess ret) $ error "failed to serialize pubkey"
+            len <- fromIntegral <$> peek ol
+            dec <- packCStringLen (castPtr o, len)
+            pk2 <- peek pk
+            return (pk2, dec)
+        return (pk1, pk2, dec)
+    assertEqual "poke/peek public key" pk1 pk2
+    assertEqual "public key matches" der dec
+  where
+    der = fst $ B16.decode
+        "03dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705"
+
+signatureStorableTest :: Assertion
+signatureStorableTest = do
+    (sig, ret) <- liftIO $ do
+        x <- contextCreate verify
+        g <- alloca $ \pc -> alloca $ \pg -> do
+            poke pc cpt
+            ret <- ecdsaSignatureParseCompact x pg (castPtr pc)
+            unless (isSuccess ret) $ error "failed to parse signature"
+            peek pg
+        alloca $ \pc -> alloca $ \pg -> do
+            poke pg g
+            ret <- ecdsaSignatureSerializeCompact x pc pg
+            c <- peek pc
+            return (c, ret)
+    assertBool "successful serialization" (isSuccess ret)
+    assertEqual "signatures match" cpt sig
+  where
+    cpt = CompactSig
+        (toShort $ fst $ B16.decode "f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37fb22")
+        (toShort $ fst $ B16.decode "6f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e3045")
+
+ecdsaSignatureParseDerTest :: Assertion
+ecdsaSignatureParseDerTest = do
+    ret <- liftIO $ useAsCStringLen der $ \(d, dl) -> alloca $ \s -> do
+        x <- contextCreate verify
+        ecdsaSignatureParseDer x s (castPtr d) (fromIntegral dl)
+    assertBool "parsed signature successfully" $ isSuccess ret
+  where
+    der = fst $ B16.decode
+        "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+        \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+        \45"
+
+parseDer :: Ptr Ctx -> ByteString -> IO Sig64
+parseDer x bs = useAsCStringLen bs $ \(d, dl) -> alloca $ \s -> do
+    ret <- ecdsaSignatureParseDer x s (castPtr d) (fromIntegral dl)
+    unless (isSuccess ret) $ error "could not parse DER"
+    peek s
+
+ecdsaSignatureSerializeDerTest :: Assertion
+ecdsaSignatureSerializeDerTest = do
+    (ret, enc) <- liftIO $ do
+        x <- contextCreate verify
+        sig <- parseDer x der
+        alloca $ \s -> alloca $ \ol -> allocaBytes 72 $ \o -> do
+            poke ol 72
+            poke s sig
+            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 = fst $ B16.decode
+        "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+        \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+        \45"
+
+ecdsaVerifyTest :: Assertion
+ecdsaVerifyTest = do
+    ret <- liftIO $ do
+        x <- contextCreate verify
+        sig <- parseDer x der
+        pk <- useAsCStringLen pub $ \(p, pl) -> alloca $ \k -> do
+            ret <-
+                ecPubKeyParse x k (castPtr p) (fromIntegral pl)
+            unless (isSuccess ret) $ error "could not parse public key"
+            peek k
+        alloca $ \m -> alloca $ \k -> alloca $ \s -> do
+            poke m msg
+            poke k pk
+            poke s sig
+            ecdsaVerify x s m k
+    assertBool "signature valid" $ isSuccess ret
+  where
+    der = fst $ B16.decode
+        "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+        \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+        \45"
+    pub = fst $ B16.decode
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+    msg = Msg32 $ toShort $ fst $ B16.decode
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+
+signCtx :: IO (Ptr Ctx)
+signCtx = contextCreate sign >>= \c ->
+    withEntropy (contextRandomize c) >>= \r ->
+        unless (isSuccess r) (error "failed to randomize context") >> return c
+
+ecdsaSignTest :: Assertion
+ecdsaSignTest = do
+    (ret, sig) <- liftIO $ do
+        x <- signCtx
+        alloca $ \s -> alloca $ \m -> alloca $ \k -> alloca $ \ol ->
+            allocaBytes 72 $ \o -> do
+                poke ol 72
+                poke m msg
+                poke k key
+                ret1 <-
+                    -- TODO:
+                    -- ecdsaSign x s m k nonce_function_default nullPtr
+                    ecdsaSign x s m k nullFunPtr nullPtr
+                ret2 <- ecdsaSignatureSerializeDer x o ol s
+                unless (isSuccess ret2) $ error "could not serialize signature"
+                len <- peek ol
+                sig <- packCStringLen (castPtr o, fromIntegral len)
+                return (ret1, sig)
+    assertBool "successful signing" $ isSuccess ret
+    assertEqual "signature matches" sig der
+  where
+    msg = Msg32 $ toShort $ fst $ B16.decode
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    key = SecKey32 $ toShort $ fst $ B16.decode
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+    der = fst $ B16.decode
+        "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+        \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+        \45"
+
+ecSecKeyVerifyTest :: Assertion
+ecSecKeyVerifyTest = do
+    ret <- liftIO $ alloca $ \k -> do
+        poke k key
+        x <- signCtx
+        ecSecKeyVerify x k
+    assertBool "valid secret key" $ isSuccess ret
+  where
+    key = SecKey32 $ toShort $ fst $ B16.decode
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+
+ecPubkeyCreateTest :: Assertion
+ecPubkeyCreateTest = do
+    (ret, pk) <- liftIO $ alloca $ \p -> alloca $ \k -> do
+        poke k key
+        x <- signCtx
+        ret <- ecPubKeyCreate x p k
+        allocaBytes 65 $ \o -> alloca $ \ol -> do
+            poke ol 65
+            rets <- ecPubKeySerialize
+                x o ol p uncompressed
+            unless (isSuccess rets) $ error "failed to serialive public key"
+            len <- fromIntegral <$> peek ol
+            pk <- packCStringLen (castPtr o, len)
+            return (ret, pk)
+    assertBool "successful pubkey creation" $ isSuccess ret
+    assertEqual "public key matches" pub pk
+  where
+    key = SecKey32 $ toShort $ fst $ B16.decode
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+    pub = fst $ B16.decode
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+
+ecSecKeyTweakAddTest :: Assertion
+ecSecKeyTweakAddTest = do
+    (ret, tweaked) <- liftIO $ do
+        x <- signCtx
+        alloca $ \w -> alloca $ \k -> do
+            poke w tweak
+            poke k key
+            ret <- ecSecKeyTweakAdd x k w
+            tweaked <- peek k
+            return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    key = SecKey32 $ toShort $ fst $ B16.decode
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+    tweak = Tweak32 $ toShort $ fst $ B16.decode
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    expected = SecKey32 $ toShort $ fst $ B16.decode
+        "ec1e3ce1cefa18a671d51125e2b249688d934b0e28f5d1665384d9b02f929059"
+
+ecSecKeyTweakMulTest :: Assertion
+ecSecKeyTweakMulTest = do
+    (ret, tweaked) <- liftIO $ do
+        x <- contextCreate sign
+        retr <- withEntropy $ contextRandomize x
+        unless (isSuccess retr) $ error "failed to randomize context"
+        alloca $ \w -> alloca $ \k -> do
+            poke w tweak
+            poke k key
+            ret <- ecSecKeyTweakMul x k w
+            tweaked <- peek k
+            return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    key = SecKey32 $ toShort $ fst $ B16.decode
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+    tweak = Tweak32 $ toShort $ fst $ B16.decode
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    expected = SecKey32 $ toShort $ fst $ B16.decode
+        "a96f5962493acb179f60a86a9785fc7a30e0c39b64c09d24fe064d9aef15e4c0"
+
+serializeKey :: Ptr 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 :: Ptr Ctx -> ByteString -> IO PubKey64
+parseKey x bs = alloca $ \p -> useAsCStringLen bs $ \(d, dl) -> do
+    ret <- ecPubKeyParse x p (castPtr d) (fromIntegral dl)
+    unless (isSuccess ret) $ error "could not parse public key"
+    peek p
+
+ecPubKeyTweakAddTest :: Assertion
+ecPubKeyTweakAddTest = do
+    (ret, tweaked) <- liftIO $ do
+        x <- contextCreate verify
+        pk <- parseKey x pub
+        alloca $ \w -> alloca $ \p -> do
+            poke w tweak
+            poke p pk
+            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 = fst $ B16.decode
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+    tweak = Tweak32 $ toShort $ fst $ B16.decode
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    expected = fst $ B16.decode
+        "04441c3982b97576646e0df0c96736063df6b42f2ee566d13b9f6424302d1379e518fd\
+        \c87a14c5435bff7a5db4552042cb4120c6b86a4bbd3d0643f3c14ad01368"
+
+ecPubKeyTweakMulTest :: Assertion
+ecPubKeyTweakMulTest = do
+    (ret, tweaked) <- liftIO $ do
+        x <- contextCreate verify
+        pk <- parseKey x pub
+        alloca $ \w -> alloca $ \p -> do
+            poke w tweak
+            poke p pk
+            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 = fst $ B16.decode
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+    tweak = Tweak32 $ toShort $ fst $ B16.decode
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    expected = fst $ B16.decode
+        "04f379dc99cdf5c83e433defa267fbb3377d61d6b779c06a0e4ce29ae3ff5353b12ae4\
+        \9c9d07e7368f2ba5a446c203255ce912322991a2d6a9d5d5761c61ed1845"
+
+ecPubKeyCombineTest :: Assertion
+ecPubKeyCombineTest = do
+    (ret, com) <- liftIO $ alloca $ \p1 -> alloca $ \p2 -> alloca $ \p3 ->
+        allocaArray 3 $ \a -> alloca $ \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 = useAsCStringLen pub $ \(d, dl) -> do
+        ret <- ecPubKeyParse x p (castPtr d) (fromIntegral dl)
+        unless (isSuccess ret) $ error "could not parse public key"
+    pub1 = fst $ B16.decode
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+    pub2 = fst $ B16.decode
+        "0487d82042d93447008dfe2af762068a1e53ff394a5bf8f68a045fa642b99ea5d153f5\
+        \77dd2dba6c7ae4cfd7b6622409d7edd2d76dd13a8092cd3af97b77bd2c77"
+    pub3 = fst $ B16.decode
+        "049b101edcbe1ee37ff6b2318526a425b629e823d7d8d9154417880595a28000ee3feb\
+        \d908754b8ce4e491aa6fe488b41fb5d4bb3788e33c9ff95a7a9229166d59"
+    expected = fst $ B16.decode
+        "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,243 @@
+module Crypto.Secp256k1Spec (spec) where
+
+import           Crypto.Secp256k1
+import qualified Data.ByteString.Base16  as B16
+import qualified Data.ByteString.Char8   as B8
+import           Data.Maybe              (fromMaybe)
+import           Data.Serialize
+import           Data.String             (fromString)
+import           Data.String.Conversions (cs)
+import           Test.Hspec
+import           Test.HUnit              (Assertion, assertEqual)
+import           Test.QuickCheck         (Property, property, (==>))
+
+spec :: Spec
+spec = do
+    describe "signatures" $ do
+        it "signs message" $ property $ signMsgTest
+        it "recovers key from signed message" $ property $ signRecMsgTest
+        it "detects bad signature" $ property $ badSignatureTest
+        it "detects bad recoverable signature" $ property $ badRecSignatureTest
+        it "normalizes signatures" $ property $ normalizeSigTest
+        it "recovers public keys" $ property $ recoverTest
+        it "Bad recover public keys" $ property $ badRecoverTest
+    describe "serialization" $ do
+        it "serializes public key" $ property $ serializePubKeyTest
+        it "serializes DER signature" $ property $ serializeSigTest
+        it "serializes compact signature" $ property $ serializeCompactSigTest
+        it "serializes compact recoverable signature" $
+            property $ serializeCompactRecSigTest
+        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 recoverable signature" $
+            property $ (showReadRecSig :: (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 recoverable signature from string" $ property $ isStringRecSig
+        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
+
+isStringPubKey :: (PubKey, Bool) -> Bool
+isStringPubKey (k, c) = k == fromString (cs hex) where
+    hex = B16.encode $ exportPubKey c k
+
+isStringSig :: (SecKey, Msg) -> Bool
+isStringSig (k, m) = g == fromString (cs hex) where
+    g = signMsg k m
+    hex = B16.encode $ exportSig g
+
+isStringRecSig :: (SecKey, Msg) -> Bool
+isStringRecSig (k, m) = g == fromString (cs hex) where
+    g = signRecMsg k m
+    hex = B16.encode . encode $ exportCompactRecSig g
+
+isStringMsg :: Msg -> Bool
+isStringMsg m = m == fromString (cs m') where
+    m' = B16.encode $ getMsg m
+
+isStringSecKey :: SecKey -> Bool
+isStringSecKey k = k == fromString (cs hex) where
+    hex = B16.encode $ getSecKey k
+
+isStringTweak :: SecKey -> Bool
+isStringTweak k = t == fromString (cs hex) where
+    t = fromMaybe e . tweak $ getSecKey k
+    hex = B16.encode $ 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
+
+showReadRecSig :: (SecKey, Msg) -> Bool
+showReadRecSig (k, m) = showRead recSig where
+    recSig = signRecMsg 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
+
+signRecMsgTest :: (Msg, SecKey) -> Bool
+signRecMsgTest (fm, fk) = verifySig fp fg fm where
+    fp = derivePubKey fk
+    fg = convertRecSig $ signRecMsg fk fm
+
+recoverTest :: (Msg, SecKey) -> Bool
+recoverTest (fm, fk) = recover fg fm == Just fp where
+    fp = derivePubKey fk
+    fg = signRecMsg fk fm
+
+badRecoverTest :: (Msg, SecKey, Msg) -> Property
+badRecoverTest (fm, fk, fm') =
+  fm' /= fm ==> fp' /= Nothing ==> fp' /= Just fp
+  where
+    fg  = signRecMsg fk fm
+    fp  = derivePubKey fk
+    fp' = recover fg fm'
+
+badSignatureTest :: (Msg, SecKey, PubKey) -> Bool
+badSignatureTest (fm, fk, fp) = not $ verifySig fp fg fm where
+    fg = signMsg fk fm
+
+badRecSignatureTest :: (Msg, SecKey, PubKey) -> Bool
+badRecSignatureTest (fm, fk, fp) = not $ verifySig fp fg fm where
+    fg = convertRecSig $ signRecMsg fk fm
+
+normalizeSigTest :: (Msg, SecKey) -> Bool
+normalizeSigTest (fm, fk) = not norm && sig == fg where
+    fg = signMsg fk fm
+    (sig, norm) = normalizeSig fg
+
+serializePubKeyTest :: (PubKey, Bool) -> Bool
+serializePubKeyTest (fp, b) =
+    case importPubKey $ exportPubKey b fp of
+        Just fp' -> fp == fp'
+        Nothing  -> False
+
+serializeSigTest :: (Msg, SecKey) -> Bool
+serializeSigTest (fm, fk) =
+    case importSig $ exportSig fg of
+        Just fg' -> fg == fg'
+        Nothing  -> False
+  where
+    fg = signMsg fk fm
+
+serializeCompactSigTest :: (Msg, SecKey) -> Bool
+serializeCompactSigTest (fm, fk) =
+    case importCompactSig $ exportCompactSig fg of
+        Just fg' -> fg == fg'
+        Nothing  -> False
+  where
+    fg = signMsg fk fm
+
+serializeCompactRecSigTest :: (Msg, SecKey) -> Bool
+serializeCompactRecSigTest (fm, fk) =
+    case importCompactRecSig $ exportCompactRecSig fg of
+        Just fg' -> fg == fg'
+        Nothing  -> False
+  where
+    fg = signRecMsg 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 $ fst $ B16.decode $ B8.pack
+            "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        twk <- tweak $ fst $ B16.decode $ B8.pack
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakAddSecKey key twk
+    expected = secKey $ fst $ B16.decode $ B8.pack
+        "ec1e3ce1cefa18a671d51125e2b249688d934b0e28f5d1665384d9b02f929059"
+
+tweakMulSecKeyTest :: Assertion
+tweakMulSecKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        key <- secKey $ fst $ B16.decode $ B8.pack
+            "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        twk <- tweak $ fst $ B16.decode $ B8.pack
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakMulSecKey key twk
+    expected = secKey $ fst $ B16.decode $ B8.pack
+        "a96f5962493acb179f60a86a9785fc7a30e0c39b64c09d24fe064d9aef15e4c0"
+
+tweakAddPubKeyTest :: Assertion
+tweakAddPubKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        pub <- importPubKey $ fst $ B16.decode $ B8.pack
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705\
+            \12213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        twk <- tweak $ fst $ B16.decode $ B8.pack
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakAddPubKey pub twk
+    expected = importPubKey $ fst $ B16.decode $ B8.pack
+        "04441c3982b97576646e0df0c96736063df6b42f2ee566d13b9f6424302d1379e518fd\
+        \c87a14c5435bff7a5db4552042cb4120c6b86a4bbd3d0643f3c14ad01368"
+
+tweakMulPubKeyTest :: Assertion
+tweakMulPubKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        pub <- importPubKey $ fst $ B16.decode $ B8.pack
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705\
+            \12213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        twk <- tweak $ fst $ B16.decode $ B8.pack
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakMulPubKey pub twk
+    expected = importPubKey $ fst $ B16.decode $ B8.pack
+        "04f379dc99cdf5c83e433defa267fbb3377d61d6b779c06a0e4ce29ae3ff5353b12ae4\
+        \9c9d07e7368f2ba5a446c203255ce912322991a2d6a9d5d5761c61ed1845"
+
+combinePubKeyTest :: Assertion
+combinePubKeyTest =
+    assertEqual "combined keys match" expected combined
+  where
+    combined = do
+        pub1 <- importPubKey $ fst $ B16.decode $ B8.pack
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705\
+            \12213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        pub2 <- importPubKey $ fst $ B16.decode $ B8.pack
+            "0487d82042d93447008dfe2af762068a1e53ff394a5bf8f68a045fa642b99ea5d1\
+            \53f577dd2dba6c7ae4cfd7b6622409d7edd2d76dd13a8092cd3af97b77bd2c77"
+        pub3 <- importPubKey $ fst $ B16.decode $ B8.pack
+            "049b101edcbe1ee37ff6b2318526a425b629e823d7d8d9154417880595a28000ee\
+            \3febd908754b8ce4e491aa6fe488b41fb5d4bb3788e33c9ff95a7a9229166d59"
+        combinePubKeys [pub1, pub2, pub3]
+    expected = importPubKey $ fst $ B16.decode $ B8.pack
+        "043d9a7ec70011efc23c33a7e62d2ea73cca87797e3b659d93bea6aa871aebde56c3bc\
+        \6134ca82e324b0ab9c0e601a6d2933afe7fb5d9f3aae900f5c5dc6e362c8"
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 #-}
