Skip to content

Commit 3ae066b

Browse files
committed
server/asset/dcr: bond validation
This adds the required bond transaction inspection functions for the server to support Decred fidelity bonds. In particular, this adds ParseBondTx for validating of a serialized bond transaction.
1 parent 1127e5b commit 3ae066b

File tree

2 files changed

+268
-12
lines changed

2 files changed

+268
-12
lines changed

server/asset/dcr/dcr.go

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@ import (
1919

2020
"decred.org/dcrdex/dex"
2121
dexdcr "decred.org/dcrdex/dex/networks/dcr"
22+
"decred.org/dcrdex/server/account"
2223
"decred.org/dcrdex/server/asset"
2324
"github.com/decred/dcrd/blockchain/stake/v4"
25+
"github.com/decred/dcrd/blockchain/v4"
2426
"github.com/decred/dcrd/chaincfg/chainhash"
2527
"github.com/decred/dcrd/chaincfg/v3"
2628
"github.com/decred/dcrd/dcrjson/v4"
2729
"github.com/decred/dcrd/dcrutil/v4"
2830
"github.com/decred/dcrd/hdkeychain/v3"
2931
chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v3"
3032
"github.com/decred/dcrd/rpcclient/v7"
33+
"github.com/decred/dcrd/txscript/v4"
3134
"github.com/decred/dcrd/txscript/v4/stdaddr"
35+
"github.com/decred/dcrd/txscript/v4/stdscript"
3236
"github.com/decred/dcrd/wire"
3337
)
3438

@@ -124,6 +128,7 @@ type dcrNode interface {
124128
GetBestBlockHash(ctx context.Context) (*chainhash.Hash, error)
125129
GetBlockChainInfo(ctx context.Context) (*chainjson.GetBlockChainInfoResult, error)
126130
GetRawTransaction(ctx context.Context, txHash *chainhash.Hash) (*dcrutil.Tx, error)
131+
SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error)
127132
}
128133

129134
// The rpcclient package functions will return a rpcclient.ErrRequestCanceled
@@ -135,6 +140,113 @@ func translateRPCCancelErr(err error) error {
135140
return err
136141
}
137142

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+
138250
// Backend is an asset backend for Decred. It has methods for fetching output
139251
// information and subscribing to block updates. It maintains a cache of block
140252
// data for quick lookups. Backend implements asset.Backend, so provides
@@ -337,6 +449,22 @@ func (dcr *Backend) BlockChannel(size int) <-chan *asset.BlockUpdate {
337449
return c
338450
}
339451

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+
340468
// Contract is part of the asset.Backend interface. An asset.Contract is an
341469
// output that has been validated as a swap contract for the passed redeem
342470
// 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
424552

425553
// ValidateXPub validates the base-58 encoded extended key, and ensures that it
426554
// is an extended public, not private, key.
427-
func (dcr *Backend) ValidateXPub(xpub string) error {
555+
func ValidateXPub(xpub string) error {
428556
xp, err := hdkeychain.NewKeyFromString(xpub, chainParams)
429557
if err != nil {
430558
return err
@@ -517,13 +645,22 @@ func (dcr *Backend) FeeCoin(coinID []byte) (addr string, val uint64, confs int64
517645
return
518646
}
519647

648+
// No stake outputs, and no multisig.
520649
if len(txOut.addresses) != 1 || txOut.sigsRequired != 1 ||
521-
txOut.scriptType != dexdcr.ScriptP2PKH /* no schorr or edwards */ ||
522650
txOut.scriptType&dexdcr.ScriptStake != 0 {
523651
return "", 0, -1, dex.UnsupportedScriptError
524652
}
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+
525661
addr = txOut.addresses[0]
526662
val = txOut.value
663+
527664
return
528665
}
529666

@@ -553,7 +690,7 @@ func (dcr *Backend) outputSummary(txHash *chainhash.Hash, vout uint32) (txOut *t
553690
}
554691

555692
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))
557694
return
558695
}
559696

0 commit comments

Comments
 (0)