diff --git a/core/services/relay/evm/evmtesting/run_tests.go b/core/services/relay/evm/evmtesting/run_tests.go index ce3df6a07ed..908c4e1a81a 100644 --- a/core/services/relay/evm/evmtesting/run_tests.go +++ b/core/services/relay/evm/evmtesting/run_tests.go @@ -146,7 +146,7 @@ func RunContractReaderEvmTests[T TestingT[T]](t T, it *EVMChainComponentsInterfa ctx := it.Helper.Context(t) err := reader.Bind(ctx, []clcommontypes.BoundContract{{Name: AnyContractName, Address: addr.Hex()}}) - require.ErrorIs(t, err, read.NoContractExistsError{Address: addr}) + require.ErrorIs(t, err, read.NoContractExistsError{Err: clcommontypes.ErrInternal, Address: addr}) }) } diff --git a/core/services/relay/evm/read/batch.go b/core/services/relay/evm/read/batch.go index 1d72e222963..dbe8c8be549 100644 --- a/core/services/relay/evm/read/batch.go +++ b/core/services/relay/evm/read/batch.go @@ -2,13 +2,13 @@ package read import ( "context" + "errors" "fmt" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" - "github.com/pkg/errors" "golang.org/x/sync/errgroup" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -47,9 +47,9 @@ type MethodCallResult struct { type BatchCall []Call type Call struct { - ContractAddress common.Address - ContractName, MethodName string - Params, ReturnVal any + ContractAddress common.Address + ContractName, ReadName string + Params, ReturnVal any } func (c BatchCall) String() string { @@ -63,7 +63,7 @@ func (c BatchCall) String() string { // Implement the String method for the Call struct func (c Call) String() string { return fmt.Sprintf("contractAddress: %s, contractName: %s, method: %s, params: %+v returnValType: %T", - c.ContractAddress.Hex(), c.ContractName, c.MethodName, c.Params, c.ReturnVal) + c.ContractAddress.Hex(), c.ContractName, c.ReadName, c.Params, c.ReturnVal) } type BatchCaller interface { @@ -127,25 +127,60 @@ func newDefaultEvmBatchCaller( } } +// batchCall formats a batch, calls the rpc client, and unpacks results. +// this function only returns errors of type ErrRead which should wrap lower errors. func (c *defaultEvmBatchCaller) batchCall(ctx context.Context, blockNumber uint64, batchCall BatchCall) ([]dataAndErr, error) { if len(batchCall) == 0 { return nil, nil } - packedOutputs := make([]string, len(batchCall)) - rpcBatchCalls := make([]rpc.BatchElem, len(batchCall)) - for i, call := range batchCall { - data, err := c.codec.Encode(ctx, call.Params, codec.WrapItemType(call.ContractName, call.MethodName, true)) - if err != nil { - return nil, err + blockNumStr := "latest" + if blockNumber > 0 { + blockNumStr = hexutil.EncodeBig(big.NewInt(0).SetUint64(blockNumber)) + } + + rpcBatchCalls, hexEncodedOutputs, err := c.createBatchCalls(ctx, batchCall, blockNumStr) + if err != nil { + return nil, err + } + + if err = c.evmClient.BatchCallContext(ctx, rpcBatchCalls); err != nil { + // return a basic read error with no detail or result since this is a general client + // error instead of an error for a specific batch call. + return nil, ErrRead{ + Err: fmt.Errorf("%w: batch call context: %s", types.ErrInternal, err.Error()), + Batch: true, } + } + + results, err := c.unpackBatchResults(ctx, batchCall, rpcBatchCalls, hexEncodedOutputs, blockNumStr) + if err != nil { + return nil, err + } - blockNumStr := "latest" - if blockNumber > 0 { - blockNumStr = hexutil.EncodeBig(big.NewInt(0).SetUint64(blockNumber)) + return results, nil +} + +func (c *defaultEvmBatchCaller) createBatchCalls( + ctx context.Context, + batchCall BatchCall, + block string, +) ([]rpc.BatchElem, []string, error) { + rpcBatchCalls := make([]rpc.BatchElem, len(batchCall)) + hexEncodedOutputs := make([]string, len(batchCall)) + + for idx, call := range batchCall { + data, err := c.codec.Encode(ctx, call.Params, codec.WrapItemType(call.ContractName, call.ReadName, true)) + if err != nil { + return nil, nil, newErrorFromCall( + fmt.Errorf("%w: encode params: %s", types.ErrInvalidConfig, err.Error()), + call, + block, + true, + ) } - rpcBatchCalls[i] = rpc.BatchElem{ + rpcBatchCalls[idx] = rpc.BatchElem{ Method: "eth_call", Args: []any{ map[string]interface{}{ @@ -153,50 +188,88 @@ func (c *defaultEvmBatchCaller) batchCall(ctx context.Context, blockNumber uint6 "to": call.ContractAddress, "data": hexutil.Bytes(data), }, - blockNumStr, + block, }, - Result: &packedOutputs[i], + Result: &hexEncodedOutputs[idx], } } - if err := c.evmClient.BatchCallContext(ctx, rpcBatchCalls); err != nil { - return nil, fmt.Errorf("batch call context: %w", err) - } + return rpcBatchCalls, hexEncodedOutputs, nil +} +func (c *defaultEvmBatchCaller) unpackBatchResults( + ctx context.Context, + batchCall BatchCall, + rpcBatchCalls []rpc.BatchElem, + hexEncodedOutputs []string, + block string, +) ([]dataAndErr, error) { results := make([]dataAndErr, len(batchCall)) - for i, call := range batchCall { - results[i] = dataAndErr{ + + for idx, call := range batchCall { + results[idx] = dataAndErr{ address: call.ContractAddress.Hex(), contractName: call.ContractName, - methodName: call.MethodName, + methodName: call.ReadName, returnVal: call.ReturnVal, } - if rpcBatchCalls[i].Error != nil { - results[i].err = rpcBatchCalls[i].Error + if rpcBatchCalls[idx].Error != nil { + results[idx].err = newErrorFromCall( + fmt.Errorf("%w: rpc call error: %w", types.ErrInternal, rpcBatchCalls[idx].Error), + call, block, true, + ) + continue } - if packedOutputs[i] == "" { + if hexEncodedOutputs[idx] == "" { // Some RPCs instead of returning "0x" are returning an empty string. // We are overriding this behaviour for consistent handling of this scenario. - packedOutputs[i] = "0x" + hexEncodedOutputs[idx] = "0x" } - b, err := hexutil.Decode(packedOutputs[i]) + packedBytes, err := hexutil.Decode(hexEncodedOutputs[idx]) if err != nil { - return nil, fmt.Errorf("decode result %s: packedOutputs %s: %w", call, packedOutputs[i], err) + callErr := newErrorFromCall( + fmt.Errorf("%w: hex decode result: %s", types.ErrInternal, err.Error()), + call, block, true, + ) + + callErr.Result = &hexEncodedOutputs[idx] + + return nil, callErr } - if err = c.codec.Decode(ctx, b, call.ReturnVal, codec.WrapItemType(call.ContractName, call.MethodName, false)); err != nil { - if len(b) == 0 { - results[i].err = fmt.Errorf("unpack result %s: %s: %w", call, err.Error(), errEmptyOutput) + if err = c.codec.Decode( + ctx, + packedBytes, + call.ReturnVal, + codec.WrapItemType(call.ContractName, call.ReadName, false), + ); err != nil { + if len(packedBytes) == 0 { + callErr := newErrorFromCall( + fmt.Errorf("%w: %w: %s", types.ErrInternal, errEmptyOutput, err.Error()), + call, block, true, + ) + + callErr.Result = &hexEncodedOutputs[idx] + + results[idx].err = callErr } else { - results[i].err = fmt.Errorf("unpack result %s: %w", call, err) + callErr := newErrorFromCall( + fmt.Errorf("%w: codec decode result: %s", types.ErrInvalidType, err.Error()), + call, block, true, + ) + + callErr.Result = &hexEncodedOutputs[idx] + results[idx].err = callErr } + continue } - results[i].returnVal = call.ReturnVal + + results[idx].returnVal = call.ReturnVal } return results, nil @@ -204,10 +277,12 @@ func (c *defaultEvmBatchCaller) batchCall(ctx context.Context, blockNumber uint6 func (c *defaultEvmBatchCaller) batchCallDynamicLimitRetries(ctx context.Context, blockNumber uint64, calls BatchCall) (BatchResult, error) { lim := c.batchSizeLimit + // Limit the batch size to the number of calls if uint(len(calls)) < lim { lim = uint(len(calls)) } + for { results, err := c.batchCallLimit(ctx, blockNumber, calls, lim) if err == nil { @@ -215,16 +290,20 @@ func (c *defaultEvmBatchCaller) batchCallDynamicLimitRetries(ctx context.Context } if lim <= 1 { - return nil, errors.Wrapf(err, "calls %+v", calls) + return nil, ErrRead{ + Err: fmt.Errorf("%w: limited call: call data: %+v", err, calls), + Batch: true, + } } newLim := lim / c.backOffMultiplier if newLim == 0 || newLim == lim { newLim = 1 } + lim = newLim - c.lggr.Errorf("retrying batch call with %d calls and %d limit that failed with error=%s", - len(calls), lim, err) + + c.lggr.Debugf("retrying batch call with %d calls and %d limit that failed with error=%s", len(calls), lim, err) } } @@ -238,6 +317,7 @@ type dataAndErr struct { func (c *defaultEvmBatchCaller) batchCallLimit(ctx context.Context, blockNumber uint64, calls BatchCall, batchSizeLimit uint) (BatchResult, error) { if batchSizeLimit <= 0 { res, err := c.batchCall(ctx, blockNumber, calls) + return convertToBatchResult(res), err } @@ -250,32 +330,40 @@ func (c *defaultEvmBatchCaller) batchCallLimit(ctx context.Context, blockNumber jobs := make([]job, 0) for i := 0; i < len(calls); i += int(batchSizeLimit) { idxFrom := i + idxTo := idxFrom + int(batchSizeLimit) if idxTo > len(calls) { idxTo = len(calls) } + jobs = append(jobs, job{blockNumber: blockNumber, calls: calls[idxFrom:idxTo], results: nil}) } if c.parallelRpcCallsLimit > 1 { eg := new(errgroup.Group) eg.SetLimit(int(c.parallelRpcCallsLimit)) + for jobIdx := range jobs { jobIdx := jobIdx + eg.Go(func() error { res, err := c.batchCall(ctx, jobs[jobIdx].blockNumber, jobs[jobIdx].calls) if err != nil { return err } + jobs[jobIdx].results = res + return nil }) } + if err := eg.Wait(); err != nil { return nil, err } } else { var err error + for jobIdx := range jobs { jobs[jobIdx].results, err = c.batchCall(ctx, jobs[jobIdx].blockNumber, jobs[jobIdx].calls) if err != nil { diff --git a/core/services/relay/evm/read/batch_caller_test.go b/core/services/relay/evm/read/batch_caller_test.go index 4f50bdc6911..e84452416b7 100644 --- a/core/services/relay/evm/read/batch_caller_test.go +++ b/core/services/relay/evm/read/batch_caller_test.go @@ -143,7 +143,7 @@ func TestDefaultEvmBatchCaller_batchCallLimit(t *testing.T) { var returnVal MethodReturn calls[j] = read.Call{ ContractName: contractName, - MethodName: methodName, + ReadName: methodName, Params: ¶ms, ReturnVal: &returnVal, } @@ -174,7 +174,7 @@ func TestDefaultEvmBatchCaller_batchCallLimit(t *testing.T) { } hasResult := false for j, result := range contractResults { - if hasResult = result.MethodName == call.MethodName; hasResult { + if hasResult = result.MethodName == call.ReadName; hasResult { require.NoError(t, result.Err) resNum, isOk := result.ReturnValue.(*MethodReturn) require.True(t, isOk) @@ -183,7 +183,7 @@ func TestDefaultEvmBatchCaller_batchCallLimit(t *testing.T) { } } if !hasResult { - t.Errorf("missing method name %s", call.MethodName) + t.Errorf("missing method name %s", call.ReadName) } } }) diff --git a/core/services/relay/evm/read/bindings.go b/core/services/relay/evm/read/bindings.go index 55f237af0eb..bfeb84a3799 100644 --- a/core/services/relay/evm/read/bindings.go +++ b/core/services/relay/evm/read/bindings.go @@ -65,23 +65,23 @@ func (b *BindingsRegistry) HasContractBinding(contractName string) bool { } // GetReader should only be called after Chain Reader is started. -func (b *BindingsRegistry) GetReader(readName string) (Reader, string, error) { +func (b *BindingsRegistry) GetReader(readIdentifier string) (Reader, string, error) { b.mu.RLock() defer b.mu.RUnlock() - values, ok := b.contractLookup.getContractForReadName(readName) + values, ok := b.contractLookup.getContractForReadName(readIdentifier) if !ok { - return nil, "", fmt.Errorf("%w: no reader for read name %s", commontypes.ErrInvalidType, readName) + return nil, "", fmt.Errorf("%w: %w", commontypes.ErrInvalidType, newMissingReadIdentifierErr(readIdentifier)) } cb, cbExists := b.contractBindings[values.contract] if !cbExists { - return nil, "", fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidType, values.contract) + return nil, "", fmt.Errorf("%w: %w", commontypes.ErrInvalidType, newMissingContractErr(readIdentifier, values.contract)) } binding, rbExists := cb.GetReaderNamed(values.readName) if !rbExists { - return nil, "", fmt.Errorf("%w: no reader named %s in contract %s", commontypes.ErrInvalidType, values.readName, values.contract) + return nil, "", fmt.Errorf("%w: %w", commontypes.ErrInvalidType, newMissingReadNameErr(readIdentifier, values.contract, values.readName)) } return binding, values.address, nil @@ -91,16 +91,16 @@ func (b *BindingsRegistry) AddReader(contractName, readName string, rdr Reader) b.mu.Lock() defer b.mu.Unlock() - switch v := rdr.(type) { - case *EventBinding: + if binding, ok := rdr.(*EventBinding); ok { // unwrap codec type naming for event data words and topics to be used by lookup for Querying by Value Comparators // For e.g. "params.contractName.eventName.IndexedTopic" -> "eventName.IndexedTopic" // or "params.contractName.eventName.someFieldInData" -> "eventName.someFieldInData" - for name := range v.eventTypes { + for name := range binding.eventTypes { split := strings.Split(name, ".") if len(split) < 3 || split[1] != contractName { - return fmt.Errorf("invalid event type name %s", name) + return fmt.Errorf("%w: invalid event type name %s", commontypes.ErrInvalidType, name) } + b.contractLookup.addReadNameForContract(contractName, strings.Join(split[2:], ".")) } } @@ -114,6 +114,7 @@ func (b *BindingsRegistry) AddReader(contractName, readName string, rdr Reader) } cb.AddReaderNamed(readName, rdr) + return nil } @@ -126,7 +127,7 @@ func (b *BindingsRegistry) Bind(ctx context.Context, reg Registrar, bindings []c for _, binding := range bindings { contract, exists := b.contractBindings[binding.Name] if !exists { - return fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidConfig, binding.Name) + return fmt.Errorf("%w: %w", commontypes.ErrInvalidConfig, newMissingContractErr("binding contract", binding.Name)) } b.contractLookup.bindAddressForContract(binding.Name, binding.Address) @@ -151,17 +152,17 @@ func (b *BindingsRegistry) BatchGetLatestValues(ctx context.Context, request com for binding, contractBatch := range request { cb := b.contractBindings[binding.Name] - for i := range contractBatch { - req := contractBatch[i] + for idx := range contractBatch { + req := contractBatch[idx] values, ok := b.contractLookup.getContractForReadName(binding.ReadIdentifier(req.ReadName)) if !ok { - return nil, fmt.Errorf("%w: no method for read name %s", commontypes.ErrInvalidType, binding.ReadIdentifier(req.ReadName)) + return nil, fmt.Errorf("%w: %w", commontypes.ErrInvalidConfig, newMissingReadNameErr(binding.ReadIdentifier(req.ReadName), binding.Name, req.ReadName)) } rdr, exists := cb.GetReaderNamed(values.readName) if !exists { - return nil, fmt.Errorf("%w: no contract read binding for %s", commontypes.ErrInvalidType, values.readName) + return nil, fmt.Errorf("%w: %w", commontypes.ErrInvalidConfig, newMissingReadNameErr(binding.ReadIdentifier(req.ReadName), binding.Name, req.ReadName)) } call, err := rdr.BatchCall(common.HexToAddress(values.address), req.Params, req.ReturnVal) @@ -212,7 +213,7 @@ func (b *BindingsRegistry) Unbind(ctx context.Context, reg Registrar, bindings [ for _, binding := range bindings { contract, exists := b.contractBindings[binding.Name] if !exists { - return fmt.Errorf("%w: no contract named %s", commontypes.ErrInvalidConfig, binding.Name) + return newMissingContractErr("unbinding contract", binding.Name) } b.contractLookup.unbindAddressForContract(binding.Name, binding.Address) @@ -269,7 +270,7 @@ func (b *BindingsRegistry) SetFilter(name string, filter logpoller.Filter) error contract, ok := b.contractBindings[name] if !ok { - return fmt.Errorf("%w: no contract binding for %s", commontypes.ErrInvalidConfig, name) + return fmt.Errorf("%w: %w", commontypes.ErrInvalidConfig, newMissingContractErr("set filter", name)) } contract.SetFilter(filter) @@ -287,10 +288,14 @@ func (b *BindingsRegistry) ReadTypeIdentifier(readName string, forEncoding bool) } // confidenceToConfirmations matches predefined chain agnostic confidence levels to predefined EVM finality. -func confidenceToConfirmations(confirmationsMapping map[primitives.ConfidenceLevel]evmtypes.Confirmations, confidenceLevel primitives.ConfidenceLevel) (evmtypes.Confirmations, error) { +func confidenceToConfirmations( + confirmationsMapping map[primitives.ConfidenceLevel]evmtypes.Confirmations, + confidenceLevel primitives.ConfidenceLevel, +) (evmtypes.Confirmations, error) { confirmations, exists := confirmationsMapping[confidenceLevel] if !exists { - return 0, fmt.Errorf("missing mapping for confidence level: %s", confidenceLevel) + return 0, fmt.Errorf("%w: missing mapping for confidence level: %s", commontypes.ErrInvalidConfig, confidenceLevel) } + return confirmations, nil } diff --git a/core/services/relay/evm/read/contract.go b/core/services/relay/evm/read/contract.go index 1b47c9ee870..c8770b93600 100644 --- a/core/services/relay/evm/read/contract.go +++ b/core/services/relay/evm/read/contract.go @@ -39,12 +39,12 @@ func newContractBinding(name string) *contractBinding { } } -// GetReaderNamed returns a reader for the provided contract name. This method is thread safe. -func (cb *contractBinding) GetReaderNamed(name string) (Reader, bool) { +// GetReaderNamed returns a reader for the provided read name. This method is thread safe. +func (cb *contractBinding) GetReaderNamed(readName string) (Reader, bool) { cb.mu.RLock() defer cb.mu.RUnlock() - binding, exists := cb.readers[name] + binding, exists := cb.readers[readName] return binding, exists } diff --git a/core/services/relay/evm/read/errors.go b/core/services/relay/evm/read/errors.go new file mode 100644 index 00000000000..bec14d7dd4b --- /dev/null +++ b/core/services/relay/evm/read/errors.go @@ -0,0 +1,127 @@ +package read + +import ( + "fmt" + "reflect" + "strings" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" +) + +type ErrRead struct { + Err error + Batch bool + Detail *readDetail + Result *string +} + +type readDetail struct { + Address string + Contract string + Method string + Params, RetVal any + Block string +} + +func newErrorFromCall(err error, call Call, block string, batch bool) ErrRead { + return ErrRead{ + Err: err, + Batch: batch, + Detail: &readDetail{ + Address: call.ContractAddress.Hex(), + Contract: call.ContractName, + Method: call.ReadName, + Params: call.Params, + RetVal: call.ReturnVal, + Block: block, + }, + } +} + +func (e ErrRead) Error() string { + var builder strings.Builder + + builder.WriteString("[rpc error]") + builder.WriteString(fmt.Sprintf(" batch: %T;", e.Batch)) + builder.WriteString(fmt.Sprintf(" err: %s;", e.Err.Error())) + + if e.Detail != nil { + builder.WriteString(fmt.Sprintf(" block: %s;", e.Detail.Block)) + builder.WriteString(fmt.Sprintf(" address: %s;", e.Detail.Address)) + builder.WriteString(fmt.Sprintf(" contract-name: %s;", e.Detail.Contract)) + builder.WriteString(fmt.Sprintf(" read-name: %s;", e.Detail.Method)) + builder.WriteString(fmt.Sprintf(" params: %+v;", e.Detail.Params)) + builder.WriteString(fmt.Sprintf(" expected return type: %s;", reflect.TypeOf(e.Detail.RetVal))) + + if e.Result != nil { + builder.WriteString(fmt.Sprintf("encoded result: %s;", *e.Result)) + } + } + + return builder.String() +} + +func (e ErrRead) Unwrap() error { + return e.Err +} + +type ConfigError struct { + Msg string +} + +func newMissingReadIdentifierErr(readIdentifier string) ConfigError { + return ConfigError{ + Msg: fmt.Sprintf("[no configured reader] read-identifier: '%s'", readIdentifier), + } +} + +func newMissingContractErr(readIdentifier, contract string) ConfigError { + return ConfigError{ + Msg: fmt.Sprintf("[no configured reader] read-identifier: %s; contract: %s;", readIdentifier, contract), + } +} + +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), + } +} + +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), + } +} + +func (e ConfigError) Error() string { + return e.Msg +} + +type FilterError struct { + Err error + Action string + Filter logpoller.Filter +} + +func (e FilterError) Error() string { + return fmt.Sprintf("[logpoller filter error] action: %s; err: %s; filter: %+v;", e.Action, e.Err.Error(), e.Filter) +} + +func (e FilterError) Unwrap() error { + return e.Err +} + +type NoContractExistsError struct { + Err error + Address common.Address +} + +func (e NoContractExistsError) Error() string { + return fmt.Sprintf("%s: contract does not exist at address: %s", e.Err.Error(), e.Address) +} + +func (e NoContractExistsError) Unwrap() error { + return e.Err +} diff --git a/core/services/relay/evm/read/event.go b/core/services/relay/evm/read/event.go index d1efe662489..a1678fbb4b9 100644 --- a/core/services/relay/evm/read/event.go +++ b/core/services/relay/evm/read/event.go @@ -2,8 +2,10 @@ package read import ( "context" + "encoding/hex" "fmt" "reflect" + "strconv" "strings" "sync" @@ -231,12 +233,33 @@ func (b *EventBinding) BatchCall(_ common.Address, _, _ any) (Call, error) { return Call{}, fmt.Errorf("%w: events are not yet supported in batch get latest values", commontypes.ErrInvalidType) } -func (b *EventBinding) GetLatestValue(ctx context.Context, address common.Address, confidenceLevel primitives.ConfidenceLevel, params, into any) error { - if err := b.validateBound(address); err != nil { +func (b *EventBinding) GetLatestValue(ctx context.Context, address common.Address, confidenceLevel primitives.ConfidenceLevel, params, into any) (err error) { + var ( + confs evmtypes.Confirmations + result *string + ) + + defer func() { + if err != nil { + callErr := newErrorFromCall(err, Call{ + ContractAddress: address, + ContractName: b.contractName, + ReadName: b.eventName, + Params: params, + ReturnVal: into, + }, strconv.Itoa(int(confs)), false) + + callErr.Result = result + + err = callErr + } + }() + + if err = b.validateBound(address); err != nil { return err } - confs, err := confidenceToConfirmations(b.confirmationsMapping, confidenceLevel) + confs, err = confidenceToConfirmations(b.confirmationsMapping, confidenceLevel) if err != nil { return err } @@ -245,7 +268,7 @@ func (b *EventBinding) GetLatestValue(ctx context.Context, address common.Addres onChainTypedVal, err := b.toNativeOnChainType(topicTypeID, params) if err != nil { - return fmt.Errorf("failed to convert params to native on chain types: %w", err) + return err } filterTopics, err := b.extractFilterTopics(topicTypeID, onChainTypedVal) @@ -270,11 +293,29 @@ func (b *EventBinding) GetLatestValue(ctx context.Context, address common.Addres } } - return b.decodeLog(ctx, log, into) + if err := b.decodeLog(ctx, log, into); err != nil { + encoded := hex.EncodeToString(log.Data) + result = &encoded + + return err + } + + return nil } -func (b *EventBinding) QueryKey(ctx context.Context, address common.Address, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]commontypes.Sequence, error) { - if err := b.validateBound(address); err != nil { +func (b *EventBinding) QueryKey(ctx context.Context, address common.Address, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) (sequences []commontypes.Sequence, err error) { + defer func() { + if err != nil { + err = newErrorFromCall(err, Call{ + ContractAddress: address, + ContractName: b.contractName, + ReadName: b.eventName, + ReturnVal: sequenceDataType, + }, "", false) + } + }() + + if err = b.validateBound(address); err != nil { return nil, err } @@ -292,7 +333,7 @@ func (b *EventBinding) QueryKey(ctx context.Context, address common.Address, fil logs, err := b.lp.FilteredLogs(ctx, remapped.Expressions, limitAndSort, b.contractName+"-"+address.String()+"-"+b.eventName) if err != nil { - return nil, err + return nil, wrapInternalErr(err) } // no need to return an error. an empty list is fine @@ -300,12 +341,18 @@ func (b *EventBinding) QueryKey(ctx context.Context, address common.Address, fil return []commontypes.Sequence{}, nil } - return b.decodeLogsIntoSequences(ctx, logs, sequenceDataType) + sequences, err = b.decodeLogsIntoSequences(ctx, logs, sequenceDataType) + if err != nil { + return nil, err + } + + return sequences, nil } func (b *EventBinding) getLatestLog(ctx context.Context, address common.Address, confs evmtypes.Confirmations, hashedTopics []common.Hash) (*logpoller.Log, error) { // Create limiter and filter for the query. limiter := query.NewLimitAndSort(query.CountLimit(1), query.NewSortBySequence(query.Desc)) + topicFilters, err := createTopicFilters(hashedTopics) if err != nil { return nil, err @@ -330,6 +377,7 @@ func (b *EventBinding) getLatestLog(ctx context.Context, address common.Address, if len(logs) == 0 { return nil, fmt.Errorf("%w: no events found", commontypes.ErrNotFound) } + return &logs[0], err } @@ -362,6 +410,7 @@ func (b *EventBinding) decodeLogsIntoSequences(ctx context.Context, logs []logpo return nil, err } } + return sequences, nil } @@ -369,17 +418,19 @@ func (b *EventBinding) decodeLogsIntoSequences(ctx context.Context, logs []logpo // returned slice will retain the order of the topics and fill in missing topics with nil, if all values are nil, empty slice is returned. func (b *EventBinding) extractFilterTopics(topicTypeID string, value any) (filterTopics []any, err error) { item := reflect.ValueOf(value) + switch item.Kind() { case reflect.Array, reflect.Slice: var native any native, err = codec.RepresentArray(item, b.eventTypes[topicTypeID]) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: error converting params to log topics: %s", commontypes.ErrInternal, err.Error()) } + filterTopics = []any{native} case reflect.Struct, reflect.Map: if filterTopics, err = codec.UnrollItem(item, b.eventTypes[topicTypeID]); err != nil { - return nil, err + return nil, fmt.Errorf("%w: error unrolling params into log topics: %s", commontypes.ErrInternal, err.Error()) } default: return nil, fmt.Errorf("%w: cannot encode kind %v", commontypes.ErrInvalidType, item.Kind()) @@ -406,14 +457,15 @@ func (b *EventBinding) hashTopics(topicTypeID string, topics []any) ([]common.Ha // make topic value for non-fixed bytes array manually because geth MakeTopics doesn't support it topicTyp, exists := b.eventTypes[topicTypeID] if !exists { - return nil, fmt.Errorf("cannot find event type entry") + return nil, fmt.Errorf("%w: cannot find event type entry for topic: %s", commontypes.ErrNotFound, topicTypeID) } if abiArg := topicTyp.Args()[i]; abiArg.Type.T == abi.ArrayTy && (abiArg.Type.Elem != nil && abiArg.Type.Elem.T == abi.UintTy) { packed, err := abi.Arguments{abiArg}.Pack(topic) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: err failed to abi pack topics: %s", commontypes.ErrInternal, err.Error()) } + topic = crypto.Keccak256Hash(packed) } @@ -435,7 +487,7 @@ func (b *EventBinding) hashTopics(topicTypeID string, topics []any) ([]common.Ha func (b *EventBinding) decodeLog(ctx context.Context, log *logpoller.Log, into any) error { // decode non indexed topics and apply output modifiers if err := b.codec.Decode(ctx, log.Data, into, codec.WrapItemType(b.contractName, b.eventName, false)); err != nil { - return err + return fmt.Errorf("%w: failed to decode log data: %s", commontypes.ErrInvalidType, err.Error()) } // decode indexed topics which is rarely useful since most indexed topic types get Keccak256 hashed and should be just used for log filtering. @@ -453,7 +505,11 @@ func (b *EventBinding) decodeLog(ctx context.Context, log *logpoller.Log, into a return fmt.Errorf("%w: %w", commontypes.ErrInvalidType, err) } - return codec.MapstructureDecode(topicsInto, into) + if err := codec.MapstructureDecode(topicsInto, into); err != nil { + return fmt.Errorf("%w: failed to decode log data: %s", commontypes.ErrInvalidEncoding, err.Error()) + } + + return nil } // remap chain agnostic primitives to chain specific logPoller primitives. @@ -516,7 +572,7 @@ func (b *EventBinding) encodeComparator(comparator *primitives.Comparator) (quer dwInfo, isDW := b.dataWords[comparator.Name] if !isDW { if _, exists := b.topics[comparator.Name]; !exists { - return query.Expression{}, fmt.Errorf("comparator name doesn't match any of the indexed topics or data words") + return query.Expression{}, fmt.Errorf("%w: comparator name doesn't match any of the indexed topics or data words", commontypes.ErrInvalidConfig) } } @@ -556,12 +612,12 @@ func (b *EventBinding) encodeValComparatorDataWord(dwTypeID string, value any) ( dwTypes, exists := b.eventTypes[dwTypeID] if !exists { - return common.Hash{}, fmt.Errorf("cannot find data word type for %s", dwTypeID) + return common.Hash{}, fmt.Errorf("%w: cannot find data word type for %s", commontypes.ErrInvalidConfig, dwTypeID) } packedArgs, err := dwTypes.Args().Pack(value) if err != nil { - return common.Hash{}, err + return common.Hash{}, fmt.Errorf("%w: failed to pack values: %w", commontypes.ErrInternal, err) } return common.BytesToHash(packedArgs), nil @@ -580,12 +636,12 @@ func (b *EventBinding) encodeValComparatorTopic(topicTypeID string, value any) ( func (b *EventBinding) toNativeOnChainType(itemType string, value any) (any, error) { offChain, err := b.codec.CreateType(itemType, true) if err != nil { - return nil, fmt.Errorf("failed to create type: %w", err) + return nil, fmt.Errorf("%w: failed to create type: %w", commontypes.ErrInvalidType, err) } // apply map struct evm hooks to correct incoming values if err = codec.MapstructureDecode(value, offChain); err != nil { - return nil, err + return nil, fmt.Errorf("%w: failed to decode offChain value: %s", commontypes.ErrInternal, err.Error()) } // apply modifiers if present @@ -593,18 +649,18 @@ func (b *EventBinding) toNativeOnChainType(itemType string, value any) (any, err if modifier, exists := b.eventModifiers[itemType]; exists { onChain, err = modifier.TransformToOnChain(offChain, "" /* unused */) if err != nil { - return nil, fmt.Errorf("failed to apply modifiers to offchain type %T: %w", onChain, err) + return nil, fmt.Errorf("%w: failed to apply modifiers to offchain type %T: %w", commontypes.ErrInvalidType, onChain, err) } } typ, exists := b.eventTypes[itemType] if !exists { - return query.Expression{}, fmt.Errorf("cannot find event type entry") + return query.Expression{}, fmt.Errorf("%w: cannot find event type entry for %s", commontypes.ErrInvalidType, itemType) } native, err := typ.ToNative(reflect.ValueOf(onChain)) if err != nil { - return query.Expression{}, err + return query.Expression{}, fmt.Errorf("%w: codec to native: %s", commontypes.ErrInvalidType, err.Error()) } return native.Interface(), nil @@ -616,12 +672,7 @@ func (b *EventBinding) validateBound(address common.Address) error { bound, exists := b.bound[address] if !exists || !bound { - return fmt.Errorf( - "%w: event %s that belongs to contract: %s, not bound", - commontypes.ErrInvalidType, - b.eventName, - b.contractName, - ) + return fmt.Errorf("%w: %w", commontypes.ErrInvalidConfig, newUnboundAddressErr(address.String(), b.contractName, b.eventName)) } return nil @@ -655,6 +706,7 @@ func derefValues(topics []any) []any { } } } + return topics } @@ -667,7 +719,8 @@ func wrapInternalErr(err error) error { if strings.Contains(errStr, "not found") || strings.Contains(errStr, "no rows") { return fmt.Errorf("%w: %w", commontypes.ErrNotFound, err) } - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + + return fmt.Errorf("%w: %s", commontypes.ErrInternal, err.Error()) } func (b *EventBinding) hasBindings() bool { diff --git a/core/services/relay/evm/read/filter.go b/core/services/relay/evm/read/filter.go index 08f45729ece..0a5f35891d6 100644 --- a/core/services/relay/evm/read/filter.go +++ b/core/services/relay/evm/read/filter.go @@ -7,7 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" - commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" ) @@ -34,7 +34,11 @@ func (r *syncedFilter) Register(ctx context.Context, registrar Registrar) error if !registrar.HasFilter(r.filter.Name) { if err := registrar.RegisterFilter(ctx, r.filter); err != nil { - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + return FilterError{ + Err: fmt.Errorf("%w: %s", types.ErrInternal, err.Error()), + Action: "register", + Filter: r.filter, + } } } @@ -50,7 +54,11 @@ func (r *syncedFilter) Unregister(ctx context.Context, registrar Registrar) erro } if err := registrar.UnregisterFilter(ctx, r.filter.Name); err != nil { - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + return FilterError{ + Err: fmt.Errorf("%w: %s", types.ErrInternal, err.Error()), + Action: "unregister", + Filter: r.filter, + } } return nil diff --git a/core/services/relay/evm/read/method.go b/core/services/relay/evm/read/method.go index 26a72544716..fc7886b74b7 100644 --- a/core/services/relay/evm/read/method.go +++ b/core/services/relay/evm/read/method.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/smartcontractkit/chainlink-common/pkg/logger" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -21,14 +22,6 @@ import ( evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" ) -type NoContractExistsError struct { - Address common.Address -} - -func (e NoContractExistsError) Error() string { - return fmt.Sprintf("contract does not exist at address: %s", e.Address) -} - type MethodBinding struct { // read-only properties contractName string @@ -75,11 +68,19 @@ func (b *MethodBinding) Bind(ctx context.Context, bindings ...common.Address) er // check for contract byte code at the latest block and provided address byteCode, err := b.client.CodeAt(ctx, binding, nil) if err != nil { - return err + return ErrRead{ + Err: fmt.Errorf("%w: code at call failure: %s", commontypes.ErrInternal, err.Error()), + Detail: &readDetail{ + Address: binding.Hex(), + Contract: b.contractName, + Params: nil, + RetVal: nil, + }, + } } if len(byteCode) == 0 { - return NoContractExistsError{Address: binding} + return NoContractExistsError{Err: commontypes.ErrInternal, Address: binding} } b.setBinding(binding) @@ -108,13 +109,13 @@ func (b *MethodBinding) SetCodec(codec commontypes.RemoteCodec) { func (b *MethodBinding) BatchCall(address common.Address, params, retVal any) (Call, error) { if !b.isBound(address) { - return Call{}, fmt.Errorf("%w: address (%s) not bound to method (%s) for contract (%s)", commontypes.ErrInvalidConfig, address.Hex(), b.method, b.contractName) + return Call{}, fmt.Errorf("%w: %w", commontypes.ErrInvalidConfig, newUnboundAddressErr(address.Hex(), b.contractName, b.method)) } return Call{ ContractAddress: address, ContractName: b.contractName, - MethodName: b.method, + ReadName: b.method, Params: params, ReturnVal: retVal, }, nil @@ -122,31 +123,68 @@ func (b *MethodBinding) BatchCall(address common.Address, params, retVal any) (C func (b *MethodBinding) GetLatestValue(ctx context.Context, addr common.Address, confidenceLevel primitives.ConfidenceLevel, params, returnVal any) error { if !b.isBound(addr) { - return fmt.Errorf("%w: method not bound", commontypes.ErrInvalidType) + return fmt.Errorf("%w: %w", commontypes.ErrInvalidConfig, newUnboundAddressErr(addr.Hex(), b.contractName, b.method)) } - data, err := b.codec.Encode(ctx, params, codec.WrapItemType(b.contractName, b.method, true)) + block, err := b.blockNumberFromConfidence(ctx, confidenceLevel) if err != nil { return err } + data, err := b.codec.Encode(ctx, params, codec.WrapItemType(b.contractName, b.method, true)) + if err != nil { + callErr := newErrorFromCall( + fmt.Errorf("%w: encoding params: %s", commontypes.ErrInvalidType, err.Error()), + Call{ + ContractAddress: addr, + ContractName: b.contractName, + ReadName: b.method, + Params: params, + ReturnVal: returnVal, + }, block.String(), false) + + return callErr + } + callMsg := ethereum.CallMsg{ To: &addr, From: addr, Data: data, } - block, err := b.blockNumberFromConfidence(ctx, confidenceLevel) + bytes, err := b.client.CallContract(ctx, callMsg, block) if err != nil { - return err + callErr := newErrorFromCall( + fmt.Errorf("%w: contract call: %s", commontypes.ErrInvalidType, err.Error()), + Call{ + ContractAddress: addr, + ContractName: b.contractName, + ReadName: b.method, + Params: params, + ReturnVal: returnVal, + }, block.String(), false) + + return callErr } - bytes, err := b.client.CallContract(ctx, callMsg, block) - if err != nil { - return fmt.Errorf("%w: %w", commontypes.ErrInternal, err) + if err = b.codec.Decode(ctx, bytes, returnVal, codec.WrapItemType(b.contractName, b.method, false)); err != nil { + callErr := newErrorFromCall( + fmt.Errorf("%w: decode return data: %s", commontypes.ErrInvalidType, err.Error()), + Call{ + ContractAddress: addr, + ContractName: b.contractName, + ReadName: b.method, + Params: params, + ReturnVal: returnVal, + }, block.String(), false) + + strResult := hexutil.Encode(bytes) + callErr.Result = &strResult + + return callErr } - return b.codec.Decode(ctx, bytes, returnVal, codec.WrapItemType(b.contractName, b.method, false)) + return nil } func (b *MethodBinding) QueryKey( @@ -165,17 +203,19 @@ func (b *MethodBinding) Unregister(_ context.Context) error { return nil } func (b *MethodBinding) blockNumberFromConfidence(ctx context.Context, confidenceLevel primitives.ConfidenceLevel) (*big.Int, error) { confirmations, err := confidenceToConfirmations(b.confirmationsMapping, confidenceLevel) if err != nil { - err = fmt.Errorf("%w for contract: %s, method: %s", err, b.contractName, b.method) + err = fmt.Errorf("%w: contract: %s; method: %s;", err, b.contractName, b.method) if confidenceLevel == primitives.Unconfirmed { - b.lggr.Errorf("%v, now falling back to default contract call behaviour that calls latest state", err) + b.lggr.Debugw("Falling back to default contract call behaviour that calls latest state", "contract", b.contractName, "method", b.method, "err", err) + return nil, nil } + return nil, err } _, finalized, err := b.ht.LatestAndFinalizedBlock(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: head tracker: %w", commontypes.ErrInternal, err) } if confirmations == evmtypes.Finalized { @@ -184,7 +224,7 @@ func (b *MethodBinding) blockNumberFromConfidence(ctx context.Context, confidenc return nil, nil } - return nil, fmt.Errorf("unknown evm confirmations: %v for contract: %s, method: %s", confirmations, b.contractName, b.method) + return nil, fmt.Errorf("%w: [unknown evm confirmations]: %v; contract: %s; method: %s;", commontypes.ErrInvalidConfig, confirmations, b.contractName, b.method) } func (b *MethodBinding) isBound(binding common.Address) bool {