Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bcfr 945 improve contract reader logs #14546

Merged
merged 13 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/services/relay/evm/evmtesting/run_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
})
}

Expand Down
162 changes: 125 additions & 37 deletions core/services/relay/evm/read/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -127,104 +127,183 @@ 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{}{
"from": common.Address{},
"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
}

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 {
return results, nil
}

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)
ilija42 marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -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
}

Expand All @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions core/services/relay/evm/read/batch_caller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func TestDefaultEvmBatchCaller_batchCallLimit(t *testing.T) {
var returnVal MethodReturn
calls[j] = read.Call{
ContractName: contractName,
MethodName: methodName,
ReadName: methodName,
Params: &params,
ReturnVal: &returnVal,
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
})
Expand Down
Loading
Loading