packages feed

ppad-tx-0.1.0: lib/Bitcoin/Prim/Tx/Sighash.hs

{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RecordWildCards #-}

-- |
-- Module: Bitcoin.Prim.Tx.Sighash
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Sighash computation for legacy and BIP143 segwit transactions.

module Bitcoin.Prim.Tx.Sighash (
    -- * Sighash Types
    SighashType(..)

    -- * Legacy Sighash
  , sighash_legacy

    -- * BIP143 Segwit Sighash
  , sighash_segwit
  ) where

import Bitcoin.Prim.Tx
    ( Tx(..)
    , TxIn(..)
    , TxOut(..)
    , put_word32_le
    , put_word64_le
    , put_compact
    , put_outpoint
    , put_txout
    , to_strict
    )
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BSB
import qualified Data.List.NonEmpty as NE
import Data.Word (Word8, Word64)
import GHC.Generics (Generic)

-- | Sighash type flags.
data SighashType
  = SIGHASH_ALL
  | SIGHASH_NONE
  | SIGHASH_SINGLE
  | SIGHASH_ALL_ANYONECANPAY
  | SIGHASH_NONE_ANYONECANPAY
  | SIGHASH_SINGLE_ANYONECANPAY
  deriving (Eq, Show, Generic)

-- | Encode sighash type to byte value.
sighash_byte :: SighashType -> Word8
sighash_byte !st = case st of
  SIGHASH_ALL                -> 0x01
  SIGHASH_NONE               -> 0x02
  SIGHASH_SINGLE             -> 0x03
  SIGHASH_ALL_ANYONECANPAY    -> 0x81
  SIGHASH_NONE_ANYONECANPAY   -> 0x82
  SIGHASH_SINGLE_ANYONECANPAY -> 0x83
{-# INLINE sighash_byte #-}

-- | Check if ANYONECANPAY flag is set.
is_anyonecanpay :: SighashType -> Bool
is_anyonecanpay !st = case st of
  SIGHASH_ALL_ANYONECANPAY    -> True
  SIGHASH_NONE_ANYONECANPAY   -> True
  SIGHASH_SINGLE_ANYONECANPAY -> True
  _                           -> False
{-# INLINE is_anyonecanpay #-}

-- | Get base sighash type (without ANYONECANPAY).
base_type :: SighashType -> SighashType
base_type !st = case st of
  SIGHASH_ALL_ANYONECANPAY    -> SIGHASH_ALL
  SIGHASH_NONE_ANYONECANPAY   -> SIGHASH_NONE
  SIGHASH_SINGLE_ANYONECANPAY -> SIGHASH_SINGLE
  other                       -> other
{-# INLINE base_type #-}

-- | 32 zero bytes.
zero32 :: BS.ByteString
zero32 = BS.replicate 32 0x00
{-# NOINLINE zero32 #-}

-- | Hash of 0x01 followed by 31 zero bytes (SIGHASH_SINGLE edge case).
sighash_single_bug :: BS.ByteString
sighash_single_bug = BS.cons 0x01 (BS.replicate 31 0x00)
{-# NOINLINE sighash_single_bug #-}

-- | Double SHA256.
hash256 :: BS.ByteString -> BS.ByteString
hash256 = SHA256.hash . SHA256.hash
{-# INLINE hash256 #-}

-- legacy sighash -------------------------------------------------------------

-- | Compute legacy sighash for P2PKH/P2SH inputs.
--
--   Modifies a copy of the transaction based on sighash flags, appends
--   the sighash type as 4-byte little-endian, and double SHA256s.
--
--   @
--   -- sign input 0 with SIGHASH_ALL
--   let hash = sighash_legacy tx 0 scriptPubKey SIGHASH_ALL
--   -- use hash with ECDSA signing
--   @
--
--   For SIGHASH_SINGLE with input index >= output count, returns the
--   special \"sighash single bug\" value (0x01 followed by 31 zero bytes).
sighash_legacy
  :: Tx
  -> Int              -- ^ input index
  -> BS.ByteString    -- ^ scriptPubKey being spent
  -> SighashType
  -> BS.ByteString    -- ^ 32-byte hash
sighash_legacy !tx !idx !script_pubkey !sighash_type
  -- SIGHASH_SINGLE edge case: index >= number of outputs
  | base == SIGHASH_SINGLE && idx >= NE.length (tx_outputs tx) =
      sighash_single_bug
  | otherwise =
      let !serialized = serialize_legacy_sighash tx idx script_pubkey sighash_type
      in  hash256 serialized
  where
    !base = base_type sighash_type

-- | Serialize transaction for legacy sighash computation.
--   Handles all sighash flags directly without constructing intermediate Tx.
serialize_legacy_sighash
  :: Tx
  -> Int
  -> BS.ByteString
  -> SighashType
  -> BS.ByteString
serialize_legacy_sighash Tx{..} !idx !script_pubkey !sighash_type =
  let !base = base_type sighash_type
      !anyonecanpay = is_anyonecanpay sighash_type
      !inputs_list = NE.toList tx_inputs
      !outputs_list = NE.toList tx_outputs

      -- Clear all scriptSigs, set signing input's script to scriptPubKey
      clear_scripts :: Int -> [TxIn] -> [TxIn]
      clear_scripts !_ [] = []
      clear_scripts !i (inp : rest)
        | i == idx  = inp { txin_script_sig = script_pubkey } : clear_rest
        | otherwise = inp { txin_script_sig = BS.empty } : clear_rest
        where
          !clear_rest = clear_scripts (i + 1) rest

      -- For NONE/SINGLE: zero out sequence numbers for other inputs
      zero_other_sequences :: Int -> [TxIn] -> [TxIn]
      zero_other_sequences !_ [] = []
      zero_other_sequences !i (inp : rest)
        | i == idx  = inp : zero_other_sequences (i + 1) rest
        | otherwise =
            inp { txin_sequence = 0 } : zero_other_sequences (i + 1) rest

      -- Process inputs based on sighash type
      !inputs_cleared = clear_scripts 0 inputs_list

      !inputs_processed = case base of
        SIGHASH_NONE   -> zero_other_sequences 0 inputs_cleared
        SIGHASH_SINGLE -> zero_other_sequences 0 inputs_cleared
        _              -> inputs_cleared

      -- ANYONECANPAY: keep only signing input
      !final_inputs
        | anyonecanpay = case safe_index inputs_processed idx of
            Just inp -> [inp]
            Nothing  -> []  -- shouldn't happen if idx is valid
        | otherwise = inputs_processed

      -- Process outputs based on sighash type
      !final_outputs = case base of
        SIGHASH_NONE   -> []
        SIGHASH_SINGLE -> build_single_outputs outputs_list idx
        _              -> outputs_list

  in  to_strict $
         put_word32_le tx_version
      <> put_compact (fromIntegral (length final_inputs))
      <> foldMap put_txin_legacy final_inputs
      <> put_compact (fromIntegral (length final_outputs))
      <> foldMap put_txout final_outputs
      <> put_word32_le tx_locktime
      <> put_word32_le (fromIntegral (sighash_byte sighash_type))

-- | Build outputs for SIGHASH_SINGLE: keep only output at idx,
--   replace earlier outputs with empty/zero outputs.
build_single_outputs :: [TxOut] -> Int -> [TxOut]
build_single_outputs !outs !target_idx = go 0 outs
  where
    go :: Int -> [TxOut] -> [TxOut]
    go !_ [] = []
    go !i (o : rest)
      | i == target_idx = [o]  -- keep this one and stop
      | i < target_idx  = empty_output : go (i + 1) rest
      | otherwise       = []   -- shouldn't reach here

    -- Empty output: -1 (0xffffffffffffffff) value, empty script
    empty_output :: TxOut
    empty_output = TxOut 0xffffffffffffffff BS.empty

-- | Safe list indexing.
safe_index :: [a] -> Int -> Maybe a
safe_index [] _ = Nothing
safe_index (x : xs) !n
  | n < 0     = Nothing
  | n == 0    = Just x
  | otherwise = safe_index xs (n - 1)
{-# INLINE safe_index #-}

-- | Encode TxIn for legacy sighash (same as normal encoding).
put_txin_legacy :: TxIn -> BSB.Builder
put_txin_legacy TxIn{..} =
       put_outpoint txin_prevout
    <> put_compact (fromIntegral (BS.length txin_script_sig))
    <> BSB.byteString txin_script_sig
    <> put_word32_le txin_sequence
{-# INLINE put_txin_legacy #-}

-- BIP143 segwit sighash -------------------------------------------------------

-- | Compute BIP143 segwit sighash.
--
--   Required for signing segwit inputs (P2WPKH, P2WSH). Unlike legacy
--   sighash, this commits to the value being spent, preventing fee
--   manipulation attacks.
--
--   Returns 'Nothing' if the input index is out of range.
--
--   @
--   -- sign P2WPKH input 0
--   let scriptCode = ...  -- P2WPKH scriptCode
--   let hash = sighash_segwit tx 0 scriptCode inputValue SIGHASH_ALL
--   -- use hash with ECDSA signing (after checking Just)
--   @
sighash_segwit
  :: Tx
  -> Int              -- ^ input index
  -> BS.ByteString    -- ^ scriptCode
  -> Word64           -- ^ value being spent (satoshis)
  -> SighashType
  -> Maybe BS.ByteString    -- ^ 32-byte hash, or Nothing if index invalid
sighash_segwit !tx !idx !script_code !value !sighash_type = do
  preimage <- build_bip143_preimage tx idx script_code value sighash_type
  pure $! hash256 preimage

-- | Build BIP143 preimage for signing.
--   Returns Nothing if the input index is out of range.
build_bip143_preimage
  :: Tx
  -> Int
  -> BS.ByteString
  -> Word64
  -> SighashType
  -> Maybe BS.ByteString
build_bip143_preimage Tx{..} !idx !script_code !value !sighash_type = do
  -- Get the input being signed; fail if index out of range
  let !inputs_list = NE.toList tx_inputs
      !outputs_list = NE.toList tx_outputs
  signing_input <- safe_index inputs_list idx

  let !base = base_type sighash_type
      !anyonecanpay = is_anyonecanpay sighash_type

      -- hashPrevouts: double SHA256 of all outpoints, or zero if ANYONECANPAY
      !hash_prevouts
        | anyonecanpay = zero32
        | otherwise    = hash256 $ to_strict $
            foldMap (put_outpoint . txin_prevout) tx_inputs

      -- hashSequence: double SHA256 of all sequences, or zero if
      -- ANYONECANPAY or NONE or SINGLE
      !hash_sequence
        | anyonecanpay = zero32
        | base == SIGHASH_SINGLE = zero32
        | base == SIGHASH_NONE   = zero32
        | otherwise = hash256 $ to_strict $
            foldMap (put_word32_le . txin_sequence) tx_inputs

      -- hashOutputs: depends on sighash type
      !hash_outputs = case base of
        SIGHASH_NONE -> zero32
        SIGHASH_SINGLE ->
          case safe_index outputs_list idx of
            Nothing  -> zero32  -- index out of range
            Just out -> hash256 $ to_strict $ put_txout out
        _ -> hash256 $ to_strict $ foldMap put_txout tx_outputs

      !outpoint = txin_prevout signing_input
      !sequence_n = txin_sequence signing_input

  pure $! to_strict $
       put_word32_le tx_version
    <> BSB.byteString hash_prevouts
    <> BSB.byteString hash_sequence
    <> put_outpoint outpoint
    <> put_compact (fromIntegral (BS.length script_code))
    <> BSB.byteString script_code
    <> put_word64_le value
    <> put_word32_le sequence_n
    <> BSB.byteString hash_outputs
    <> put_word32_le tx_locktime
    <> put_word32_le (fromIntegral (sighash_byte sighash_type))