mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-06-12 09:06:52 +00:00

* [NOD-738] Move rpcmodel helper functions to copytopointer package * [NOD-738] Rename copytopointer->pointers
474 lines
15 KiB
Go
474 lines
15 KiB
Go
package rpc
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"github.com/kaspanet/kaspad/dagconfig"
|
|
"github.com/kaspanet/kaspad/database"
|
|
"github.com/kaspanet/kaspad/rpcmodel"
|
|
"github.com/kaspanet/kaspad/txscript"
|
|
"github.com/kaspanet/kaspad/util"
|
|
"github.com/kaspanet/kaspad/util/daghash"
|
|
"github.com/kaspanet/kaspad/util/pointers"
|
|
"github.com/kaspanet/kaspad/wire"
|
|
)
|
|
|
|
// retrievedTx represents a transaction that was either loaded from the
|
|
// transaction memory pool or from the database. When a transaction is loaded
|
|
// from the database, it is loaded with the raw serialized bytes while the
|
|
// mempool has the fully deserialized structure. This structure therefore will
|
|
// have one of the two fields set depending on where is was retrieved from.
|
|
// This is mainly done for efficiency to avoid extra serialization steps when
|
|
// possible.
|
|
type retrievedTx struct {
|
|
txBytes []byte
|
|
blkHash *daghash.Hash // Only set when transaction is in a block.
|
|
tx *util.Tx
|
|
}
|
|
|
|
// handleSearchRawTransactions implements the searchRawTransactions command.
|
|
func handleSearchRawTransactions(s *Server, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) {
|
|
// Respond with an error if the address index is not enabled.
|
|
addrIndex := s.cfg.AddrIndex
|
|
if addrIndex == nil {
|
|
return nil, &rpcmodel.RPCError{
|
|
Code: rpcmodel.ErrRPCMisc,
|
|
Message: "Address index must be enabled (--addrindex)",
|
|
}
|
|
}
|
|
|
|
// Override the flag for including extra previous output information in
|
|
// each input if needed.
|
|
c := cmd.(*rpcmodel.SearchRawTransactionsCmd)
|
|
vinExtra := false
|
|
if c.VinExtra != nil {
|
|
vinExtra = *c.VinExtra
|
|
}
|
|
|
|
// Including the extra previous output information requires the
|
|
// transaction index. Currently the address index relies on the
|
|
// transaction index, so this check is redundant, but it's better to be
|
|
// safe in case the address index is ever changed to not rely on it.
|
|
if vinExtra && s.cfg.TxIndex == nil {
|
|
return nil, &rpcmodel.RPCError{
|
|
Code: rpcmodel.ErrRPCMisc,
|
|
Message: "Transaction index must be enabled (--txindex)",
|
|
}
|
|
}
|
|
|
|
// Attempt to decode the supplied address.
|
|
params := s.cfg.DAGParams
|
|
addr, err := util.DecodeAddress(c.Address, params.Prefix)
|
|
if err != nil {
|
|
return nil, &rpcmodel.RPCError{
|
|
Code: rpcmodel.ErrRPCInvalidAddressOrKey,
|
|
Message: "Invalid address or key: " + err.Error(),
|
|
}
|
|
}
|
|
|
|
// Override the default number of requested entries if needed. Also,
|
|
// just return now if the number of requested entries is zero to avoid
|
|
// extra work.
|
|
numRequested := 100
|
|
if c.Count != nil {
|
|
numRequested = *c.Count
|
|
if numRequested < 0 {
|
|
numRequested = 1
|
|
}
|
|
}
|
|
if numRequested == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Override the default number of entries to skip if needed.
|
|
var numToSkip int
|
|
if c.Skip != nil {
|
|
numToSkip = *c.Skip
|
|
if numToSkip < 0 {
|
|
numToSkip = 0
|
|
}
|
|
}
|
|
|
|
// Override the reverse flag if needed.
|
|
var reverse bool
|
|
if c.Reverse != nil {
|
|
reverse = *c.Reverse
|
|
}
|
|
|
|
// Add transactions from mempool first if client asked for reverse
|
|
// order. Otherwise, they will be added last (as needed depending on
|
|
// the requested counts).
|
|
//
|
|
// NOTE: This code doesn't sort by dependency. This might be something
|
|
// to do in the future for the client's convenience, or leave it to the
|
|
// client.
|
|
numSkipped := uint32(0)
|
|
addressTxns := make([]retrievedTx, 0, numRequested)
|
|
if reverse {
|
|
// Transactions in the mempool are not in a block header yet,
|
|
// so the block header field in the retieved transaction struct
|
|
// is left nil.
|
|
mpTxns, mpSkipped := fetchMempoolTxnsForAddress(s, addr,
|
|
uint32(numToSkip), uint32(numRequested))
|
|
numSkipped += mpSkipped
|
|
for _, tx := range mpTxns {
|
|
addressTxns = append(addressTxns, retrievedTx{tx: tx})
|
|
}
|
|
}
|
|
|
|
// Fetch transactions from the database in the desired order if more are
|
|
// needed.
|
|
if len(addressTxns) < numRequested {
|
|
err = s.cfg.DB.View(func(dbTx database.Tx) error {
|
|
regions, dbSkipped, err := addrIndex.TxRegionsForAddress(
|
|
dbTx, addr, uint32(numToSkip)-numSkipped,
|
|
uint32(numRequested-len(addressTxns)), reverse)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load the raw transaction bytes from the database.
|
|
serializedTxns, err := dbTx.FetchBlockRegions(regions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the transaction and the hash of the block it is
|
|
// contained in to the list. Note that the transaction
|
|
// is left serialized here since the caller might have
|
|
// requested non-verbose output and hence there would be
|
|
// no point in deserializing it just to reserialize it
|
|
// later.
|
|
for i, serializedTx := range serializedTxns {
|
|
addressTxns = append(addressTxns, retrievedTx{
|
|
txBytes: serializedTx,
|
|
blkHash: regions[i].Hash,
|
|
})
|
|
}
|
|
numSkipped += dbSkipped
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
context := "Failed to load address index entries"
|
|
return nil, internalRPCError(err.Error(), context)
|
|
}
|
|
|
|
}
|
|
|
|
// Add transactions from mempool last if client did not request reverse
|
|
// order and the number of results is still under the number requested.
|
|
if !reverse && len(addressTxns) < numRequested {
|
|
// Transactions in the mempool are not in a block header yet,
|
|
// so the block header field in the retieved transaction struct
|
|
// is left nil.
|
|
mpTxns, mpSkipped := fetchMempoolTxnsForAddress(s, addr,
|
|
uint32(numToSkip)-numSkipped, uint32(numRequested-
|
|
len(addressTxns)))
|
|
numSkipped += mpSkipped
|
|
for _, tx := range mpTxns {
|
|
addressTxns = append(addressTxns, retrievedTx{tx: tx})
|
|
}
|
|
}
|
|
|
|
// Address has never been used if neither source yielded any results.
|
|
if len(addressTxns) == 0 {
|
|
return []rpcmodel.SearchRawTransactionsResult{}, nil
|
|
}
|
|
|
|
// Serialize all of the transactions to hex.
|
|
hexTxns := make([]string, len(addressTxns))
|
|
for i := range addressTxns {
|
|
// Simply encode the raw bytes to hex when the retrieved
|
|
// transaction is already in serialized form.
|
|
rtx := &addressTxns[i]
|
|
if rtx.txBytes != nil {
|
|
hexTxns[i] = hex.EncodeToString(rtx.txBytes)
|
|
continue
|
|
}
|
|
|
|
// Serialize the transaction first and convert to hex when the
|
|
// retrieved transaction is the deserialized structure.
|
|
hexTxns[i], err = messageToHex(rtx.tx.MsgTx())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// When not in verbose mode, simply return a list of serialized txns.
|
|
if c.Verbose != nil && !*c.Verbose {
|
|
return hexTxns, nil
|
|
}
|
|
|
|
// Normalize the provided filter addresses (if any) to ensure there are
|
|
// no duplicates.
|
|
filterAddrMap := make(map[string]struct{})
|
|
if c.FilterAddrs != nil && len(*c.FilterAddrs) > 0 {
|
|
for _, addr := range *c.FilterAddrs {
|
|
filterAddrMap[addr] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// The verbose flag is set, so generate the JSON object and return it.
|
|
srtList := make([]rpcmodel.SearchRawTransactionsResult, len(addressTxns))
|
|
for i := range addressTxns {
|
|
// The deserialized transaction is needed, so deserialize the
|
|
// retrieved transaction if it's in serialized form (which will
|
|
// be the case when it was lookup up from the database).
|
|
// Otherwise, use the existing deserialized transaction.
|
|
rtx := &addressTxns[i]
|
|
var mtx *wire.MsgTx
|
|
if rtx.tx == nil {
|
|
// Deserialize the transaction.
|
|
mtx = new(wire.MsgTx)
|
|
err := mtx.Deserialize(bytes.NewReader(rtx.txBytes))
|
|
if err != nil {
|
|
context := "Failed to deserialize transaction"
|
|
return nil, internalRPCError(err.Error(),
|
|
context)
|
|
}
|
|
} else {
|
|
mtx = rtx.tx.MsgTx()
|
|
}
|
|
|
|
result := &srtList[i]
|
|
result.Hex = hexTxns[i]
|
|
result.TxID = mtx.TxID().String()
|
|
result.Vin, err = createVinListPrevOut(s, mtx, params, vinExtra,
|
|
filterAddrMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Vout = createVoutList(mtx, params, filterAddrMap)
|
|
result.Version = mtx.Version
|
|
result.LockTime = mtx.LockTime
|
|
|
|
// Transactions grabbed from the mempool aren't yet in a block,
|
|
// so conditionally fetch block details here. This will be
|
|
// reflected in the final JSON output (mempool won't have
|
|
// confirmations or block information).
|
|
var blkHeader *wire.BlockHeader
|
|
var blkHashStr string
|
|
if blkHash := rtx.blkHash; blkHash != nil {
|
|
// Fetch the header from DAG.
|
|
header, err := s.cfg.DAG.HeaderByHash(blkHash)
|
|
if err != nil {
|
|
return nil, &rpcmodel.RPCError{
|
|
Code: rpcmodel.ErrRPCBlockNotFound,
|
|
Message: "Block not found",
|
|
}
|
|
}
|
|
|
|
blkHeader = header
|
|
blkHashStr = blkHash.String()
|
|
}
|
|
|
|
// Add the block information to the result if there is any.
|
|
if blkHeader != nil {
|
|
result.Time = uint64(blkHeader.Timestamp.Unix())
|
|
result.Blocktime = uint64(blkHeader.Timestamp.Unix())
|
|
result.BlockHash = blkHashStr
|
|
}
|
|
|
|
// rtx.tx is only set when the transaction was retrieved from the mempool
|
|
result.IsInMempool = rtx.tx != nil
|
|
|
|
if s.cfg.TxIndex != nil && !result.IsInMempool {
|
|
confirmations, err := txConfirmations(s, mtx.TxID())
|
|
if err != nil {
|
|
context := "Failed to obtain block confirmations"
|
|
return nil, internalRPCError(err.Error(), context)
|
|
}
|
|
result.Confirmations = &confirmations
|
|
}
|
|
}
|
|
|
|
return srtList, nil
|
|
}
|
|
|
|
// createVinListPrevOut returns a slice of JSON objects for the inputs of the
|
|
// passed transaction.
|
|
func createVinListPrevOut(s *Server, mtx *wire.MsgTx, dagParams *dagconfig.Params, vinExtra bool, filterAddrMap map[string]struct{}) ([]rpcmodel.VinPrevOut, error) {
|
|
// Use a dynamically sized list to accommodate the address filter.
|
|
vinList := make([]rpcmodel.VinPrevOut, 0, len(mtx.TxIn))
|
|
|
|
// Lookup all of the referenced transaction outputs needed to populate the
|
|
// previous output information if requested. Coinbase transactions do not contain
|
|
// valid inputs: block hash instead of transaction ID.
|
|
var originOutputs map[wire.Outpoint]wire.TxOut
|
|
if !mtx.IsCoinBase() && (vinExtra || len(filterAddrMap) > 0) {
|
|
var err error
|
|
originOutputs, err = fetchInputTxos(s, mtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, txIn := range mtx.TxIn {
|
|
// The disassembled string will contain [error] inline
|
|
// if the script doesn't fully parse, so ignore the
|
|
// error here.
|
|
disbuf, _ := txscript.DisasmString(txIn.SignatureScript)
|
|
|
|
// Create the basic input entry without the additional optional
|
|
// previous output details which will be added later if
|
|
// requested and available.
|
|
prevOut := &txIn.PreviousOutpoint
|
|
vinEntry := rpcmodel.VinPrevOut{
|
|
TxID: prevOut.TxID.String(),
|
|
Vout: prevOut.Index,
|
|
Sequence: txIn.Sequence,
|
|
ScriptSig: &rpcmodel.ScriptSig{
|
|
Asm: disbuf,
|
|
Hex: hex.EncodeToString(txIn.SignatureScript),
|
|
},
|
|
}
|
|
|
|
// Add the entry to the list now if it already passed the filter
|
|
// since the previous output might not be available.
|
|
passesFilter := len(filterAddrMap) == 0
|
|
if passesFilter {
|
|
vinList = append(vinList, vinEntry)
|
|
}
|
|
|
|
// Only populate previous output information if requested and
|
|
// available.
|
|
if len(originOutputs) == 0 {
|
|
continue
|
|
}
|
|
originTxOut, ok := originOutputs[*prevOut]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Ignore the error here since an error means the script
|
|
// couldn't parse and there is no additional information about
|
|
// it anyways.
|
|
_, addr, _ := txscript.ExtractScriptPubKeyAddress(
|
|
originTxOut.ScriptPubKey, dagParams)
|
|
|
|
var encodedAddr *string
|
|
if addr != nil {
|
|
// Encode the address while checking if the address passes the
|
|
// filter when needed.
|
|
encodedAddr = pointers.String(addr.EncodeAddress())
|
|
|
|
// If the filter doesn't already pass, make it pass if
|
|
// the address exists in the filter.
|
|
if _, exists := filterAddrMap[*encodedAddr]; exists {
|
|
passesFilter = true
|
|
}
|
|
}
|
|
|
|
// Ignore the entry if it doesn't pass the filter.
|
|
if !passesFilter {
|
|
continue
|
|
}
|
|
|
|
// Add entry to the list if it wasn't already done above.
|
|
if len(filterAddrMap) != 0 {
|
|
vinList = append(vinList, vinEntry)
|
|
}
|
|
|
|
// Update the entry with previous output information if
|
|
// requested.
|
|
if vinExtra {
|
|
vinListEntry := &vinList[len(vinList)-1]
|
|
vinListEntry.PrevOut = &rpcmodel.PrevOut{
|
|
Address: encodedAddr,
|
|
Value: util.Amount(originTxOut.Value).ToKAS(),
|
|
}
|
|
}
|
|
}
|
|
|
|
return vinList, nil
|
|
}
|
|
|
|
// fetchInputTxos fetches the outpoints from all transactions referenced by the
|
|
// inputs to the passed transaction by checking the transaction mempool first
|
|
// then the transaction index for those already mined into blocks.
|
|
func fetchInputTxos(s *Server, tx *wire.MsgTx) (map[wire.Outpoint]wire.TxOut, error) {
|
|
mp := s.cfg.TxMemPool
|
|
originOutputs := make(map[wire.Outpoint]wire.TxOut)
|
|
for txInIndex, txIn := range tx.TxIn {
|
|
// Attempt to fetch and use the referenced transaction from the
|
|
// memory pool.
|
|
origin := &txIn.PreviousOutpoint
|
|
originTx, err := mp.FetchTransaction(&origin.TxID)
|
|
if err == nil {
|
|
txOuts := originTx.MsgTx().TxOut
|
|
if origin.Index >= uint32(len(txOuts)) {
|
|
errStr := fmt.Sprintf("unable to find output "+
|
|
"%s referenced from transaction %s:%d",
|
|
origin, tx.TxID(), txInIndex)
|
|
return nil, internalRPCError(errStr, "")
|
|
}
|
|
|
|
originOutputs[*origin] = *txOuts[origin.Index]
|
|
continue
|
|
}
|
|
|
|
// Look up the location of the transaction.
|
|
blockRegion, err := s.cfg.TxIndex.TxFirstBlockRegion(&origin.TxID)
|
|
if err != nil {
|
|
context := "Failed to retrieve transaction location"
|
|
return nil, internalRPCError(err.Error(), context)
|
|
}
|
|
if blockRegion == nil {
|
|
return nil, rpcNoTxInfoError(&origin.TxID)
|
|
}
|
|
|
|
// Load the raw transaction bytes from the database.
|
|
var txBytes []byte
|
|
err = s.cfg.DB.View(func(dbTx database.Tx) error {
|
|
var err error
|
|
txBytes, err = dbTx.FetchBlockRegion(blockRegion)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, rpcNoTxInfoError(&origin.TxID)
|
|
}
|
|
|
|
// Deserialize the transaction
|
|
var msgTx wire.MsgTx
|
|
err = msgTx.Deserialize(bytes.NewReader(txBytes))
|
|
if err != nil {
|
|
context := "Failed to deserialize transaction"
|
|
return nil, internalRPCError(err.Error(), context)
|
|
}
|
|
|
|
// Add the referenced output to the map.
|
|
if origin.Index >= uint32(len(msgTx.TxOut)) {
|
|
errStr := fmt.Sprintf("unable to find output %s "+
|
|
"referenced from transaction %s:%d", origin,
|
|
tx.TxID(), txInIndex)
|
|
return nil, internalRPCError(errStr, "")
|
|
}
|
|
originOutputs[*origin] = *msgTx.TxOut[origin.Index]
|
|
}
|
|
|
|
return originOutputs, nil
|
|
}
|
|
|
|
// fetchMempoolTxnsForAddress queries the address index for all unconfirmed
|
|
// transactions that involve the provided address. The results will be limited
|
|
// by the number to skip and the number requested.
|
|
func fetchMempoolTxnsForAddress(s *Server, addr util.Address, numToSkip, numRequested uint32) ([]*util.Tx, uint32) {
|
|
// There are no entries to return when there are less available than the
|
|
// number being skipped.
|
|
mpTxns := s.cfg.AddrIndex.UnconfirmedTxnsForAddress(addr)
|
|
numAvailable := uint32(len(mpTxns))
|
|
if numToSkip > numAvailable {
|
|
return nil, numAvailable
|
|
}
|
|
|
|
// Filter the available entries based on the number to skip and number
|
|
// requested.
|
|
rangeEnd := numToSkip + numRequested
|
|
if rangeEnd > numAvailable {
|
|
rangeEnd = numAvailable
|
|
}
|
|
return mpTxns[numToSkip:rangeEnd], numToSkip
|
|
}
|