/
testing.go
398 lines (386 loc) · 11.9 KB
/
testing.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.
package btc
import (
"context"
"encoding/hex"
"errors"
"fmt"
"testing"
dexbtc "decred.org/dcrdex/dex/networks/btc"
"decred.org/dcrdex/server/asset"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
)
// LiveP2SHStats will scan the provided Backend's node for inputs that spend
// pay-to-script-hash outputs. The pubkey scripts and redeem scripts are
// examined to ensure the backend understands what they are and can extract
// addresses. Ideally, the stats will show no scripts which were unparseable by
// the backend, but the presence of unknowns is not an error.
func LiveP2SHStats(btc *Backend, t *testing.T, numToDo int) {
type scriptStats struct {
unknown int
p2pk int
p2pkh int
p2wpkh int
p2wsh int
multisig int
escrow int
found int
zeros int
empty int
swaps int
emptyRedeems int
addrErr int
nonStd int
noSigs int
}
var stats scriptStats
hash, err := btc.node.GetBestBlockHash()
if err != nil {
t.Fatalf("error getting best block hash: %v", err)
}
block, err := btc.node.GetBlockVerbose(hash)
if err != nil {
t.Fatalf("error getting best block verbose: %v", err)
}
unknowns := []string{}
// For each txIn, grab the previous outpoint. If the outpoint pkScript is
// p2sh or p2wsh, locate the redeem script and take some stats on how the
// redeem script parses.
out:
for {
for _, txid := range block.Tx {
txHash, err := chainhash.NewHashFromStr(txid)
if err != nil {
t.Fatalf("error parsing transaction hash from %s: %v", txid, err)
}
tx, err := btc.node.GetRawTransactionVerbose(txHash)
if err != nil {
t.Fatalf("error fetching transaction %s: %v", txHash, err)
}
for vin, txIn := range tx.Vin {
txOutHash, err := chainhash.NewHashFromStr(txIn.Txid)
if err != nil {
t.Fatalf("error decoding txhash from hex %s: %v", txIn.Txid, err)
}
if *txOutHash == zeroHash {
stats.zeros++
continue
}
prevOutTx, err := btc.node.GetRawTransactionVerbose(txOutHash)
if err != nil {
t.Fatalf("error fetching previous outpoint: %v", err)
}
prevOutpoint := prevOutTx.Vout[int(txIn.Vout)]
pkScript, err := hex.DecodeString(prevOutpoint.ScriptPubKey.Hex)
if err != nil {
t.Fatalf("error decoding script from hex %s: %v", prevOutpoint.ScriptPubKey.Hex, err)
}
scriptType := dexbtc.ParseScriptType(pkScript, nil)
if scriptType.IsP2SH() {
stats.found++
if stats.found > numToDo {
break out
}
var redeemScript []byte
if scriptType.IsSegwit() {
// if it's segwit, the script is the last input witness data.
redeemHex := txIn.Witness[len(txIn.Witness)-1]
redeemScript, err = hex.DecodeString(redeemHex)
if err != nil {
t.Fatalf("error decoding redeem script from hex %s: %v", redeemHex, err)
}
} else {
// If it's non-segwit P2SH, the script is the last data push
// in the scriptSig.
scriptSig, err := hex.DecodeString(txIn.ScriptSig.Hex)
if err != nil {
t.Fatalf("error decoding redeem script from hex %s: %v", txIn.ScriptSig.Hex, err)
}
pushed, err := txscript.PushedData(scriptSig)
if err != nil {
t.Fatalf("error parsing scriptSig: %v", err)
}
if len(pushed) == 0 {
stats.empty++
continue
}
redeemScript = pushed[len(pushed)-1]
}
scriptType := dexbtc.ParseScriptType(pkScript, redeemScript)
scriptClass := txscript.GetScriptClass(redeemScript)
switch scriptClass {
case txscript.MultiSigTy:
if !scriptType.IsMultiSig() {
t.Fatalf("multi-sig script class but not parsed as multi-sig")
}
stats.multisig++
case txscript.PubKeyTy:
stats.p2pk++
case txscript.PubKeyHashTy:
stats.p2pkh++
case txscript.WitnessV0PubKeyHashTy:
stats.p2wpkh++
case txscript.WitnessV0ScriptHashTy:
stats.p2wsh++
default:
_, _, _, _, err = dexbtc.ExtractSwapDetails(redeemScript, btc.segwit, btc.chainParams)
if err == nil {
stats.swaps++
continue
}
if isEscrowScript(redeemScript) {
stats.escrow++
continue
}
if len(redeemScript) == 0 {
stats.emptyRedeems++
}
unknowns = append(unknowns, txHash.String()+":"+fmt.Sprintf("%d", vin))
stats.unknown++
}
evalScript := pkScript
if scriptType.IsP2SH() {
evalScript = redeemScript
}
scriptAddrs, nonStandard, err := dexbtc.ExtractScriptAddrs(evalScript, btc.chainParams)
if err != nil {
stats.addrErr++
continue
}
if nonStandard {
stats.nonStd++
}
if scriptAddrs.NRequired == 0 {
stats.noSigs++
}
}
}
}
prevHash, err := chainhash.NewHashFromStr(block.PreviousHash)
if err != nil {
t.Fatalf("error decoding previous block hash: %v", err)
}
block, err = btc.node.GetBlockVerbose(prevHash)
if err != nil {
t.Fatalf("error getting previous block verbose: %v", err)
}
}
t.Logf("%d P2WPKH redeem scripts", stats.p2wpkh)
t.Logf("%d P2WSH redeem scripts", stats.p2wsh)
t.Logf("%d multi-sig redeem scripts", stats.multisig)
t.Logf("%d P2PK redeem scripts", stats.p2pk)
t.Logf("%d P2PKH redeem scripts", stats.p2pkh)
t.Logf("%d unknown redeem scripts, %d of which were empty", stats.unknown, stats.emptyRedeems)
t.Logf("%d previous outpoint zero hashes (coinbase)", stats.zeros)
t.Logf("%d atomic swap contract redeem scripts", stats.swaps)
t.Logf("%d escrow scripts", stats.escrow)
t.Logf("%d error parsing addresses from script", stats.addrErr)
t.Logf("%d scripts parsed with 0 required signatures", stats.noSigs)
t.Logf("%d unexpected empty scriptSig", stats.empty)
numUnknown := len(unknowns)
if numUnknown > 0 {
numToShow := 5
if numUnknown < numToShow {
numToShow = numUnknown
}
t.Logf("showing %d of %d unknown scripts", numToShow, numUnknown)
for i, unknown := range unknowns {
if i == numToShow {
break
}
t.Logf(" %x", unknown)
}
} else {
t.Logf("no unknown script types")
}
}
// LiveUTXOStats will scan the provided Backend's node for transaction
// outputs. The outputs are requested with GetRawTransactionVerbose, and
// statistics collected regarding spendability and pubkey script types. This
// test does not request via the Backend.UTXO method and is not meant to
// cover that code. Instead, these tests check the backend's real-world
// blockchain literacy. Ideally, the stats will show no scripts which were
// unparseable by the backend, but the presence of unknowns is not an error.
func LiveUTXOStats(btc *Backend, t *testing.T) {
const numToDo = 5000
hash, err := btc.node.GetBestBlockHash()
if err != nil {
t.Fatalf("error getting best block hash: %v", err)
}
block, verboseHeader, err := btc.node.getBlockWithVerboseHeader(hash)
if err != nil {
t.Fatalf("error getting best block verbose: %v", err)
}
height := verboseHeader.Height
t.Logf("Processing block %v (%d)", hash, height)
type testStats struct {
p2pkh int
p2wpkh int
p2pk int
p2sh int
p2wsh int
zeros int
unknown int
found int
checked int
utxoErr int
utxoVal uint64
feeRates []uint64
}
var stats testStats
var unknowns [][]byte
var processed int
out:
for {
for _, msgTx := range block.Transactions {
for vout, txOut := range msgTx.TxOut {
if txOut.Value == 0 {
stats.zeros++
continue
}
pkScript := txOut.PkScript
scriptType := dexbtc.ParseScriptType(pkScript, nil)
if scriptType == dexbtc.ScriptUnsupported {
unknowns = append(unknowns, pkScript)
stats.unknown++
continue
}
processed++
if processed >= numToDo {
break out
}
txhash := msgTx.TxHash()
if scriptType.IsP2PKH() {
stats.p2pkh++
} else if scriptType.IsP2WPKH() {
stats.p2wpkh++
} else if scriptType.IsP2SH() {
stats.p2sh++
continue // no redeem script, can't use the utxo method
} else if scriptType.IsP2WSH() {
stats.p2wsh++
continue // no redeem script, can't use the utxo method
} else if scriptType.IsP2PK() { // rare, so last
t.Logf("p2pk: txout %v:%d", txhash, vout)
stats.p2pk++
} else {
stats.unknown++
t.Logf("other unknown script type: %v", scriptType)
}
stats.checked++
utxo, err := btc.utxo(&txhash, uint32(vout), nil)
if err != nil {
if !errors.Is(err, asset.CoinNotFoundError) {
t.Log(err, txhash)
stats.utxoErr++
}
continue
}
stats.feeRates = append(stats.feeRates, utxo.FeeRate())
stats.found++
stats.utxoVal += utxo.Value()
}
}
prevHash := block.Header.PrevBlock
block, verboseHeader, err = btc.node.getBlockWithVerboseHeader(&prevHash)
if err != nil {
t.Fatalf("error getting previous block verbose: %v", err)
}
height = verboseHeader.Height
h0 := block.BlockHash()
hash = &h0
t.Logf("Processing block %v (%d)", hash, height)
}
t.Logf("%d P2PKH scripts", stats.p2pkh)
t.Logf("%d P2WPKH scripts", stats.p2wpkh)
t.Logf("%d P2PK scripts", stats.p2pk)
t.Logf("%d P2SH scripts", stats.p2sh)
t.Logf("%d P2WSH scripts", stats.p2wsh)
t.Logf("%d zero-valued outputs", stats.zeros)
t.Logf("%d P2(W)PK(H) UTXOs found of %d checked, %.1f%%", stats.found, stats.checked, float64(stats.found)/float64(stats.checked)*100)
t.Logf("total unspent value counted: %.2f", float64(stats.utxoVal)/1e8)
t.Logf("%d P2PK(H) UTXO retrieval errors", stats.utxoErr)
numUnknown := len(unknowns)
if numUnknown > 0 {
numToShow := 5
if numUnknown < numToShow {
numToShow = numUnknown
}
t.Logf("showing %d of %d unknown scripts", numToShow, numUnknown)
for i, unknown := range unknowns {
if i == numToShow {
break
}
t.Logf(" %s", hex.EncodeToString(unknown))
}
} else {
t.Logf("no unknown script types")
}
// Fees
feeCount := len(stats.feeRates)
if feeCount > 0 {
var feeSum uint64
for _, r := range stats.feeRates {
feeSum += r
}
t.Logf("%d fees, avg rate %d", feeCount, feeSum/uint64(feeCount))
}
}
// LiveFeeRates scans a mapping of txid -> fee rate checking that the backend
// returns the expected fee rate.
func LiveFeeRates(btc *Backend, t *testing.T, standards map[string]uint64) {
for txid, expRate := range standards {
txHash, err := chainhash.NewHashFromStr(txid)
if err != nil {
t.Fatalf("error parsing transaction hash from %s: %v", txid, err)
}
verboseTx, err := btc.node.GetRawTransactionVerbose(txHash)
if err != nil {
t.Fatalf("error getting raw transaction: %v", err)
}
tx, err := btc.transaction(txHash, verboseTx)
if err != nil {
t.Fatalf("error retrieving transaction %s", txid)
}
if tx.feeRate != expRate {
t.Fatalf("unexpected fee rate for %s. expected %d, got %d", txid, expRate, tx.feeRate)
}
}
}
// This is an unsupported type of script, but one of the few that is fairly
// common.
func isEscrowScript(script []byte) bool {
if len(script) != 77 {
return false
}
if script[0] == txscript.OP_IF &&
script[1] == txscript.OP_DATA_33 &&
script[35] == txscript.OP_ELSE &&
script[36] == txscript.OP_DATA_2 &&
script[39] == txscript.OP_CHECKSEQUENCEVERIFY &&
script[40] == txscript.OP_DROP &&
script[41] == txscript.OP_DATA_33 &&
script[75] == txscript.OP_ENDIF &&
script[76] == txscript.OP_CHECKSIG {
return true
}
return false
}
func TestMedianFees(btc *Backend, t *testing.T) {
// The easy way.
medianFees, err := btc.node.medianFeeRate()
if err != nil {
t.Fatalf("medianFeeRate error: %v", err)
}
fmt.Printf("medianFeeRate: %v \n", medianFees)
}
func TestMedianFeesTheHardWay(btc *Backend, t *testing.T) {
// The hard way.
medianFees, err := btc.node.medianFeesTheHardWay(context.Background())
if err != nil {
t.Fatalf("medianFeesTheHardWay error: %v", err)
}
fmt.Printf("medianFeesTheHardWay: %v \n", medianFees)
}