bolt3

Lightning transaction and script formats, per BOLT #3.
git clone git://git.ppad.tech/bolt3.git
Log | Files | Refs | README | LICENSE

commit d8dbc23a79dc8ce18a3ad1eb78d69b53d935b7b0
parent 23add542ee6973a0714d712dce912113c7310d4f
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 11:06:18 +0400

Merge branch 'impl/scripts'

Diffstat:
Mflake.lock | 270++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mflake.nix | 9++++++++-
Mlib/Lightning/Protocol/BOLT3/Scripts.hs | 655++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mppad-bolt3.cabal | 1+
4 files changed, 912 insertions(+), 23 deletions(-)

diff --git a/flake.lock b/flake.lock @@ -18,6 +18,42 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1766840161, @@ -34,6 +70,106 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1766840161, + "narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1766840161, + "narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "ppad-base16": { + "inputs": { + "flake-utils": [ + "ppad-ripemd160", + "ppad-base16", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-ripemd160", + "ppad-base16", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-ripemd160", + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1766934151, + "narHash": "sha256-BUFpuLfrGXE2xi3Wa9TYCEhhRhFp175Ghxnr0JRbG2I=", + "ref": "master", + "rev": "58dfb7922401a60d5de76825fcd5f6ecbcd7afe0", + "revCount": 26, + "type": "git", + "url": "git://git.ppad.tech/base16.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/base16.git" + } + }, + "ppad-base16_2": { + "inputs": { + "flake-utils": [ + "ppad-sha256", + "ppad-base16", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-sha256", + "ppad-base16", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-sha256", + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1766934151, + "narHash": "sha256-BUFpuLfrGXE2xi3Wa9TYCEhhRhFp175Ghxnr0JRbG2I=", + "ref": "master", + "rev": "58dfb7922401a60d5de76825fcd5f6ecbcd7afe0", + "revCount": 26, + "type": "git", + "url": "git://git.ppad.tech/base16.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/base16.git" + } + }, "ppad-nixpkgs": { "inputs": { "flake-utils": "flake-utils", @@ -54,6 +190,106 @@ "url": "git://git.ppad.tech/nixpkgs.git" } }, + "ppad-nixpkgs_2": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1766932084, + "narHash": "sha256-GvVsbTfW+B7IQ9K/QP2xcXJAm1lhBin1jYZWNjOzT+o=", + "ref": "master", + "rev": "353e61763b959b960a55321a85423501e3e9ed7a", + "revCount": 2, + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + } + }, + "ppad-nixpkgs_3": { + "inputs": { + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1766932084, + "narHash": "sha256-GvVsbTfW+B7IQ9K/QP2xcXJAm1lhBin1jYZWNjOzT+o=", + "ref": "master", + "rev": "353e61763b959b960a55321a85423501e3e9ed7a", + "revCount": 2, + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + } + }, + "ppad-ripemd160": { + "inputs": { + "flake-utils": [ + "ppad-ripemd160", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-ripemd160", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-base16": "ppad-base16", + "ppad-nixpkgs": "ppad-nixpkgs_2" + }, + "locked": { + "lastModified": 1766957035, + "narHash": "sha256-Ltal2K/ika4svHpb7emUyeRAfZCyhvZy59syD+BJM8k=", + "ref": "master", + "rev": "a82424ea6b9f48ed42c4f2a239600283b088ab8d", + "revCount": 30, + "type": "git", + "url": "git://git.ppad.tech/ripemd160.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/ripemd160.git" + } + }, + "ppad-sha256": { + "inputs": { + "flake-utils": [ + "ppad-sha256", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-sha256", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-base16": "ppad-base16_2", + "ppad-nixpkgs": "ppad-nixpkgs_3" + }, + "locked": { + "lastModified": 1768121850, + "narHash": "sha256-RxgAI88nZi4o4xYj1v+GC0X5E9adae12dDSmv/GFu2Y=", + "ref": "master", + "rev": "916595b21319ca270ce8beb9c742bf7e632cccc9", + "revCount": 118, + "type": "git", + "url": "git://git.ppad.tech/sha256.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/sha256.git" + } + }, "root": { "inputs": { "flake-utils": [ @@ -64,7 +300,9 @@ "ppad-nixpkgs", "nixpkgs" ], - "ppad-nixpkgs": "ppad-nixpkgs" + "ppad-nixpkgs": "ppad-nixpkgs", + "ppad-ripemd160": "ppad-ripemd160", + "ppad-sha256": "ppad-sha256" } }, "systems": { @@ -81,6 +319,36 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix @@ -17,12 +17,17 @@ url = "git://git.ppad.tech/secp256k1.git"; ref = "master"; }; + ppad-ripemd160 = { + type = "git"; + url = "git://git.ppad.tech/ripemd160.git"; + ref = "master"; + }; flake-utils.follows = "ppad-nixpkgs/flake-utils"; nixpkgs.follows = "ppad-nixpkgs/nixpkgs"; }; outputs = { self, nixpkgs, flake-utils, ppad-nixpkgs - , ppad-sha256, ppad-secp256k1 }: + , ppad-sha256, ppad-secp256k1, ppad-ripemd160 }: flake-utils.lib.eachDefaultSystem (system: let lib = "ppad-bolt3"; @@ -34,10 +39,12 @@ sha256 = ppad-sha256.packages.${system}.default; secp256k1 = ppad-secp256k1.packages.${system}.default; + ripemd160 = ppad-ripemd160.packages.${system}.default; hpkgs = pkgs.haskell.packages.ghc910.extend (new: old: { ppad-sha256 = sha256; ppad-secp256k1 = secp256k1; + ppad-ripemd160 = ripemd160; ${lib} = new.callCabal2nix lib ./. { }; }); diff --git a/lib/Lightning/Protocol/BOLT3/Scripts.hs b/lib/Lightning/Protocol/BOLT3/Scripts.hs @@ -1,5 +1,6 @@ {-# OPTIONS_HADDOCK prune #-} {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE OverloadedStrings #-} -- | -- Module: Lightning.Protocol.BOLT3.Scripts @@ -21,41 +22,653 @@ module Lightning.Protocol.BOLT3.Scripts ( -- * Funding output - -- funding_script - -- , funding_witness + funding_script + , funding_witness -- * to_local output - -- , to_local_script - -- , to_local_witness_spend - -- , to_local_witness_revoke + , to_local_script + , to_local_witness_spend + , to_local_witness_revoke -- * to_remote output - -- , to_remote_script - -- , to_remote_witness + , to_remote_script + , to_remote_witness -- * Anchor outputs - -- , anchor_script - -- , anchor_witness_owner - -- , anchor_witness_anyone + , anchor_script + , anchor_witness_owner + , anchor_witness_anyone -- * Offered HTLC output - -- , offered_htlc_script - -- , offered_htlc_witness_preimage - -- , offered_htlc_witness_revoke + , offered_htlc_script + , offered_htlc_witness_preimage + , offered_htlc_witness_revoke -- * Received HTLC output - -- , received_htlc_script - -- , received_htlc_witness_timeout - -- , received_htlc_witness_revoke + , received_htlc_script + , received_htlc_witness_timeout + , received_htlc_witness_revoke -- * HTLC-timeout/success output (same as to_local) - -- , htlc_output_script - -- , htlc_output_witness_spend - -- , htlc_output_witness_revoke + , htlc_output_script + , htlc_output_witness_spend + , htlc_output_witness_revoke -- * P2WSH helpers - -- , to_p2wsh - -- , witness_script_hash + , to_p2wsh + , witness_script_hash ) where +import Data.Bits ((.&.), shiftR) +import Data.Word (Word8, Word16, Word32) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Builder as BSB +import qualified Data.ByteString.Lazy as BSL +import qualified Crypto.Hash.SHA256 as SHA256 +import qualified Crypto.Hash.RIPEMD160 as RIPEMD160 import Lightning.Protocol.BOLT3.Types + +-- opcodes --------------------------------------------------------------------- + +-- | OP_0 / OP_FALSE (0x00) +op_0 :: Word8 +op_0 = 0x00 + +-- | OP_PUSHDATA for 1-75 bytes just uses the length as opcode +-- For data <=75 bytes, opcode is just the length + +-- | OP_IF (0x63) +op_if :: Word8 +op_if = 0x63 + +-- | OP_NOTIF (0x64) +op_notif :: Word8 +op_notif = 0x64 + +-- | OP_ELSE (0x67) +op_else :: Word8 +op_else = 0x67 + +-- | OP_ENDIF (0x68) +op_endif :: Word8 +op_endif = 0x68 + +-- | OP_DROP (0x75) +op_drop :: Word8 +op_drop = 0x75 + +-- | OP_DUP (0x76) +op_dup :: Word8 +op_dup = 0x76 + +-- | OP_SWAP (0x7c) +op_swap :: Word8 +op_swap = 0x7c + +-- | OP_SIZE (0x82) +op_size :: Word8 +op_size = 0x82 + +-- | OP_EQUAL (0x87) +op_equal :: Word8 +op_equal = 0x87 + +-- | OP_EQUALVERIFY (0x88) +op_equalverify :: Word8 +op_equalverify = 0x88 + +-- | OP_IFDUP (0x73) +op_ifdup :: Word8 +op_ifdup = 0x73 + +-- | OP_HASH160 (0xa9) +op_hash160 :: Word8 +op_hash160 = 0xa9 + +-- | OP_CHECKSIG (0xac) +op_checksig :: Word8 +op_checksig = 0xac + +-- | OP_CHECKSIGVERIFY (0xad) +op_checksigverify :: Word8 +op_checksigverify = 0xad + +-- | OP_CHECKMULTISIG (0xae) +op_checkmultisig :: Word8 +op_checkmultisig = 0xae + +-- | OP_CHECKLOCKTIMEVERIFY (0xb1) +op_checklocktimeverify :: Word8 +op_checklocktimeverify = 0xb1 + +-- | OP_CHECKSEQUENCEVERIFY (0xb2) +op_checksequenceverify :: Word8 +op_checksequenceverify = 0xb2 + +-- | OP_1 (0x51) +op_1 :: Word8 +op_1 = 0x51 + +-- | OP_2 (0x52) +op_2 :: Word8 +op_2 = 0x52 + +-- | OP_16 (0x60) +op_16 :: Word8 +op_16 = 0x60 + +-- helpers --------------------------------------------------------------------- + +-- | Push a bytestring onto the stack (handles length encoding). +-- +-- For data <= 75 bytes, the length itself is the opcode. +push_data :: BS.ByteString -> BSB.Builder +push_data !bs + | len <= 75 = BSB.word8 (fromIntegral len) <> BSB.byteString bs + | len <= 255 = BSB.word8 0x4c <> BSB.word8 (fromIntegral len) + <> BSB.byteString bs + | len <= 65535 = BSB.word8 0x4d <> BSB.word16LE (fromIntegral len) + <> BSB.byteString bs + | otherwise = BSB.word8 0x4e <> BSB.word32LE (fromIntegral len) + <> BSB.byteString bs + where + !len = BS.length bs +{-# INLINE push_data #-} + +-- | Encode a Word16 as minimal script number (for CSV delays). +push_csv_delay :: Word16 -> BSB.Builder +push_csv_delay !n + | n == 0 = BSB.word8 op_0 + | n <= 16 = BSB.word8 (0x50 + fromIntegral n) + | n <= 0x7f = push_data (BS.singleton (fromIntegral n)) + | n <= 0x7fff = push_data (BS.pack [lo, hi]) + | otherwise = push_data (BS.pack [lo, hi, 0x00]) -- need sign byte + where + !lo = fromIntegral (n .&. 0xff) + !hi = fromIntegral ((n `shiftR` 8) .&. 0xff) +{-# INLINE push_csv_delay #-} + +-- | Encode a Word32 as minimal script number (for CLTV). +push_cltv :: Word32 -> BSB.Builder +push_cltv !n + | n == 0 = BSB.word8 op_0 + | n <= 16 = BSB.word8 (0x50 + fromIntegral n) + | otherwise = push_data (encode_scriptnum n) + where + encode_scriptnum :: Word32 -> BS.ByteString + encode_scriptnum 0 = BS.empty + encode_scriptnum !v = + let -- Build bytes little-endian + go :: Word32 -> [Word8] -> [Word8] + go 0 acc = acc + go !x acc = go (x `shiftR` 8) (fromIntegral (x .&. 0xff) : acc) + !bytes = reverse (go v []) + -- If high bit set, need to add 0x00 for positive numbers + !result = case bytes of + [] -> [] + (b:_) | b .&. 0x80 /= 0 -> 0x00 : bytes + _ -> bytes + in BS.pack (reverse result) +{-# INLINE push_cltv #-} + +-- | Build script from builder. +build_script :: BSB.Builder -> Script +build_script = Script . BSL.toStrict . BSB.toLazyByteString +{-# INLINE build_script #-} + +-- | HASH160 = RIPEMD160(SHA256(x)) +hash160 :: BS.ByteString -> BS.ByteString +hash160 = RIPEMD160.hash . SHA256.hash +{-# INLINE hash160 #-} + +-- P2WSH helpers --------------------------------------------------------------- + +-- | Compute SHA256 hash of a witness script. +-- +-- >>> witness_script_hash (Script "some_script") +-- <32-byte SHA256 hash> +witness_script_hash :: Script -> BS.ByteString +witness_script_hash (Script !s) = SHA256.hash s +{-# INLINE witness_script_hash #-} + +-- | Convert a witness script to P2WSH scriptPubKey. +-- +-- P2WSH format: OP_0 <32-byte-hash> +-- +-- >>> to_p2wsh some_witness_script +-- Script "\x00\x20<32-byte-hash>" +to_p2wsh :: Script -> Script +to_p2wsh !script = + let !h = witness_script_hash script + in build_script (BSB.word8 op_0 <> push_data h) +{-# INLINE to_p2wsh #-} + +-- funding output -------------------------------------------------------------- + +-- | Funding output witness script (2-of-2 multisig). +-- +-- Script: @2 <pubkey1> <pubkey2> 2 OP_CHECKMULTISIG@ +-- +-- Where pubkey1 is lexicographically lesser. +-- +-- >>> funding_script pk1 pk2 +-- Script "R!<pk_lesser>!<pk_greater>R\xae" +funding_script :: FundingPubkey -> FundingPubkey -> Script +funding_script (FundingPubkey (Pubkey !pk1)) (FundingPubkey (Pubkey !pk2)) = + let (!lesser, !greater) = if pk1 <= pk2 then (pk1, pk2) else (pk2, pk1) + in build_script $ + BSB.word8 op_2 + <> push_data lesser + <> push_data greater + <> BSB.word8 op_2 + <> BSB.word8 op_checkmultisig + +-- | Witness for spending funding output. +-- +-- Witness: @0 <sig1> <sig2>@ +-- +-- Signatures ordered to match pubkey order in script. +-- +-- >>> funding_witness sig1 sig2 +-- Witness ["", sig1, sig2] +funding_witness :: BS.ByteString -> BS.ByteString -> Witness +funding_witness !sig1 !sig2 = Witness [BS.empty, sig1, sig2] + +-- to_local output ------------------------------------------------------------- + +-- | to_local witness script (revocable with CSV delay). +-- +-- Script: +-- +-- @ +-- OP_IF +-- <revocationpubkey> +-- OP_ELSE +-- <to_self_delay> +-- OP_CHECKSEQUENCEVERIFY +-- OP_DROP +-- <local_delayedpubkey> +-- OP_ENDIF +-- OP_CHECKSIG +-- @ +-- +-- >>> to_local_script revpk delay localpk +-- Script "c!<revpk>g<delay>\xb2u!<localpk>h\xac" +to_local_script + :: RevocationPubkey + -> ToSelfDelay + -> LocalDelayedPubkey + -> Script +to_local_script + (RevocationPubkey (Pubkey !revpk)) + (ToSelfDelay !delay) + (LocalDelayedPubkey (Pubkey !localpk)) = + build_script $ + BSB.word8 op_if + <> push_data revpk + <> BSB.word8 op_else + <> push_csv_delay delay + <> BSB.word8 op_checksequenceverify + <> BSB.word8 op_drop + <> push_data localpk + <> BSB.word8 op_endif + <> BSB.word8 op_checksig + +-- | Witness for delayed spend of to_local output. +-- +-- Input nSequence must be set to to_self_delay. +-- +-- Witness: @<local_delayedsig> <>@ +-- +-- >>> to_local_witness_spend sig +-- Witness [sig, ""] +to_local_witness_spend :: BS.ByteString -> Witness +to_local_witness_spend !sig = Witness [sig, BS.empty] + +-- | Witness for revocation spend of to_local output. +-- +-- Witness: @<revocation_sig> 1@ +-- +-- >>> to_local_witness_revoke sig +-- Witness [sig, "\x01"] +to_local_witness_revoke :: BS.ByteString -> Witness +to_local_witness_revoke !sig = Witness [sig, BS.singleton 0x01] + +-- to_remote output ------------------------------------------------------------ + +-- | to_remote witness script. +-- +-- With option_anchors: +-- +-- @ +-- <remotepubkey> OP_CHECKSIGVERIFY 1 OP_CHECKSEQUENCEVERIFY +-- @ +-- +-- Without option_anchors: P2WPKH (just the pubkey hash). +-- +-- >>> to_remote_script pk (ChannelFeatures True) +-- Script "!<pk>\xadQ\xb2" +to_remote_script :: RemotePubkey -> ChannelFeatures -> Script +to_remote_script (RemotePubkey (Pubkey !pk)) !features + | has_anchors features = + -- Anchors: script with 1-block CSV + build_script $ + push_data pk + <> BSB.word8 op_checksigverify + <> BSB.word8 op_1 + <> BSB.word8 op_checksequenceverify + | otherwise = + -- No anchors: P2WPKH (OP_0 <20-byte-hash>) + let !h = hash160 pk + in build_script (BSB.word8 op_0 <> push_data h) + +-- | Witness for spending to_remote output. +-- +-- With option_anchors, input nSequence must be 1. +-- +-- Witness: @<remote_sig>@ (for anchors, witness script appended by caller) +-- For P2WPKH: @<remote_sig> <remotepubkey>@ +-- +-- >>> to_remote_witness sig +-- Witness [sig] +to_remote_witness :: BS.ByteString -> Witness +to_remote_witness !sig = Witness [sig] + +-- anchor outputs -------------------------------------------------------------- + +-- | Anchor output witness script. +-- +-- Script: +-- +-- @ +-- <funding_pubkey> OP_CHECKSIG OP_IFDUP +-- OP_NOTIF +-- OP_16 OP_CHECKSEQUENCEVERIFY +-- OP_ENDIF +-- @ +-- +-- >>> anchor_script fundpk +-- Script "!<fundpk>\xac\x73d`\xb2h" +anchor_script :: FundingPubkey -> Script +anchor_script (FundingPubkey (Pubkey !pk)) = + build_script $ + push_data pk + <> BSB.word8 op_checksig + <> BSB.word8 op_ifdup + <> BSB.word8 op_notif + <> BSB.word8 op_16 + <> BSB.word8 op_checksequenceverify + <> BSB.word8 op_endif + +-- | Witness for owner to spend anchor output. +-- +-- Witness: @<sig>@ +-- +-- >>> anchor_witness_owner sig +-- Witness [sig] +anchor_witness_owner :: BS.ByteString -> Witness +anchor_witness_owner !sig = Witness [sig] + +-- | Witness for anyone to sweep anchor output after 16 blocks. +-- +-- Witness: @<>@ +-- +-- >>> anchor_witness_anyone +-- Witness [""] +anchor_witness_anyone :: Witness +anchor_witness_anyone = Witness [BS.empty] + +-- offered HTLC output --------------------------------------------------------- + +-- | Offered HTLC witness script. +-- +-- Without option_anchors: +-- +-- @ +-- OP_DUP OP_HASH160 <RIPEMD160(SHA256(revocationpubkey))> OP_EQUAL +-- OP_IF +-- OP_CHECKSIG +-- OP_ELSE +-- <remote_htlcpubkey> OP_SWAP OP_SIZE 32 OP_EQUAL +-- OP_NOTIF +-- OP_DROP 2 OP_SWAP <local_htlcpubkey> 2 OP_CHECKMULTISIG +-- OP_ELSE +-- OP_HASH160 <RIPEMD160(payment_hash)> OP_EQUALVERIFY +-- OP_CHECKSIG +-- OP_ENDIF +-- OP_ENDIF +-- @ +-- +-- With option_anchors, adds @1 OP_CHECKSEQUENCEVERIFY OP_DROP@ before +-- final OP_ENDIF. +offered_htlc_script + :: RevocationPubkey + -> RemoteHtlcPubkey + -> LocalHtlcPubkey + -> PaymentHash + -> ChannelFeatures + -> Script +offered_htlc_script + (RevocationPubkey (Pubkey !revpk)) + (RemoteHtlcPubkey (Pubkey !remotepk)) + (LocalHtlcPubkey (Pubkey !localpk)) + (PaymentHash !ph) + !features = + let !revpk_hash = hash160 revpk + !payment_hash160 = RIPEMD160.hash ph + !csv_suffix = if has_anchors features + then BSB.word8 op_1 + <> BSB.word8 op_checksequenceverify + <> BSB.word8 op_drop + else mempty + in build_script $ + -- OP_DUP OP_HASH160 <revpk_hash> OP_EQUAL + BSB.word8 op_dup + <> BSB.word8 op_hash160 + <> push_data revpk_hash + <> BSB.word8 op_equal + -- OP_IF OP_CHECKSIG + <> BSB.word8 op_if + <> BSB.word8 op_checksig + -- OP_ELSE + <> BSB.word8 op_else + -- <remote_htlcpubkey> OP_SWAP OP_SIZE 32 OP_EQUAL + <> push_data remotepk + <> BSB.word8 op_swap + <> BSB.word8 op_size + <> push_data (BS.singleton 32) + <> BSB.word8 op_equal + -- OP_NOTIF + <> BSB.word8 op_notif + -- OP_DROP 2 OP_SWAP <local_htlcpubkey> 2 OP_CHECKMULTISIG + <> BSB.word8 op_drop + <> BSB.word8 op_2 + <> BSB.word8 op_swap + <> push_data localpk + <> BSB.word8 op_2 + <> BSB.word8 op_checkmultisig + -- OP_ELSE + <> BSB.word8 op_else + -- OP_HASH160 <payment_hash160> OP_EQUALVERIFY OP_CHECKSIG + <> BSB.word8 op_hash160 + <> push_data payment_hash160 + <> BSB.word8 op_equalverify + <> BSB.word8 op_checksig + -- OP_ENDIF + <> BSB.word8 op_endif + -- CSV suffix for anchors + <> csv_suffix + -- OP_ENDIF + <> BSB.word8 op_endif + +-- | Witness for remote node to claim offered HTLC with preimage. +-- +-- With option_anchors, input nSequence must be 1. +-- +-- Witness: @<remotehtlcsig> <payment_preimage>@ +-- +-- >>> offered_htlc_witness_preimage sig preimage +-- Witness [sig, preimage] +offered_htlc_witness_preimage + :: BS.ByteString -> PaymentPreimage -> Witness +offered_htlc_witness_preimage !sig (PaymentPreimage !preimage) = + Witness [sig, preimage] + +-- | Witness for revocation spend of offered HTLC. +-- +-- Witness: @<revocation_sig> <revocationpubkey>@ +-- +-- >>> offered_htlc_witness_revoke sig revpk +-- Witness [sig, revpk] +offered_htlc_witness_revoke :: BS.ByteString -> Pubkey -> Witness +offered_htlc_witness_revoke !sig (Pubkey !revpk) = Witness [sig, revpk] + +-- received HTLC output -------------------------------------------------------- + +-- | Received HTLC witness script. +-- +-- Without option_anchors: +-- +-- @ +-- OP_DUP OP_HASH160 <RIPEMD160(SHA256(revocationpubkey))> OP_EQUAL +-- OP_IF +-- OP_CHECKSIG +-- OP_ELSE +-- <remote_htlcpubkey> OP_SWAP OP_SIZE 32 OP_EQUAL +-- OP_IF +-- OP_HASH160 <RIPEMD160(payment_hash)> OP_EQUALVERIFY +-- 2 OP_SWAP <local_htlcpubkey> 2 OP_CHECKMULTISIG +-- OP_ELSE +-- OP_DROP <cltv_expiry> OP_CHECKLOCKTIMEVERIFY OP_DROP +-- OP_CHECKSIG +-- OP_ENDIF +-- OP_ENDIF +-- @ +-- +-- With option_anchors, adds @1 OP_CHECKSEQUENCEVERIFY OP_DROP@ before +-- final OP_ENDIF. +received_htlc_script + :: RevocationPubkey + -> RemoteHtlcPubkey + -> LocalHtlcPubkey + -> PaymentHash + -> CltvExpiry + -> ChannelFeatures + -> Script +received_htlc_script + (RevocationPubkey (Pubkey !revpk)) + (RemoteHtlcPubkey (Pubkey !remotepk)) + (LocalHtlcPubkey (Pubkey !localpk)) + (PaymentHash !ph) + (CltvExpiry !expiry) + !features = + let !revpk_hash = hash160 revpk + !payment_hash160 = RIPEMD160.hash ph + !csv_suffix = if has_anchors features + then BSB.word8 op_1 + <> BSB.word8 op_checksequenceverify + <> BSB.word8 op_drop + else mempty + in build_script $ + -- OP_DUP OP_HASH160 <revpk_hash> OP_EQUAL + BSB.word8 op_dup + <> BSB.word8 op_hash160 + <> push_data revpk_hash + <> BSB.word8 op_equal + -- OP_IF OP_CHECKSIG + <> BSB.word8 op_if + <> BSB.word8 op_checksig + -- OP_ELSE + <> BSB.word8 op_else + -- <remote_htlcpubkey> OP_SWAP OP_SIZE 32 OP_EQUAL + <> push_data remotepk + <> BSB.word8 op_swap + <> BSB.word8 op_size + <> push_data (BS.singleton 32) + <> BSB.word8 op_equal + -- OP_IF + <> BSB.word8 op_if + -- OP_HASH160 <payment_hash160> OP_EQUALVERIFY + <> BSB.word8 op_hash160 + <> push_data payment_hash160 + <> BSB.word8 op_equalverify + -- 2 OP_SWAP <local_htlcpubkey> 2 OP_CHECKMULTISIG + <> BSB.word8 op_2 + <> BSB.word8 op_swap + <> push_data localpk + <> BSB.word8 op_2 + <> BSB.word8 op_checkmultisig + -- OP_ELSE + <> BSB.word8 op_else + -- OP_DROP <cltv_expiry> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG + <> BSB.word8 op_drop + <> push_cltv expiry + <> BSB.word8 op_checklocktimeverify + <> BSB.word8 op_drop + <> BSB.word8 op_checksig + -- OP_ENDIF + <> BSB.word8 op_endif + -- CSV suffix for anchors + <> csv_suffix + -- OP_ENDIF + <> BSB.word8 op_endif + +-- | Witness for remote node to timeout received HTLC. +-- +-- With option_anchors, input nSequence must be 1. +-- +-- Witness: @<remotehtlcsig> <>@ +-- +-- >>> received_htlc_witness_timeout sig +-- Witness [sig, ""] +received_htlc_witness_timeout :: BS.ByteString -> Witness +received_htlc_witness_timeout !sig = Witness [sig, BS.empty] + +-- | Witness for revocation spend of received HTLC. +-- +-- Witness: @<revocation_sig> <revocationpubkey>@ +-- +-- >>> received_htlc_witness_revoke sig revpk +-- Witness [sig, revpk] +received_htlc_witness_revoke :: BS.ByteString -> Pubkey -> Witness +received_htlc_witness_revoke !sig (Pubkey !revpk) = Witness [sig, revpk] + +-- HTLC-timeout/success output ------------------------------------------------- + +-- | HTLC output witness script (same structure as to_local). +-- +-- Used for HTLC-timeout and HTLC-success transaction outputs. +-- +-- Script: +-- +-- @ +-- OP_IF +-- <revocationpubkey> +-- OP_ELSE +-- <to_self_delay> +-- OP_CHECKSEQUENCEVERIFY +-- OP_DROP +-- <local_delayedpubkey> +-- OP_ENDIF +-- OP_CHECKSIG +-- @ +htlc_output_script + :: RevocationPubkey + -> ToSelfDelay + -> LocalDelayedPubkey + -> Script +htlc_output_script = to_local_script + +-- | Witness for delayed spend of HTLC output. +-- +-- Input nSequence must be set to to_self_delay. +-- +-- Witness: @<local_delayedsig> 0@ +htlc_output_witness_spend :: BS.ByteString -> Witness +htlc_output_witness_spend = to_local_witness_spend + +-- | Witness for revocation spend of HTLC output. +-- +-- Witness: @<revocationsig> 1@ +htlc_output_witness_revoke :: BS.ByteString -> Witness +htlc_output_witness_revoke = to_local_witness_revoke diff --git a/ppad-bolt3.cabal b/ppad-bolt3.cabal @@ -35,6 +35,7 @@ library build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13 + , ppad-ripemd160 , ppad-secp256k1 , ppad-sha256