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