diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2018, Monadic GmbH
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Monadic GmbH nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+Haskell implementation of [multihash](https://github.com/multiformats/multihash): "self-identifying hashes".
+
+In contrast to other implementations, this library assumes that applications
+mainly work with the `Crypto.Hash.Digest` type (from the [cryptonite](https://hackage.haskell.org/package/cryptonite) package),
+and use multihash as a wire encoding, or treat an encoded multihash as an opaque
+value i.e. destructuring into a product is not provided.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,6 @@
+module Main where
+
+import Distribution.Extra.Doctest (defaultMainWithDoctests)
+
+main :: IO ()
+main = defaultMainWithDoctests "doctests"
diff --git a/multihash-cryptonite.cabal b/multihash-cryptonite.cabal
new file mode 100644
--- /dev/null
+++ b/multihash-cryptonite.cabal
@@ -0,0 +1,102 @@
+cabal-version: 2.2
+build-type:    Custom
+
+name:          multihash-cryptonite
+version:       0.1.0.0
+synopsis:      Self-identifying hashes, implementation of <https://github.com/multiformats/multihash>
+homepage:      https://github.com/oscoin/ipfs
+bug-reports:   https://github.com/oscoin/ipfs/issues
+license:       BSD-3-Clause
+license-file:  LICENSE
+author:        Kim Altintop
+maintainer:    Kim Altintop <kim@monadic.xyz>, Monadic Team <team@monadic.xyz>
+copyright:     2018 Monadic GmbH
+
+category:      Data
+
+extra-source-files:
+      CHANGELOG.md
+    , README.md
+
+source-repository head
+    type: git
+    location: https://github.com/oscoin/ipfs
+
+custom-setup
+    setup-depends:
+          base >= 4.9 && < 5
+        , Cabal
+        , cabal-doctest >= 1
+
+common common
+    default-language: Haskell2010
+    ghc-options:
+        -Wall
+        -Wcompat
+        -Wincomplete-uni-patterns
+        -Wincomplete-record-updates
+        -Wredundant-constraints
+        -fprint-expanded-synonyms
+        -funbox-small-strict-fields
+    default-extensions:
+        DeriveGeneric
+        LambdaCase
+        MultiWayIf
+        NamedFieldPuns
+        RecordWildCards
+        StrictData
+        TupleSections
+        TypeApplications
+
+library
+    import: common
+    hs-source-dirs: src
+    exposed-modules:
+        Data.Multihash
+        Data.Multihash.Internal
+        Data.Multihash.Lazy
+
+    build-depends:
+        base >= 4.9 && < 5
+      , binary >= 0.8.3
+      , binary-varint
+      , bytestring >= 0.10.4
+      , deepseq
+      , cryptonite >= 0.13
+      , hashable
+      , memory >= 0.11
+
+test-suite properties
+    import: common
+    main-is: Main.hs
+    hs-source-dirs: test/properties
+    type: exitcode-stdio-1.0
+
+    build-depends:
+        base
+      , cryptonite
+      , hedgehog
+      , multihash-cryptonite
+
+    ghc-options:
+        -threaded
+        -rtsopts
+        -with-rtsopts=-N
+
+test-suite doctests
+    import: common
+    main-is: Main.hs
+    hs-source-dirs: test/doctests
+    type: exitcode-stdio-1.0
+
+    other-modules:   Build_doctests
+    autogen-modules: Build_doctests
+
+    build-depends:
+        base
+      , doctest
+
+    ghc-options:
+        -threaded
+        -rtsopts
+        -with-rtsopts=-N
diff --git a/src/Data/Multihash.hs b/src/Data/Multihash.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Multihash.hs
@@ -0,0 +1,164 @@
+{-# LANGUAGE ExistentialQuantification  #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+
+-- |
+-- Copyright   : 2018 Monadic GmbH
+-- License     : BSD3
+-- Maintainer  : kim@monadic.xyz, team@monadic.xyz
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+--
+-- <https://github.com/multiformats/multihash multihash> encoding of 'Crypto.Hash.Digest's.
+--
+-- Example:
+--
+-- >>> :set -XOverloadedStrings
+-- >>> import qualified Crypto.Hash as C
+-- >>> import Data.ByteArray.Encoding (Base(..), convertToBase)
+-- >>> import Data.ByteString (ByteString)
+-- >>> import qualified Data.ByteString.Char8 as C8
+-- >>> import System.IO (stdout)
+-- >>> :{
+--     let
+--         input :: ByteString
+--         input = "multihash"
+--         atBase :: Base -> Multihash -> ByteString
+--         atBase base = convertToBase base . encodedBytes
+--      in
+--         C8.hPutStr stdout $ C8.unlines
+--             [ atBase Base16 $ multihash C.SHA1   input
+--             , atBase Base32 $ multihash C.SHA1   input
+--             , atBase Base16 $ multihash C.SHA256 input
+--             , atBase Base32 $ multihash C.SHA256 input
+--             ]
+-- :}
+-- 111488c2f11fb2ce392acb5b2986e640211c4690073e
+-- CEKIRQXRD6ZM4OJKZNNSTBXGIAQRYRUQA47A====
+-- 12209cbc07c3f991725836a3aa2a581ca2029198aa420b9d99bc0e131d9f3e2cbe47
+-- CIQJZPAHYP4ZC4SYG2R2UKSYDSRAFEMYVJBAXHMZXQHBGHM7HYWL4RY=
+--
+module Data.Multihash
+    ( Multihash
+    , fromDigest
+    , encodedBytes
+    , multihash
+    , decode
+    , decodeDigest
+    , getMultihash
+
+    -- * Compact representation
+    , CompactMultihash
+    , compact
+    , expand
+
+    -- * Re-exports
+    , HashAlgorithm
+    , Multihashable
+    , fromCryptonite
+    , toCode
+    , fromCode
+    , digestSize
+    )
+where
+
+import           Data.Multihash.Internal
+
+import           Control.DeepSeq (NFData)
+import qualified Crypto.Hash as C
+import           Data.Bifunctor (bimap)
+import qualified Data.Binary.Get as Binary
+import           Data.Binary.VarInt (buildVarInt)
+import           Data.ByteArray (ByteArrayAccess, convert)
+import           Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Builder as Builder
+import qualified Data.ByteString.Lazy as LBS
+import           Data.ByteString.Short (ShortByteString, fromShort, toShort)
+import           Data.Coerce (coerce)
+import           Data.Hashable (Hashable)
+
+-- | A multihash-encoded strict 'ByteString'.
+newtype Multihash = Multihash ByteString
+    deriving (Eq, Ord, Hashable, NFData)
+
+-- | A 'Multihash' backed by a 'ShortByteString'.
+--
+-- This is useful when holding many 'Multihash'es in memory, due to lower memory
+-- overhead and less heap fragmentation. See the documentation for
+-- 'ShortByteString' for details.
+newtype CompactMultihash = Compact ShortByteString
+    deriving (Eq, Ord, Hashable, NFData)
+
+-- | Encode a 'C.Digest' as a 'Multihash'.
+fromDigest :: forall a. Multihashable a => C.Digest a -> Multihash
+fromDigest dig =
+    Multihash
+        . LBS.toStrict . Builder.toLazyByteString
+        $ code <> len <> Builder.byteString bytes
+  where
+    code  = buildVarInt . toCode $ fromCryptonite dig
+    bytes = convert dig
+    len   = buildVarInt $ BS.length bytes
+
+-- | Extract the raw, multihash-encoded bytes of a 'Multihash'.
+encodedBytes :: Multihash -> ByteString
+encodedBytes = coerce
+{-# INLINE encodedBytes #-}
+
+-- | Hash a value to a 'Multihash'
+multihash :: (ByteArrayAccess ba, Multihashable a) => a -> ba -> Multihash
+multihash rithm = fromDigest . C.hashWith rithm
+
+-- | Decode a 'Multihash' from a 'ByteString'.
+decode :: ByteString -> Either String Multihash
+decode =
+    bimap _3 _3
+        . Binary.runGetOrFail getMultihash
+        . LBS.fromStrict
+
+-- | Decode a 'C.Digest' from a multihash-encoded 'ByteString'.
+decodeDigest
+    :: forall a. Multihashable a
+    => ByteString
+    -> Either String (C.Digest a)
+decodeDigest =
+    bimap _3 _3
+        . Binary.runGetOrFail getMultihashedDigest
+        . LBS.fromStrict
+
+getMultihash :: Binary.Get Multihash
+getMultihash = do
+    algo <- Binary.lookAhead getHashAlgorithm
+    case algo of
+        Blake2s_160 -> fromDigest <$> getMultihashedDigest @C.Blake2s_160
+        Blake2s_224 -> fromDigest <$> getMultihashedDigest @C.Blake2s_224
+        Blake2s_256 -> fromDigest <$> getMultihashedDigest @C.Blake2s_256
+        Blake2b_160 -> fromDigest <$> getMultihashedDigest @C.Blake2b_160
+        Blake2b_224 -> fromDigest <$> getMultihashedDigest @C.Blake2b_224
+        Blake2b_256 -> fromDigest <$> getMultihashedDigest @C.Blake2b_256
+        Blake2b_384 -> fromDigest <$> getMultihashedDigest @C.Blake2b_384
+        Blake2b_512 -> fromDigest <$> getMultihashedDigest @C.Blake2b_512
+        MD4         -> fromDigest <$> getMultihashedDigest @C.MD4
+        MD5         -> fromDigest <$> getMultihashedDigest @C.MD5
+        SHA1        -> fromDigest <$> getMultihashedDigest @C.SHA1
+        SHA256      -> fromDigest <$> getMultihashedDigest @C.SHA256
+        SHA512      -> fromDigest <$> getMultihashedDigest @C.SHA512
+        Keccak_224  -> fromDigest <$> getMultihashedDigest @C.Keccak_224
+        Keccak_256  -> fromDigest <$> getMultihashedDigest @C.Keccak_256
+        Keccak_384  -> fromDigest <$> getMultihashedDigest @C.Keccak_384
+        Keccak_512  -> fromDigest <$> getMultihashedDigest @C.Keccak_512
+        SHA3_224    -> fromDigest <$> getMultihashedDigest @C.SHA3_224
+        SHA3_256    -> fromDigest <$> getMultihashedDigest @C.SHA3_256
+        SHA3_384    -> fromDigest <$> getMultihashedDigest @C.SHA3_384
+        SHA3_512    -> fromDigest <$> getMultihashedDigest @C.SHA3_512
+
+-- | Convert a 'Multihash' to a compact representation.
+compact :: Multihash -> CompactMultihash
+compact = coerce . toShort . coerce
+{-# INLINE compact #-}
+
+-- | Convert a 'CompactMultihash' to the regular representation.
+expand :: CompactMultihash -> Multihash
+expand = coerce . fromShort . coerce
+{-# INLINE expand #-}
diff --git a/src/Data/Multihash/Internal.hs b/src/Data/Multihash/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Multihash/Internal.hs
@@ -0,0 +1,181 @@
+{-# LANGUAGE ConstraintKinds           #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE ScopedTypeVariables       #-}
+
+-- |
+-- Copyright   : 2018 Monadic GmbH
+-- License     : BSD3
+-- Maintainer  : kim@monadic.xyz, team@monadic.xyz
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+--
+module Data.Multihash.Internal
+    ( HashAlgorithm (..)
+    , Multihashable
+
+    , getMultihashedDigest
+    , getHashAlgorithm
+    , getLength
+
+    , toCode
+    , fromCode
+    , fromCryptonite
+    , digestSize
+
+    , _3
+    )
+where
+
+import           Control.Monad (when)
+import qualified Crypto.Hash as C
+import qualified Data.Binary.Get as Binary
+import           Data.Binary.VarInt (getVarInt)
+import           Data.Proxy (Proxy(..))
+import           Data.Word (Word16)
+
+type Multihashable a = (C.HashAlgorithm a, FromCryptonite a)
+
+getMultihashedDigest :: forall a. Multihashable a => Binary.Get (C.Digest a)
+getMultihashedDigest = do
+    algo <- getHashAlgorithm
+    when (fromCryptonite (Proxy @a) /= algo) $ fail "Algorithm mismatch"
+    len <- getLength
+    dig <- Binary.getByteString len
+    maybe (fail "Invalid Digest") pure $ C.digestFromByteString dig
+
+getHashAlgorithm :: Binary.Get HashAlgorithm
+getHashAlgorithm = do
+    code <- Binary.getWord8 >>= getVarInt
+    case fromCode code of
+        Nothing -> fail ("Unknown HashAlgorithm " <> show code)
+        Just ha -> pure ha
+
+getLength :: Binary.Get Int
+getLength = Binary.getWord8 >>= getVarInt
+
+-- | 'Crypto.Hash.HashAlgorithm's for which we know a multihash code.
+--
+-- Note that this currently excludes variable output-length algorithms.
+data HashAlgorithm =
+      Blake2s_160
+    | Blake2s_224
+    | Blake2s_256
+    | Blake2b_160
+    | Blake2b_224
+    | Blake2b_256
+    | Blake2b_384
+    | Blake2b_512
+    | MD4
+    | MD5
+    | SHA1
+    | SHA256
+    | SHA512
+    | Keccak_224
+    | Keccak_256
+    | Keccak_384
+    | Keccak_512
+    | SHA3_224
+    | SHA3_256
+    | SHA3_384
+    | SHA3_512
+    deriving (Eq, Enum, Bounded)
+
+class FromCryptonite a where
+    fromCryptonite :: proxy a -> HashAlgorithm
+
+instance FromCryptonite C.Blake2s_160 where fromCryptonite _ = Blake2s_160
+instance FromCryptonite C.Blake2s_224 where fromCryptonite _ = Blake2s_224
+instance FromCryptonite C.Blake2s_256 where fromCryptonite _ = Blake2s_256
+instance FromCryptonite C.Blake2b_160 where fromCryptonite _ = Blake2b_160
+instance FromCryptonite C.Blake2b_224 where fromCryptonite _ = Blake2b_224
+instance FromCryptonite C.Blake2b_256 where fromCryptonite _ = Blake2b_256
+instance FromCryptonite C.Blake2b_384 where fromCryptonite _ = Blake2b_384
+instance FromCryptonite C.Blake2b_512 where fromCryptonite _ = Blake2b_512
+instance FromCryptonite C.MD4         where fromCryptonite _ = MD4
+instance FromCryptonite C.MD5         where fromCryptonite _ = MD5
+instance FromCryptonite C.SHA1        where fromCryptonite _ = SHA1
+instance FromCryptonite C.SHA256      where fromCryptonite _ = SHA256
+instance FromCryptonite C.SHA512      where fromCryptonite _ = SHA512
+instance FromCryptonite C.Keccak_224  where fromCryptonite _ = Keccak_224
+instance FromCryptonite C.Keccak_256  where fromCryptonite _ = Keccak_256
+instance FromCryptonite C.Keccak_384  where fromCryptonite _ = Keccak_384
+instance FromCryptonite C.Keccak_512  where fromCryptonite _ = Keccak_512
+instance FromCryptonite C.SHA3_224    where fromCryptonite _ = SHA3_224
+instance FromCryptonite C.SHA3_256    where fromCryptonite _ = SHA3_256
+instance FromCryptonite C.SHA3_384    where fromCryptonite _ = SHA3_384
+instance FromCryptonite C.SHA3_512    where fromCryptonite _ = SHA3_512
+
+toCode :: HashAlgorithm -> Word16
+toCode Blake2s_160 = 0xb254
+toCode Blake2s_224 = 0xb25c
+toCode Blake2s_256 = 0xb260
+toCode Blake2b_160 = 0xb214
+toCode Blake2b_224 = 0xb21c
+toCode Blake2b_256 = 0xb220
+toCode Blake2b_384 = 0xb230
+toCode Blake2b_512 = 0xb240
+toCode MD4         = 0xd4
+toCode MD5         = 0xd5
+toCode SHA1        = 0x11
+toCode SHA256      = 0x12
+toCode SHA512      = 0x13
+toCode Keccak_224  = 0x1A
+toCode Keccak_256  = 0x1B
+toCode Keccak_384  = 0x1C
+toCode Keccak_512  = 0x1D
+toCode SHA3_224    = 0x17
+toCode SHA3_256    = 0x16
+toCode SHA3_384    = 0x15
+toCode SHA3_512    = 0x14
+
+fromCode :: Word16 -> Maybe HashAlgorithm
+fromCode 0xb254 = pure Blake2s_160
+fromCode 0xb25c = pure Blake2s_224
+fromCode 0xb260 = pure Blake2s_256
+fromCode 0xb214 = pure Blake2b_160
+fromCode 0xb21c = pure Blake2b_224
+fromCode 0xb220 = pure Blake2b_256
+fromCode 0xb230 = pure Blake2b_384
+fromCode 0xb240 = pure Blake2b_512
+fromCode 0xd4   = pure MD4
+fromCode 0xd5   = pure MD5
+fromCode 0x11   = pure SHA1
+fromCode 0x12   = pure SHA256
+fromCode 0x13   = pure SHA512
+fromCode 0x1A   = pure Keccak_224
+fromCode 0x1B   = pure Keccak_256
+fromCode 0x1C   = pure Keccak_384
+fromCode 0x1D   = pure Keccak_512
+fromCode 0x17   = pure SHA3_224
+fromCode 0x16   = pure SHA3_256
+fromCode 0x15   = pure SHA3_384
+fromCode 0x14   = pure SHA3_512
+fromCode _      = Nothing
+
+digestSize :: HashAlgorithm -> Int
+digestSize Blake2s_160 = C.hashDigestSize C.Blake2s_160
+digestSize Blake2s_224 = C.hashDigestSize C.Blake2s_224
+digestSize Blake2s_256 = C.hashDigestSize C.Blake2s_256
+digestSize Blake2b_160 = C.hashDigestSize C.Blake2b_160
+digestSize Blake2b_224 = C.hashDigestSize C.Blake2b_224
+digestSize Blake2b_256 = C.hashDigestSize C.Blake2b_256
+digestSize Blake2b_384 = C.hashDigestSize C.Blake2b_384
+digestSize Blake2b_512 = C.hashDigestSize C.Blake2b_512
+digestSize MD4         = C.hashDigestSize C.MD4
+digestSize MD5         = C.hashDigestSize C.MD5
+digestSize SHA1        = C.hashDigestSize C.SHA1
+digestSize SHA256      = C.hashDigestSize C.SHA256
+digestSize SHA512      = C.hashDigestSize C.SHA512
+digestSize Keccak_224  = C.hashDigestSize C.Keccak_224
+digestSize Keccak_256  = C.hashDigestSize C.Keccak_256
+digestSize Keccak_384  = C.hashDigestSize C.Keccak_384
+digestSize Keccak_512  = C.hashDigestSize C.Keccak_512
+digestSize SHA3_224    = C.hashDigestSize C.SHA3_224
+digestSize SHA3_256    = C.hashDigestSize C.SHA3_256
+digestSize SHA3_384    = C.hashDigestSize C.SHA3_384
+digestSize SHA3_512    = C.hashDigestSize C.SHA3_512
+
+--------------------------------------------------------------------------------
+
+_3 :: (a, b, c) -> c
+_3 (_, _, x) = x
diff --git a/src/Data/Multihash/Lazy.hs b/src/Data/Multihash/Lazy.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Multihash/Lazy.hs
@@ -0,0 +1,133 @@
+{-# LANGUAGE ExistentialQuantification  #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+
+-- |
+-- Copyright   : 2018 Monadic GmbH
+-- License     : BSD3
+-- Maintainer  : kim@monadic.xyz, team@monadic.xyz
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
+--
+-- Like "Data.Multihash", but using a lazy 'ByteString' representation.
+--
+-- Example:
+--
+-- >>> :set -XOverloadedStrings
+-- >>> import qualified Crypto.Hash as C
+-- >>> import Data.ByteArray.Encoding (Base(..), convertToBase)
+-- >>> import qualified Data.ByteString as Strict
+-- >>> import qualified Data.ByteString.Lazy as Lazy
+-- >>> import Data.ByteString.Lazy (toStrict)
+-- >>> import qualified Data.ByteString.Char8 as C8
+-- >>> import System.IO (stdout)
+-- >>> :{
+--     let
+--         input :: Strict.ByteString
+--         input = "multihash"
+--         atBase :: Base -> Multihash -> Strict.ByteString
+--         atBase base = convertToBase base . toStrict . encodedBytes
+--      in
+--         C8.hPutStr stdout $ C8.unlines
+--             [ atBase Base16 $ multihash C.SHA1   input
+--             , atBase Base32 $ multihash C.SHA1   input
+--             , atBase Base16 $ multihash C.SHA256 input
+--             , atBase Base32 $ multihash C.SHA256 input
+--             ]
+-- :}
+-- 111488c2f11fb2ce392acb5b2986e640211c4690073e
+-- CEKIRQXRD6ZM4OJKZNNSTBXGIAQRYRUQA47A====
+-- 12209cbc07c3f991725836a3aa2a581ca2029198aa420b9d99bc0e131d9f3e2cbe47
+-- CIQJZPAHYP4ZC4SYG2R2UKSYDSRAFEMYVJBAXHMZXQHBGHM7HYWL4RY=
+--
+module Data.Multihash.Lazy
+    ( Multihash
+    , fromDigest
+    , encodedBytes
+    , multihash
+    , decode
+    , decodeDigest
+    , getMultihash
+
+    -- * Re-exports
+    , Multihashable
+    )
+where
+
+import           Data.Multihash.Internal
+
+import           Control.DeepSeq (NFData)
+import qualified Crypto.Hash as C
+import           Data.Bifunctor (bimap)
+import qualified Data.Binary.Get as Binary
+import           Data.Binary.VarInt (buildVarInt)
+import           Data.ByteArray (ByteArrayAccess, convert)
+import qualified Data.ByteString as Strict
+import qualified Data.ByteString.Builder as Builder
+import           Data.ByteString.Lazy (ByteString)
+import           Data.Coerce (coerce)
+import           Data.Hashable (Hashable)
+
+-- | A multihash-encoded lazy 'ByteString'
+newtype Multihash = Multihash ByteString
+    deriving (Eq, Ord, Hashable, NFData)
+
+-- | Encode a 'C.Digest' as a 'Multihash'.
+fromDigest :: forall a. Multihashable a => C.Digest a -> Multihash
+fromDigest dig =
+    Multihash
+        . Builder.toLazyByteString
+        $ code <> len <> Builder.byteString bytes
+  where
+    code  = buildVarInt . toCode $ fromCryptonite dig
+    bytes = convert dig
+    len   = buildVarInt $ Strict.length bytes
+
+-- | Extract the raw, multihash-encoded bytes of a 'Multihash'.
+encodedBytes :: Multihash -> ByteString
+encodedBytes = coerce
+{-# INLINE encodedBytes #-}
+
+-- | Hash a value to a 'Multihash'.
+--
+-- Note that lazy 'ByteString's are /not/ an instance of 'ByteArrayAccess', ie.
+-- you will have to pass a strict 'Strict.ByteString' here.
+multihash :: (ByteArrayAccess ba, Multihashable a) => a -> ba -> Multihash
+multihash rithm = fromDigest . C.hashWith rithm
+
+-- | Decode a 'Multihash' from a lazy 'ByteString'.
+decode :: ByteString -> Either String Multihash
+decode = bimap _3 _3 . Binary.runGetOrFail getMultihash
+
+-- | Decode a 'C.Digest' from a multihash-encoded lazy 'ByteString'.
+decodeDigest
+    :: forall a. Multihashable a
+    => ByteString
+    -> Either String (C.Digest a)
+decodeDigest = bimap _3 _3 . Binary.runGetOrFail getMultihashedDigest
+
+getMultihash :: Binary.Get Multihash
+getMultihash = do
+    algo <- Binary.lookAhead getHashAlgorithm
+    case algo of
+        Blake2s_160 -> fromDigest <$> getMultihashedDigest @C.Blake2s_160
+        Blake2s_224 -> fromDigest <$> getMultihashedDigest @C.Blake2s_224
+        Blake2s_256 -> fromDigest <$> getMultihashedDigest @C.Blake2s_256
+        Blake2b_160 -> fromDigest <$> getMultihashedDigest @C.Blake2b_160
+        Blake2b_224 -> fromDigest <$> getMultihashedDigest @C.Blake2b_224
+        Blake2b_256 -> fromDigest <$> getMultihashedDigest @C.Blake2b_256
+        Blake2b_384 -> fromDigest <$> getMultihashedDigest @C.Blake2b_384
+        Blake2b_512 -> fromDigest <$> getMultihashedDigest @C.Blake2b_512
+        MD4         -> fromDigest <$> getMultihashedDigest @C.MD4
+        MD5         -> fromDigest <$> getMultihashedDigest @C.MD5
+        SHA1        -> fromDigest <$> getMultihashedDigest @C.SHA1
+        SHA256      -> fromDigest <$> getMultihashedDigest @C.SHA256
+        SHA512      -> fromDigest <$> getMultihashedDigest @C.SHA512
+        Keccak_224  -> fromDigest <$> getMultihashedDigest @C.Keccak_224
+        Keccak_256  -> fromDigest <$> getMultihashedDigest @C.Keccak_256
+        Keccak_384  -> fromDigest <$> getMultihashedDigest @C.Keccak_384
+        Keccak_512  -> fromDigest <$> getMultihashedDigest @C.Keccak_512
+        SHA3_224    -> fromDigest <$> getMultihashedDigest @C.SHA3_224
+        SHA3_256    -> fromDigest <$> getMultihashedDigest @C.SHA3_256
+        SHA3_384    -> fromDigest <$> getMultihashedDigest @C.SHA3_384
+        SHA3_512    -> fromDigest <$> getMultihashedDigest @C.SHA3_512
diff --git a/test/doctests/Main.hs b/test/doctests/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/doctests/Main.hs
@@ -0,0 +1,7 @@
+module Main (main) where
+
+import           Build_doctests (flags, module_sources, pkgs)
+import           Test.DocTest (doctest)
+
+main :: IO ()
+main = doctest $ flags <> pkgs <> module_sources
diff --git a/test/properties/Main.hs b/test/properties/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/properties/Main.hs
@@ -0,0 +1,93 @@
+{-# LANGUAGE TemplateHaskell  #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Main (main) where
+
+import           Control.Monad (unless)
+import qualified Crypto.Hash as C
+import           System.Exit (exitFailure)
+import           System.IO (BufferMode(..), hSetBuffering, stderr, stdout)
+
+import qualified Data.Multihash as Strict
+import qualified Data.Multihash.Lazy as Lazy
+
+import           Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+main :: IO ()
+main = do
+    hSetBuffering stdout LineBuffering
+    hSetBuffering stderr LineBuffering
+
+    ok <- props
+    unless ok exitFailure
+
+props :: IO Bool
+props = checkParallel $$(discover)
+
+prop_roundtripStrict :: Property
+prop_roundtripStrict = property $ do
+    bs <- forAll $ Gen.bytes (Range.singleton 128)
+    let
+        hashed  = Strict.multihash C.Blake2b_256 bs
+        bytes   = Strict.encodedBytes hashed
+        decoded = Strict.fromDigest @C.Blake2b_256 <$> Strict.decodeDigest bytes
+     in
+        assert $ decoded == Right hashed
+
+prop_roundtripBytesStrict :: Property
+prop_roundtripBytesStrict = property $ do
+    bs <- forAll $ Gen.bytes (Range.singleton 128)
+    let
+        hashed  = Strict.multihash C.Blake2b_256 bs
+        bytes   = Strict.encodedBytes hashed
+        decoded = Strict.decode bytes
+     in
+        assert $ decoded == Right hashed
+
+prop_roundtripLazy :: Property
+prop_roundtripLazy = property $ do
+    bs <- forAll $ Gen.bytes (Range.singleton 128)
+    let
+        hashed  = Lazy.multihash C.Blake2b_256 bs
+        bytes   = Lazy.encodedBytes hashed
+        decoded = Lazy.fromDigest @C.Blake2b_256 <$> Lazy.decodeDigest bytes
+     in
+        assert $ decoded == Right hashed
+
+prop_roundtripBytesLazy :: Property
+prop_roundtripBytesLazy = property $ do
+    bs <- forAll $ Gen.bytes (Range.singleton 128)
+    let
+        hashed  = Lazy.multihash C.Blake2b_256 bs
+        bytes   = Lazy.encodedBytes hashed
+        decoded = Lazy.decode bytes
+     in
+        assert $ decoded == Right hashed
+
+prop_algorithmMismatchStrict :: Property
+prop_algorithmMismatchStrict = property $ do
+    bs <- forAll $ Gen.bytes (Range.singleton 128)
+    let
+        hashed  = Strict.multihash C.Blake2b_256 bs
+        bytes   = Strict.encodedBytes hashed
+        decoded = Strict.fromDigest @C.SHA256 <$> Strict.decodeDigest bytes
+     in do
+        annotate (render decoded)
+        assert $ decoded == Left "Algorithm mismatch"
+
+prop_algorithmMismatchLazy :: Property
+prop_algorithmMismatchLazy = property $ do
+    bs <- forAll $ Gen.bytes (Range.singleton 128)
+    let
+        hashed  = Lazy.multihash C.Blake2b_256 bs
+        bytes   = Lazy.encodedBytes hashed
+        decoded = Lazy.fromDigest @C.SHA256 <$> Lazy.decodeDigest bytes
+     in do
+        annotate (render decoded)
+        assert $ decoded == Left "Algorithm mismatch"
+
+render :: Either String a -> String
+render (Left  s) = "Left " <> s
+render (Right _) = "Right <multihash>"
