diff --git a/internal/webapi/payfee.go b/internal/webapi/payfee.go index 77547fd7..9eedd1d4 100644 --- a/internal/webapi/payfee.go +++ b/internal/webapi/payfee.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2024 The Decred developers +// Copyright (c) 2021-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -143,6 +143,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 c40bc33a..75bc2edd 100644 --- a/rpc/dcrd.go +++ b/rpc/dcrd.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2025 The Decred developers +// Copyright (c) 2021-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -306,6 +306,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.