haskoin-core: Bitcoin & Bitcoin Cash library for Haskell

[ bitcoin, finance, library, mit, network, public-domain ] [ Propose Tags ] [ Report a vulnerability ]

Please see the README on GitHub at https://github.com/jprupp/haskoin-core#readme


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.2.0, 0.3.0, 0.3.1, 0.4.0, 0.4.1, 0.4.2, 0.5.0, 0.5.1, 0.5.2, 0.6.0, 0.6.1, 0.7.0, 0.8.0, 0.8.1, 0.8.2, 0.8.3, 0.8.4, 0.9.0, 0.9.1, 0.9.2, 0.9.3, 0.9.4, 0.9.5, 0.9.6, 0.9.7, 0.9.8, 0.10.0, 0.10.1, 0.12.0, 0.13.0, 0.13.1, 0.13.2, 0.13.3, 0.13.4, 0.13.5, 0.13.6, 0.14.0, 0.14.1, 0.15.0, 0.17.0, 0.17.1, 0.17.2, 0.17.3, 0.17.4, 0.17.5, 0.17.6, 0.18.0, 0.19.0, 0.20.0, 0.20.1, 0.20.2, 0.20.3, 0.20.4, 0.20.5, 0.21.0, 0.21.1, 0.21.2, 0.22.0, 1.0.0, 1.0.1, 1.0.2, 1.0.4, 1.1.0, 1.2.1, 1.2.2, 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0
Change log CHANGELOG.md
Dependencies aeson (>=1.4.6.0), array (>=0.5.4.0), base (>=4.9 && <5), base16 (>=2.0), base64 (>=1.0), binary (>=0.8.8), bytes (>=0.17), bytestring (>=0.10.10.0), cereal (>=0.5.8), conduit (>=1.3.1.2), containers (>=0.6.2.1), crypton (>=1.0.0), data-default (>=0.7.1.1), deepseq (>=1.4.4.0), entropy (>=0.4.1.5), hashable (>=1.3.0.0), hspec (>=2.7.1), mtl (>=2.2.2), murmur3 (>=1.0.3), network (>=3.1.1.1), QuickCheck (>=2.13.2), ram (>=0.22.0), safe (>=0.3.18), scientific (>=0.3.6.2), secp256k1-haskell (>=2.0.0), split (>=0.2.3.3), string-conversions (>=0.4.0.1), text (>=1.2.3.0), time (>=1.9.3), transformers (>=0.5.6.2), unicode-data (>=0.2), unicode-transforms (>=0.4), unordered-containers (>=0.2.10.0), vector (>=0.12.1.2) [details]
License MIT
Author JP Rupp
Maintainer jprupp@protonmail.ch
Uploaded by jprupp at 2026-11-06T14:16:33Z
Category Bitcoin, Finance, Network
Home page http://github.com/jprupp/haskoin-core#readme
Bug tracker http://github.com/jprupp/haskoin-core/issues
Source repo head: git clone https://github.com/jprupp/haskoin-core.git
Distributions LTSHaskell:1.2.2
Reverse Dependencies 15 direct, 2 indirect [details]
Downloads 30384 total (267 in the last 30 days)
Rating 2.0 (votes: 1) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for haskoin-core-1.6.0

[back to package description]

Haskoin Core

Hackage License: MIT

Haskoin Core is a Bitcoin and Bitcoin Cash library for Haskell. It covers keys, addresses, scripts, transactions, PSBTs, block headers and the peer-to-peer message format.

It does no I/O of its own. It never opens a socket or writes a file. You can use it in a wallet, a block explorer or a node, and bring your own networking and storage.

Contents

Features

Area What you get
Networks Bitcoin and Bitcoin Cash: mainnet, testnet3, testnet4 and regtest
Keys secp256k1 keys, WIF, BIP-32 extended keys and paths, BIP-39 mnemonics, SLIP-39 Shamir shares
Signatures Deterministic ECDSA (RFC 6979) and BIP-340 Schnorr
Addresses Base58, Bech32, Bech32m and CashAddr, including token-aware CashAddr types
Scripts Script parsing and serialization, standard input and output templates, BIP-67 sorted multisig
Transactions Building, signing and verifying P2PK, P2PKH, P2SH, multisig, P2WPKH, P2WSH, nested SegWit and Taproot key path spends
Wallet tools Coin selection, and weight, size and fee estimates
PSBT Versions 0 and 2, with the Taproot fields (BIP-174, BIP-370, BIP-371)
Taproot Script trees, output keys, witnesses, control blocks and signature hashes (BIP-341, BIP-342)
Blocks Header validation, proof of work, the difficulty rules of both chains, Merkle trees and an in-memory header chain
Peer-to-peer Message encoding, including addrv2, compact blocks and compact filter messages, and BIP-37 Bloom filters
Testing QuickCheck generators for the library's types

What is not included

  • Peer connections and chain sync. Use haskoin-node.
  • Address and transaction indexing. Use haskoin-store.
  • A script interpreter. verifyStdTx recognises the standard templates. It is not a consensus check.
  • Signet, output descriptors, Miniscript and MuSig2.
  • Schnorr transaction signatures, SIGHASH_UTXOS and token outputs of Bitcoin Cash. An input or an output that uses them does not decode or verify.
  • BIP-324 encrypted transport.
  • BIP-39 word lists other than English.

Installation

System library

Haskoin Core needs libsecp256k1 0.2.0 or later, and pkg-config to find it.

# Debian and Ubuntu
sudo apt-get install libsecp256k1-dev pkg-config

# macOS with Homebrew
brew install secp256k1 pkg-config

# Nix
nix-shell -p secp256k1 pkg-config

Check what you have with pkg-config --modversion libsecp256k1. Debian 11 and other older systems ship a version below 0.2.0. On those, build the library from source and enable its recovery module with ./configure --enable-module-recovery.

Compiler

The library is tested with GHC 9.10 and 9.12. It cannot build below GHC 9.2, because it uses OverloadedRecordDot.

With Cabal

Add the packages to your .cabal file:

build-depends:
    base
  , bytes
  , haskoin-core
  , text

You need bytes to serialize values with runPutS and serialize.

Run cabal update first. Haskoin Core needs a recent secp256k1-haskell, and a stale package index makes the build fail with a dependency error.

With Stack

Haskoin Core is not in Stackage, so add it to stack.yaml as an extra dependency. Use the latest version from Hackage.

resolver: nightly-2026-10-28
extra-deps:
  - haskoin-core-1.6.0
  - base16-2.0
  - secp256k1-haskell-2.0.0

Quick start

This program turns a mnemonic into the first receiving address of a BIP-84 wallet.

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import Data.Text (Text)
import Haskoin

-- This is the public BIP-39 test mnemonic. Never send coins to it.
mnemonic :: Text
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"

main :: IO ()
main = withContext $ \ctx -> do
  seed <- either fail pure (mnemonicToSeed "" mnemonic)
  let master = makeXPrvKey seed
      -- Hardened steps need the private key.
      account = derivePath ctx ("m/84'/0'/0'" :: HardPath) master
      accountPub = deriveXPubKey ctx account
      -- The remaining steps only need the public key.
      receiving = derivePubPath ctx ("m/0" :: SoftPath) accountPub
      (address, _publicKey) = deriveWitnessAddr ctx receiving 0
  print (addrToText btc address)
  -- => Just "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"

The address is the one that BIP-84 publishes for this mnemonic.

Concepts

A few conventions apply across the whole library. Reading this section first will save you time.

The context

Most cryptographic functions take a Ctx, a secp256k1 context. Create one when your program starts, with withContext or createContext. Then pass it around. You can share it between threads.

Networks

A Network is a record of the parameters of one chain. Functions that depend on the chain take one as an argument. Functions that need both take the Network first and the Ctx second.

Value Chain Name for netByName
btc Bitcoin mainnet btc
btcTest Bitcoin testnet3 btctest
btcTest4 Bitcoin testnet4 btctest4
btcRegTest Bitcoin regtest btcreg
bch Bitcoin Cash mainnet bch
bchTest Bitcoin Cash testnet3 bchtest
bchTest4 Bitcoin Cash testnet4 bchtest4
bchRegTest Bitcoin Cash regtest bchreg

Record fields

The library is built with NoFieldSelectors, so record fields are not functions. Turn on OverloadedRecordDot in your modules and read fields with a dot.

print btc.defaultPort
-- => 8333
print (map (.value) btc.genesisTx.outputs)
-- => [5000000000]

Many records share field names. Tx and Psbt both have inputs, for example. To update such a field, import the module qualified and name the field through it, as the PSBT example does.

Errors

Functions that can fail return Maybe or Either String. The examples below turn those into IO failures with either fail pure.

Hashes, paths and public keys have IsString instances, so you can write them as literals. Those instances call error on bad input. For input you do not control, use hexToTxHash, hexToBlockHash, parsePath and textToAddr.

Amounts

Amounts are satoshi, as Word64.

Serialization

Most types have Serial instances, from the bytes package, and also Binary and Serialize instances. Some types need a context or a network to encode, so they have Marshal or MarshalJSON instances instead.

Task Encode Decode
Binary runPutS (serialize x) runGetS deserialize bytes
Binary with context marshal ctx x unmarshal ctx bytes
Hexadecimal encodeHex bytes decodeHex text
JSON toJSON x parseJSON value
JSON with context marshalJSON ctx x unmarshalJSON ctx json

The first argument depends on the type. For example, it is a Ctx for PublicKey, a Network for XPrvKey and a (Network, Ctx) pair for XPubKey.

unmarshal fails when bytes are left over after the value, and decodeTx does the same for a transaction. runGetS deserialize reads a value from the start of the bytes and ignores the rest.

Hash byte order

TxHash and BlockHash show as hexadecimal in reversed byte order. This is the form that Bitcoin Core and block explorers display, so you can paste those values straight in. The serialized bytes keep the internal order.

Usage guide

The examples below are fragments of main in this program. Paste one in place of the comment to run it. If an example starts with import lines, add them to the top of the file. A -- => comment shows what the line above it prints.

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import Data.Bytes.Put (runPutS)
import Data.Bytes.Serial (serialize)
import Haskoin

main :: IO ()
main = withContext $ \ctx -> do
  -- This is the public BIP-39 test mnemonic. Never send coins to it.
  seed <-
    either fail pure $
      mnemonicToSeed "" "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
  let master = makeXPrvKey seed
  -- Paste an example here.
  pure ()

Mnemonics

Generate entropy with a cryptographic source. This example uses the entropy package.

import qualified Data.ByteString as B
import System.Entropy (getEntropy)

-- 16 bytes of entropy give 12 words. 32 bytes give 24 words.
entropy <- getEntropy 16
words12 <- either fail pure (toMnemonic entropy)

-- The passphrase is optional. Use "" for none.
newSeed <- either fail pure (mnemonicToSeed "my passphrase" words12)
print (B.length newSeed)
-- => 64

mnemonicToSeed checks the words and the checksum. fromMnemonic gives the entropy back.

Keys and signatures

-- A secret key is 32 bytes. decodeSecKey refuses zero and values at or above
-- the curve order.
bytes <- maybe (fail "bad hex") pure (decodeHex "0000000000000000000000000000000000000000000000000000000000000001")
secret <- maybe (fail "bad key") pure (decodeSecKey bytes)

-- True asks for the compressed form of the public key.
let private = wrapSecKey True secret
    public = derivePublicKey ctx private

print private
-- => PrivateKey {key = <redacted>, compress = True}
print (toWif btc private)
-- => "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"
print (encodeHex (marshal ctx public))
-- => "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
print (addrToText btc (pubKeyAddr ctx public))
-- => Just "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH"

-- Sign and verify a hash.
let digest = doubleSHA256 bytes
    signature = signHash ctx secret digest
print (verifyHashSig ctx digest signature public.point)
-- => True

fromWif btc text reads a WIF key back. Haskoin also re-exports the secp256k1-haskell API, which has the Schnorr functions such as signSchnorr and verifySchnorr.

Extended keys

Paths have three types. A HardPath has only hardened steps. A SoftPath has only normal steps, which a public key can derive. A DerivPath can mix both. Hardened steps can be written ', h or H.

let account = derivePath ctx ("m/84'/0'/0'" :: HardPath) master
    accountPub = deriveXPubKey ctx account

print (xPubExport btc ctx accountPub)
-- => "xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V"

-- The account's public key derives all its receiving addresses.
let receiving = derivePubPath ctx ("m/0" :: SoftPath) accountPub
    addresses = [address | (address, _, _) <- deriveWitnessAddrs ctx receiving 0]
print (map (addrToText btc) (take 2 addresses))
-- => [Just "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",Just "bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g"]

Pick the address function that matches the wallet standard:

Standard Address type Path One address Endless list
BIP-44 P2PKH m/44'/0'/0' deriveAddr deriveAddrs
BIP-49 P2WPKH nested in P2SH m/49'/0'/0' deriveCompatWitnessAddr deriveCompatWitnessAddrs
BIP-84 P2WPKH m/84'/0'/0' deriveWitnessAddr deriveWitnessAddrs
BIP-86 Taproot m/86'/0'/0' See Taproot

xPrvExport and xPubExport write the standard xprv and xpub forms. xPrvImport and xPubImport read them back. The ypub and zpub prefixes are not supported.

parsePath reads a path at run time. It returns a ParsedPath, which applyPath applies to a private or a public key.

Addresses

let text = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"

print (textToAddr btc text)
-- => Just (WitnessPubKeyAddress {hash160 = "c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e2"})

-- An address of another network is refused.
print (textToAddr bch text)
-- => Nothing

-- The output script an address pays to.
print (encodeHex <$> (addressToScriptBS ctx =<< textToAddr btc text))
-- => Just "0014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e2"

textToAddr and addrToText handle every address format of a network. addrToBase58, addrToBech32 and addrToCash each write one chosen format.

Scripts

ScriptOutput is a standard output script. Its constructors are PayPK, PayPKHash, PayMulSig, PayScriptHash, PayWitnessPKHash, PayWitnessScriptHash, PayWitness, PayScript32Hash and DataCarrier.

let keyA = wrapPubKey True "02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8"
    keyB = wrapPubKey True "02fe6f0a5a297eb38c391581c4413e084773ea23954d93f7753db7dc0adc188b2f"
    -- BIP-67 sorts the keys, so that all signers build the same script.
    multisig = sortMulSig ctx (PayMulSig [keyA, keyB] 2)

-- The address can fail, because a P2SH redeem script holds at most 520 bytes.
print (addrToText btc <$> payToScriptAddress ctx multisig)
-- => Right (Just "39bgKC7RFbpoCRbtD5KEdkYKtNyhpsNa3Z")

-- OP_RETURN is 6a, then the push of 5 bytes, then "hello".
print (encodeHex (marshal ctx (DataCarrier ["hello"])))
-- => "6a0568656c6c6f"

-- Read the same bytes back.
script <- maybe (fail "bad hex") pure (decodeHex "6a0568656c6c6f")
print (unmarshal ctx script :: Either String ScriptOutput)
-- => Right (DataCarrier {pushes = ["hello"]})

Decoding a ScriptOutput fails for a script that matches no template. A Script holds any script, as a list of ScriptOp values, so use that type for the others. encodeOutput and decodeOutput convert between ScriptOutput and Script.

payToWitnessScriptAddress and payToNestedScriptAddress give the P2WSH and P2SH-P2WSH addresses of a script.

The three functions return Either. A P2SH redeem script holds at most 520 bytes, which is a multisig of 15 compressed keys. Coins sent to a longer script could never be spent, so payToScriptAddress refuses it. A P2WSH script holds up to 10,000 bytes, so a multisig of 16 to 20 keys needs P2WSH.

Building and signing a transaction

-- The key at m/84'/0'/0'/0/0 and its address.
let key = derivePath ctx ("m/84'/0'/0'/0/0" :: DerivPath) master
    address = xPubWitnessAddr ctx (deriveXPubKey ctx key)

-- The coin to spend, and the transaction that paid it. Both are made up.
-- The coin holds 100,000 satoshi.
coinScript <- maybe (fail "no output script") pure (addressToOutput address)
let amount = 100000
    fundingInput = TxIn (OutPoint "1111111111111111111111111111111111111111111111111111111111111111" 0) "" 0xffffffff
    funding = Tx 2 [fundingInput] [TxOut amount (marshal ctx coinScript)] [] 0
    coin = OutPoint (txHash funding) 0

-- Pay 99,000 satoshi. The 1,000 satoshi left over are the fee.
unsigned <-
  either fail pure $
    buildAddrTx btc ctx [coin] [("bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g", 99000)]

-- SigInput describes the coin. The last argument is the redeem script.
signed <-
  either fail pure $
    signTx btc ctx unsigned [SigInput coinScript amount coin sigHashAll Nothing] [key.key]

print (verifyStdTx btc ctx signed [(coinScript, amount, coin)])
-- => True
print (txVirtualSize signed)
-- => 110
print (txHash signed)
-- => "9a079e6b7939909415b7b027a19e093bc539dac5a2ed50df8beb0942ec821848"

-- This hexadecimal form is what you broadcast.
print (encodeHex (runPutS (serialize signed)))
-- => "01000000000101..."

buildAddrTx adds no change output. Whatever the outputs do not spend goes to the miners as a fee, so list your change output along with the payments.

Fill in SigInput and pick the signing function by the type of the coin:

Coin script redeem Function
P2PKH PayPKHash Nothing signTx
P2SH multisig PayScriptHash Just (PayMulSig ks m) signTx
P2WPKH PayWitnessPKHash Nothing signTx
P2WSH PayWitnessScriptHash Just script signTx
P2SH-P2WPKH PayWitnessPKHash Nothing signNestedWitnessTx
P2SH-P2WSH PayWitnessScriptHash Just script signNestedWitnessTx
Taproot key path PayWitness 1 key Nothing signTx

For multisig, each signer can call signTx with their own keys. mergeTxs combines the results for legacy inputs. For SegWit multisig, use a PSBT.

Coin selection and fees

Make your coin type an instance of Coin. The library then picks coins and estimates the fee from the kinds of the inputs and outputs. This example is a complete program.

{-# LANGUAGE OverloadedRecordDot #-}

module Main (main) where

import Data.Word (Word64)
import Haskoin

newtype Utxo = Utxo {amount :: Word64}

instance Coin Utxo where
  coinValue utxo = utxo.amount
  coinInputKind _ = InputWitnessPKHash

main :: IO ()
main = do
  let outputs = [OutputWitnessPKHash, OutputWitnessPKHash]
      coins = [Utxo 50000, Utxo 120000, Utxo 300000]
      target = 150000
      feeRate = 5

  -- The size is in virtual bytes, and the fee rate in satoshi per virtual byte.
  print (guessTxSize [InputWitnessPKHash] outputs)
  -- => 141
  print (guessTxFee feeRate [InputWitnessPKHash] outputs)
  -- => Right 705

  -- The result is the chosen coins and the change to send back to yourself.
  -- True keeps searching the rest of the list for a smaller solution.
  case chooseCoins target feeRate outputs True coins of
    Left err -> putStrLn err
    Right (chosen, change) -> print (map (.amount) chosen, change)
  -- => ([120000,50000],18955)

The change here is 170,000 - 150,000 - 1,045, where 1,045 is the fee for two inputs and two outputs.

chooseCoins takes the coins in the order of the list, so sort them the way you prefer first. The output list must include the change output.

The change can be smaller than the dustThreshold of its output kind, down to one satoshi. Nodes do not relay such an output, so leave that change to the fee. chooseCoins refuses a selection above maxStandardTxWeight, which nodes do not relay either.

The estimates are upper bounds, because they count 72 bytes per signature. The kinds include InputPKHash, InputWitnessPKHash and InputTaproot. See InputKind and OutputKind in Haskoin.Transaction.Builder for all of them.

PSBT

Use a PSBT when several parties or devices sign a transaction. This example continues the one in Building and signing.

-- Continues the example above.
import qualified Haskoin.Transaction.Psbt as Psbt

-- Creator: start from the unsigned transaction.
let created = psbtOf unsigned

-- Updater: attach the transaction that paid each coin. The signer checks the
-- amount of the coin against it.
    updated = created {Psbt.inputs = map (insertNonWitnessUtxo funding) created.inputs}

-- This base64 text is what you pass to the signers.
let shared = encodePsbtBase64 updated

-- Signer: read the text, and sign every input the keys can sign.
received <- either fail pure (decodePsbtBase64 shared)
signedPsbt <- either fail pure (signPsbt btc ctx (keySigner ctx key.key) received)

-- Finalizer and extractor: check the signatures and build the transaction.
finalized <- either fail pure (finalizePsbt btc ctx signedPsbt)
extracted <- either fail pure (psbtTransaction finalized)

-- RFC 6979 signatures are deterministic, so this is the same transaction.
print (extracted == signed)
-- => True

Some other useful functions:

  • xPrvSigner signs from an extended private key, following the derivation records of the inputs.
  • combinePsbt merges the copies that different signers return.
  • insertWitnessUtxo attaches a coin without the transaction that paid it. That is enough for a Taproot coin. For a SegWit version 0 coin, the amount is then a claim that the signature does not cover for the other inputs, and a signer can be led to burn coins as fee (CVE-2020-14199). keySigner and xPrvSigner therefore refuse such a coin. Set requirePreviousTx = False on the Signer to accept it.
  • psbtFee and inputCoins give the fee and the coins as the signer checks them. Show those to the user, not amounts read from the fields yourself.
  • psbtTransaction refuses a PSBT whose inputs are not all final. psbtPartialTransaction gives the transaction of a PSBT that is still being signed.
  • insertInputRedeemScript and insertInputWitnessScript attach the scripts of P2SH and P2WSH coins.
  • withVersion Version2 converts a PSBT to version 2. addInput and addOutput then extend it.

Taproot

let key = derivePath ctx ("m/86'/0'/0'/0/0" :: DerivPath) master
    internalKey = (deriveXPubKey ctx key).key
    -- Nothing means that the output has no script tree.
    output = taprootScriptOutput ctx (TaprootOutput internalKey Nothing)

print (addrToText btc =<< outputAddress btc ctx output)
-- => Just "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"

-- Spend a made-up coin of 100,000 satoshi by its key path.
let coin = OutPoint "1111111111111111111111111111111111111111111111111111111111111111" 0
    unsigned = buildTx ctx [coin] [(output, 99000)]

signed <-
  either fail pure $
    signTx btc ctx unsigned [SigInput output 100000 coin sigHashDefault Nothing] [key.key]

print (verifyStdTx btc ctx signed [(output, 100000, coin)])
-- => True

The signature commits to every coin the transaction spends. signTx therefore needs a SigInput for every input of a Taproot transaction.

To commit to scripts, pass a MAST in place of Nothing. signTx only signs key path spends. signPsbt also signs a script path when the leaf is a single key check, <key> OP_CHECKSIG. For other scripts, build the witness yourself with taprootSigHash and encodeTaprootWitness.

checkTapTree tells whether every leaf of a tree can be spent. A leaf can be at most 128 levels deep, and its version must be one that BIP 341 allows.

SLIP-39 secret sharing

SLIP-39 splits a secret into mnemonic shares, and a threshold of shares rebuilds it. It is available from version 1.6.0. The Haskoin module does not re-export it, so import it yourself.

import qualified Haskoin.Crypto.Keys.Slip39 as Slip39

-- One group of 3 members, of which 2 rebuild the secret.
let scheme = Slip39.Scheme 1 [(2, 3)]
    -- A secret has at least 16 bytes, and an even number of them.
    -- Use random bytes in practice.
    secret = "0123456789abcdef"

-- True makes the shares extendable. 1 sets the cost of the encryption.
groups <- either fail pure =<< Slip39.generateMnemonics scheme True 1 "passphrase" secret
let shares = concat groups

print (length shares)
-- => 3
print (Slip39.combineMnemonics "passphrase" (take 2 shares))
-- => Right "0123456789abcdef"
print (Slip39.combineMnemonics "passphrase" (take 1 shares))
-- => Left "Group 0 has 1 share, but its member threshold is 2"

A wrong passphrase is not an error. It returns a different secret, as the specification requires. The passphrase must be printable ASCII, which the specification requires too. A SLIP-39 share is not a BIP-39 mnemonic, and one cannot be converted into the other.

Scheme can also describe two levels. For example, Scheme 2 [(1, 1), (2, 3), (3, 5)] needs two of the three groups, each with its own member threshold.

Bitcoin Cash

Pass bch wherever the examples above pass btc. A few things differ.

-- The BIP-44 coin type of Bitcoin Cash is 145.
let key = derivePath ctx ("m/44'/145'/0'/0/0" :: DerivPath) master
    address = xPubAddr ctx (deriveXPubKey ctx key)

-- addrToText writes CashAddr on Bitcoin Cash.
print (addrToText bch address)
-- => Just "bitcoincash:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6"
print (addrToBase58 bch address)
-- => Just "1mW6fDEMjKrDHvLvoEsaeLxSCzZBf3Bfg"

-- textToAddr reads both forms.
print ((== Just address) (textToAddr bch "1mW6fDEMjKrDHvLvoEsaeLxSCzZBf3Bfg"))
-- => True

-- SegWit addresses do not exist on Bitcoin Cash.
print (addrToText bch (xPubWitnessAddr ctx (deriveXPubKey ctx key)))
-- => Nothing

signTx sets the fork id flag of the signature hash when the network needs it. Pass sigHashAll as usual.

Blocks and headers

print (headerHash btc.genesisHeader)
-- => "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
print (isValidPOW btc btc.genesisHeader)
-- => True

-- 3.125 BTC since the halving at this height.
print (computeSubsidy btc 840000)
-- => 312500000

connectBlocks validates headers and adds them to a chain. This covers proof of work, difficulty, timestamps, versions and checkpoints. It runs in any monad with a BlockHeaders instance. StateT HeaderMemory is the in-memory one. Write your own instance to keep headers in a database.

Once the best block has the minimumChainWork of the network, a fork with less work is refused. Below that work nothing bounds the side chains a peer can send, so a node that is still syncing should not keep headers from a peer it does not trust.

This example needs the mtl package.

import Control.Monad.State.Strict (runStateT)

-- Mine three regtest headers on top of the genesis block.
let net = btcRegTest
    headers = appendBlocks net 7 net.genesisHeader 3
    now = net.genesisHeader.timestamp + 3600

(result, chain) <- runStateT (connectBlocks net now headers) (initialChain net)
nodes <- either fail pure result

-- The nodes come back with the highest one first.
print (map (.height) nodes)
-- => [3,2,1]
print chain.best.height
-- => 3

buildMerkleRoot and buildPartialMerkle compute Merkle trees. extractMatches checks a partial tree from a merkleblock message against its Merkle root. The header and the number of transactions are claims of the sender, which the caller must check.

Peer-to-peer messages

The library encodes and decodes messages. Moving the bytes is up to you.

import Data.Bytes.Get (runGetS)

let bytes = runPutS (putMessage btc MVerAck)

print (encodeHex bytes)
-- => "f9beb4d976657261636b000000000000000000005df6e0e2"
print (runGetS (getMessage btc) bytes)
-- => Right MVerAck

getMessage checks the network magic and the checksum, and bounds the payload by the maxPayloadSize of its command. Read the 24-byte MessageHeader first to learn how many more bytes to take from the socket, and refuse a length above maxPayloadSize net command before you read them.

Message has a constructor for each message type. bloomCreate, bloomInsert and bloomContains build BIP-37 filters for filterload.

Module overview

import Haskoin brings in everything below except the two modules marked as separate. It exports many short names, so you may prefer to import only the modules you need, or to import it qualified.

Module Contents
Haskoin.Address Address types and Base58, Bech32 and CashAddr encoding
Haskoin.Block Block types, header chain, validation, difficulty and Merkle trees
Haskoin.Crypto Hashes, keys, signatures and the Crypto.Secp256k1 API
Haskoin.Crypto.Keys.Slip39 SLIP-39 secret sharing (separate import)
Haskoin.Network Network constants, P2P messages, Bloom filters
Haskoin.Script Scripts, standard templates and signature hashes
Haskoin.Transaction Transaction types, builder, signing, SegWit, Taproot and PSBT
Haskoin.Util Hexadecimal helpers, Marshal and small utilities
Haskoin.Util.Arbitrary QuickCheck generators such as arbitraryTx (separate import)

Security notes

Bugs in code that handles keys can lose money. Try your program on regtest or a test network before you use it on mainnet.

  • Randomness. Create keys and mnemonics with a cryptographic source, such as getEntropy from the entropy package. Never use System.Random.
  • Secrets in logs. PrivateKey and XPrvKey show as <redacted> and have no Read instance. toWif and xPrvExport write a key out on purpose. A bare SecKey, such as the key field of a PrivateKey, still shows its bytes. The errors of fromMnemonic and mnemonicToSeed name the position of a bad word, never the word.
  • Secrets in memory. Keys live in the GHC heap. The library cannot wipe them, and the garbage collector can copy them.
  • Review before you sign. signPsbt checks that a PSBT is sound, but not that it pays who you expect. Show the outputs and psbtFee to the user first. By default the signer wants the previous transaction of every SegWit version 0 coin, because an amount in a witness output alone is a claim.
  • Signature hash types. By default signPsbt signs only SIGHASH_ALL and SIGHASH_DEFAULT, because the other types leave part of the transaction unsigned. To accept others, set acceptedSigHashes on the Signer.
  • Verification. verifyStdTx checks the signatures of the standard templates by the rules that nodes relay by, and that outputs do not exceed inputs. It is not a script interpreter and does not apply every consensus rule.
  • Untrusted text. The cost of decodeBase58 and decodeBase58Check grows with the square of the input length. Limit the length of input you pass to them directly. The address and key decoders of the library already do.

Development

git clone https://github.com/jprupp/haskoin-core.git
cd haskoin-core

stack build                                         # build the library
stack test                                          # run all the tests
stack test --test-arguments='--match "Slip39"'      # run the tests of one module
stack haddock --no-haddock-deps                     # build the API documentation

cabal build and cabal test work too. On NixOS, or with stack --nix, the nix section of stack.yaml provides libsecp256k1.

package.yaml is the source of the package description. Change it rather than haskoin-core.cabal, which hpack generates. Stack regenerates the file whenever it builds.

The tests are in test/ and use hspec. The test vectors from the BIPs, Bitcoin Core and other implementations are in data/.

Contributing

Pull requests are welcome.

License

Haskoin Core is released under the MIT License.