A pure Haskell implementation of the Lightning Network peer protocol, BOLT #2: the messages for channel establishment (v1 and v2), interactive transaction construction, quiescence, channel close, normal operation and message retransmission, with their wire codecs.
A sample GHCi session:
> :set -XOverloadedStrings
> import qualified Data.ByteString as BS
> import qualified Lightning.Protocol.BOLT1 as BOLT1
> import Lightning.Protocol.BOLT2
>
> -- a channel_ready carrying an scid alias (a typed TLV record)
> let Just cid = BOLT1.channel_id (BS.replicate 32 0xab)
> let Just point = BOLT1.point (BS.cons 0x02 (BS.replicate 32 0x11))
> let alias = BOLT1.short_channel_id 539268 845 1
> let ready = ChannelReady cid point alias BOLT1.empty_tlv_stream
> let Right wire = encode_message (MsgChannelReady ready)
> BS.length wire
77
> decode_message wire == Right (MsgChannelReady ready)
True
>
> -- unknown odd TLV records are kept, so re-encoding is exact
> let payload = BS.drop 2 wire
> let Right ext = decode_channel_ready (payload <> "\x2b\x01\x2a")
> channel_ready_tlvs ext
TlvStream [TlvRecord {tlv_type = 43, tlv_value = "*"}]
> encode_channel_ready ext == Right (payload <> "\x2b\x01\x2a")
True
>
> -- unknown even records and malformed known ones are rejected
> decode_channel_ready (payload <> "\x2a\x00")
Left (DecodeTlvError (TlvUnknownEvenType 42))
> let fixed = BS.take 65 payload
> decode_channel_ready (fixed <> "\x01\x07" <> BS.replicate 7 0x00)
Left (DecodeInvalidTlvValue 1)
>
> -- messages from other BOLTs (or splicing) aren't BOLT #2's
> decode_message ("\x00\x50" <> payload)
Left (DecodeUnknownEvenType 80)
Haddocks (API documentation, etc.) are hosted at docs.ppad.tech/bolt2.
This library implements BOLT #2 as of lightning/bolts commit
1aadb719 (2026-09-21); a copy lives in etc/.
Fundamental types and the TLV format come from ppad-bolt1,
transaction ids from ppad-tx, and channel_type feature vectors
from ppad-bolt9. Every TLV record the specification defines
outside of splicing has a typed field; unknown odd records are kept.
Not implemented:
- Splicing: the
splice_init,splice_ackandsplice_lockedmessages, and the splicing-only TLV records.tx_add_input'sshared_input_txidandtx_signatures'shared_input_signatureare rejected as unknown even types;channel_reestablish'smy_current_funding_lockedis kept as an unknown odd record. - The
start_batchmessage. - Channel-level validation: the library checks what decoding needs
(field lengths, point prefixes, amounts of at most 21M BTC, TLV
rules), and leaves rules that depend on channel state or negotiated
features (reserves, feerates, shutdown script forms,
serial_idparity, required TLV records, points being on the curve, etc.) to the caller. - Interpretation of the onion routing packet and attribution data, which are kept opaque (see BOLT #4).
The aim is best-in-class performance for pure Haskell code. Run the benchmarks with:
$ cabal bench
This library has received no security audit. It decodes strictly: every message must be consumed exactly, unknown even TLV records are rejected, and encoding a decoded message reproduces its bytes.
You'll require Nix with flake support enabled. Enter a development shell with:
$ nix develop
Then do e.g.:
$ cabal repl ppad-bolt2
to get a REPL for the main library.