Skip to content

Commit

Permalink
rpc: implement gettxout JSON-RPC method for SPV mode
Browse files Browse the repository at this point in the history
In RPC mode, the method performs passthrough manually, to keep
existing behavior.

In SPV mode, only wallet-related unspent outputs can be looked up
currently.
  • Loading branch information
itswisdomagain committed Mar 11, 2021
1 parent ea31d5c commit 9661a7c
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 2 deletions.
108 changes: 108 additions & 0 deletions internal/rpc/jsonrpc/methods.go
Expand Up @@ -104,6 +104,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 @@ -2049,6 +2050,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
23 changes: 23 additions & 0 deletions internal/rpchelp/helpdescs_en_US.go
Expand Up @@ -332,6 +332,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 @@ -62,6 +62,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 @@ -1261,6 +1261,7 @@ func init() {
{"getblockhash", (*GetBlockHashCmd)(nil)},
{"getinfo", (*GetInfoCmd)(nil)},
{"getpeerinfo", (*GetPeerInfoCmd)(nil)},
{"gettxout", (*GetTxOutCmd)(nil)},
{"help", (*HelpCmd)(nil)},
{"sendrawtransaction", (*SendRawTransactionCmd)(nil)},
{"ticketsforaddress", (*TicketsForAddressCmd)(nil)},
Expand Down Expand Up @@ -1292,6 +1293,7 @@ type (
GetBlockHashCmd dcrdtypes.GetBlockHashCmd
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 @@ -3333,6 +3333,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 9661a7c

Please sign in to comment.