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

evm implementation of contract reader query keys API #15336

Merged
merged 4 commits into from
Dec 9, 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/scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ require (
github.com/prometheus/client_golang v1.20.5
github.com/shopspring/decimal v1.4.0
github.com/smartcontractkit/chainlink-automation v0.8.1
github.com/smartcontractkit/chainlink-common v0.3.1-0.20241206011233-b6684ee6508f
github.com/smartcontractkit/chainlink-common v0.3.1-0.20241209151352-70300ddcc776
github.com/smartcontractkit/chainlink/deployment v0.0.0-00010101000000-000000000000
github.com/smartcontractkit/chainlink/v2 v2.14.0-mercury-20240807.0.20241106193309-5560cd76211a
github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12
Expand Down
4 changes: 2 additions & 2 deletions core/scripts/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1142,8 +1142,8 @@ github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgB
github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08=
github.com/smartcontractkit/chainlink-ccip v0.0.0-20241204015713-8956bb614e9e h1:GnM6ZWV6vlk2+n6c6o+v/R1LtXzBGVVx7r37nt/h6Uc=
github.com/smartcontractkit/chainlink-ccip v0.0.0-20241204015713-8956bb614e9e/go.mod h1:80vGBbOfertJig0xFKsRfm+i17FkjdKkk1dAaGE45Os=
github.com/smartcontractkit/chainlink-common v0.3.1-0.20241206011233-b6684ee6508f h1:hH+cAG2zt+WK4I2m572LXAnAJg3wtGEAwzBKR8FiXo8=
github.com/smartcontractkit/chainlink-common v0.3.1-0.20241206011233-b6684ee6508f/go.mod h1:bQktEJf7sJ0U3SmIcXvbGUox7SmXcnSEZ4kUbT8R5Nk=
github.com/smartcontractkit/chainlink-common v0.3.1-0.20241209151352-70300ddcc776 h1:NATQA1LfrEPXCdtEed9/G4SxaVuF8EZp5O2ucOK5C98=
github.com/smartcontractkit/chainlink-common v0.3.1-0.20241209151352-70300ddcc776/go.mod h1:bQktEJf7sJ0U3SmIcXvbGUox7SmXcnSEZ4kUbT8R5Nk=
github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20241202195413-82468150ac1e h1:PRoeby6ZlTuTkv2f+7tVU4+zboTfRzI+beECynF4JQ0=
github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20241202195413-82468150ac1e/go.mod h1:mUh5/woemsVaHgTorA080hrYmO3syBCmPdnWc/5dOqk=
github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20241202141438-a90db35252db h1:N1RH1hSr2ACzOFc9hkCcjE8pRBTdcU3p8nsTJByaLes=
Expand Down
36 changes: 36 additions & 0 deletions core/services/relay/evm/chain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"iter"
"maps"
"slices"
"strings"
Expand Down Expand Up @@ -298,6 +299,41 @@ func (cr *chainReader) QueryKey(
return sequenceOfValues, nil
}

func (cr *chainReader) QueryKeys(ctx context.Context, filters []commontypes.ContractKeyFilter,
limitAndSort query.LimitAndSort) (iter.Seq2[string, commontypes.Sequence], error) {
eventQueries := make([]read.EventQuery, 0, len(filters))
for _, filter := range filters {
binding, address, err := cr.bindings.GetReader(filter.Contract.ReadIdentifier(filter.KeyFilter.Key))
if err != nil {
return nil, err
}

sequenceDataType := filter.SequenceDataType
_, isValuePtr := filter.SequenceDataType.(*values.Value)
if isValuePtr {
sequenceDataType, err = cr.CreateContractType(filter.Contract.ReadIdentifier(filter.Key), false)
if err != nil {
return nil, err
}
}

eventBinding, ok := binding.(*read.EventBinding)
if !ok {
return nil, fmt.Errorf("query key %s is not an event", filter.KeyFilter.Key)
}

eventQueries = append(eventQueries, read.EventQuery{
Filter: filter.KeyFilter,
SequenceDataType: sequenceDataType,
IsValuePtr: isValuePtr,
EventBinding: eventBinding,
Address: common.HexToAddress(address),
})
}

return read.MultiEventTypeQuery(ctx, cr.lp, eventQueries, limitAndSort)
}

func (cr *chainReader) CreateContractType(readIdentifier string, forEncoding bool) (any, error) {
return cr.codec.CreateType(cr.bindings.ReadTypeIdentifier(readIdentifier, forEncoding), forEncoding)
}
Expand Down
7 changes: 7 additions & 0 deletions core/services/relay/evm/evmtesting/bindings_test_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func WrapContractReaderTesterWithBindings(t *testing.T, wrapped *EVMChainCompone
interfacetests.ContractReaderBatchGetLatestValueSetsErrorsProperly, interfacetests.ContractReaderBatchGetLatestValueNoArgumentsWithSliceReturn, interfacetests.ContractReaderBatchGetLatestValueWithModifiersOwnMapstructureOverride,
interfacetests.ContractReaderQueryKeyNotFound, interfacetests.ContractReaderQueryKeyReturnsData, interfacetests.ContractReaderQueryKeyReturnsDataAsValuesDotValue, interfacetests.ContractReaderQueryKeyReturnsDataAsValuesDotValue,
interfacetests.ContractReaderQueryKeyCanFilterWithValueComparator, interfacetests.ContractReaderQueryKeyCanLimitResultsWithCursor,
interfacetests.ContractReaderQueryKeysNotFound, interfacetests.ContractReaderQueryKeysReturnsData, interfacetests.ContractReaderQueryKeysReturnsDataTwoEventTypes, interfacetests.ContractReaderQueryKeysReturnsDataAsValuesDotValue,
interfacetests.ContractReaderQueryKeysCanFilterWithValueComparator, interfacetests.ContractReaderQueryKeysCanLimitResultsWithCursor,
ContractReaderQueryKeyFilterOnDataWordsWithValueComparator, ContractReaderQueryKeyOnDataWordsWithValueComparatorOnNestedField,
ContractReaderQueryKeyFilterOnDataWordsWithValueComparatorOnDynamicField, ContractReaderQueryKeyFilteringOnDataWordsUsingValueComparatorsOnFieldsWithManualIndex,
// TODO BCFR-1073 - Fix flaky tests
Expand Down Expand Up @@ -71,6 +73,7 @@ func newBindingsMapping() bindingsMapping {
interfacetests.MethodSettingStruct: "AddTestStruct",
interfacetests.MethodSettingUint64: "SetAlterablePrimitiveValue",
interfacetests.MethodTriggeringEvent: "TriggerEvent",
interfacetests.MethodTriggeringEventWithDynamicTopic: "TriggerEventWithDynamicTopic",
}
methodNameMappingByContract[interfacetests.AnySecondContractName] = map[string]string{
interfacetests.MethodReturningUint64: "GetDifferentPrimitiveValue",
Expand Down Expand Up @@ -249,6 +252,10 @@ func (b bindingChainWriterProxy) SubmitTransaction(ctx context.Context, contract
bindingsInput := bindings.TriggerEventInput{}
_ = convertStruct(args, &bindingsInput)
return chainReaderTesters.TriggerEvent(ctx, bindingsInput, transactionID, toAddress, meta)
case interfacetests.MethodTriggeringEventWithDynamicTopic:
bindingsInput := bindings.TriggerEventWithDynamicTopicInput{}
_ = convertStruct(args, &bindingsInput)
return chainReaderTesters.TriggerEventWithDynamicTopic(ctx, bindingsInput, transactionID, toAddress, meta)
default:
return errors.New("No logic implemented for method: " + method)
}
Expand Down
52 changes: 52 additions & 0 deletions core/services/relay/evm/read/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,58 @@ func (e Error) Unwrap() error {
return e.Err
}

type MultiCallError struct {
Err error
Type readType
Detail *callsReadDetail
Result *string
}

type callsReadDetail struct {
Calls []Call
Block string
}

func newErrorFromCalls(err error, calls []Call, block string, tp readType) MultiCallError {
return MultiCallError{
Err: err,
Type: tp,
Detail: &callsReadDetail{
Calls: calls,
Block: block,
},
}
}

func (e MultiCallError) Error() string {
var builder strings.Builder
ettec marked this conversation as resolved.
Show resolved Hide resolved

builder.WriteString("[read error]")
builder.WriteString(fmt.Sprintf(" err: %s;", e.Err.Error()))
builder.WriteString(fmt.Sprintf(" type: %s;", e.Type))

if e.Detail != nil {
builder.WriteString(fmt.Sprintf(" block: %s;", e.Detail.Block))
for _, call := range e.Detail.Calls {
builder.WriteString(fmt.Sprintf(" address: %s;", call.ContractAddress.Hex()))
builder.WriteString(fmt.Sprintf(" contract-name: %s;", call.ContractName))
builder.WriteString(fmt.Sprintf(" read-name: %s;", call.ReadName))
builder.WriteString(fmt.Sprintf(" params: %+v;", call.Params))
builder.WriteString(fmt.Sprintf(" expected return type: %s;", reflect.TypeOf(call.ReturnVal)))
}

if e.Result != nil {
builder.WriteString(fmt.Sprintf("encoded result: %s;", *e.Result))
}
}

return builder.String()
}

func (e MultiCallError) Unwrap() error {
return e.Err
}

type ConfigError struct {
Msg string
}
Expand Down
216 changes: 216 additions & 0 deletions core/services/relay/evm/read/multieventtype.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package read

import (
"context"
"errors"
"fmt"
"iter"
"reflect"
"sort"
"strconv"
"strings"

"github.com/ethereum/go-ethereum/common"

commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
"github.com/smartcontractkit/chainlink-common/pkg/types/query"
"github.com/smartcontractkit/chainlink-common/pkg/values"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller"
)

type EventQuery struct {
Filter query.KeyFilter
EventBinding *EventBinding
SequenceDataType any
IsValuePtr bool
Address common.Address
}

func MultiEventTypeQuery(ctx context.Context, lp logpoller.LogPoller, eventQueries []EventQuery, limitAndSort query.LimitAndSort) (iter.Seq2[string, commontypes.Sequence], error) {
seqIter, err := multiEventTypeQueryWithoutErrorWrapping(ctx, lp, eventQueries, limitAndSort)
if err != nil {
if len(eventQueries) > 0 {
var calls []Call
for _, eq := range eventQueries {
calls = append(calls, Call{
ContractAddress: eq.Address,
ContractName: eq.EventBinding.contractName,
ReadName: eq.EventBinding.eventName,
ReturnVal: eq.SequenceDataType,
})
}

err = newErrorFromCalls(err, calls, "", eventReadType)
} else {
err = fmt.Errorf("no event queries provided: %w", err)
}
}

return seqIter, err
}

func multiEventTypeQueryWithoutErrorWrapping(ctx context.Context, lp logpoller.LogPoller, eventQueries []EventQuery, limitAndSort query.LimitAndSort) (iter.Seq2[string, commontypes.Sequence], error) {
if err := validateEventQueries(eventQueries); err != nil {
return nil, fmt.Errorf("error validating event queries: %w", err)
}

for _, eq := range eventQueries {
if err := eq.EventBinding.validateBound(eq.Address); err != nil {
return nil, err
}
}

allFilterExpressions := make([]query.Expression, 0, len(eventQueries))
for _, eq := range eventQueries {
var expressions []query.Expression

defaultExpressions := []query.Expression{
logpoller.NewAddressFilter(eq.Address),
logpoller.NewEventSigFilter(eq.EventBinding.hash),
}
expressions = append(expressions, defaultExpressions...)

remapped, remapErr := eq.EventBinding.remap(eq.Filter)
if remapErr != nil {
return nil, fmt.Errorf("error remapping filter: %w", remapErr)
}
expressions = append(expressions, remapped.Expressions...)

filterExpression := query.And(expressions...)

allFilterExpressions = append(allFilterExpressions, filterExpression)
}

eventQuery := query.Or(allFilterExpressions...)

queryName := createQueryName(eventQueries)

logs, err := lp.FilteredLogs(ctx, []query.Expression{eventQuery}, limitAndSort, queryName)
if err != nil {
return nil, wrapInternalErr(err)
}

seqIter, err := decodeMultiEventTypeLogsIntoSequences(ctx, logs, eventQueries)
if err != nil {
return nil, wrapInternalErr(err)
}

return seqIter, nil
}

func createQueryName(eventQueries []EventQuery) string {
queryName := ""
contractToEvents := map[string][]string{}
for _, eq := range eventQueries {
contractName := eq.EventBinding.contractName + "-" + eq.Address.String()

if _, exists := contractToEvents[contractName]; !exists {
contractToEvents[contractName] = []string{}
}
contractToEvents[contractName] = append(contractToEvents[contractName], eq.EventBinding.eventName)
}

contractNames := make([]string, 0, len(contractToEvents))
for contractName := range contractToEvents {
contractNames = append(contractNames, contractName)
}

sort.Strings(contractNames)

for _, contractName := range contractNames {
queryName += contractName + "-"
for _, event := range contractToEvents[contractName] {
queryName += event + "-"
}
}

queryName = strings.TrimSuffix(queryName, "-")
return queryName
}

func validateEventQueries(eventQueries []EventQuery) error {
duplicateCheck := map[common.Hash]EventQuery{}
for _, eq := range eventQueries {
if eq.EventBinding == nil {
return errors.New("event binding is nil")
}

if eq.SequenceDataType == nil {
return errors.New("sequence data type is nil")
}

// TODO support queries with the same event signature but different filters
if _, exists := duplicateCheck[eq.EventBinding.hash]; exists {
ilija42 marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("duplicate event query for event signature %s, event name %s", eq.EventBinding.hash, eq.EventBinding.eventName)
}
duplicateCheck[eq.EventBinding.hash] = eq
}
return nil
}

func decodeMultiEventTypeLogsIntoSequences(ctx context.Context, logs []logpoller.Log, eventQueries []EventQuery) (iter.Seq2[string, commontypes.Sequence], error) {
type sequenceWithKey struct {
Key string
Sequence commontypes.Sequence
}
sequenceWithKeys := make([]sequenceWithKey, 0, len(logs))
eventSigToEventQuery := map[common.Hash]EventQuery{}
ilija42 marked this conversation as resolved.
Show resolved Hide resolved
for _, eq := range eventQueries {
eventSigToEventQuery[eq.EventBinding.hash] = eq
}

for _, logEntry := range logs {
eventSignatureHash := logEntry.EventSig

eq, exists := eventSigToEventQuery[eventSignatureHash]
if !exists {
return nil, fmt.Errorf("no event query found for log with event signature %s", eventSignatureHash)
}

seqWithKey := sequenceWithKey{
Key: eq.Filter.Key,
Sequence: commontypes.Sequence{
Cursor: logpoller.FormatContractReaderCursor(logEntry),
Head: commontypes.Head{
Height: strconv.FormatInt(logEntry.BlockNumber, 10),
Hash: logEntry.BlockHash.Bytes(),
Timestamp: uint64(logEntry.BlockTimestamp.Unix()), //nolint:gosec // G115 false positive
},
},
}

var typeVal reflect.Value

typeInto := reflect.TypeOf(eq.SequenceDataType)
if typeInto.Kind() == reflect.Pointer {
typeVal = reflect.New(typeInto.Elem())
} else {
typeVal = reflect.Indirect(reflect.New(typeInto))
}
ettec marked this conversation as resolved.
Show resolved Hide resolved

// create a new value of the same type as 'into' for the data to be extracted to
seqWithKey.Sequence.Data = typeVal.Interface()

if err := eq.EventBinding.decodeLog(ctx, &logEntry, seqWithKey.Sequence.Data); err != nil {
return nil, err
}

if eq.IsValuePtr {
wrappedValue, err := values.Wrap(seqWithKey.Sequence.Data)
if err != nil {
return nil, err
}
seqWithKey.Sequence.Data = &wrappedValue
}

sequenceWithKeys = append(sequenceWithKeys, seqWithKey)
}

return func(yield func(string, commontypes.Sequence) bool) {
for _, s := range sequenceWithKeys {
if !yield(s.Key, s.Sequence) {
return
}
}
}, nil
}
Loading
Loading