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

Chain reader for EVM #11261

Closed
wants to merge 1 commit into from
Closed
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/chains/evm/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TestChainOpts_Validate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := evm.ChainOpts{
o := Opts{
AppConfig: tt.fields.AppConfig,
EventBroadcaster: tt.fields.EventBroadcaster,
MailMon: tt.fields.MailMon,
Expand Down
1 change: 0 additions & 1 deletion core/chains/evm/logpoller/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ func SetupTH(t testing.TB, useFinalityTag bool, finalityDepth, backfillBatchSize
chainID := testutils.NewRandomEVMChainID()
chainID2 := testutils.NewRandomEVMChainID()
db := pgtest.NewSqlxDB(t)

o := logpoller.NewORM(chainID, db, lggr, pgtest.NewQConfig(true))
o2 := logpoller.NewORM(chainID2, db, lggr, pgtest.NewQConfig(true))
owner := testutils.MustNewSimTransactor(t)
Expand Down
2 changes: 2 additions & 0 deletions core/scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,5 @@ replace (
github.com/mwitkow/grpc-proxy => github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f

)

replace github.com/smartcontractkit/chainlink-relay => ../../../chainlink-relay
2 changes: 0 additions & 2 deletions core/scripts/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1465,8 +1465,6 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv
github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M=
github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 h1:4iO3Ei1b/Lb0yprzclk93e1aQnYF92sIe+EJzMG87y4=
github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353/go.mod h1:hMhGr9ok3p4442keFtK6u6Ei9yWfG66fmDwsFi3aHcw=
github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264 h1:64bH7MmWzcy5tB16x40266DzgKr2iIVcDPjOro6Q3Us=
github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ=
github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 h1:DaPSVnxe7oz1QJ+AVIhQWs1W3ubQvwvGo9NbHpMs1OQ=
github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05/go.mod h1:o0Pn1pbaUluboaK6/yhf8xf7TiFCkyFl6WUOdwqamuU=
github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb h1:HiluOfEVGOQTM6BTDImOqYdMZZ7qq7fkZ3TJdmItNr8=
Expand Down
228 changes: 139 additions & 89 deletions core/services/relay/evm/chain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,34 @@ package evm

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"

"github.com/ethereum/go-ethereum/common"
"github.com/pkg/errors"
"github.com/smartcontractkit/chainlink-relay/pkg/codec"
relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types"

evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller"
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services"
"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"
)

// constructor for ChainReader, returns nil if there is any error
func newChainReader(lggr logger.Logger, chain evm.Chain, ropts *types.RelayOpts) (*chainReader, error) {
type ClosableChainReader interface {
relaytypes.ChainReader
Start(ctx context.Context) error
Close() error
}

// NewChainReader is a constructor for ChainReader, returns nil if there is any error
func NewChainReader(lggr logger.Logger, chain evm.Chain, ropts *types.RelayOpts) (ClosableChainReader, error) {
relayConfig, err := ropts.RelayConfig()
if err != nil {
return nil, fmt.Errorf("Failed parsing RelayConfig: %w", err)
Expand All @@ -28,122 +39,130 @@ func newChainReader(lggr logger.Logger, chain evm.Chain, ropts *types.RelayOpts)
return nil, relaytypes.ErrorChainReaderUnsupported{}
}

if err = ValidateChainReaderConfig(*relayConfig.ChainReader); err != nil {
return nil, fmt.Errorf("invalid ChainReader configuration: %w", err)
crConfig := *relayConfig.ChainReader

parsed := &parsedTypes{
encoderDefs: map[string]*CodecEntry{},
decoderDefs: map[string]*CodecEntry{},
}
for k, v := range crConfig.ChainCodecConfigs {
args := abi.Arguments{}
if err = json.Unmarshal(([]byte)(v.TypeAbi), &args); err != nil {
return nil, err
}

item := &CodecEntry{Args: args}
if err = item.Init(); err != nil {
return nil, err
}

parsed.encoderDefs[k] = item
parsed.decoderDefs[k] = item
}

if err = addTypes(crConfig.ChainContractReaders, parsed); err != nil {
return nil, err
}

enc := &encoder{Definitions: parsed.encoderDefs}
dec, err := codec.DecoderFromMapDecoder(&mapDecoder{Definitions: parsed.decoderDefs})
if err != nil {
return nil, err
}

return NewChainReaderService(lggr, chain.LogPoller())
return &chainReader{
lggr: lggr.Named("ChainReader"),
lp: chain.LogPoller(),
Encoder: enc,
Decoder: dec,
client: chain.Client(),
types: parsed,
}, nil
}

func ValidateChainReaderConfig(cfg types.ChainReaderConfig) error {
for contractName, chainContractReader := range cfg.ChainContractReaders {
abi, err := abi.JSON(strings.NewReader(chainContractReader.ContractABI))
func addTypes(chainContractReaders map[string]types.ChainContractReader, parsed *parsedTypes) error {
for contractName, chainContractReader := range chainContractReaders {
contractAbi, err := abi.JSON(strings.NewReader(chainContractReader.ContractABI))
if err != nil {
return err
}

for chainReadingDefinitionName, chainReaderDefinition := range chainContractReader.ChainReaderDefinitions {
switch chainReaderDefinition.ReadType {
case types.Method:
err = validateMethods(abi, chainReaderDefinition)
err = addMethods(chainReadingDefinitionName, contractAbi, chainReaderDefinition, parsed)
case types.Event:
err = validateEvents(abi, chainReaderDefinition)
err = addEventTypes(chainReadingDefinitionName, contractAbi, chainReaderDefinition, parsed)
default:
return fmt.Errorf("invalid chain reader definition read type: %d", chainReaderDefinition.ReadType)
}
if err != nil {
return fmt.Errorf("invalid chain reader config for contract: %q chain reading definition: %q, err: %w", contractName, chainReadingDefinitionName, err)
return errors.Wrap(err, fmt.Sprintf("invalid chain reader config for contract: %q chain reading definition: %q", contractName, chainReadingDefinitionName))
}
}
}

return nil
}

func validateEvents(contractABI abi.ABI, chainReaderDefinition types.ChainReaderDefinition) error {
func addEventTypes(name string, contractABI abi.ABI, chainReaderDefinition types.ChainReaderDefinition, parsed *parsedTypes) error {
event, methodExists := contractABI.Events[chainReaderDefinition.ChainSpecificName]
if !methodExists {
return fmt.Errorf("method: %s doesn't exist", chainReaderDefinition.ChainSpecificName)
}

if !areChainReaderArgumentsValid(event.Inputs, chainReaderDefinition.ReturnValues) {
var abiEventInputsNames []string
for _, input := range event.Inputs {
abiEventInputsNames = append(abiEventInputsNames, input.Name)
}
return fmt.Errorf("return values: [%s] don't match abi event inputs: [%s]", strings.Join(chainReaderDefinition.ReturnValues, ","), strings.Join(abiEventInputsNames, ","))
}

var abiEventIndexedInputs []abi.Argument
for _, eventInput := range event.Inputs {
if eventInput.Indexed {
abiEventIndexedInputs = append(abiEventIndexedInputs, eventInput)
}
}

var chainReaderEventParams []string
for chainReaderEventParam := range chainReaderDefinition.Params {
chainReaderEventParams = append(chainReaderEventParams, chainReaderEventParam)
if err := addOverrides(chainReaderDefinition, event.Inputs); err != nil {
return err
}

if !areChainReaderArgumentsValid(abiEventIndexedInputs, chainReaderEventParams) {
var abiEventIndexedInputsNames []string
for _, abiEventIndexedInput := range abiEventIndexedInputs {
abiEventIndexedInputsNames = append(abiEventIndexedInputsNames, abiEventIndexedInput.Name)
}
return fmt.Errorf("params: [%s] don't match abi event indexed inputs: [%s]", strings.Join(chainReaderEventParams, ","), strings.Join(abiEventIndexedInputsNames, ","))
}
return nil
return addDecoderDef(name, event.Inputs, parsed)
}

func validateMethods(abi abi.ABI, chainReaderDefinition types.ChainReaderDefinition) error {
func addMethods(name string, abi abi.ABI, chainReaderDefinition types.ChainReaderDefinition, parsed *parsedTypes) error {
method, methodExists := abi.Methods[chainReaderDefinition.ChainSpecificName]
if !methodExists {
return fmt.Errorf("method: %q doesn't exist", chainReaderDefinition.ChainSpecificName)
}

var methodNames []string
for methodName := range chainReaderDefinition.Params {
methodNames = append(methodNames, methodName)
if err := addOverrides(chainReaderDefinition, method.Inputs); err != nil {
return err
}

if !areChainReaderArgumentsValid(method.Inputs, methodNames) {
var abiMethodInputs []string
for _, input := range method.Inputs {
abiMethodInputs = append(abiMethodInputs, input.Name)
}
return fmt.Errorf("params: [%s] don't match abi method inputs: [%s]", strings.Join(methodNames, ","), strings.Join(abiMethodInputs, ","))
// ABI.Pack prepends the method.ID to the encodings, we'll need the encoder to do the same.
input := &CodecEntry{Args: method.Inputs, encodingPrefix: method.ID}
if err := input.Init(); err != nil {
return err
}

if !areChainReaderArgumentsValid(method.Outputs, chainReaderDefinition.ReturnValues) {
var abiMethodOutputs []string
for _, input := range method.Outputs {
abiMethodOutputs = append(abiMethodOutputs, input.Name)
}
return fmt.Errorf("return values: [%s] don't match abi method outputs: [%s]", strings.Join(chainReaderDefinition.ReturnValues, ","), strings.Join(abiMethodOutputs, ","))
}
parsed.encoderDefs[name] = input
return addDecoderDef(name, method.Outputs, parsed)
}

func addDecoderDef(name string, outputs abi.Arguments, parsed *parsedTypes) error {
output := &CodecEntry{Args: outputs}
parsed.decoderDefs[name] = output
return output.Init()
}

func (cr *chainReader) initialize() error {
// Initialize chain reader, start cache polling loop, etc.
return nil
}

func areChainReaderArgumentsValid(contractArgs []abi.Argument, chainReaderArgs []string) bool {
for _, chArgName := range chainReaderArgs {
found := false
for _, contractArg := range contractArgs {
if chArgName == contractArg.Name {
found = true
break
func addOverrides(chainReaderDefinition types.ChainReaderDefinition, inputs abi.Arguments) error {
// TODO add transforms to add params artificially
paramsLoop:
for argName, param := range chainReaderDefinition.Params {
// TODO add type check too
_ = param
for _, input := range inputs {
if argName == input.Name {
continue paramsLoop
}
}
if !found {
return false
}
return fmt.Errorf("cannot find parameter %v in %v", argName, chainReaderDefinition.ChainSpecificName)
}
return true
}

func (cr *chainReader) initialize() error {
// Initialize chain reader, start cache polling loop, etc.
return nil
}

Expand All @@ -155,31 +174,34 @@ type ChainReaderService interface {
type chainReader struct {
lggr logger.Logger
lp logpoller.LogPoller
relaytypes.Encoder
relaytypes.Decoder
types *parsedTypes
client evmclient.Client
}

// chainReader constructor
func NewChainReaderService(lggr logger.Logger, lp logpoller.LogPoller) (*chainReader, error) {
return &chainReader{lggr.Named("ChainReader"), lp}, nil
}
var _ relaytypes.RemoteCodec = &chainReader{}

func (cr *chainReader) Encode(ctx context.Context, item any, itemType string) (ocrtypes.Report, error) {
return nil, fmt.Errorf("Unimplemented method Encode called %w", relaytypes.ErrorChainReaderUnsupported{})
}
func (cr *chainReader) GetLatestValue(ctx context.Context, bc relaytypes.BoundContract, method string, params any, returnVal any) error {
data, err := cr.Encode(ctx, params, method)
if err != nil {
return err
}

func (cr *chainReader) Decode(_ context.Context, raw []byte, into any, itemType string) error {
return fmt.Errorf("Unimplemented method Decode called %w", relaytypes.ErrorChainReaderUnsupported{})
}
address := common.HexToAddress(bc.Address)
callMsg := ethereum.CallMsg{
To: &address,
From: address,
Data: data,
}

func (cr *chainReader) GetMaxEncodingSize(ctx context.Context, n int, itemType string) (int, error) {
return 0, fmt.Errorf("Unimplemented method GetMaxDecodingSize called %w", relaytypes.ErrorChainReaderUnsupported{})
}
output, err := cr.client.CallContract(ctx, callMsg, nil)

func (cr *chainReader) GetMaxDecodingSize(ctx context.Context, n int, itemType string) (int, error) {
return 0, fmt.Errorf("Unimplemented method GetMaxDecodingSize called %w", relaytypes.ErrorChainReaderUnsupported{})
}
if err != nil {
return err
}

func (cr *chainReader) GetLatestValue(ctx context.Context, bc relaytypes.BoundContract, method string, params any, returnVal any) error {
return fmt.Errorf("Unimplemented method GetLatestValue called %w", relaytypes.ErrorChainReaderUnsupported{})
return cr.Decode(ctx, output, returnVal, method)
}

func (cr *chainReader) Start(ctx context.Context) error {
Expand All @@ -195,3 +217,31 @@ func (cr *chainReader) HealthReport() map[string]error {
return map[string]error{cr.Name(): nil}
}
func (cr *chainReader) Name() string { return cr.lggr.Name() }

func (cr *chainReader) CreateType(itemType string, forceSlice, forEncoding bool) (any, error) {
var itemTypes map[string]*CodecEntry
if forEncoding {
itemTypes = cr.types.encoderDefs
} else {
itemTypes = cr.types.decoderDefs
}

def, ok := itemTypes[itemType]
if !ok {
return nil, relaytypes.InvalidTypeError{}
}

if forceSlice {
if def.checkedArrayType == nil {
return nil, relaytypes.InvalidTypeError{}
}
return def.checkedArrayType, nil
}

return def.checkedType, nil
}

type parsedTypes struct {
encoderDefs map[string]*CodecEntry
decoderDefs map[string]*CodecEntry
}
Loading
Loading