Skip to content

Commit

Permalink
rpc: implement gettxout JSON-RPC method for SPV mode (decred#1903)
Browse files Browse the repository at this point in the history
  • Loading branch information
itswisdomagain committed Mar 11, 2021
1 parent 45d6f32 commit e9a9072
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 10 deletions.
108 changes: 108 additions & 0 deletions internal/rpc/jsonrpc/methods.go
Expand Up @@ -111,6 +111,7 @@ var handlers = map[string]handler{
"getstakeinfo": {fn: (*Server).getStakeInfo},
"gettickets": {fn: (*Server).getTickets},
"gettransaction": {fn: (*Server).getTransaction},
"gettxout": {fn: (*Server).getTxOut},
"getunconfirmedbalance": {fn: (*Server).getUnconfirmedBalance},
"getvotechoices": {fn: (*Server).getVoteChoices},
"getwalletfee": {fn: (*Server).getWalletFee},
Expand Down Expand Up @@ -2480,6 +2481,113 @@ func (s *Server) getTransaction(ctx context.Context, icmd interface{}) (interfac
return ret, nil
}

// getTxOut handles a gettxout request by returning details about an unspent
// output. In SPV mode, details are only returned for transaction outputs that
// are relevant to the wallet.
// To match the behavior in RPC mode, (nil, nil) is returned if the transaction
// output could not be found (never existed or was pruned) or is spent by another
// transaction already in the main chain. Mined transactions that are spent by
// a mempool transaction are not affected by this.
func (s *Server) getTxOut(ctx context.Context, icmd interface{}) (interface{}, error) {
cmd := icmd.(*types.GetTxOutCmd)
w, ok := s.walletLoader.LoadedWallet()
if !ok {
return nil, errUnloadedWallet
}

// Attempt RPC passthrough if connected to DCRD.
n, err := w.NetworkBackend()
if err != nil {
return nil, err
}
if rpc, ok := n.(*dcrd.RPC); ok {
var resp json.RawMessage
err := rpc.Call(ctx, "gettxout", &resp, cmd.Txid, cmd.Vout, cmd.Tree, cmd.IncludeMempool)
return resp, err
}

txHash, err := chainhash.NewHashFromStr(cmd.Txid)
if err != nil {
return nil, rpcError(dcrjson.ErrRPCDecodeHexString, err)
}

if cmd.Tree != wire.TxTreeRegular && cmd.Tree != wire.TxTreeStake {
return nil, rpcErrorf(dcrjson.ErrRPCInvalidParameter, "Tx tree must be regular or stake")
}

// Check if the transaction is known to wallet.
walletTx, err := wallet.UnstableAPI(w).TxDetails(ctx, txHash)
if err != nil && !errors.Is(err, errors.NotExist) {
return nil, err
}
if walletTx == nil {
return nil, nil // Tx not found in wallet.
}
if len(walletTx.MsgTx.TxOut) <= int(cmd.Vout) {
return nil, rpcErrorf(dcrjson.ErrRPCInvalidTxVout, "invalid vout %d", cmd.Vout)
}

tx := &walletTx.MsgTx
txTree := wire.TxTreeRegular
if stake.DetermineTxType(tx, true) != stake.TxTypeRegular {
txTree = wire.TxTreeStake
}
if txTree != cmd.Tree {
// Not an error because it is technically possible (though extremely unlikely)
// that the required tx (same hash, different tree) exists on the blockchain.
return nil, nil
}

// Attempt to read the unspent txout info from wallet.
outpoint := wire.OutPoint{Hash: *txHash, Index: cmd.Vout, Tree: cmd.Tree}
walletUnspent, err := w.UnspentOutput(ctx, outpoint, *cmd.IncludeMempool)
if err != nil && !errors.Is(err, errors.NotExist) {
return nil, err
}
if walletUnspent == nil {
return nil, nil // output is spent
}

txout := tx.TxOut[cmd.Vout]
pkScript := txout.PkScript
scriptVersion := txout.Version

// Disassemble script into single line printable format. The
// disassembled string will contain [error] inline if the script
// doesn't fully parse, so ignore the error here.
disbuf, _ := txscript.DisasmString(pkScript)

// Get further info about the script. Ignore the error here since an
// error means the script couldn't parse and there is no additional
// information about it anyways.
scriptClass, addrs, reqSigs, _ := txscript.ExtractPkScriptAddrs(
scriptVersion, pkScript, s.activeNet, true) // Yes treasury
addresses := make([]string, len(addrs))
for i, addr := range addrs {
addresses[i] = addr.Address()
}

bestHash, bestHeight := w.MainChainTip(ctx)
var confirmations int64
if walletTx.Block.Height != -1 {
confirmations = int64(1 + bestHeight - walletTx.Block.Height)
}

return &dcrdtypes.GetTxOutResult{
BestBlock: bestHash.String(),
Confirmations: confirmations,
Value: dcrutil.Amount(txout.Value).ToUnit(dcrutil.AmountCoin),
ScriptPubKey: dcrdtypes.ScriptPubKeyResult{
Asm: disbuf,
Hex: hex.EncodeToString(pkScript),
ReqSigs: int32(reqSigs),
Type: scriptClass.String(),
Addresses: addresses,
},
Coinbase: blockchain.IsCoinBaseTx(tx, false) || blockchain.IsCoinBaseTx(tx, true),
}, nil
}

// getVoteChoices handles a getvotechoices request by returning configured vote
// preferences for each agenda of the latest supported stake version.
func (s *Server) getVoteChoices(ctx context.Context, icmd interface{}) (interface{}, error) {
Expand Down
3 changes: 2 additions & 1 deletion internal/rpc/jsonrpc/rpcserverhelp.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion internal/rpc/rpcserver/server.go
Expand Up @@ -2042,7 +2042,7 @@ func (s *walletServer) ValidateAddress(ctx context.Context, req *pb.ValidateAddr
// further information available, so just set the script type
// a non-standard and break out now.
class, addrs, reqSigs, err := txscript.ExtractPkScriptAddrs(
0, script, s.wallet.ChainParams(), false) // No reasury
0, script, s.wallet.ChainParams(), false) // No treasury
if err != nil {
result.ScriptType = pb.ValidateAddressResponse_NonStandardTy
break
Expand Down
31 changes: 23 additions & 8 deletions internal/rpchelp/helpdescs_en_US.go
Expand Up @@ -260,14 +260,6 @@ var helpDescsEnUS = map[string]string{
"vout-version": "The version of the public key script",
"vout-scriptPubKey": "The public key script used to pay coins as a JSON object",

// ScriptPubKeyResult help.
"scriptpubkeyresult-asm": "Disassembly of the script",
"scriptpubkeyresult-hex": "Hex-encoded bytes of the script",
"scriptpubkeyresult-reqSigs": "The number of required signatures",
"scriptpubkeyresult-type": "The type of the script (e.g. 'pubkeyhash')",
"scriptpubkeyresult-addresses": "The Decred addresses associated with this script",
"scriptpubkeyresult-commitamt": "The ticket commitment value if the script is for a staking commitment",

// GetCFilterV2Cmd help.
"getcfilterv2--synopsis": "Returns the version 2 block filter for the given block along with the key required to query it for matches against committed scripts.",
"getcfilterv2-blockhash": "The block hash of the filter to retrieve",
Expand Down Expand Up @@ -434,6 +426,29 @@ var helpDescsEnUS = map[string]string{
"help--result0": "List of commands",
"help--result1": "Help for specified command",

// GetTxOutCmd help.
"gettxout--synopsis": "Returns information about an unspent transaction output.",
"gettxout-txid": "The hash of the transaction",
"gettxout-vout": "The index of the output",
"gettxout-tree": "The tree of the transaction",
"gettxout-includemempool": "Include the mempool when true",

// GetTxOutResult help.
"gettxoutresult-bestblock": "The block hash that contains the transaction output",
"gettxoutresult-confirmations": "The number of confirmations",
"gettxoutresult-value": "The transaction amount in DCR",
"gettxoutresult-scriptPubKey": "The public key script used to pay coins as a JSON object",
"gettxoutresult-version": "The transaction version",
"gettxoutresult-coinbase": "Whether or not the transaction is a coinbase",

// ScriptPubKeyResult help.
"scriptpubkeyresult-asm": "Disassembly of the script",
"scriptpubkeyresult-hex": "Hex-encoded bytes of the script",
"scriptpubkeyresult-reqSigs": "The number of required signatures",
"scriptpubkeyresult-type": "The type of the script (e.g. 'pubkeyhash')",
"scriptpubkeyresult-addresses": "The Decred addresses associated with this script",
"scriptpubkeyresult-commitamt": "The ticket commitment value if the script is for a staking commitment",

// ImportCFiltersV2Cmd help.
"importcfiltersv2--synopsis": "Imports a list of v2 cfilters into the wallet. Does not perform validation on the filters",
"importcfiltersv2-startheight": "The starting block height for this list of cfilters",
Expand Down
1 change: 1 addition & 0 deletions internal/rpchelp/methods.go
Expand Up @@ -63,6 +63,7 @@ var Methods = []struct {
{"getstakeinfo", []interface{}{(*types.GetStakeInfoResult)(nil)}},
{"gettickets", []interface{}{(*types.GetTicketsResult)(nil)}},
{"gettransaction", []interface{}{(*types.GetTransactionResult)(nil)}},
{"gettxout", []interface{}{(*dcrdtypes.GetTxOutResult)(nil)}},
{"getunconfirmedbalance", returnsNumber},
{"getvotechoices", []interface{}{(*types.GetVoteChoicesResult)(nil)}},
{"getwalletfee", returnsNumber},
Expand Down
2 changes: 2 additions & 0 deletions rpc/jsonrpc/types/methods.go
Expand Up @@ -1267,6 +1267,7 @@ func init() {
{"getcfilterv2", (*GetCFilterV2Cmd)(nil)},
{"getinfo", (*GetInfoCmd)(nil)},
{"getpeerinfo", (*GetPeerInfoCmd)(nil)},
{"gettxout", (*GetTxOutCmd)(nil)},
{"help", (*HelpCmd)(nil)},
{"sendrawtransaction", (*SendRawTransactionCmd)(nil)},
{"ticketsforaddress", (*TicketsForAddressCmd)(nil)},
Expand Down Expand Up @@ -1300,6 +1301,7 @@ type (
GetCFilterV2Cmd dcrdtypes.GetCFilterV2Cmd
GetInfoCmd dcrdtypes.GetInfoCmd
GetPeerInfoCmd dcrdtypes.GetPeerInfoCmd
GetTxOutCmd dcrdtypes.GetTxOutCmd
HelpCmd dcrdtypes.HelpCmd
SendRawTransactionCmd dcrdtypes.SendRawTransactionCmd
TicketsForAddressCmd dcrdtypes.TicketsForAddressCmd
Expand Down
22 changes: 22 additions & 0 deletions wallet/udb/txmined.go
Expand Up @@ -2341,6 +2341,28 @@ func (s *Store) UnspentOutputs(dbtx walletdb.ReadTx) ([]*Credit, error) {
return unspent, nil
}

// UnspentOutput returns details for an unspent received transaction output.
// Returns error NotExist if the specified outpoint cannot be found or has been
// spent by a mined transaction. Mined transactions that are spent by a mempool
// transaction are not affected by this.
func (s *Store) UnspentOutput(ns walletdb.ReadBucket, op wire.OutPoint, includeMempool bool) (*Credit, error) {
k := canonicalOutPoint(&op.Hash, op.Index)
// Check if unspent output is in mempool (if includeMempool == true).
if includeMempool && existsRawUnminedCredit(ns, k) != nil {
return s.outputCreditInfo(ns, op, nil)
}
// Check for unspent output in bucket for mined unspents.
if v := ns.NestedReadBucket(bucketUnspent).Get(k); v != nil {
var block Block
err := readUnspentBlock(v, &block)
if err != nil {
return nil, err
}
return s.outputCreditInfo(ns, op, &block)
}
return nil, errors.E(errors.NotExist, errors.Errorf("no unspent output %v", op))
}

// ForEachUnspentOutpoint calls f on each UTXO outpoint.
// The order is undefined.
func (s *Store) ForEachUnspentOutpoint(dbtx walletdb.ReadTx, f func(*wire.OutPoint) error) error {
Expand Down
15 changes: 15 additions & 0 deletions wallet/wallet.go
Expand Up @@ -3343,6 +3343,21 @@ func (w *Wallet) Spender(ctx context.Context, out *wire.OutPoint) (*wire.MsgTx,
return spender, spenderIndex, err
}

// UnspentOutput returns information about an unspent received transaction
// output. Returns error NotExist if the specified outpoint cannot be found or
// has been spent by a mined transaction. Mined transactions that are spent by
// a mempool transaction are not affected by this.
func (w *Wallet) UnspentOutput(ctx context.Context, op wire.OutPoint, includeMempool bool) (*udb.Credit, error) {
var utxo *udb.Credit
err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
var err error
utxo, err = w.txStore.UnspentOutput(txmgrNs, op, includeMempool)
return err
})
return utxo, err
}

// AccountProperties contains properties associated with each account, such as
// the account name, number, and the nubmer of derived and imported keys. If no
// address usage has been recorded on any of the external or internal branches,
Expand Down

0 comments on commit e9a9072

Please sign in to comment.