From 437bf33bc911b5a1d3db0781375d12a099fb5865 Mon Sep 17 00:00:00 2001 From: De Clercq Wentzel <10665586+wentzeld@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:45:03 -0800 Subject: [PATCH 1/2] wave 1 error improvements --- pkg/client/compatibility_helper.go | 2 +- pkg/client/errors.go | 10 ++++----- pkg/client/limited_transport.go | 2 +- pkg/client/rpc_client.go | 24 +++++++++++----------- pkg/client/rpc_client_internal_test.go | 2 +- pkg/client/rpc_client_test.go | 6 +++--- pkg/client/simulated_backend_client.go | 10 ++++----- pkg/read/errors.go | 20 +++++++++--------- pkg/txmgr/attempts.go | 6 +++--- pkg/txmgr/evm_tx_store.go | 28 +++++++++++++------------- pkg/txmgr/finalizer.go | 2 +- pkg/txmgr/stuck_tx_detector.go | 2 +- pkg/txmgr/transmitchecker.go | 8 ++++---- 13 files changed, 61 insertions(+), 61 deletions(-) diff --git a/pkg/client/compatibility_helper.go b/pkg/client/compatibility_helper.go index b9be76301a..9310e0a038 100644 --- a/pkg/client/compatibility_helper.go +++ b/pkg/client/compatibility_helper.go @@ -83,7 +83,7 @@ func toFilterArg(q ethereum.FilterQuery) (interface{}, error) { if q.BlockHash != nil { arg["blockHash"] = *q.BlockHash if q.FromBlock != nil || q.ToBlock != nil { - return nil, errors.New("cannot specify both BlockHash and FromBlock/ToBlock") + return nil, errors.New("invalid filter query: cannot specify both BlockHash and FromBlock/ToBlock parameters simultaneously. Use either BlockHash for a single block or FromBlock/ToBlock for a block range") } } else { if q.FromBlock == nil { diff --git a/pkg/client/errors.go b/pkg/client/errors.go index 38fb3bff40..485a12140d 100644 --- a/pkg/client/errors.go +++ b/pkg/client/errors.go @@ -307,7 +307,7 @@ var monad = ClientErrors{ InsufficientEth: regexp.MustCompile("Signer had insufficient balance"), } -const TerminallyStuckMsg = "transaction terminally stuck" +const TerminallyStuckMsg = "transaction is terminally stuck in the mempool and cannot be included in a block: this transaction will not be retried and must be investigated manually" // Tx.Error messages that are set internally so they are not chain or client specific var internal = ClientErrors{ @@ -567,20 +567,20 @@ func ExtractRPCErrorOrNil(err error) *JsonError { // { "error": { "code": 3, "data": "0xABC123...", "message": "execution reverted: hello world" } } // revert reason automatically parsed if a simple require and included in message. func ExtractRPCError(baseErr error) (*JsonError, error) { if baseErr == nil { - return nil, pkgerrors.New("no error present") + return nil, pkgerrors.New("failed to extract RPC error: no error was provided to ExtractRPCError") } cause := pkgerrors.Cause(baseErr) jsonBytes, err := json.Marshal(cause) if err != nil { - return nil, pkgerrors.Wrap(err, "unable to marshal err to json") + return nil, pkgerrors.Wrap(err, "failed to extract RPC error: unable to marshal the underlying error to JSON for inspection") } jErr := JsonError{} err = json.Unmarshal(jsonBytes, &jErr) if err != nil { - return nil, pkgerrors.Wrapf(err, "unable to unmarshal json into jsonError struct (got: %v)", baseErr) + return nil, pkgerrors.Wrapf(err, "failed to extract RPC error: unable to unmarshal JSON into JsonError struct, the error may not be a standard JSON-RPC error (got: %v)", baseErr) } if jErr.Code == 0 { - return nil, pkgerrors.Errorf("not a RPCError because it does not have a code (got: %v)", baseErr) + return nil, pkgerrors.Errorf("failed to extract RPC error: error does not contain a JSON-RPC error code, so it is not a standard RPC error (got: %v)", baseErr) } return &jErr, nil } diff --git a/pkg/client/limited_transport.go b/pkg/client/limited_transport.go index cc8c8ccc65..64aab9b079 100644 --- a/pkg/client/limited_transport.go +++ b/pkg/client/limited_transport.go @@ -54,7 +54,7 @@ func GetResponseSizeLimit(ctx context.Context) uint32 { return limit } -var errResponseTooLarge = errors.New("response is too large") +var errResponseTooLarge = errors.New("response is too large: the RPC response exceeded the configured maximum size limit. Consider increasing the response size limit in node configuration or narrowing the query scope") // limitReader returns a Reader that reads from r // but stops with EOF after n bytes. diff --git a/pkg/client/rpc_client.go b/pkg/client/rpc_client.go index e66765d574..b7f946dc63 100644 --- a/pkg/client/rpc_client.go +++ b/pkg/client/rpc_client.go @@ -191,7 +191,7 @@ func (r *RPCClient) Dial(callerCtx context.Context) error { ws := r.ws.Load() httpClient := r.http.Load() if ws == nil && httpClient == nil { - return errors.New("cannot dial rpc client when both ws and http info are missing") + return errors.New("failed to dial RPC client: both WebSocket and HTTP URLs are missing. At least one connection URL must be configured") } promEVMPoolRPCNodeDials.WithLabelValues(r.chainID.String(), r.name).Inc() @@ -356,8 +356,8 @@ func (r *RPCClient) BatchCallContext(rootCtx context.Context, b []rpc.BatchElem) if r.chainType == chaintype.ChainAstar { for _, el := range b { if el.Method == "eth_getLogs" { - r.rpcLog.Critical("evmclient.BatchCallContext: eth_getLogs is not supported") - return errors.New("evmclient.BatchCallContext: eth_getLogs is not supported") + r.rpcLog.Critical("evmclient.BatchCallContext failed: eth_getLogs is not supported for Astar chain type in batch calls") + return errors.New("evmclient.BatchCallContext failed: eth_getLogs is not supported for Astar chain type in batch calls. Use individual log queries instead") } if !isRequestingFinalizedBlock(el) { continue @@ -441,7 +441,7 @@ func (r *RPCClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H } if ws == nil { - return nil, nil, errors.New("SubscribeNewHead is not allowed without ws url") + return nil, nil, errors.New("failed to subscribe to new heads: WebSocket URL is required for head subscriptions but none was configured. Enable HTTP polling or configure a WebSocket URL") } if multinode.CtxIsHealthCheckRequest(ctx) { @@ -639,7 +639,7 @@ func (r *RPCClient) astarLatestFinalizedBlock(ctx context.Context, result interf } if astarHead.Number == nil { - return r.wrapRPCClientError(errors.New("expected non empty head number of finalized block")) + return r.wrapRPCClientError(errors.New("failed to get Astar finalized block: the finalized block header returned a nil block number. The RPC node may be syncing or unhealthy")) } err = r.ethGetBlockByNumber(ctx, astarHead.Number.String(), result) @@ -790,7 +790,7 @@ func (r *RPCClient) SendTransaction(ctx context.Context, tx *types.Transaction) lggr.Debug("RPC call: evmclient.Client#SendTransaction") start := time.Now() if r.isChainType(chaintype.ChainTron) { - err := errors.New("SendTransaction not implemented for Tron, this should never be called") + err := errors.New("SendTransaction is not supported for Tron chain type: Tron uses a different transaction submission mechanism. This method should never be called for Tron nodes") return struct{}{}, multinode.Fatal, err } @@ -804,7 +804,7 @@ func (r *RPCClient) SendTransaction(ctx context.Context, tx *types.Transaction) func (r *RPCClient) SimulateTransaction(ctx context.Context, tx *types.Transaction) error { // Not Implemented - return pkgerrors.New("SimulateTransaction not implemented") + return pkgerrors.New("SimulateTransaction is not implemented for this RPC client") } func (r *RPCClient) SendEmptyTransaction( @@ -816,7 +816,7 @@ func (r *RPCClient) SendEmptyTransaction( fromAddress common.Address, ) (txhash string, err error) { // Not Implemented - return "", pkgerrors.New("SendEmptyTransaction not implemented") + return "", pkgerrors.New("SendEmptyTransaction is not implemented for this RPC client") } // PendingSequenceAt returns one higher than the highest nonce from both mempool and mined transactions @@ -831,7 +831,7 @@ func (r *RPCClient) PendingSequenceAt(ctx context.Context, account common.Addres // Tron doesn't have the concept of nonces, this shouldn't be called but just in case we'll return an error if r.isChainType(chaintype.ChainTron) { - err = errors.New("tron does not support eth_getTransactionCount") + err = errors.New("Tron chain type does not support eth_getTransactionCount: Tron does not use the nonce-based transaction model") return } @@ -861,7 +861,7 @@ func (r *RPCClient) NonceAt(ctx context.Context, account common.Address, blockNu // Tron doesn't have the concept of nonces, this shouldn't be called but just in case we'll return an error if r.isChainType(chaintype.ChainTron) { - err = errors.New("tron does not support eth_getTransactionCount") + err = errors.New("Tron chain type does not support eth_getTransactionCount: Tron does not use the nonce-based transaction model") return } @@ -1225,7 +1225,7 @@ func (r *RPCClient) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQu ctx, cancel, chStopInFlight, ws, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) defer cancel() if ws == nil { - return nil, errors.New("SubscribeFilterLogs is not allowed without ws url") + return nil, errors.New("failed to subscribe to filter logs: WebSocket URL is required for log subscriptions but none was configured. Configure a WebSocket URL to enable log subscriptions") } lggr := r.newRqLggr().With("q", q) @@ -1486,7 +1486,7 @@ func (r *RPCClient) doWithConfidence(ctx context.Context, request rpc.BatchElem, } if referencedHead == nil { - return errors.New("referenced block request returned nil. RPC is unhealthy or chain does not support specified tag") + return errors.New("referenced block request returned nil block header: the RPC node may be unhealthy, still syncing, or the chain does not support the specified block tag. Verify the RPC node status and chain compatibility") } maxAvailableHeight, err := r.referenceHeadToMaxAvailableHeight(confidence, referencedHead.Number) diff --git a/pkg/client/rpc_client_internal_test.go b/pkg/client/rpc_client_internal_test.go index b8a3b1fa75..f6f0f1bf47 100644 --- a/pkg/client/rpc_client_internal_test.go +++ b/pkg/client/rpc_client_internal_test.go @@ -217,7 +217,7 @@ func TestRPCClient_doWithConfidence(t *testing.T) { EthCallResult: "0x00", ExpectedTag: "safe", BlockByNumberResult: "null", - ExpectedError: "referenced block request returned nil. RPC is unhealthy or chain does not support specified tag", + ExpectedError: "referenced block request returned nil block header: the RPC node may be unhealthy, still syncing, or the chain does not support the specified block tag. Verify the RPC node status and chain compatibility", }, { Name: "Happy path", diff --git a/pkg/client/rpc_client_test.go b/pkg/client/rpc_client_test.go index ffb1e5028b..8a171d60d1 100644 --- a/pkg/client/rpc_client_test.go +++ b/pkg/client/rpc_client_test.go @@ -98,7 +98,7 @@ func TestRPCClient_SubscribeToHeads(t *testing.T) { t.Run("WS and HTTP URL cannot be both empty", func(t *testing.T) { // ws is optional when LogBroadcaster is disabled, however SubscribeFilterLogs will return error if ws is missing rpcClient := client.NewTestRPCClient(t, client.RPCClientOpts{}) - require.Equal(t, errors.New("cannot dial rpc client when both ws and http info are missing"), rpcClient.Dial(ctx)) + require.Equal(t, errors.New("failed to dial RPC client: both WebSocket and HTTP URLs are missing. At least one connection URL must be configured"), rpcClient.Dial(ctx)) }) t.Run("Updates chain info on new blocks", func(t *testing.T) { @@ -391,7 +391,7 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) { require.NoError(t, rpcClient.Dial(ctx)) _, err = rpcClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) - require.Equal(t, errors.New("SubscribeFilterLogs is not allowed without ws url"), err) + require.Equal(t, errors.New("failed to subscribe to filter logs: WebSocket URL is required for log subscriptions but none was configured. Configure a WebSocket URL to enable log subscriptions"), err) }) t.Run("Failed SubscribeFilterLogs logs and returns proper error", func(t *testing.T) { server := testutils.NewWSServer(t, chainID, func(reqMethod string, reqParams gjson.Result) (resp testutils.JSONRPCResponse) { @@ -901,7 +901,7 @@ func TestRPCClient_Tron(t *testing.T) { // Verify it returns the expected error for Tron require.Error(t, err) - assert.Equal(t, "SendTransaction not implemented for Tron, this should never be called", err.Error()) + assert.Equal(t, "SendTransaction is not supported for Tron chain type: Tron uses a different transaction submission mechanism. This method should never be called for Tron nodes", err.Error()) }) t.Run("NonceAt", func(t *testing.T) { diff --git a/pkg/client/simulated_backend_client.go b/pkg/client/simulated_backend_client.go index 144601d156..4480877d32 100644 --- a/pkg/client/simulated_backend_client.go +++ b/pkg/client/simulated_backend_client.go @@ -167,7 +167,7 @@ func (c *SimulatedBackendClient) TokenBalance(ctx context.Context, address commo } err = balanceOfABI.UnpackIntoInterface(balance, "balanceOf", b) if err != nil { - return nil, errors.New("unable to unpack balance") + return nil, errors.New("failed to unpack ERC20 balanceOf response: the returned data could not be decoded. The contract may not implement the standard ERC20 balanceOf interface") } return balance, nil } @@ -243,7 +243,7 @@ func (c *SimulatedBackendClient) blockNumber(ctx context.Context, number interfa return nil, nil } if n.Sign() < 0 { - return nil, errors.New("block number must be non-negative") + return nil, errors.New("invalid block number: block number must be non-negative. Use nil for the latest block or provide a valid block number >= 0") } return n, nil default: @@ -959,13 +959,13 @@ func interfaceToAddress(value interface{}) (common.Address, error) { return *v, nil case string: if ok := common.IsHexAddress(v); !ok { - return common.Address{}, errors.New("string not formatted as a hex encoded evm address") + return common.Address{}, errors.New("invalid address format: string is not a valid hex-encoded EVM address. Expected format: 0x followed by 40 hex characters (e.g., 0x1234...abcd)") } return common.HexToAddress(v), nil case *big.Int: if v.Uint64() > 0 || len(v.Bytes()) > 20 { - return common.Address{}, errors.New("invalid *big.Int; value must be larger than 0 with a byte length <= 20") + return common.Address{}, errors.New("invalid *big.Int for address conversion: value must be greater than 0 with a byte length of 20 or fewer bytes to represent a valid EVM address") } return common.BigToAddress(v), nil @@ -983,7 +983,7 @@ func interfaceToHash(value interface{}) (*common.Hash, error) { case string: b, err := hex.DecodeString(v) if err != nil || len(b) != 32 { - return nil, errors.New("string does not represent a 32-byte hexadecimal number") + return nil, errors.New("invalid hash format: string does not represent a valid 32-byte (64 hex character) hash. Ensure the input is a valid hex-encoded 256-bit hash") } h := common.Hash(b) return &h, nil diff --git a/pkg/read/errors.go b/pkg/read/errors.go index bbeb77b9b2..e99f87a17d 100644 --- a/pkg/read/errors.go +++ b/pkg/read/errors.go @@ -51,9 +51,9 @@ func newErrorFromCall(err error, call Call, block string, tp readType) Error { func (e Error) Error() string { var builder strings.Builder - builder.WriteString("[read error]") + builder.WriteString("[contract read error]") builder.WriteString(fmt.Sprintf(" err: %s;", e.Err.Error())) - builder.WriteString(fmt.Sprintf(" type: %s;", e.Type)) + builder.WriteString(fmt.Sprintf(" read type: %s;", e.Type)) if e.Detail != nil { builder.WriteString(fmt.Sprintf(" block: %s;", e.Detail.Block)) @@ -101,9 +101,9 @@ func newErrorFromCalls(err error, calls []Call, block string, tp readType) Multi func (e MultiCallError) Error() string { var builder strings.Builder - builder.WriteString("[read error]") + builder.WriteString("[batch contract read error]") builder.WriteString(fmt.Sprintf(" err: %s;", e.Err.Error())) - builder.WriteString(fmt.Sprintf(" type: %s;", e.Type)) + builder.WriteString(fmt.Sprintf(" read type: %s;", e.Type)) if e.Detail != nil { builder.WriteString(fmt.Sprintf(" block: %s;", e.Detail.Block)) @@ -133,25 +133,25 @@ type ConfigError struct { func newMissingReadIdentifierErr(readIdentifier string) ConfigError { return ConfigError{ - Msg: fmt.Sprintf("[no configured reader] read-identifier: '%s'", readIdentifier), + Msg: fmt.Sprintf("[contract reader configuration error] no configured reader found for read-identifier: '%s'. Ensure the contract and read name are registered in the chain reader configuration", readIdentifier), } } func newMissingContractErr(readIdentifier, contract string) ConfigError { return ConfigError{ - Msg: fmt.Sprintf("[no configured reader] read-identifier: %s; contract: %s;", readIdentifier, contract), + Msg: fmt.Sprintf("[contract reader configuration error] no configured reader found for contract '%s' (read-identifier: %s). Ensure the contract is registered in the chain reader configuration", contract, readIdentifier), } } func newMissingReadNameErr(readIdentifier, contract, readName string) ConfigError { return ConfigError{ - Msg: fmt.Sprintf("[no configured reader] read-identifier: %s; contract: %s; read-name: %s;", readIdentifier, contract, readName), + Msg: fmt.Sprintf("[contract reader configuration error] no configured reader found for read-name '%s' on contract '%s' (read-identifier: %s). Ensure the method or event is registered in the chain reader configuration", readName, contract, readIdentifier), } } func newUnboundAddressErr(address, contract, readName string) ConfigError { return ConfigError{ - Msg: fmt.Sprintf("[address not bound] address: %s; contract: %s; read-name: %s;", address, contract, readName), + Msg: fmt.Sprintf("[contract reader configuration error] address '%s' is not bound to contract '%s' for read-name '%s'. Bind the address to the contract before attempting reads", address, contract, readName), } } @@ -166,7 +166,7 @@ type FilterError struct { } func (e FilterError) Error() string { - return fmt.Sprintf("[logpoller filter error] action: %s; err: %s; filter: %+v;", e.Action, e.Err.Error(), e.Filter) + return fmt.Sprintf("[log poller filter error] failed during '%s' action: %s; filter details: %+v. Check that the filter configuration matches the expected contract events and addresses", e.Action, e.Err.Error(), e.Filter) } func (e FilterError) Unwrap() error { @@ -179,7 +179,7 @@ type NoContractExistsError struct { } func (e NoContractExistsError) Error() string { - return fmt.Sprintf("%s: contract does not exist at address: %s", e.Err.Error(), e.Address) + return fmt.Sprintf("%s: no contract exists at address %s. Verify that the contract has been deployed to this address on the correct chain and that the address is not an externally-owned account (EOA)", e.Err.Error(), e.Address) } func (e NoContractExistsError) Unwrap() error { diff --git a/pkg/txmgr/attempts.go b/pkg/txmgr/attempts.go index 5e2c2aac36..5984055d26 100644 --- a/pkg/txmgr/attempts.go +++ b/pkg/txmgr/attempts.go @@ -142,7 +142,7 @@ func (c *evmTxAttemptBuilder) NewEmptyTxAttempt(ctx context.Context, nonce evmty payload := []byte{} if fee.GasPrice == nil { - return attempt, pkgerrors.New("NewEmptyTranscation: gas price cannot be nil") + return attempt, pkgerrors.New("NewEmptyTransaction failed: gas price cannot be nil. A valid gas price is required to create a transaction for force rebroadcast") } tx := newLegacyTransaction( @@ -213,10 +213,10 @@ func validateDynamicFeeGas(kse keySpecificEstimator, fee gas.DynamicFee, etx Tx) // Assertions from: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md // Prevent impossibly large numbers if gasFeeCap.ToInt().Cmp(Max256BitUInt) > 0 { - return pkgerrors.New("impossibly large fee cap") + return pkgerrors.New("gas fee cap validation failed: fee cap exceeds the maximum 256-bit unsigned integer value (2^256). This likely indicates a configuration error in gas price settings") } if gasTipCap.ToInt().Cmp(Max256BitUInt) > 0 { - return pkgerrors.New("impossibly large tip cap") + return pkgerrors.New("gas tip cap validation failed: tip cap exceeds the maximum 256-bit unsigned integer value (2^256). This likely indicates a configuration error in gas price settings") } // The total must be at least as large as the tip if gasFeeCap.Cmp(gasTipCap) < 0 { diff --git a/pkg/txmgr/evm_tx_store.go b/pkg/txmgr/evm_tx_store.go index 7a37ce4891..e3d08b6775 100644 --- a/pkg/txmgr/evm_tx_store.go +++ b/pkg/txmgr/evm_tx_store.go @@ -34,7 +34,7 @@ import ( ) var ( - ErrKeyNotUpdated = errors.New("evmTxStore: Key not updated") + ErrKeyNotUpdated = errors.New("evmTxStore: key was not updated in the database. The key may not exist or the update conditions were not met") ) // EvmTxStore combines the txmgr tx store interface and the interface needed for the API to read from the tx DB @@ -1197,7 +1197,7 @@ func (o *evmTxStore) SaveInsufficientFundsAttempt(ctx context.Context, timeout t ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if !(attempt.State == txmgrtypes.TxAttemptInProgress || attempt.State == txmgrtypes.TxAttemptInsufficientFunds) { - return errors.New("expected state to be either in_progress or insufficient_eth") + return errors.New("SaveInsufficientFundsAttempt failed: expected attempt state to be either 'in_progress' or 'insufficient_funds', but got an unexpected state. Ensure the attempt has not already been finalized or broadcast") } attempt.State = txmgrtypes.TxAttemptInsufficientFunds ctx, cancel = context.WithTimeout(ctx, timeout) @@ -1207,7 +1207,7 @@ func (o *evmTxStore) SaveInsufficientFundsAttempt(ctx context.Context, timeout t func (o *evmTxStore) saveSentAttempt(ctx context.Context, timeout time.Duration, attempt *TxAttempt, broadcastAt time.Time) error { if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("expected state to be in_progress") + return errors.New("saveSentAttempt failed: expected attempt state to be 'in_progress' before marking as broadcast. The attempt may have already been processed") } attempt.State = txmgrtypes.TxAttemptBroadcast ctx, cancel := context.WithTimeout(ctx, timeout) @@ -1243,10 +1243,10 @@ func (o *evmTxStore) DeleteInProgressAttempt(ctx context.Context, attempt TxAtte ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("DeleteInProgressAttempt: expected attempt state to be in_progress") + return errors.New("DeleteInProgressAttempt failed: expected attempt state to be 'in_progress', but got a different state. Only in-progress attempts can be deleted") } if attempt.ID == 0 { - return errors.New("DeleteInProgressAttempt: expected attempt to have an id") + return errors.New("DeleteInProgressAttempt failed: attempt has no database ID (id=0). The attempt may not have been persisted yet") } _, err := o.q.ExecContext(ctx, `DELETE FROM evm.tx_attempts WHERE id = $1`, attempt.ID) return pkgerrors.Wrap(err, "DeleteInProgressAttempt failed") @@ -1258,7 +1258,7 @@ func (o *evmTxStore) SaveInProgressAttempt(ctx context.Context, attempt *TxAttem ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("SaveInProgressAttempt failed: attempt state must be in_progress") + return errors.New("SaveInProgressAttempt failed: attempt state must be 'in_progress' to be saved. The attempt may have already been broadcast or finalized") } var dbAttempt DbEthTxAttempt dbAttempt.FromTxAttempt(attempt) @@ -1401,10 +1401,10 @@ func (o *evmTxStore) SaveReplacementInProgressAttempt(ctx context.Context, oldAt ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if oldAttempt.State != txmgrtypes.TxAttemptInProgress || replacementAttempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("expected attempts to be in_progress") + return errors.New("SaveReplacementInProgressAttempt failed: both the old and replacement attempts must have state 'in_progress'. Verify that neither attempt has been broadcast or finalized") } if oldAttempt.ID == 0 { - return errors.New("expected oldAttempt to have an ID") + return errors.New("SaveReplacementInProgressAttempt failed: the old attempt has no database ID (id=0). It may not have been persisted yet") } return o.Transact(ctx, false, func(orm *evmTxStore) error { if _, err := orm.q.ExecContext(ctx, `DELETE FROM evm.tx_attempts WHERE id=$1`, oldAttempt.ID); err != nil { @@ -1443,7 +1443,7 @@ func (o *evmTxStore) UpdateTxFatalErrorAndDeleteAttempts(ctx context.Context, et ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if !etx.Error.Valid { - return errors.New("expected error field to be set") + return errors.New("UpdateTxFatalErrorAndDeleteAttempts failed: the transaction's Error field must be set before marking it as fatally errored") } etx.Sequence = nil @@ -1467,16 +1467,16 @@ func (o *evmTxStore) UpdateTxAttemptInProgressToBroadcast(ctx context.Context, e ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if etx.BroadcastAt == nil { - return errors.New("unconfirmed transaction must have broadcast_at time") + return errors.New("UpdateTxAttemptInProgressToBroadcast failed: unconfirmed transaction is missing its broadcast_at timestamp. This field is required to track when the transaction was broadcast") } if etx.InitialBroadcastAt == nil { - return errors.New("unconfirmed transaction must have initial_broadcast_at time") + return errors.New("UpdateTxAttemptInProgressToBroadcast failed: unconfirmed transaction is missing its initial_broadcast_at timestamp. This field is required to track when the transaction was first broadcast") } if etx.State != txmgr.TxInProgress { return pkgerrors.Errorf("can only transition to unconfirmed from in_progress, transaction is currently %s", etx.State) } if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("attempt must be in in_progress state") + return errors.New("UpdateTxAttemptInProgressToBroadcast failed: attempt must be in 'in_progress' state before transitioning to broadcast. The attempt may have already been broadcast or finalized") } if NewAttemptState != txmgrtypes.TxAttemptBroadcast { return pkgerrors.Errorf("new attempt state must be broadcast, got: %s", NewAttemptState) @@ -1505,13 +1505,13 @@ func (o *evmTxStore) UpdateTxUnstartedToInProgress(ctx context.Context, etx *Tx, ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if etx.Sequence == nil { - return errors.New("in_progress transaction must have nonce") + return errors.New("UpdateTxUnstartedToInProgress failed: transaction must have a nonce (Sequence) assigned before transitioning to 'in_progress' state") } if etx.State != txmgr.TxUnstarted { return pkgerrors.Errorf("can only transition to in_progress from unstarted, transaction is currently %s", etx.State) } if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("attempt state must be in_progress") + return errors.New("UpdateTxUnstartedToInProgress failed: attempt state must be 'in_progress' when beginning transaction processing") } etx.State = txmgr.TxInProgress return o.Transact(ctx, false, func(orm *evmTxStore) error { diff --git a/pkg/txmgr/finalizer.go b/pkg/txmgr/finalizer.go index 64ef4b7b4e..b79691aa3e 100644 --- a/pkg/txmgr/finalizer.go +++ b/pkg/txmgr/finalizer.go @@ -29,7 +29,7 @@ var _ Finalizer = (*evmFinalizer)(nil) var ( // ErrCouldNotGetReceipt is the error string we save if we reach our LatestFinalizedBlockNum for a confirmed transaction // without ever getting a receipt. This most likely happened because an external wallet used the account for this nonce - ErrCouldNotGetReceipt = "could not get receipt" + ErrCouldNotGetReceipt = "failed to retrieve transaction receipt before finalization: the transaction reached the finalized block without a receipt. This likely means an external wallet used the same account nonce, or the transaction was dropped by the network. Verify that no other wallet or Chainlink instance is using the same private key" ) // processHeadTimeout represents a sanity limit on how long ProcessHead should take to complete diff --git a/pkg/txmgr/stuck_tx_detector.go b/pkg/txmgr/stuck_tx_detector.go index 564ba19ad8..df3ea2b3af 100644 --- a/pkg/txmgr/stuck_tx_detector.go +++ b/pkg/txmgr/stuck_tx_detector.go @@ -195,7 +195,7 @@ func (d *stuckTxDetector) FindUnconfirmedTxWithLowestNonce(ctx context.Context, // 5. If 4 is true, the transaction is likely stuck due to overflow func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context, txs []Tx, blockNum int64) ([]Tx, error) { if d.cfg.Threshold() == nil || d.cfg.MinAttempts() == nil { - err := errors.New("missing required configs for the stuck transaction heuristic. Transactions.AutoPurge.Threshold and Transactions.AutoPurge.MinAttempts are required") + err := errors.New("stuck transaction detection cannot run: missing required configuration values. Both Transactions.AutoPurge.Threshold and Transactions.AutoPurge.MinAttempts must be set in the chain configuration to enable automatic stuck transaction detection") d.lggr.Error(err.Error()) return txs, err } diff --git a/pkg/txmgr/transmitchecker.go b/pkg/txmgr/transmitchecker.go index e62a8b32be..7db985f16b 100644 --- a/pkg/txmgr/transmitchecker.go +++ b/pkg/txmgr/transmitchecker.go @@ -73,7 +73,7 @@ func (c *CheckerFactory) BuildChecker(spec TransmitCheckerSpec) (TransmitChecker "failed to create VRF V2 coordinator at address %v", spec.VRFCoordinatorAddress) } if spec.VRFRequestBlockNumber == nil { - return nil, pkgerrors.New("VRFRequestBlockNumber parameter must be non-nil") + return nil, pkgerrors.New("VRF checker configuration error: VRFRequestBlockNumber parameter must be non-nil. This block number is required to verify VRF request fulfillment status") } return &VRFV2Checker{ GetCommitment: coord.GetCommitment, @@ -90,7 +90,7 @@ func (c *CheckerFactory) BuildChecker(spec TransmitCheckerSpec) (TransmitChecker "failed to create VRF V2 coordinator plus at address %v", spec.VRFCoordinatorAddress) } if spec.VRFRequestBlockNumber == nil { - return nil, pkgerrors.New("VRFRequestBlockNumber parameter must be non-nil") + return nil, pkgerrors.New("VRF checker configuration error: VRFRequestBlockNumber parameter must be non-nil. This block number is required to verify VRF request fulfillment status") } return &VRFV2Checker{ GetCommitment: coord.SRequestCommitments, @@ -258,7 +258,7 @@ func (v *VRFV1Checker) Check( "ethTxID", tx.ID, "meta", tx.Meta, "reqID", reqID) - return pkgerrors.New("request already fulfilled") + return pkgerrors.New("VRF transmit check failed: request already fulfilled on-chain. Skipping transaction to avoid unnecessary gas expenditure on a duplicate fulfillment") } // Request not fulfilled return nil @@ -349,7 +349,7 @@ func (v *VRFV2Checker) Check( "ethTxID", tx.ID, "meta", tx.Meta, "vrfRequestId", vrfRequestID) - return pkgerrors.New("request already fulfilled") + return pkgerrors.New("VRF transmit check failed: request already fulfilled on-chain. Skipping transaction to avoid unnecessary gas expenditure on a duplicate fulfillment") } l.Debugw("Request not yet fulfilled", "ethTxID", tx.ID, From 1c03554f77e0cf6ac590a0737b59dd9c1e9e321a Mon Sep 17 00:00:00 2001 From: De Clercq Wentzel <10665586+wentzeld@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:14:46 -0800 Subject: [PATCH 2/2] remove txnmggr changes --- pkg/txmgr/attempts.go | 6 +++--- pkg/txmgr/evm_tx_store.go | 28 ++++++++++++++-------------- pkg/txmgr/finalizer.go | 2 +- pkg/txmgr/stuck_tx_detector.go | 2 +- pkg/txmgr/transmitchecker.go | 8 ++++---- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pkg/txmgr/attempts.go b/pkg/txmgr/attempts.go index 5984055d26..5e2c2aac36 100644 --- a/pkg/txmgr/attempts.go +++ b/pkg/txmgr/attempts.go @@ -142,7 +142,7 @@ func (c *evmTxAttemptBuilder) NewEmptyTxAttempt(ctx context.Context, nonce evmty payload := []byte{} if fee.GasPrice == nil { - return attempt, pkgerrors.New("NewEmptyTransaction failed: gas price cannot be nil. A valid gas price is required to create a transaction for force rebroadcast") + return attempt, pkgerrors.New("NewEmptyTranscation: gas price cannot be nil") } tx := newLegacyTransaction( @@ -213,10 +213,10 @@ func validateDynamicFeeGas(kse keySpecificEstimator, fee gas.DynamicFee, etx Tx) // Assertions from: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md // Prevent impossibly large numbers if gasFeeCap.ToInt().Cmp(Max256BitUInt) > 0 { - return pkgerrors.New("gas fee cap validation failed: fee cap exceeds the maximum 256-bit unsigned integer value (2^256). This likely indicates a configuration error in gas price settings") + return pkgerrors.New("impossibly large fee cap") } if gasTipCap.ToInt().Cmp(Max256BitUInt) > 0 { - return pkgerrors.New("gas tip cap validation failed: tip cap exceeds the maximum 256-bit unsigned integer value (2^256). This likely indicates a configuration error in gas price settings") + return pkgerrors.New("impossibly large tip cap") } // The total must be at least as large as the tip if gasFeeCap.Cmp(gasTipCap) < 0 { diff --git a/pkg/txmgr/evm_tx_store.go b/pkg/txmgr/evm_tx_store.go index e3d08b6775..7a37ce4891 100644 --- a/pkg/txmgr/evm_tx_store.go +++ b/pkg/txmgr/evm_tx_store.go @@ -34,7 +34,7 @@ import ( ) var ( - ErrKeyNotUpdated = errors.New("evmTxStore: key was not updated in the database. The key may not exist or the update conditions were not met") + ErrKeyNotUpdated = errors.New("evmTxStore: Key not updated") ) // EvmTxStore combines the txmgr tx store interface and the interface needed for the API to read from the tx DB @@ -1197,7 +1197,7 @@ func (o *evmTxStore) SaveInsufficientFundsAttempt(ctx context.Context, timeout t ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if !(attempt.State == txmgrtypes.TxAttemptInProgress || attempt.State == txmgrtypes.TxAttemptInsufficientFunds) { - return errors.New("SaveInsufficientFundsAttempt failed: expected attempt state to be either 'in_progress' or 'insufficient_funds', but got an unexpected state. Ensure the attempt has not already been finalized or broadcast") + return errors.New("expected state to be either in_progress or insufficient_eth") } attempt.State = txmgrtypes.TxAttemptInsufficientFunds ctx, cancel = context.WithTimeout(ctx, timeout) @@ -1207,7 +1207,7 @@ func (o *evmTxStore) SaveInsufficientFundsAttempt(ctx context.Context, timeout t func (o *evmTxStore) saveSentAttempt(ctx context.Context, timeout time.Duration, attempt *TxAttempt, broadcastAt time.Time) error { if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("saveSentAttempt failed: expected attempt state to be 'in_progress' before marking as broadcast. The attempt may have already been processed") + return errors.New("expected state to be in_progress") } attempt.State = txmgrtypes.TxAttemptBroadcast ctx, cancel := context.WithTimeout(ctx, timeout) @@ -1243,10 +1243,10 @@ func (o *evmTxStore) DeleteInProgressAttempt(ctx context.Context, attempt TxAtte ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("DeleteInProgressAttempt failed: expected attempt state to be 'in_progress', but got a different state. Only in-progress attempts can be deleted") + return errors.New("DeleteInProgressAttempt: expected attempt state to be in_progress") } if attempt.ID == 0 { - return errors.New("DeleteInProgressAttempt failed: attempt has no database ID (id=0). The attempt may not have been persisted yet") + return errors.New("DeleteInProgressAttempt: expected attempt to have an id") } _, err := o.q.ExecContext(ctx, `DELETE FROM evm.tx_attempts WHERE id = $1`, attempt.ID) return pkgerrors.Wrap(err, "DeleteInProgressAttempt failed") @@ -1258,7 +1258,7 @@ func (o *evmTxStore) SaveInProgressAttempt(ctx context.Context, attempt *TxAttem ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("SaveInProgressAttempt failed: attempt state must be 'in_progress' to be saved. The attempt may have already been broadcast or finalized") + return errors.New("SaveInProgressAttempt failed: attempt state must be in_progress") } var dbAttempt DbEthTxAttempt dbAttempt.FromTxAttempt(attempt) @@ -1401,10 +1401,10 @@ func (o *evmTxStore) SaveReplacementInProgressAttempt(ctx context.Context, oldAt ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if oldAttempt.State != txmgrtypes.TxAttemptInProgress || replacementAttempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("SaveReplacementInProgressAttempt failed: both the old and replacement attempts must have state 'in_progress'. Verify that neither attempt has been broadcast or finalized") + return errors.New("expected attempts to be in_progress") } if oldAttempt.ID == 0 { - return errors.New("SaveReplacementInProgressAttempt failed: the old attempt has no database ID (id=0). It may not have been persisted yet") + return errors.New("expected oldAttempt to have an ID") } return o.Transact(ctx, false, func(orm *evmTxStore) error { if _, err := orm.q.ExecContext(ctx, `DELETE FROM evm.tx_attempts WHERE id=$1`, oldAttempt.ID); err != nil { @@ -1443,7 +1443,7 @@ func (o *evmTxStore) UpdateTxFatalErrorAndDeleteAttempts(ctx context.Context, et ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if !etx.Error.Valid { - return errors.New("UpdateTxFatalErrorAndDeleteAttempts failed: the transaction's Error field must be set before marking it as fatally errored") + return errors.New("expected error field to be set") } etx.Sequence = nil @@ -1467,16 +1467,16 @@ func (o *evmTxStore) UpdateTxAttemptInProgressToBroadcast(ctx context.Context, e ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if etx.BroadcastAt == nil { - return errors.New("UpdateTxAttemptInProgressToBroadcast failed: unconfirmed transaction is missing its broadcast_at timestamp. This field is required to track when the transaction was broadcast") + return errors.New("unconfirmed transaction must have broadcast_at time") } if etx.InitialBroadcastAt == nil { - return errors.New("UpdateTxAttemptInProgressToBroadcast failed: unconfirmed transaction is missing its initial_broadcast_at timestamp. This field is required to track when the transaction was first broadcast") + return errors.New("unconfirmed transaction must have initial_broadcast_at time") } if etx.State != txmgr.TxInProgress { return pkgerrors.Errorf("can only transition to unconfirmed from in_progress, transaction is currently %s", etx.State) } if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("UpdateTxAttemptInProgressToBroadcast failed: attempt must be in 'in_progress' state before transitioning to broadcast. The attempt may have already been broadcast or finalized") + return errors.New("attempt must be in in_progress state") } if NewAttemptState != txmgrtypes.TxAttemptBroadcast { return pkgerrors.Errorf("new attempt state must be broadcast, got: %s", NewAttemptState) @@ -1505,13 +1505,13 @@ func (o *evmTxStore) UpdateTxUnstartedToInProgress(ctx context.Context, etx *Tx, ctx, cancel = o.stopCh.Ctx(ctx) defer cancel() if etx.Sequence == nil { - return errors.New("UpdateTxUnstartedToInProgress failed: transaction must have a nonce (Sequence) assigned before transitioning to 'in_progress' state") + return errors.New("in_progress transaction must have nonce") } if etx.State != txmgr.TxUnstarted { return pkgerrors.Errorf("can only transition to in_progress from unstarted, transaction is currently %s", etx.State) } if attempt.State != txmgrtypes.TxAttemptInProgress { - return errors.New("UpdateTxUnstartedToInProgress failed: attempt state must be 'in_progress' when beginning transaction processing") + return errors.New("attempt state must be in_progress") } etx.State = txmgr.TxInProgress return o.Transact(ctx, false, func(orm *evmTxStore) error { diff --git a/pkg/txmgr/finalizer.go b/pkg/txmgr/finalizer.go index b79691aa3e..64ef4b7b4e 100644 --- a/pkg/txmgr/finalizer.go +++ b/pkg/txmgr/finalizer.go @@ -29,7 +29,7 @@ var _ Finalizer = (*evmFinalizer)(nil) var ( // ErrCouldNotGetReceipt is the error string we save if we reach our LatestFinalizedBlockNum for a confirmed transaction // without ever getting a receipt. This most likely happened because an external wallet used the account for this nonce - ErrCouldNotGetReceipt = "failed to retrieve transaction receipt before finalization: the transaction reached the finalized block without a receipt. This likely means an external wallet used the same account nonce, or the transaction was dropped by the network. Verify that no other wallet or Chainlink instance is using the same private key" + ErrCouldNotGetReceipt = "could not get receipt" ) // processHeadTimeout represents a sanity limit on how long ProcessHead should take to complete diff --git a/pkg/txmgr/stuck_tx_detector.go b/pkg/txmgr/stuck_tx_detector.go index df3ea2b3af..564ba19ad8 100644 --- a/pkg/txmgr/stuck_tx_detector.go +++ b/pkg/txmgr/stuck_tx_detector.go @@ -195,7 +195,7 @@ func (d *stuckTxDetector) FindUnconfirmedTxWithLowestNonce(ctx context.Context, // 5. If 4 is true, the transaction is likely stuck due to overflow func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context, txs []Tx, blockNum int64) ([]Tx, error) { if d.cfg.Threshold() == nil || d.cfg.MinAttempts() == nil { - err := errors.New("stuck transaction detection cannot run: missing required configuration values. Both Transactions.AutoPurge.Threshold and Transactions.AutoPurge.MinAttempts must be set in the chain configuration to enable automatic stuck transaction detection") + err := errors.New("missing required configs for the stuck transaction heuristic. Transactions.AutoPurge.Threshold and Transactions.AutoPurge.MinAttempts are required") d.lggr.Error(err.Error()) return txs, err } diff --git a/pkg/txmgr/transmitchecker.go b/pkg/txmgr/transmitchecker.go index 7db985f16b..e62a8b32be 100644 --- a/pkg/txmgr/transmitchecker.go +++ b/pkg/txmgr/transmitchecker.go @@ -73,7 +73,7 @@ func (c *CheckerFactory) BuildChecker(spec TransmitCheckerSpec) (TransmitChecker "failed to create VRF V2 coordinator at address %v", spec.VRFCoordinatorAddress) } if spec.VRFRequestBlockNumber == nil { - return nil, pkgerrors.New("VRF checker configuration error: VRFRequestBlockNumber parameter must be non-nil. This block number is required to verify VRF request fulfillment status") + return nil, pkgerrors.New("VRFRequestBlockNumber parameter must be non-nil") } return &VRFV2Checker{ GetCommitment: coord.GetCommitment, @@ -90,7 +90,7 @@ func (c *CheckerFactory) BuildChecker(spec TransmitCheckerSpec) (TransmitChecker "failed to create VRF V2 coordinator plus at address %v", spec.VRFCoordinatorAddress) } if spec.VRFRequestBlockNumber == nil { - return nil, pkgerrors.New("VRF checker configuration error: VRFRequestBlockNumber parameter must be non-nil. This block number is required to verify VRF request fulfillment status") + return nil, pkgerrors.New("VRFRequestBlockNumber parameter must be non-nil") } return &VRFV2Checker{ GetCommitment: coord.SRequestCommitments, @@ -258,7 +258,7 @@ func (v *VRFV1Checker) Check( "ethTxID", tx.ID, "meta", tx.Meta, "reqID", reqID) - return pkgerrors.New("VRF transmit check failed: request already fulfilled on-chain. Skipping transaction to avoid unnecessary gas expenditure on a duplicate fulfillment") + return pkgerrors.New("request already fulfilled") } // Request not fulfilled return nil @@ -349,7 +349,7 @@ func (v *VRFV2Checker) Check( "ethTxID", tx.ID, "meta", tx.Meta, "vrfRequestId", vrfRequestID) - return pkgerrors.New("VRF transmit check failed: request already fulfilled on-chain. Skipping transaction to avoid unnecessary gas expenditure on a duplicate fulfillment") + return pkgerrors.New("request already fulfilled") } l.Debugw("Request not yet fulfilled", "ethTxID", tx.ID,