-
Notifications
You must be signed in to change notification settings - Fork 89
/
utxo.go
404 lines (372 loc) · 14 KB
/
utxo.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
399
400
401
402
403
404
// 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 dcr
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"decred.org/dcrdex/dex"
dexdcr "decred.org/dcrdex/dex/networks/dcr"
"decred.org/dcrdex/server/asset"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/txscript/v4/stdscript"
)
const ErrReorgDetected = dex.ErrorKind("reorg detected")
// TXIO is common information stored with an Input or Output.
type TXIO struct {
// Because a TXIO's validity and block info can change after creation, keep a
// Backend around to query the state of the tx and update the block info.
dcr *Backend
tx *Tx
// The height and hash of the transaction's best known block.
height uint32
blockHash chainhash.Hash
// The number of confirmations needed for maturity. For outputs of coinbase
// transactions and stake-related transactions, this will be set to
// chaincfg.Params.CoinbaseMaturity (256 for mainchain). For other supported
// script types, this will be zero.
maturity int32
// While the TXIO's tx is still in mempool, the tip hash will be stored.
// This enables an optimization in the Confirmations method to return zero
// without extraneous RPC calls.
lastLookup *chainhash.Hash
}
// confirmations returns the number of confirmations for a tx's transaction.
// Because a tx can become invalid after once being considered valid, validity
// should be verified again on every call. An error will be returned if this tx
// is no longer ready to spend. An unmined transaction should have zero
// confirmations. A transaction in the current best block should have one
// confirmation. The value -1 will be returned with any error. This function is
// NOT thread-safe.
func (txio *TXIO) confirmations(ctx context.Context, checkApproval bool) (int64, error) {
tipHash := txio.dcr.blockCache.tipHash()
// If the tx was in a mempool transaction, check if it has been confirmed.
if txio.height == 0 {
// If the tip hasn't changed, don't do anything here.
if txio.lastLookup == nil || *txio.lastLookup != tipHash {
txio.lastLookup = &tipHash
verboseTx, err := txio.dcr.node.GetRawTransactionVerbose(ctx, &txio.tx.hash)
if err != nil {
if isTxNotFoundErr(err) {
return -1, asset.CoinNotFoundError
}
return -1, fmt.Errorf("confirmations: GetRawTransactionVerbose for txid %s: %w", txio.tx.hash, translateRPCCancelErr(err))
}
// More than zero confirmations would indicate that the transaction has
// been mined. Collect the block info and update the tx fields.
if verboseTx.Confirmations > 0 {
blk, err := txio.dcr.getBlockInfo(ctx, verboseTx.BlockHash)
if err != nil {
return -1, err
}
txio.height = blk.height
txio.blockHash = blk.hash
}
return verboseTx.Confirmations, nil
}
} else {
// The tx was included in a block, but make sure that the tx's block has
// not been orphaned or voted as invalid.
mainchainBlock, found := txio.dcr.blockCache.atHeight(txio.height)
if !found || mainchainBlock.hash != txio.blockHash {
return -1, ErrReorgDetected
}
if mainchainBlock != nil && checkApproval {
nextBlock, err := txio.dcr.getMainchainDcrBlock(ctx, txio.height+1)
if err != nil {
return -1, fmt.Errorf("error retrieving approving block tx %s: %w", txio.tx.hash, err)
}
if nextBlock != nil && !nextBlock.vote {
return -1, fmt.Errorf("transaction %s block %s has been voted as invalid", txio.tx.hash, nextBlock.hash)
}
}
}
// If the height is still 0, this is a mempool transaction.
if txio.height == 0 {
return 0, nil
}
// Otherwise just check that there hasn't been a reorg which would render the
// output immature. This would be exceedingly rare (impossible?).
confs := int32(txio.dcr.blockCache.tipHeight()) - int32(txio.height) + 1
if confs < txio.maturity {
return -1, fmt.Errorf("transaction %s became immature", txio.tx.hash)
}
return int64(confs), nil
}
// TxID is a string identifier for the transaction, typically a hexadecimal
// representation of the byte-reversed transaction hash.
func (txio *TXIO) TxID() string {
return txio.tx.hash.String()
}
// FeeRate returns the transaction fee rate, in atoms/byte.
func (txio *TXIO) FeeRate() uint64 {
return txio.tx.feeRate
}
// Input is a transaction input.
type Input struct {
TXIO
vin uint32
}
var _ asset.Coin = (*Input)(nil)
// Value is the value of the previous output spent by the input.
func (input *Input) Value() uint64 {
return input.TXIO.tx.ins[input.vin].value
}
// String creates a human-readable representation of a Decred transaction input
// in the format "{txid = [transaction hash], vin = [input index]}".
func (input *Input) String() string {
return fmt.Sprintf("{txid = %s, vin = %d}", input.TxID(), input.vin)
}
// Confirmations returns the number of confirmations on this input's
// transaction.
func (input *Input) Confirmations(ctx context.Context) (int64, error) {
confs, err := input.confirmations(ctx, false)
if errors.Is(err, ErrReorgDetected) {
newInput, err := input.dcr.input(&input.tx.hash, input.vin)
if err != nil {
return -1, fmt.Errorf("input block is not mainchain")
}
*input = *newInput
return input.Confirmations(ctx)
}
return confs, err
}
// ID returns the coin ID.
func (input *Input) ID() []byte {
return toCoinID(&input.tx.hash, input.vin)
}
// spendsCoin checks whether a particular coin is spent in this coin's tx.
func (input *Input) spendsCoin(coinID []byte) (bool, error) {
txHash, vout, err := decodeCoinID(coinID)
if err != nil {
return false, fmt.Errorf("error decoding coin ID %x: %w", coinID, err)
}
if uint32(len(input.tx.ins)) < input.vin+1 {
return false, nil
}
txIn := input.tx.ins[input.vin]
return txIn.prevTx == *txHash && txIn.vout == vout, nil
}
// Output represents a transaction output.
type Output struct {
TXIO
vout uint32
// The output value.
value uint64
// Addresses encoded by the pkScript or the redeem script in the case of a
// P2SH pkScript.
addresses []string
// A bitmask for script type information.
scriptType dexdcr.ScriptType
scriptVersion uint16
// If the pkScript, or redeemScript in the case of a P2SH pkScript, is
// non-standard according to txscript.
nonStandardScript bool
// The output's scriptPubkey.
pkScript []byte
// If the pubkey script is P2SH, the Output will only be generated if
// the redeem script is supplied and the script-hash validated. If the
// pubkey script is not P2SH, redeemScript will be nil.
redeemScript []byte
// numSigs is the number of signatures required to spend this output.
numSigs int
// spendSize stores the best estimate of the size (bytes) of the serialized
// transaction input that spends this Output.
spendSize uint32
}
// Confirmations returns the number of confirmations for a transaction output.
// Because it is possible for an output that was once considered valid to later
// be considered invalid, this method can return an error to indicate the output
// is no longer valid. The definition of output validity should not be confused
// with the validity of regular tree transactions that is voted on by
// stakeholders. While stakeholder approval is a part of output validity, there
// are other considerations as well.
func (output *Output) Confirmations(ctx context.Context) (int64, error) {
confs, err := output.confirmations(ctx, false)
if errors.Is(err, ErrReorgDetected) {
newOut, err := output.dcr.output(&output.tx.hash, output.vout, output.redeemScript)
if err != nil {
if !errors.Is(err, asset.ErrRequestTimeout) {
err = fmt.Errorf("output block is not mainchain")
}
return -1, err
}
*output = *newOut
return output.Confirmations(ctx)
}
return confs, err
}
var _ asset.Coin = (*Output)(nil)
// SpendSize returns the maximum size of the serialized TxIn that spends this
// Output, in bytes.
func (output *Output) SpendSize() uint32 {
return output.spendSize
}
// ID returns the coin ID.
func (output *Output) ID() []byte {
return toCoinID(&output.tx.hash, output.vout)
}
// Value is the output value, in atoms.
func (output *Output) Value() uint64 {
return output.value // == output.TXIO.tx.outs[output.vout].value
}
func (output *Output) Addresses() []string {
return output.addresses
}
// String creates a human-readable representation of a Decred transaction output
// in the format "{txid = [transaction hash], vout = [output index]}".
func (output *Output) String() string {
return fmt.Sprintf("{txid = %s, vout = %d}", output.TxID(), output.vout)
}
// Auth verifies that the output pays to the supplied public key(s). This is an
// asset.FundingCoin method.
func (output *Output) Auth(pubkeys, sigs [][]byte, msg []byte) error {
if len(pubkeys) < output.numSigs {
return fmt.Errorf("not enough signatures for output %s:%d. expected %d, got %d", output.tx.hash, output.vout, output.numSigs, len(pubkeys))
}
evalScript := output.pkScript
if output.scriptType.IsP2SH() {
evalScript = output.redeemScript
}
scriptType, scriptAddrs := dexdcr.ExtractScriptAddrs(output.scriptVersion, evalScript, chainParams)
if scriptType == dexdcr.ScriptUnsupported {
return fmt.Errorf("non-standard script")
}
// Ensure that at least 1 signature is required to spend this output.
// Non-standard scripts are already be caught, but check again here in case
// this can happen another way. Note that Auth may be called via an
// interface, where this requirement may not fit into a generic spendability
// check.
if scriptAddrs.NRequired == 0 {
return fmt.Errorf("script requires no signatures to spend")
}
if scriptAddrs.NRequired != output.numSigs {
return fmt.Errorf("signature requirement mismatch for output %s:%d. %d != %d", output.tx.hash, output.vout, scriptAddrs.NRequired, output.numSigs)
}
matches, err := pkMatches(pubkeys, scriptAddrs.PubKeys, nil)
if err != nil {
return fmt.Errorf("error during pubkey matching: %w", err)
}
m, err := pkMatches(pubkeys, scriptAddrs.PkHashes, stdaddr.Hash160)
if err != nil {
return fmt.Errorf("error during pubkey hash matching: %w", err)
}
matches = append(matches, m...)
if len(matches) < output.numSigs {
return fmt.Errorf("not enough pubkey matches to satisfy the script for output %s:%d. expected %d, got %d", output.tx.hash, output.vout, output.numSigs, len(matches))
}
for _, match := range matches {
err := checkSig(msg, match.pubkey, sigs[match.idx], match.sigType)
if err != nil {
return err
}
}
return nil
}
// TODO: Eliminate the UTXO type. Instead use Output (asset.Coin) and check for
// spendability in the consumer as needed. This is left as is to retain current
// behavior with respect to the unspent requirements.
// A UTXO is information regarding an unspent transaction output.
type UTXO struct {
*Output
}
// Confirmations returns the number of confirmations on this output's
// transaction. See also (*Output).Confirmations. This function differs from the
// Output method in that it is necessary to relocate the utxo after a reorg, it
// may error if the output is spent.
func (utxo *UTXO) Confirmations(ctx context.Context) (int64, error) {
confs, err := utxo.confirmations(ctx, !utxo.scriptType.IsStake())
if errors.Is(err, ErrReorgDetected) {
// See if we can find the utxo in another block.
newUtxo, err := utxo.dcr.utxo(ctx, &utxo.tx.hash, utxo.vout, utxo.redeemScript)
if err != nil {
return -1, fmt.Errorf("utxo block is not mainchain")
}
*utxo = *newUtxo
return utxo.Confirmations(ctx)
}
return confs, err
}
var _ asset.FundingCoin = (*UTXO)(nil)
type pkMatch struct {
pubkey []byte
sigType dcrec.SignatureType
idx int
}
// pkMatches looks through a set of addresses and a returns a set of match
// structs with details about the match.
func pkMatches(pubkeys [][]byte, addrs []stdaddr.Address, hasher func([]byte) []byte) ([]pkMatch, error) {
matches := make([]pkMatch, 0, len(pubkeys))
if hasher == nil {
hasher = func(a []byte) []byte { return a }
}
matchIndex := make(map[string]struct{})
for _, addr := range addrs {
addrStr := addr.String()
addrScript, err := dexdcr.AddressScript(addr)
if err != nil {
return nil, err
}
sigType, err := dexdcr.AddressSigType(addr)
if err != nil {
return nil, err
}
for i, pubkey := range pubkeys {
if bytes.Equal(addrScript, hasher(pubkey)) {
_, alreadyFound := matchIndex[addrStr]
if alreadyFound {
continue
}
matchIndex[addrStr] = struct{}{}
matches = append(matches, pkMatch{
pubkey: pubkey,
sigType: sigType,
idx: i,
})
break
}
}
}
return matches, nil
}
// auditContract checks that the Contract is a swap contract and extracts the
// receiving address and contract value on success.
func auditContract(op *Output) (*asset.Contract, error) {
tx := op.tx
if len(tx.outs) <= int(op.vout) {
return nil, fmt.Errorf("invalid index %d for transaction %s", op.vout, tx.hash)
}
// InputInfo via (*Backend).output would already have screened out script
// versions >0, but do it again to be safe. However, note that this will
// break when other versions become standard.
if op.scriptVersion != 0 {
return nil, fmt.Errorf("invalid script version %d", op.scriptVersion)
}
output := tx.outs[int(op.vout)]
if output.version != 0 {
return nil, fmt.Errorf("unsupported script version %d", output.version)
}
scriptHash := stdscript.ExtractScriptHashV0(output.pkScript)
if scriptHash == nil {
return nil, fmt.Errorf("specified output %s:%d is not P2SH", tx.hash, op.vout)
}
if !bytes.Equal(stdaddr.Hash160(op.redeemScript), scriptHash) {
return nil, fmt.Errorf("swap contract hash mismatch for %s:%d", tx.hash, op.vout)
}
_, receiver, lockTime, secretHash, err := dexdcr.ExtractSwapDetails(op.redeemScript, chainParams)
if err != nil {
return nil, fmt.Errorf("error parsing swap contract for %s:%d: %w", tx.hash, op.vout, err)
}
return &asset.Contract{
Coin: op,
SwapAddress: receiver.String(),
RedeemScript: op.redeemScript,
SecretHash: secretHash,
LockTime: time.Unix(int64(lockTime), 0),
TxData: op.tx.raw,
}, nil
}