@@ -19,16 +19,20 @@ import (
19
19
20
20
"decred.org/dcrdex/dex"
21
21
dexdcr "decred.org/dcrdex/dex/networks/dcr"
22
+ "decred.org/dcrdex/server/account"
22
23
"decred.org/dcrdex/server/asset"
23
24
"github.com/decred/dcrd/blockchain/stake/v4"
25
+ "github.com/decred/dcrd/blockchain/v4"
24
26
"github.com/decred/dcrd/chaincfg/chainhash"
25
27
"github.com/decred/dcrd/chaincfg/v3"
26
28
"github.com/decred/dcrd/dcrjson/v4"
27
29
"github.com/decred/dcrd/dcrutil/v4"
28
30
"github.com/decred/dcrd/hdkeychain/v3"
29
31
chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v3"
30
32
"github.com/decred/dcrd/rpcclient/v7"
33
+ "github.com/decred/dcrd/txscript/v4"
31
34
"github.com/decred/dcrd/txscript/v4/stdaddr"
35
+ "github.com/decred/dcrd/txscript/v4/stdscript"
32
36
"github.com/decred/dcrd/wire"
33
37
)
34
38
@@ -124,6 +128,7 @@ type dcrNode interface {
124
128
GetBestBlockHash (ctx context.Context ) (* chainhash.Hash , error )
125
129
GetBlockChainInfo (ctx context.Context ) (* chainjson.GetBlockChainInfoResult , error )
126
130
GetRawTransaction (ctx context.Context , txHash * chainhash.Hash ) (* dcrutil.Tx , error )
131
+ SendRawTransaction (ctx context.Context , tx * wire.MsgTx , allowHighFees bool ) (* chainhash.Hash , error )
127
132
}
128
133
129
134
// The rpcclient package functions will return a rpcclient.ErrRequestCanceled
@@ -135,6 +140,113 @@ func translateRPCCancelErr(err error) error {
135
140
return err
136
141
}
137
142
143
+ // ParseBondTx performs basic validation of a serialized time-locked fidelity
144
+ // bond transaction given the bond's P2SH redeem script.
145
+ //
146
+ // The transaction must have at least two outputs: out 0 pays to a P2SH address
147
+ // (the bond), and out 1 is a nulldata output that commits to an account ID.
148
+ // There may also be a change output.
149
+ //
150
+ // Returned: The bond's coin ID (i.e. encoded UTXO) of the bond output. The bond
151
+ // output's amount and P2SH address. The lockTime and pubkey hash data pushes
152
+ // from the script. The account ID from the second output is also returned.
153
+ //
154
+ // Properly formed transactions:
155
+ //
156
+ // 1. The bond output (vout 0) must be a P2SH output.
157
+ // 2. The bond's redeem script must be of the form:
158
+ // <lockTime[4]> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 <pubkeyhash[20]> OP_EQUALVERIFY OP_CHECKSIG
159
+ // 3. The null data output (vout 1) must have a 58-byte data push (ver | account ID | lockTime | pubkeyHash).
160
+ // 4. The transaction must have a zero locktime and expiry.
161
+ // 5. All inputs must have the max sequence num set (finalized).
162
+ // 6. The transaction must pass the checks in the
163
+ // blockchain.CheckTransactionSanity function.
164
+ //
165
+ // For DCR, and possibly all assets, the bond script is reconstructed from the
166
+ // null data output, and it is verified that the bond output pays to this
167
+ // script.
168
+ func ParseBondTx (ver uint16 , rawTx []byte ) (bondCoinID []byte , amt int64 , bondAddr string ,
169
+ bondPubKeyHash []byte , lockTime int64 , acct account.AccountID , err error ) {
170
+ if ver != 0 {
171
+ err = errors .New ("only version 0 bonds supported" )
172
+ return
173
+ }
174
+ // While the dcr package uses a package-level chainParams variable, ensure
175
+ // that a backend has been instantiated first. Alternatively, we can add a
176
+ // dex.Network argument to this function, or make it a Backend method.
177
+ if chainParams == nil {
178
+ err = errors .New ("dcr asset package config not yet loaded" )
179
+ return
180
+ }
181
+ msgTx := wire .NewMsgTx ()
182
+ if err = msgTx .Deserialize (bytes .NewReader (rawTx )); err != nil {
183
+ return
184
+ }
185
+
186
+ if msgTx .LockTime != 0 {
187
+ err = errors .New ("transaction locktime not zero" )
188
+ return
189
+ }
190
+ if msgTx .Expiry != wire .NoExpiryValue {
191
+ err = errors .New ("transaction has an expiration" )
192
+ return
193
+ }
194
+
195
+ if err = blockchain .CheckTransactionSanity (msgTx , chainParams ); err != nil {
196
+ return
197
+ }
198
+
199
+ if len (msgTx .TxOut ) < 2 {
200
+ err = fmt .Errorf ("expected at least 2 outputs, found %d" , len (msgTx .TxOut ))
201
+ return
202
+ }
203
+
204
+ for _ , txIn := range msgTx .TxIn {
205
+ if txIn .Sequence != wire .MaxTxInSequenceNum {
206
+ err = errors .New ("input has non-max sequence number" )
207
+ return
208
+ }
209
+ }
210
+
211
+ // Fidelity bond (output 0)
212
+ bondOut := msgTx .TxOut [0 ]
213
+ class , addrs := stdscript .ExtractAddrs (bondOut .Version , bondOut .PkScript , chainParams )
214
+ if class != stdscript .STScriptHash || len (addrs ) != 1 { // addrs check is redundant for p2sh
215
+ err = fmt .Errorf ("bad bond pkScript (class = %v)" , class )
216
+ return
217
+ }
218
+ scriptHash := txscript .ExtractScriptHash (bondOut .PkScript )
219
+
220
+ // Bond account commitment (output 1)
221
+ acctCommitOut := msgTx .TxOut [1 ]
222
+ acct , lock , pkh , err := dexdcr .ExtractBondCommitDataV0 (acctCommitOut .Version , acctCommitOut .PkScript )
223
+ if err != nil {
224
+ err = fmt .Errorf ("invalid bond commitment output: %w" , err )
225
+ return
226
+ }
227
+
228
+ // Reconstruct and check the bond redeem script.
229
+ bondScript , err := dexdcr .MakeBondScript (ver , lock , pkh [:])
230
+ if err != nil {
231
+ err = fmt .Errorf ("failed to build bond output redeem script: %w" , err )
232
+ return
233
+ }
234
+ if ! bytes .Equal (dcrutil .Hash160 (bondScript ), scriptHash ) {
235
+ err = fmt .Errorf ("script hash check failed for output 0 of %s" , msgTx .TxHash ())
236
+ return
237
+ }
238
+ // lock, pkh, _ := dexdcr.ExtractBondDetailsV0(bondOut.Version, bondScript)
239
+
240
+ txid := msgTx .TxHash ()
241
+ bondCoinID = toCoinID (& txid , 0 )
242
+ amt = bondOut .Value
243
+ bondAddr = addrs [0 ].String () // don't convert address, must match type we specified
244
+ lockTime = int64 (lock )
245
+ bondPubKeyHash = pkh [:]
246
+
247
+ return
248
+ }
249
+
138
250
// Backend is an asset backend for Decred. It has methods for fetching output
139
251
// information and subscribing to block updates. It maintains a cache of block
140
252
// data for quick lookups. Backend implements asset.Backend, so provides
@@ -337,6 +449,22 @@ func (dcr *Backend) BlockChannel(size int) <-chan *asset.BlockUpdate {
337
449
return c
338
450
}
339
451
452
+ // SendRawTransaction broadcasts a raw transaction, returning a coin ID.
453
+ func (dcr * Backend ) SendRawTransaction (rawtx []byte ) (coinID []byte , err error ) {
454
+ msgTx := wire .NewMsgTx ()
455
+ if err = msgTx .Deserialize (bytes .NewReader (rawtx )); err != nil {
456
+ return nil , err
457
+ }
458
+
459
+ var hash * chainhash.Hash
460
+ hash , err = dcr .node .SendRawTransaction (dcr .ctx , msgTx , false ) // or allow high fees?
461
+ if err != nil {
462
+ return
463
+ }
464
+ coinID = toCoinID (hash , 0 )
465
+ return
466
+ }
467
+
340
468
// Contract is part of the asset.Backend interface. An asset.Contract is an
341
469
// output that has been validated as a swap contract for the passed redeem
342
470
// script. A spendable output is one that can be spent in the next block. Every
@@ -424,7 +552,7 @@ func (dcr *Backend) FundingCoin(ctx context.Context, coinID []byte, redeemScript
424
552
425
553
// ValidateXPub validates the base-58 encoded extended key, and ensures that it
426
554
// is an extended public, not private, key.
427
- func ( dcr * Backend ) ValidateXPub (xpub string ) error {
555
+ func ValidateXPub (xpub string ) error {
428
556
xp , err := hdkeychain .NewKeyFromString (xpub , chainParams )
429
557
if err != nil {
430
558
return err
@@ -517,13 +645,22 @@ func (dcr *Backend) FeeCoin(coinID []byte) (addr string, val uint64, confs int64
517
645
return
518
646
}
519
647
648
+ // No stake outputs, and no multisig.
520
649
if len (txOut .addresses ) != 1 || txOut .sigsRequired != 1 ||
521
- txOut .scriptType != dexdcr .ScriptP2PKH /* no schorr or edwards */ ||
522
650
txOut .scriptType & dexdcr .ScriptStake != 0 {
523
651
return "" , 0 , - 1 , dex .UnsupportedScriptError
524
652
}
653
+
654
+ // Needs to work for legacy fee and new bond txns.
655
+ switch txOut .scriptType {
656
+ case dexdcr .ScriptP2SH , dexdcr .ScriptP2PKH :
657
+ default :
658
+ return "" , 0 , - 1 , dex .UnsupportedScriptError
659
+ }
660
+
525
661
addr = txOut .addresses [0 ]
526
662
val = txOut .value
663
+
527
664
return
528
665
}
529
666
@@ -553,7 +690,7 @@ func (dcr *Backend) outputSummary(txHash *chainhash.Hash, vout uint32) (txOut *t
553
690
}
554
691
555
692
if int (vout ) > len (verboseTx .Vout )- 1 {
556
- err = asset . CoinNotFoundError // should be something fatal?
693
+ err = fmt . Errorf ( "invalid output index for tx with %d outputs" , len ( verboseTx . Vout ))
557
694
return
558
695
}
559
696
0 commit comments