Skip to content

Commit

Permalink
server/asset/dcr: bond validation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chappjc committed Oct 16, 2022
1 parent 1127e5b commit 3ae066b
Show file tree
Hide file tree
Showing 2 changed files with 268 additions and 12 deletions.
143 changes: 140 additions & 3 deletions server/asset/dcr/dcr.go
Expand Up @@ -19,16 +19,20 @@ import (

"decred.org/dcrdex/dex"
dexdcr "decred.org/dcrdex/dex/networks/dcr"
"decred.org/dcrdex/server/account"
"decred.org/dcrdex/server/asset"
"github.com/decred/dcrd/blockchain/stake/v4"
"github.com/decred/dcrd/blockchain/v4"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrjson/v4"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/hdkeychain/v3"
chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v3"
"github.com/decred/dcrd/rpcclient/v7"
"github.com/decred/dcrd/txscript/v4"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/txscript/v4/stdscript"
"github.com/decred/dcrd/wire"
)

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

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

// ParseBondTx performs basic validation of a serialized time-locked fidelity
// bond transaction given the bond's P2SH redeem script.
//
// The transaction must have at least two outputs: out 0 pays to a P2SH address
// (the bond), and out 1 is a nulldata output that commits to an account ID.
// There may also be a change output.
//
// Returned: The bond's coin ID (i.e. encoded UTXO) of the bond output. The bond
// output's amount and P2SH address. The lockTime and pubkey hash data pushes
// from the script. The account ID from the second output is also returned.
//
// Properly formed transactions:
//
// 1. The bond output (vout 0) must be a P2SH output.
// 2. The bond's redeem script must be of the form:
// <lockTime[4]> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 <pubkeyhash[20]> OP_EQUALVERIFY OP_CHECKSIG
// 3. The null data output (vout 1) must have a 58-byte data push (ver | account ID | lockTime | pubkeyHash).
// 4. The transaction must have a zero locktime and expiry.
// 5. All inputs must have the max sequence num set (finalized).
// 6. The transaction must pass the checks in the
// blockchain.CheckTransactionSanity function.
//
// For DCR, and possibly all assets, the bond script is reconstructed from the
// null data output, and it is verified that the bond output pays to this
// script.
func ParseBondTx(ver uint16, rawTx []byte) (bondCoinID []byte, amt int64, bondAddr string,
bondPubKeyHash []byte, lockTime int64, acct account.AccountID, err error) {
if ver != 0 {
err = errors.New("only version 0 bonds supported")
return
}
// While the dcr package uses a package-level chainParams variable, ensure
// that a backend has been instantiated first. Alternatively, we can add a
// dex.Network argument to this function, or make it a Backend method.
if chainParams == nil {
err = errors.New("dcr asset package config not yet loaded")
return
}
msgTx := wire.NewMsgTx()
if err = msgTx.Deserialize(bytes.NewReader(rawTx)); err != nil {
return
}

if msgTx.LockTime != 0 {
err = errors.New("transaction locktime not zero")
return
}
if msgTx.Expiry != wire.NoExpiryValue {
err = errors.New("transaction has an expiration")
return
}

if err = blockchain.CheckTransactionSanity(msgTx, chainParams); err != nil {
return
}

if len(msgTx.TxOut) < 2 {
err = fmt.Errorf("expected at least 2 outputs, found %d", len(msgTx.TxOut))
return
}

for _, txIn := range msgTx.TxIn {
if txIn.Sequence != wire.MaxTxInSequenceNum {
err = errors.New("input has non-max sequence number")
return
}
}

// Fidelity bond (output 0)
bondOut := msgTx.TxOut[0]
class, addrs := stdscript.ExtractAddrs(bondOut.Version, bondOut.PkScript, chainParams)
if class != stdscript.STScriptHash || len(addrs) != 1 { // addrs check is redundant for p2sh
err = fmt.Errorf("bad bond pkScript (class = %v)", class)
return
}
scriptHash := txscript.ExtractScriptHash(bondOut.PkScript)

// Bond account commitment (output 1)
acctCommitOut := msgTx.TxOut[1]
acct, lock, pkh, err := dexdcr.ExtractBondCommitDataV0(acctCommitOut.Version, acctCommitOut.PkScript)
if err != nil {
err = fmt.Errorf("invalid bond commitment output: %w", err)
return
}

// Reconstruct and check the bond redeem script.
bondScript, err := dexdcr.MakeBondScript(ver, lock, pkh[:])
if err != nil {
err = fmt.Errorf("failed to build bond output redeem script: %w", err)
return
}
if !bytes.Equal(dcrutil.Hash160(bondScript), scriptHash) {
err = fmt.Errorf("script hash check failed for output 0 of %s", msgTx.TxHash())
return
}
// lock, pkh, _ := dexdcr.ExtractBondDetailsV0(bondOut.Version, bondScript)

txid := msgTx.TxHash()
bondCoinID = toCoinID(&txid, 0)
amt = bondOut.Value
bondAddr = addrs[0].String() // don't convert address, must match type we specified
lockTime = int64(lock)
bondPubKeyHash = pkh[:]

return
}

// Backend is an asset backend for Decred. It has methods for fetching output
// information and subscribing to block updates. It maintains a cache of block
// data for quick lookups. Backend implements asset.Backend, so provides
Expand Down Expand Up @@ -337,6 +449,22 @@ func (dcr *Backend) BlockChannel(size int) <-chan *asset.BlockUpdate {
return c
}

// SendRawTransaction broadcasts a raw transaction, returning a coin ID.
func (dcr *Backend) SendRawTransaction(rawtx []byte) (coinID []byte, err error) {
msgTx := wire.NewMsgTx()
if err = msgTx.Deserialize(bytes.NewReader(rawtx)); err != nil {
return nil, err
}

var hash *chainhash.Hash
hash, err = dcr.node.SendRawTransaction(dcr.ctx, msgTx, false) // or allow high fees?
if err != nil {
return
}
coinID = toCoinID(hash, 0)
return
}

// Contract is part of the asset.Backend interface. An asset.Contract is an
// output that has been validated as a swap contract for the passed redeem
// script. A spendable output is one that can be spent in the next block. Every
Expand Down Expand Up @@ -424,7 +552,7 @@ func (dcr *Backend) FundingCoin(ctx context.Context, coinID []byte, redeemScript

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

// No stake outputs, and no multisig.
if len(txOut.addresses) != 1 || txOut.sigsRequired != 1 ||
txOut.scriptType != dexdcr.ScriptP2PKH /* no schorr or edwards */ ||
txOut.scriptType&dexdcr.ScriptStake != 0 {
return "", 0, -1, dex.UnsupportedScriptError
}

// Needs to work for legacy fee and new bond txns.
switch txOut.scriptType {
case dexdcr.ScriptP2SH, dexdcr.ScriptP2PKH:
default:
return "", 0, -1, dex.UnsupportedScriptError
}

addr = txOut.addresses[0]
val = txOut.value

return
}

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

if int(vout) > len(verboseTx.Vout)-1 {
err = asset.CoinNotFoundError // should be something fatal?
err = fmt.Errorf("invalid output index for tx with %d outputs", len(verboseTx.Vout))
return
}

Expand Down

0 comments on commit 3ae066b

Please sign in to comment.