From ae1a1178f61246f14e8b382c91e77c80571fca12 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Sat, 18 Apr 2026 12:23:43 +0800 Subject: [PATCH] Enhance fee transaction sanity checks. Confirm the existence of all of the inputs to fee transactions provided by clients. If any of the inputs do not exist, or are not known by the local dcrd instance, an error is returned to the client and the ticket is not registered with the VSP. --- internal/webapi/payfee.go | 23 +++++++++++++++++++++++ rpc/dcrd.go | 14 ++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/internal/webapi/payfee.go b/internal/webapi/payfee.go index 40db720f..3fab7a74 100644 --- a/internal/webapi/payfee.go +++ b/internal/webapi/payfee.go @@ -142,6 +142,29 @@ func (w *WebAPI) payFee(c *gin.Context) { return } + // Confirm all inputs of the fee transaction exist. + for _, input := range feeTx.TxIn { + prevOut := input.PreviousOutPoint + txOut, err := dcrdClient.GetTxOut(prevOut.Hash, prevOut.Index, prevOut.Tree) + if err != nil { + w.log.Errorf("%s: dcrd.GetTxOut for fee tx input failed "+ + "(ticketHash=%s, output=%s:%d:%d, clientIP=%s): %v", + funcName, ticket.Hash, prevOut.Hash, prevOut.Index, prevOut.Tree, + c.ClientIP(), err) + w.sendError(types.ErrInternalError, c) + return + } + if txOut == nil { + w.log.Warnf("%s: Fee tx contains non-existent input "+ + "(ticketHash=%s, output=%s:%d:%d, clientIP=%s)", + funcName, ticket.Hash, prevOut.Hash, prevOut.Index, prevOut.Tree, + c.ClientIP()) + w.sendErrorWithMsg("fee tx includes non-existent input", + types.ErrInvalidFeeTx, c) + return + } + } + // Decode fee address to get its payment script details. feeAddr, err := stdaddr.DecodeAddress(ticket.FeeAddress, w.cfg.Network) if err != nil { diff --git a/rpc/dcrd.go b/rpc/dcrd.go index cb5913b9..8a68bdba 100644 --- a/rpc/dcrd.go +++ b/rpc/dcrd.go @@ -325,6 +325,20 @@ func (c *DcrdRPC) GetBlockHash(height int64) (string, error) { return resp, nil } +// GetTxOut returns the transaction output info if it's unspent and nil +// otherwise. +func (c *DcrdRPC) GetTxOut(hash chainhash.Hash, index uint32, + tree int8) (*dcrdtypes.GetTxOutResult, error) { + var resp *dcrdtypes.GetTxOutResult + const includeMempool = true + err := c.Call(context.TODO(), "gettxout", &resp, hash.String(), index, tree, + includeMempool) + if err != nil { + return nil, err + } + return resp, nil +} + // GetCFilterV2 retrieves the GCS filter for the provided block header, // optionally verifies the inclusion proof, then returns the filter along with // its key.