psbt.go raw

   1  // Package psbt writes the BIP174 form of a spend the wallet is about to make.
   2  //
   3  // The wallet signs locally, so this is not how a signature gets made; it is how a spend is
   4  // described to anything else that speaks PSBT — the swap, a coordinator, a person checking the
   5  // bytes. What it must get right is the record set: the witness UTXO and the witness script are
   6  // what let a signer know what it is signing, and the SHA-256 preimage record is what lets a
   7  // P2WSH claim be finalised without the preimage being in the transaction.
   8  package psbt
   9  
  10  import (
  11  	"crypto/sha256"
  12  	"encoding/binary"
  13  	"errors"
  14  	"fmt"
  15  
  16  	"git.smesh.lol/tinywallet/pkg/tx"
  17  )
  18  
  19  // Magic is the file magic: "psbt" and 0xff.
  20  var magic = []byte{0x70, 0x73, 0x62, 0x74, 0xff}
  21  
  22  // The record keys this package writes.
  23  const (
  24  	// globalUnsignedTx is the global map's only required key.
  25  	globalUnsignedTx byte = 0x00
  26  	// inputWitnessUTXO carries the output being spent, for segwit inputs.
  27  	inputWitnessUTXO byte = 0x01
  28  	// inputWitnessScript is the script of a P2WSH input.
  29  	inputWitnessScript byte = 0x05
  30  	// inputSHA256Preimage carries the preimage for an OP_SHA256 check.
  31  	inputSHA256Preimage byte = 0x0b
  32  )
  33  
  34  // Errors the writer refuses.
  35  var (
  36  	// ErrNoInputs is a spend with nothing to spend.
  37  	ErrNoInputs = errors.New("psbt: a psbt needs an input")
  38  	// ErrLockedByPreimage is a preimage whose hash is not the one the script demands.
  39  	ErrLockedByPreimage = errors.New("psbt: the preimage does not match the secret hash")
  40  )
  41  
  42  // SegwitInput describes one segwit input the wallet is spending.
  43  type SegwitInput struct {
  44  	// Amount is the value of the output being spent, in satoshis. BIP174 requires it for a
  45  	// witness input, and BIP143 commits to it.
  46  	Amount uint64
  47  	// ScriptPubKey is the script being spent.
  48  	ScriptPubKey []byte
  49  	// WitnessScript is the P2WSH script, if this is a script-hash spend.
  50  	WitnessScript []byte
  51  	// Preimage is the SHA-256 preimage, if the script needs one.
  52  	Preimage []byte
  53  	// SecretHash is the hash the script compares the preimage against.
  54  	SecretHash [32]byte
  55  }
  56  
  57  // Write renders a PSBT for an unsigned transaction and its inputs.
  58  func Write(transaction *tx.Tx, inputs []SegwitInput) ([]byte, error) {
  59  	if len(inputs) == 0 || len(inputs) != len(transaction.Inputs) {
  60  		return nil, ErrNoInputs
  61  	}
  62  
  63  	unsigned, err := transaction.Serialize()
  64  
  65  	if err != nil {
  66  		return nil, err
  67  	}
  68  
  69  	out := append([]byte{}, magic...)
  70  	out = appendRecord(out, globalUnsignedTx, unsigned)
  71  	out = append(out, 0x00) // end of the global map
  72  
  73  	for _, input := range inputs {
  74  		utxo := make([]byte, 0, 8+len(input.ScriptPubKey))
  75  		utxo = binary.LittleEndian.AppendUint64(utxo, input.Amount)
  76  		utxo = append(utxo, compactSize(len(input.ScriptPubKey))...)
  77  		utxo = append(utxo, input.ScriptPubKey...)
  78  
  79  		out = appendRecord(out, inputWitnessUTXO, utxo)
  80  
  81  		if len(input.WitnessScript) > 0 {
  82  			out = appendRecord(out, inputWitnessScript, input.WitnessScript)
  83  		}
  84  
  85  		if len(input.Preimage) > 0 {
  86  			if err := checkPreimage(input); err != nil {
  87  				return nil, err
  88  			}
  89  
  90  			key := append([]byte{inputSHA256Preimage}, input.SecretHash[:]...)
  91  
  92  			out = appendKeyedRecord(out, key, input.Preimage)
  93  		}
  94  
  95  		out = append(out, 0x00) // end of this input's map
  96  	}
  97  
  98  	for range transaction.Outputs {
  99  		out = append(out, 0x00) // an output map, empty
 100  	}
 101  
 102  	return out, nil
 103  }
 104  
 105  // checkPreimage refuses a preimage that does not open the hash it is offered with.
 106  func checkPreimage(input SegwitInput) error {
 107  	sum := sha256Of(input.Preimage)
 108  
 109  	if string(sum) != string(input.SecretHash[:]) {
 110  		return ErrLockedByPreimage
 111  	}
 112  
 113  	return nil
 114  }
 115  
 116  // appendRecord writes a single-key record.
 117  func appendRecord(out []byte, key byte, value []byte) []byte {
 118  	return appendKeyedRecord(out, []byte{key}, value)
 119  }
 120  
 121  // appendKeyedRecord writes one key and its value in BIP174's form.
 122  func appendKeyedRecord(out, key, value []byte) []byte {
 123  	out = append(out, compactSize(len(key))...)
 124  	out = append(out, key...)
 125  	out = append(out, compactSize(len(value))...)
 126  
 127  	return append(out, value...)
 128  }
 129  
 130  // compactSize writes BIP174's length prefix.
 131  func compactSize(value int) []byte {
 132  	switch {
 133  	case value < 0xfd:
 134  		return []byte{byte(value)}
 135  	case value <= 0xffff:
 136  		return []byte{0xfd, byte(value), byte(value >> 8)}
 137  	}
 138  
 139  	return []byte{0xfe, byte(value), byte(value >> 8), byte(value >> 16), byte(value >> 24)}
 140  }
 141  
 142  // sha256Of is the hash the HTLC's OP_SHA256 computes.
 143  func sha256Of(preimage []byte) []byte {
 144  	sum := sha256.Sum256(preimage)
 145  
 146  	return sum[:]
 147  }
 148  
 149  // String names a record for an error message.
 150  func recordName(key byte) string {
 151  	switch key {
 152  	case globalUnsignedTx:
 153  		return "unsigned transaction"
 154  	case inputWitnessUTXO:
 155  		return "witness utxo"
 156  	case inputWitnessScript:
 157  		return "witness script"
 158  	case inputSHA256Preimage:
 159  		return "sha256 preimage"
 160  	}
 161  
 162  	return fmt.Sprintf("record %#x", key)
 163  }
 164