From beb82ec059bfd3be954f3b27894df524966e9c87 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Mon, 16 Dec 2024 11:59:22 -0500 Subject: [PATCH] Contract Reader Handle Empty Bytes (#15688) In some cases, a contract call will return an empty result `0x`, which decodes to an empty set of bytes. The codec can't decode this empty set except in specific cases where a slice of bytes is the expected result. This commit adds a short-circuit to decoding an empty result where the return value is unaltered. --- core/services/relay/evm/chain_reader.go | 6 ++- core/services/relay/evm/read/batch.go | 54 +++++++++++++------------ core/services/relay/evm/read/method.go | 9 +++++ 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/core/services/relay/evm/chain_reader.go b/core/services/relay/evm/chain_reader.go index d86c5cd635a..ffe9cd19aea 100644 --- a/core/services/relay/evm/chain_reader.go +++ b/core/services/relay/evm/chain_reader.go @@ -206,7 +206,11 @@ func (cr *chainReader) GetLatestValue(ctx context.Context, readName string, conf ptrToValue, isValue := returnVal.(*values.Value) if !isValue { _, err = binding.GetLatestValueWithHeadData(ctx, common.HexToAddress(address), confidenceLevel, params, returnVal) - return err + if err != nil { + return err + } + + return nil } contractType, err := cr.CreateContractType(readName, false) diff --git a/core/services/relay/evm/read/batch.go b/core/services/relay/evm/read/batch.go index 16333149f11..ce1c546ad73 100644 --- a/core/services/relay/evm/read/batch.go +++ b/core/services/relay/evm/read/batch.go @@ -241,32 +241,36 @@ func (c *defaultEvmBatchCaller) unpackBatchResults( return nil, callErr } - 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, batchReadType, - ) - - callErr.Result = &hexEncodedOutputs[idx] - - results[idx].err = callErr - } else { - callErr := newErrorFromCall( - fmt.Errorf("%w: codec decode result: %s", types.ErrInvalidType, err.Error()), - call, block, batchReadType, - ) - - callErr.Result = &hexEncodedOutputs[idx] - results[idx].err = callErr - } + // the codec can't do anything with no bytes, so skip decoding and allow + // the result to be the empty struct or value + if len(packedBytes) > 0 { + 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, batchReadType, + ) + + callErr.Result = &hexEncodedOutputs[idx] + + results[idx].err = callErr + } else { + callErr := newErrorFromCall( + fmt.Errorf("%w: codec decode result: %s", types.ErrInvalidType, err.Error()), + call, block, batchReadType, + ) + + callErr.Result = &hexEncodedOutputs[idx] + results[idx].err = callErr + } - continue + continue + } } results[idx].returnVal = call.ReturnVal diff --git a/core/services/relay/evm/read/method.go b/core/services/relay/evm/read/method.go index e988e4352f7..ed44e1aa9ca 100644 --- a/core/services/relay/evm/read/method.go +++ b/core/services/relay/evm/read/method.go @@ -2,6 +2,7 @@ package read import ( "context" + "errors" "fmt" "math/big" "sync" @@ -22,6 +23,8 @@ import ( evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" ) +var ErrEmptyContractReturnValue = errors.New("the contract return value was empty") + type MethodBinding struct { // read-only properties contractName string @@ -173,6 +176,12 @@ func (b *MethodBinding) GetLatestValueWithHeadData(ctx context.Context, addr com return nil, callErr } + // there may be cases where the contract value has not been set and the RPC returns with a value of 0x + // which is a set of empty bytes. there is no need for the codec to run in this case. + if len(bytes) == 0 { + return block.ToChainAgnosticHead(), nil + } + 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()),