Skip to content

Commit

Permalink
[BCFR-912] Contract Reader Data Word querying index override (#14622)
Browse files Browse the repository at this point in the history
* Implement Chain Reader data word index override from config

* Add contract and half-baked test case for CR DW override querying

* Fix Contract Reader test case for CR DW override querying

* lint and changesets

* Add unit test cases for Contract Reader data word init

* lint

* lint
  • Loading branch information
ilija42 authored Oct 8, 2024
1 parent 2931822 commit c654322
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-swans-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

#internal Add support for data word detail manual input in Contract Reader for searching through EVM log event data with Contract Reader QueryKey ValueComparators.
5 changes: 5 additions & 0 deletions contracts/.changeset/funny-meals-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/contracts': minor
---

#internal Modify Contract Reader tester helper BCFR-912
18 changes: 18 additions & 0 deletions contracts/src/v0.8/shared/test/helpers/ChainReaderTester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ contract ChainReaderTester {
// first topic is event hash, second and third topics get hashed before getting stored
event TriggeredWithFourTopicsWithHashed(string indexed field1, uint8[32] indexed field2, bytes32 indexed field3);

// emits dynamic bytes which encode data in the same way every time.
event StaticBytes(bytes message);

TestStruct[] private s_seen;
uint64[] private s_arr;
uint64 private s_value;
Expand Down Expand Up @@ -181,4 +184,19 @@ contract ChainReaderTester {
function triggerWithFourTopicsWithHashed(string memory field1, uint8[32] memory field2, bytes32 field3) public {
emit TriggeredWithFourTopicsWithHashed(field1, field2, field3);
}

// emulate CCTP message event.
function triggerStaticBytes(
uint32 val1,
uint32 val2,
uint32 val3,
uint64 val4,
bytes32 val5,
bytes32 val6,
bytes32 val7,
bytes memory raw
) public {
bytes memory _message = abi.encodePacked(val1, val2, val3, val4, val5, val6, val7, raw);
emit StaticBytes(_message);
}
}
147 changes: 145 additions & 2 deletions core/gethwrappers/generated/chain_reader_tester/chain_reader_tester.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ batch_vrf_coordinator_v2: ../../contracts/solc/v0.8.6/BatchVRFCoordinatorV2/Batc
batch_vrf_coordinator_v2plus: ../../contracts/solc/v0.8.19/BatchVRFCoordinatorV2Plus/BatchVRFCoordinatorV2Plus.abi ../../contracts/solc/v0.8.19/BatchVRFCoordinatorV2Plus/BatchVRFCoordinatorV2Plus.bin f13715b38b5b9084b08bffa571fb1c8ef686001535902e1255052f074b31ad4e
blockhash_store: ../../contracts/solc/v0.8.19/BlockhashStore/BlockhashStore.abi ../../contracts/solc/v0.8.19/BlockhashStore/BlockhashStore.bin 31b118f9577240c8834c35f8b5a1440e82a6ca8aea702970de2601824b6ab0e1
chain_module_base: ../../contracts/solc/v0.8.19/ChainModuleBase/ChainModuleBase.abi ../../contracts/solc/v0.8.19/ChainModuleBase/ChainModuleBase.bin 7a82cc28014761090185c2650239ad01a0901181f1b2b899b42ca293bcda3741
chain_reader_tester: ../../contracts/solc/v0.8.19/ChainReaderTester/ChainReaderTester.abi ../../contracts/solc/v0.8.19/ChainReaderTester/ChainReaderTester.bin b9a488fc786f584a617764d8dc1722acdb30defb6b8f638e0ae03442795eaf3e
chain_reader_tester: ../../contracts/solc/v0.8.19/ChainReaderTester/ChainReaderTester.abi ../../contracts/solc/v0.8.19/ChainReaderTester/ChainReaderTester.bin 21fcc5fae2a95ce11590bcfc9ea2bde5ad9158f8dcb4efce7264492f8ad2b0a6
chain_specific_util_helper: ../../contracts/solc/v0.8.6/ChainSpecificUtilHelper/ChainSpecificUtilHelper.abi ../../contracts/solc/v0.8.6/ChainSpecificUtilHelper/ChainSpecificUtilHelper.bin 66eb30b0717fefe05672df5ec863c0b9a5a654623c4757307a2726d8f31e26b1
counter: ../../contracts/solc/v0.8.6/Counter/Counter.abi ../../contracts/solc/v0.8.6/Counter/Counter.bin 6ca06e000e8423573ffa0bdfda749d88236ab3da2a4cbb4a868c706da90488c9
cron_upkeep_factory_wrapper: ../../contracts/solc/v0.8.6/CronUpkeepFactory/CronUpkeepFactory.abi - dacb0f8cdf54ae9d2781c5e720fc314b32ed5e58eddccff512c75d6067292cd7
Expand Down
59 changes: 58 additions & 1 deletion core/services/relay/evm/chain_components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,66 @@ func TestContractReaderEventsInitValidation(t *testing.T) {
},
},
expectedError: fmt.Errorf(
"%w: event %s doesn't exist",
"%w: event %q doesn't exist",
clcommontypes.ErrInvalidConfig, "EventName"),
},
{
name: "Event has a unnecessary data word index override",
chainContractReaders: map[string]types.ChainContractReader{
"ContractWithConflict": {
ContractABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"someDW\",\"type\":\"address\"}],\"name\":\"EventName\",\"type\":\"event\"}]",
ContractPollingFilter: types.ContractPollingFilter{
GenericEventNames: []string{"SomeEvent"},
},
Configs: map[string]*types.ChainReaderDefinition{
"SomeEvent": {
ChainSpecificName: "EventName",
ReadType: types.Event,

EventDefinitions: &types.EventDefinitions{
GenericDataWordDetails: map[string]types.DataWordDetail{
"DW": {
Name: "someDW",
Index: ptr(0),
},
},
},
},
},
},
},
expectedError: fmt.Errorf("failed to init dw querying for event: %q, err: data word: %q at index: %d details, were calculated automatically and shouldn't be manully overridden by cfg",
"SomeEvent", "DW", 0),
},
{
name: "Event has a bad type defined in data word detail override config",
chainContractReaders: map[string]types.ChainContractReader{
"ContractWithConflict": {
ContractABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"string\",\"name\":\"someDW\",\"type\":\"string\"}],\"name\":\"EventName\",\"type\":\"event\"}]",
ContractPollingFilter: types.ContractPollingFilter{
GenericEventNames: []string{"SomeEvent"},
},
Configs: map[string]*types.ChainReaderDefinition{
"SomeEvent": {
ChainSpecificName: "EventName",
ReadType: types.Event,

EventDefinitions: &types.EventDefinitions{
GenericDataWordDetails: map[string]types.DataWordDetail{
"DW": {
Name: "someDW",
Index: ptr(0),
Type: "abcdefg",
},
},
},
},
},
},
},
expectedError: fmt.Errorf("failed to init dw querying for event: %q, err: bad abi type: \"abcdefg\" provided for data word: %q at index: %d in config",
"SomeEvent", "DW", 0),
},
}

for _, tt := range tests {
Expand Down
67 changes: 48 additions & 19 deletions core/services/relay/evm/chain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ func (cr *chainReader) addMethod(
func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chainReaderDefinition types.ChainReaderDefinition) error {
event, eventExists := a.Events[chainReaderDefinition.ChainSpecificName]
if !eventExists {
return fmt.Errorf("%w: event %s doesn't exist", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName)
return fmt.Errorf("%w: event %q doesn't exist", commontypes.ErrInvalidConfig, chainReaderDefinition.ChainSpecificName)
}

indexedAsUnIndexedABITypes, indexedTopicsCodecTypes, eventDWs := getEventTypes(event)
Expand Down Expand Up @@ -318,9 +318,9 @@ func (cr *chainReader) addEvent(contractName, eventName string, a abi.ABI, chain
maps.Copy(codecModifiers, topicsModifiers)

// TODO BCFR-44 no dw modifier for now
dataWordsDetails, dWSCodecTypeInfo, initDWQueryingErr := cr.initDWQuerying(contractName, eventName, eventDWs, eventDefinitions.GenericDataWordNames)
dataWordsDetails, dWSCodecTypeInfo, initDWQueryingErr := cr.initDWQuerying(contractName, eventName, eventDWs, eventDefinitions.GenericDataWordDetails)
if initDWQueryingErr != nil {
return initDWQueryingErr
return fmt.Errorf("failed to init dw querying for event: %q, err: %w", eventName, initDWQueryingErr)
}
maps.Copy(codecTypes, dWSCodecTypeInfo)

Expand Down Expand Up @@ -359,32 +359,61 @@ func (cr *chainReader) initTopicQuerying(contractName, eventName string, eventIn
}

// initDWQuerying registers codec types for evm data words to be used for typing value comparator QueryKey filters.
func (cr *chainReader) initDWQuerying(contractName, eventName string, eventDWs map[string]read.DataWordDetail, dWDefs map[string]string) (map[string]read.DataWordDetail, map[string]types.CodecEntry, error) {
func (cr *chainReader) initDWQuerying(contractName, eventName string, abiDWsDetails map[string]read.DataWordDetail, cfgDWsDetails map[string]types.DataWordDetail) (map[string]read.DataWordDetail, map[string]types.CodecEntry, error) {
dWsDetail, err := cr.constructDWDetails(cfgDWsDetails, abiDWsDetails)
if err != nil {
return nil, nil, err
}

dwsCodecTypeInfo := make(map[string]types.CodecEntry)
dWsDetail := make(map[string]read.DataWordDetail)
for genericName := range cfgDWsDetails {
dwDetail, exists := dWsDetail[genericName]
if !exists {
return nil, nil, fmt.Errorf("failed to find data word: %q, it either doesn't exist or can't be searched for", genericName)
}

dwTypeID := eventName + "." + genericName
if err = cr.addEncoderDef(contractName, dwTypeID, abi.Arguments{abi.Argument{Type: dwDetail.Type}}, nil, nil); err != nil {
return nil, nil, fmt.Errorf("failed to init codec for data word: %q on index: %d, err: %w", genericName, dwDetail.Index, err)
}

dwCodecTypeID := codec.WrapItemType(contractName, dwTypeID, true)
dwsCodecTypeInfo[dwCodecTypeID] = cr.parsed.EncoderDefs[dwCodecTypeID]
}

return dWsDetail, dwsCodecTypeInfo, nil
}

for genericName, onChainName := range dWDefs {
for eventID, dWDetail := range eventDWs {
// constructDWDetails combines data word details from config and abi.
func (cr *chainReader) constructDWDetails(cfgDWsDetails map[string]types.DataWordDetail, abiDWsDetails map[string]read.DataWordDetail) (map[string]read.DataWordDetail, error) {
dWsDetail := make(map[string]read.DataWordDetail)
for genericName, cfgDWDetail := range cfgDWsDetails {
for eventID, dWDetail := range abiDWsDetails {
// Extract field name in this manner to account for nested fields
fieldName := strings.Join(strings.Split(eventID, ".")[1:], ".")
if fieldName == onChainName {
if fieldName == cfgDWDetail.Name {
dWsDetail[genericName] = dWDetail

dwTypeID := eventName + "." + genericName
if err := cr.addEncoderDef(contractName, dwTypeID, abi.Arguments{abi.Argument{Type: dWDetail.Type}}, nil, nil); err != nil {
return nil, nil, fmt.Errorf("%w: failed to init codec for data word %s on index %d querying for event: %q", err, genericName, dWDetail.Index, eventName)
}

dwCodecTypeID := codec.WrapItemType(contractName, dwTypeID, true)
dwsCodecTypeInfo[dwCodecTypeID] = cr.parsed.EncoderDefs[dwCodecTypeID]
break
}
}
if _, ok := dWsDetail[genericName]; !ok {
return nil, nil, fmt.Errorf("failed to find data word: %q for event: %q, it either doesn't exist or can't be searched for", genericName, eventName)
}

// if dw detail isn't set, the index can't be programmatically determined, so we get index and type from cfg
for genericName, cfgDWDetail := range cfgDWsDetails {
dwDetail, exists := dWsDetail[genericName]
if exists && cfgDWDetail.Index != nil {
return nil, fmt.Errorf("data word: %q at index: %d details, were calculated automatically and shouldn't be manully overridden by cfg", genericName, dwDetail.Index)
}

if cfgDWDetail.Index != nil {
abiTyp, err := abi.NewType(cfgDWDetail.Type, "", nil)
if err != nil {
return nil, fmt.Errorf("bad abi type: %q provided for data word: %q at index: %d in config", cfgDWDetail.Type, genericName, *cfgDWDetail.Index)
}
dWsDetail[genericName] = read.DataWordDetail{Argument: abi.Argument{Type: abiTyp}, Index: *cfgDWDetail.Index}
}
}
return dWsDetail, dwsCodecTypeInfo, nil
return dWsDetail, nil
}

// getEventItemTypeAndModifier returns codec entry for expected incoming event item and the modifier.
Expand Down
2 changes: 1 addition & 1 deletion core/services/relay/evm/codec/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (c *evmCodec) CreateType(itemType string, forEncoding bool) (any, error) {

def, ok := itemTypes[itemType]
if !ok {
return nil, fmt.Errorf("%w: cannot find type name %s", commontypes.ErrInvalidType, itemType)
return nil, fmt.Errorf("%w: cannot find type name %q", commontypes.ErrInvalidType, itemType)
}

// we don't need double pointers, and they can also mess up reflection variable creation and mapstruct decode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
clcommontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
. "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with .
"github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives"
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
Expand All @@ -38,6 +39,7 @@ const (
triggerWithDynamicTopic = "TriggeredEventWithDynamicTopic"
triggerWithAllTopics = "TriggeredWithFourTopics"
triggerWithAllTopicsWithHashed = "TriggeredWithFourTopicsWithHashed"
staticBytesEventName = "StaticBytes"
finalityDepth = 4
)

Expand Down Expand Up @@ -120,7 +122,7 @@ func (it *EVMChainComponentsInterfaceTester[T]) Setup(t T) {
AnyContractName: {
ContractABI: chain_reader_tester.ChainReaderTesterMetaData.ABI,
ContractPollingFilter: types.ContractPollingFilter{
GenericEventNames: []string{EventName, EventWithFilterName, triggerWithAllTopicsWithHashed},
GenericEventNames: []string{EventName, EventWithFilterName, triggerWithAllTopicsWithHashed, staticBytesEventName},
},
Configs: map[string]*types.ChainReaderDefinition{
MethodTakingLatestParamsReturningTestStruct: &methodTakingLatestParamsReturningTestStructConfig,
Expand All @@ -138,17 +140,31 @@ func (it *EVMChainComponentsInterfaceTester[T]) Setup(t T) {
ReadType: types.Event,
EventDefinitions: &types.EventDefinitions{
GenericTopicNames: map[string]string{"field": "Field"},
GenericDataWordNames: map[string]string{
"OracleID": "oracleId",
"NestedStaticStruct.Inner.IntVal": "nestedStaticStruct.Inner.IntVal",
"BigField": "bigField",
GenericDataWordDetails: map[string]types.DataWordDetail{
"OracleID": {Name: "oracleId"},
// this is just to illustrate an example, generic names shouldn't really be formatted like this since other chains might not store it in the same way
"NestedStaticStruct.Inner.IntVal": {Name: "nestedStaticStruct.Inner.IntVal"},
"BigField": {Name: "bigField"},
},
},
OutputModifications: codec.ModifiersConfig{
&codec.RenameModifierConfig{Fields: map[string]string{"NestedDynamicStruct.Inner.IntVal": "I"}},
&codec.RenameModifierConfig{Fields: map[string]string{"NestedStaticStruct.Inner.IntVal": "I"}},
},
},
staticBytesEventName: {
ChainSpecificName: staticBytesEventName,
ReadType: types.Event,
EventDefinitions: &types.EventDefinitions{
GenericDataWordDetails: map[string]types.DataWordDetail{
"msgTransmitterEvent": {
Name: "msgTransmitterEvent",
Index: testutils.Ptr(2),
Type: "bytes32",
},
},
},
},
EventWithFilterName: {
ChainSpecificName: "Triggered",
ReadType: types.Event,
Expand Down Expand Up @@ -263,6 +279,12 @@ func (it *EVMChainComponentsInterfaceTester[T]) Setup(t T) {
GasLimit: 2_000_000,
Checker: "simulate",
},
"triggerStaticBytes": {
ChainSpecificName: "triggerStaticBytes",
FromAddress: it.Helper.Accounts(t)[1].From,
GasLimit: 2_000_000,
Checker: "simulate",
},
},
},
AnySecondContractName: {
Expand Down
72 changes: 72 additions & 0 deletions core/services/relay/evm/evmtesting/run_tests.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package evmtesting

import (
"encoding/binary"
"math/big"
"reflect"
"time"
Expand Down Expand Up @@ -221,6 +222,49 @@ func RunContractReaderInLoopTests[T TestingT[T]](t T, it ChainComponentsInterfac
return err == nil && len(sequences) == 1 && reflect.DeepEqual(&ts2, sequences[0].Data)
}, it.MaxWaitTimeForEvents(), time.Millisecond*10)
})

t.Run("Filtering can be done on data words using value comparators on fields that require manual index input", func(t T) {
empty12Bytes := [12]byte{}
val1, val2, val3, val4 := uint32(1), uint32(2), uint32(3), uint64(4)
val5, val6, val7 := [32]byte{}, [32]byte{6}, [32]byte{7}
copy(val5[:], append(empty12Bytes[:], 5))
raw := []byte{9, 8}

var buf []byte
buf = binary.BigEndian.AppendUint32(buf, val1)
buf = binary.BigEndian.AppendUint32(buf, val2)
buf = binary.BigEndian.AppendUint32(buf, val3)
buf = binary.BigEndian.AppendUint64(buf, val4)
dataWordOnChainValueToQuery := buf[:]

resExpected := append(buf, common.LeftPadBytes(val5[:], 32)...)
resExpected = append(resExpected, common.LeftPadBytes(val6[:], 32)...)
resExpected = append(resExpected, common.LeftPadBytes(val7[:], 32)...)
resExpected = append(resExpected, raw...)

type eventResAsStruct struct {
Message *[]uint8
}
wrapExpectedRes := eventResAsStruct{Message: &resExpected}

// emit the one we want to search for and a couple of random ones to confirm that filtering works
triggerStaticBytes(t, it, val1, val2, val3, val4, val5, val6, val7, raw)
triggerStaticBytes(t, it, 1337, 7331, 4747, val4, val5, val6, val7, raw)
triggerStaticBytes(t, it, 7331, 4747, 1337, val4, val5, val6, val7, raw)
triggerStaticBytes(t, it, 4747, 1337, 7331, val4, val5, val6, val7, raw)

assert.Eventually(t, func() bool {
sequences, err := cr.QueryKey(ctx, boundContract, query.KeyFilter{Key: staticBytesEventName, Expressions: []query.Expression{
query.Comparator("msgTransmitterEvent",
primitives.ValueComparator{
Value: dataWordOnChainValueToQuery,
Operator: primitives.Eq,
}),
},
}, query.LimitAndSort{}, eventResAsStruct{})
return err == nil && len(sequences) == 1 && reflect.DeepEqual(wrapExpectedRes, sequences[0].Data)
}, it.MaxWaitTimeForEvents(), time.Millisecond*10)
})
}

func triggerFourTopics[T TestingT[T]](t T, it *EVMChainComponentsInterfaceTester[T], i1, i2, i3 int32) {
Expand All @@ -242,3 +286,31 @@ func triggerFourTopicsWithHashed[T TestingT[T]](t T, it *EVMChainComponentsInter
contracts := it.GetBindings(t)
SubmitTransactionToCW(t, it, "triggerWithFourTopicsWithHashed", DynamicEvent{Field1: i1, Field2: i2, Field3: i3}, contracts[0], types.Unconfirmed)
}

// triggerStaticBytes emits a staticBytes events and returns the expected event bytes.
func triggerStaticBytes[T TestingT[T]](t T, it ChainComponentsInterfaceTester[T], val1, val2, val3 uint32, val4 uint64, val5, val6, val7 [32]byte, raw []byte) {
type StaticBytesEvent struct {
Val1 uint32
Val2 uint32
Val3 uint32
Val4 uint64
Val5 [32]byte
Val6 [32]byte
Val7 [32]byte
Raw []byte
}

contracts := it.GetBindings(t)
SubmitTransactionToCW(t, it, "triggerStaticBytes",
StaticBytesEvent{
Val1: val1,
Val2: val2,
Val3: val3,
Val4: val4,
Val5: val5,
Val6: val6,
Val7: val7,
Raw: raw,
},
contracts[0], types.Unconfirmed)
}
2 changes: 1 addition & 1 deletion core/services/relay/evm/read/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ type TopicDetail struct {
}

// DataWordDetail contains all the information about a single evm Data word.
// For b.g. first evm data word(32bytes) of USDC log event is uint256 var called valub.
// For e.g. first evm data word(32bytes) of USDC log event is uint256 var called value.
type DataWordDetail struct {
Index int
abi.Argument
Expand Down
Loading

0 comments on commit c654322

Please sign in to comment.