psbt_test.go raw
1 package psbt
2
3 import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/binary"
7 "testing"
8
9 "git.smesh.lol/tinywallet/pkg/script"
10 "git.smesh.lol/tinywallet/pkg/tx"
11 )
12
13 // sha256Sum is the hash, named locally so the test does not depend on the implementation.
14 func sha256Sum(data []byte) [32]byte { return sha256.Sum256(data) }
15
16 // build makes an HTLC spend and the PSBT that describes it.
17 func build(t *testing.T) (*tx.Tx, SegwitInput, []byte) {
18 t.Helper()
19
20 preimage := make([]byte, 32)
21 preimage[31] = 0x01
22
23 var secretHash [32]byte
24 copy(secretHash[:], sha256Of(preimage))
25
26 claimant := bytes.Repeat([]byte{0x02}, 33)
27 refunder := bytes.Repeat([]byte{0x03}, 33)
28
29 witnessScript, err := script.HTLC(claimant, refunder, secretHash, 500)
30
31 if err != nil {
32 t.Fatal(err)
33 }
34
35 transaction := tx.New()
36
37 var outpoint [32]byte
38 outpoint[0] = 0x44
39
40 transaction.AddInput(tx.Outpoint{Hash: outpoint, Index: 1}, tx.SequenceFinal)
41 transaction.AddOutput(90000, script.P2WSH(witnessScript))
42
43 input := SegwitInput{
44 Amount: 100000,
45 ScriptPubKey: script.P2WSH(witnessScript),
46 WitnessScript: witnessScript,
47 Preimage: preimage,
48 SecretHash: secretHash,
49 }
50
51 return transaction, input, witnessScript
52 }
53
54 // TestThePSBTIsWellFormed: magic, an unsigned transaction, one input map with the records a
55 // script-hash spend needs, and an empty output map.
56 func TestThePSBTIsWellFormed(t *testing.T) {
57 transaction, input, witnessScript := build(t)
58
59 raw, err := Write(transaction, []SegwitInput{input})
60
61 if err != nil {
62 t.Fatal(err)
63 }
64
65 if !bytes.HasPrefix(raw, magic) {
66 t.Fatalf("the file starts %x", raw[:5])
67 }
68
69 unsigned, err := transaction.Serialize()
70
71 if err != nil {
72 t.Fatal(err)
73 }
74
75 if !bytes.Contains(raw, unsigned) {
76 t.Fatal("the unsigned transaction is not in the psbt")
77 }
78
79 if !bytes.Contains(raw, witnessScript) {
80 t.Fatal("the witness script is not in the psbt")
81 }
82
83 if !bytes.Contains(raw, input.Preimage) {
84 t.Fatal("the preimage is not in the psbt")
85 }
86
87 // The preimage record's key is the type byte followed by the hash.
88 if !bytes.Contains(raw, append([]byte{inputSHA256Preimage}, input.SecretHash[:]...)) {
89 t.Fatal("the preimage record is not keyed by its hash")
90 }
91 }
92
93 // TestTheWitnessUTXOCarriesTheAmount: BIP174 and BIP143 both need the value.
94 func TestTheWitnessUTXOCarriesTheAmount(t *testing.T) {
95 transaction, input, _ := build(t)
96
97 raw, err := Write(transaction, []SegwitInput{input})
98
99 if err != nil {
100 t.Fatal(err)
101 }
102
103 found := false
104
105 for start := 0; start+9 < len(raw); start++ {
106 if raw[start] != 0x01 || binary.LittleEndian.Uint64(raw[start+2:start+10]) != input.Amount {
107 continue
108 }
109
110 found = true
111
112 break
113 }
114
115 if !found {
116 t.Fatal("no witness utxo record carries the amount")
117 }
118 }
119
120 // TestAWrongPreimageIsRefused: a preimage that does not open the hash is a mistake, and a
121 // PSBT carrying it would fail later, on chain, where it costs a fee.
122 func TestAWrongPreimageIsRefused(t *testing.T) {
123 transaction, input, _ := build(t)
124
125 input.Preimage = make([]byte, 32)
126
127 if _, err := Write(transaction, []SegwitInput{input}); err != ErrLockedByPreimage {
128 t.Fatalf("a wrong preimage was written: %v", err)
129 }
130 }
131
132 // TestInputCountMustMatch: one description per input, or none.
133 func TestInputCountMustMatch(t *testing.T) {
134 transaction, _, _ := build(t)
135
136 if _, err := Write(transaction, nil); err != ErrNoInputs {
137 t.Fatalf("no inputs: %v", err)
138 }
139 }
140
141 // TestRecordNames: the names are what an error would say.
142 func TestRecordNames(t *testing.T) {
143 if recordName(inputSHA256Preimage) != "sha256 preimage" {
144 t.Fatal("the preimage record has no name")
145 }
146 }
147
148 // TestWritingAndReadingAgree: what the writer produces, the reader understands, record for
149 // record — which is the only check that keeps the two directions honest.
150 func TestWritingAndReadingAgree(t *testing.T) {
151 transaction, input, witnessScript := build(t)
152
153 raw, err := Write(transaction, []SegwitInput{input})
154
155 if err != nil {
156 t.Fatal(err)
157 }
158
159 file, err := Read(raw)
160
161 if err != nil {
162 t.Fatal(err)
163 }
164
165 unsigned, err := transaction.Serialize()
166
167 if err != nil {
168 t.Fatal(err)
169 }
170
171 if !bytes.Equal(file.Unsigned, unsigned) {
172 t.Fatal("the unsigned transaction did not survive")
173 }
174
175 if len(file.Inputs) != 1 {
176 t.Fatalf("%d input maps", len(file.Inputs))
177 }
178
179 read := file.Inputs[0]
180
181 if read.Amount != input.Amount || !bytes.Equal(read.ScriptPubKey, input.ScriptPubKey) {
182 t.Fatalf("the witness utxo reads as %d %x", read.Amount, read.ScriptPubKey)
183 }
184
185 if !bytes.Equal(read.WitnessScript, witnessScript) {
186 t.Fatal("the witness script did not survive")
187 }
188
189 if len(read.Preimages) != 1 || !bytes.Equal(read.Preimages[0].Preimage, input.Preimage) {
190 t.Fatal("the preimage did not survive")
191 }
192
193 if read.Preimages[0].SecretHash != input.SecretHash {
194 t.Fatal("the preimage is keyed by a different hash")
195 }
196 }
197
198 // TestNotAPSBT: magic first, everything else after.
199 func TestNotAPSBT(t *testing.T) {
200 if _, err := Read([]byte("not a psbt")); err != ErrNotPSBT {
201 t.Fatalf("a non-psbt: %v", err)
202 }
203 }
204
205 // TestATruncatedFileIsRefused: a length that runs past the end is an error, not a shorter file.
206 func TestATruncatedFileIsRefused(t *testing.T) {
207 transaction, input, _ := build(t)
208
209 raw, err := Write(transaction, []SegwitInput{input})
210
211 if err != nil {
212 t.Fatal(err)
213 }
214
215 for _, cut := range []int{6, 12, 40, len(raw) - 1} {
216 if _, err := Read(raw[:cut]); err == nil {
217 t.Fatalf("a %d-byte psbt parsed", cut)
218 }
219 }
220 }
221