Skip to content

Commit

Permalink
evm implementation of contract reader query keys API (#15336)
Browse files Browse the repository at this point in the history
* evm implementation of query keys api

* lint

* test fix

* review
  • Loading branch information
ettec authored Dec 9, 2024
1 parent b6bf8f2 commit 7b7815e
Show file tree
Hide file tree
Showing 15 changed files with 445 additions and 15 deletions.
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

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 {
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{}
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))
}

// 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

0 comments on commit 7b7815e

Please sign in to comment.