commit d8dbc23a79dc8ce18a3ad1eb78d69b53d935b7b0
parent 23add542ee6973a0714d712dce912113c7310d4f
Author: Jared Tobin <jared@jtobin.io>
Date: Sun, 25 Jan 2026 11:06:18 +0400
Merge branch 'impl/scripts'
Diffstat:
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