Skip to content
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
120 changes: 117 additions & 3 deletions pkg/bindings/bindings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import (

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"

"github.com/smartcontractkit/chainlink-evm/pkg/bindings"
"github.com/smartcontractkit/chainlink-evm/pkg/bindings/mocks"
datastorage "github.com/smartcontractkit/chainlink-evm/pkg/bindings/testdata"
"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
"github.com/smartcontractkit/cre-sdk-go/sdk"

"github.com/smartcontractkit/chainlink-evm/pkg/bindings/mocks"
)

func TestGenerateBindings(t *testing.T) {
Expand Down Expand Up @@ -193,6 +193,8 @@ func TestRegisterUnregisterLogTracking(t *testing.T) {
require.Equal(t, req.Filter.Name, "AccessLogged-"+common.Bytes2Hex(ds.Address))
require.Equal(t, [][]byte{ds.Address}, req.Filter.Addresses)
require.Equal(t, [][]byte{ds.Codec.AccessLoggedLogHash()}, req.Filter.EventSigs)
require.Len(t, req.Filter.Topic2, 1)
require.Equal(t, req.Filter.Topic2[0], common.HexToHash("0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2").Bytes())
}).Return(nil).Once()

client.
Expand All @@ -203,7 +205,14 @@ func TestRegisterUnregisterLogTracking(t *testing.T) {
}).
Return(nil).Once()

ds.RegisterLogTrackingAccessLogged(mocks.NewRuntime(t), &bindings.LogTrackingOptions{})
err = ds.RegisterLogTrackingAccessLogged(mocks.NewRuntime(t), &bindings.LogTrackingOptions[datastorage.AccessLogged]{
Filters: []datastorage.AccessLogged{
{
Caller: common.HexToAddress("0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2"),
},
},
})
require.NoError(t, err)
ds.UnregisterLogTrackingAccessLogged(mocks.NewRuntime(t))
}

Expand Down Expand Up @@ -247,6 +256,111 @@ func TestFilterLogs(t *testing.T) {
require.Equal(t, ds.Address, response.Logs[0].Address)
}

func TestLogTrigger(t *testing.T) {
client := mocks.NewEVMClient(t)
ds, err := datastorage.NewDataStorage(client, nil, &bindings.ContractInitOptions{})
require.NoError(t, err, "Failed to create DataStorage instance")
t.Run("simple event", func(t *testing.T) {
ev := ds.ABI.Events["DataStored"]
events := []datastorage.DataStored{
{
Sender: common.HexToAddress("0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2"),
Key: "testKey",
Value: "testValue",
},
{
Sender: common.HexToAddress("0xBb8483F64d9C6d1EcF9b849Ae677dD3315835cb2"),
Key: "testKey",
Value: "testValue",
},
}

encoded, err := ds.Codec.EncodeDataStoredTopics(ev, events)
require.NoError(t, err, "Encoding DataStored topics should not return an error")

require.Equal(t, ds.Codec.DataStoredLogHash(), encoded[0].Values[0], "First topic value should be AccessLogged log hash")
require.Len(t, encoded[1].Values, 2, "Second topic should have two values")
expected1, err := abi.Arguments{ev.Inputs[0]}.Pack(events[0].Sender)
require.NoError(t, err)
require.Equal(t, expected1, encoded[1].Values[0])
expected2, err := abi.Arguments{ev.Inputs[0]}.Pack(events[1].Sender)
require.NoError(t, err)
require.Equal(t, expected2, encoded[1].Values[1])

trigger, err := ds.LogTriggerDataStoredLog(evm.ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED, events)
require.NotNil(t, trigger)
require.NoError(t, err)
})
t.Run("dynamic event", func(t *testing.T) {
ev := ds.ABI.Events["DynamicEvent"]
events := []datastorage.DynamicEvent{
{
Key: "testKey1",
UserData: datastorage.DataStorageUserData{
Key: "userKey1",
Value: "userValue1",
},
Sender: "testSender1",
Metadata: common.HexToHash("metadata1"),
MetadataArray: [][]byte{
[]byte("meta1"),
[]byte("meta2"),
},
},
{
Key: "testKey2",
UserData: datastorage.DataStorageUserData{
Key: "userKey2",
Value: "userValue2",
},
Sender: "testSender2",
Metadata: common.HexToHash("metadata2"),
MetadataArray: [][]byte{
[]byte("meta3"),
[]byte("meta4"),
},
},
}

encoded, err := ds.Codec.EncodeDynamicEventTopics(ev, events)
require.NoError(t, err, "Encoding DynamicEvent topics should not return an error")

require.Len(t, encoded, 4, "Trigger should have four topics")
require.Equal(t, ds.Codec.DynamicEventLogHash(), encoded[0].Values[0], "First topic value should be DynamicEvent log hash")
require.Len(t, encoded[1].Values, 2, "Second topic should have two values")
packed1, err := abi.Arguments{ev.Inputs[1]}.Pack(events[0].UserData)

expected1 := crypto.Keccak256(packed1)
require.NoError(t, err)
require.Equal(t, expected1, encoded[1].Values[0])
packed2, err := abi.Arguments{ev.Inputs[1]}.Pack(events[1].UserData)

expected2 := crypto.Keccak256(packed2)
require.NoError(t, err)
require.Equal(t, expected2, encoded[1].Values[1])

expected3 := events[0].Metadata.Bytes()
require.Equal(t, expected3, encoded[2].Values[0])

expected4 := events[1].Metadata.Bytes()
require.Equal(t, expected4, encoded[2].Values[1])

packed3, err := abi.Arguments{ev.Inputs[4]}.Pack(events[0].MetadataArray)
expected5 := crypto.Keccak256(packed3)
require.NoError(t, err)
require.Equal(t, expected5, encoded[3].Values[0])

packed4, err := abi.Arguments{ev.Inputs[4]}.Pack(events[1].MetadataArray)
require.NoError(t, err)
expected6 := crypto.Keccak256(packed4)
require.Equal(t, expected6, encoded[3].Values[1])

trigger, err := ds.LogTriggerDynamicEventLog(evm.ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED, events)
require.NotNil(t, trigger)
require.NoError(t, err)
})
}

func newDataStorage(t *testing.T) *datastorage.DataStorage {
client := mocks.NewEVMClient(t)
ds, err := datastorage.NewDataStorage(client, nil, &bindings.ContractInitOptions{})
Expand Down
51 changes: 43 additions & 8 deletions pkg/bindings/common.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package bindings

import (
"fmt"
"math/big"
"reflect"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/crypto"

"github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
"github.com/smartcontractkit/cre-sdk-go/sdk"
Expand All @@ -26,13 +31,11 @@ type ReadOptions struct {
BlockNumber *big.Int
}

type LogTrackingOptions struct {
MaxLogsKept uint64 `protobuf:"varint,1,opt,name=max_logs_kept,json=maxLogsKept,proto3" json:"max_logs_kept,omitempty"` // maximum number of logs to retain ( 0 = unlimited )
RetentionTime int64 `protobuf:"varint,2,opt,name=retention_time,json=retentionTime,proto3" json:"retention_time,omitempty"` // maximum amount of time to retain logs in seconds
LogsPerBlock uint64 `protobuf:"varint,3,opt,name=logs_per_block,json=logsPerBlock,proto3" json:"logs_per_block,omitempty"` // rate limit ( maximum # of logs per block, 0 = unlimited )
Topic2 [][]byte `protobuf:"bytes,7,rep,name=topic2,proto3" json:"topic2,omitempty"` // list of possible values for topic2
Topic3 [][]byte `protobuf:"bytes,8,rep,name=topic3,proto3" json:"topic3,omitempty"` // list of possible values for topic3
Topic4 [][]byte `protobuf:"bytes,9,rep,name=topic4,proto3" json:"topic4,omitempty"` // list of possible values for topic4
type LogTrackingOptions[T any] struct {
MaxLogsKept uint64 `protobuf:"varint,1,opt,name=max_logs_kept,json=maxLogsKept,proto3" json:"max_logs_kept,omitempty"` // maximum number of logs to retain ( 0 = unlimited )
RetentionTime int64 `protobuf:"varint,2,opt,name=retention_time,json=retentionTime,proto3" json:"retention_time,omitempty"` // maximum amount of time to retain logs in seconds
LogsPerBlock uint64 `protobuf:"varint,3,opt,name=logs_per_block,json=logsPerBlock,proto3" json:"logs_per_block,omitempty"` // rate limit ( maximum # of logs per block, 0 = unlimited )
Filters []T
}

type FilterOptions struct {
Expand All @@ -41,7 +44,7 @@ type FilterOptions struct {
ToBlock *big.Int
}

func ValidateLogTrackingOptions(opts *LogTrackingOptions) {
func ValidateLogTrackingOptions[T any](opts *LogTrackingOptions[T]) {
if opts.MaxLogsKept == 0 {
opts.MaxLogsKept = 1000
}
Expand All @@ -52,3 +55,35 @@ func ValidateLogTrackingOptions(opts *LogTrackingOptions) {
opts.LogsPerBlock = 100
}
}

func PrepareTopicArg(arg abi.Argument, value interface{}) (interface{}, error) {
t := reflect.TypeOf(value)

// only pre-hash:
// - dynamic slices that aren't []byte
// - fixed arrays that aren't [N]byte
// - structs (i.e. tuple types)
if (t.Kind() == reflect.Slice && t.Elem().Kind() != reflect.Uint8) ||
(t.Kind() == reflect.Array && t.Elem().Kind() != reflect.Uint8) ||
t.Kind() == reflect.Struct {

packed, err := abi.Arguments{arg}.Pack(value)
if err != nil {
return nil, fmt.Errorf("packing %q for topic: %w", arg.Name, err)
}
// hash the packed bytes:
return crypto.Keccak256Hash(packed), nil
}

return value, nil
}

func PadTopics(topics []*evm.TopicValues) []*evm.TopicValues {
for i := len(topics); i < 4; i++ {
topics = append(topics, &evm.TopicValues{
Values: [][]byte{},
})
}

return topics
}
86 changes: 75 additions & 11 deletions pkg/bindings/sourcecre.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ var (
_ = abi.ConvertType
_ = emptypb.Empty{}
_ = pb.NewBigIntFromInt
_ = bindings.ValidateLogTrackingOptions
_ = bindings.FilterOptions{}
_ = evm.FilterLogTriggerRequest{}
_ = sdk.ConsensusResponseMapKeyPayload
)
Expand Down Expand Up @@ -118,6 +118,7 @@ type {{$contract.Type}}Codec interface {

{{- range $event := .Events}}
{{.Normalized.Name}}LogHash() []byte
Encode{{.Normalized.Name}}Topics(evt abi.Event, values []{{.Normalized.Name}}) ([]*evm.TopicValues, error)
Decode{{.Normalized.Name}}(log *evm.Log) (*{{.Normalized.Name}}, error)
{{- end}}
}
Expand Down Expand Up @@ -208,6 +209,49 @@ func (c *{{decapitalise $contract.Type}}CodecImpl) {{.Normalized.Name}}LogHash()
return c.abi.Events["{{.Original.Name}}"].ID.Bytes()
}

func (c *{{decapitalise $contract.Type}}CodecImpl) Encode{{.Normalized.Name}}Topics(
evt abi.Event,
values []{{.Normalized.Name}},
) ([]*evm.TopicValues, error) {
{{- range $idx, $inp := .Normalized.Inputs }}
{{- if $inp.Indexed }}
var {{ decapitalise $inp.Name }}Rule []interface{}
for _, v := range values {
fieldVal, err := bindings.PrepareTopicArg(evt.Inputs[{{$idx}}], v.{{capitalise $inp.Name}})
if err != nil {
return nil, err
}
{{ decapitalise $inp.Name }}Rule = append({{ decapitalise $inp.Name }}Rule, fieldVal)
}
{{- end }}
{{- end }}

rawTopics, err := abi.MakeTopics(
{{- range $inp := .Normalized.Inputs }}
{{- if $inp.Indexed }}
{{ decapitalise $inp.Name }}Rule,
{{- end }}
{{- end }}
)
if err != nil {
return nil, err
}

topics := make([]*evm.TopicValues, len(rawTopics)+1)
topics[0] = &evm.TopicValues{
Values: [][]byte{evt.ID.Bytes()},
}
for i, hashList := range rawTopics {
bs := make([][]byte, len(hashList))
for j, h := range hashList {
bs[j] = h.Bytes()
}
topics[i+1] = &evm.TopicValues{Values: bs}
}
return topics, nil
}


// Decode{{.Normalized.Name}} decodes a log into a {{.Normalized.Name}} struct.
func (c *{{decapitalise $contract.Type}}CodecImpl) Decode{{.Normalized.Name}}(log *evm.Log) (*{{.Normalized.Name}}, error) {
event := new({{.Normalized.Name}})
Expand Down Expand Up @@ -312,21 +356,41 @@ func (c *{{$contract.Type}}) UnpackError(data []byte) (any, error) {

{{range $event := $contract.Events}}

func (c *{{$contract.Type}}) RegisterLogTracking{{.Normalized.Name}}(runtime sdk.Runtime, options *bindings.LogTrackingOptions) {
bindings.ValidateLogTrackingOptions(options)
func (c *{{$contract.Type}}) LogTrigger{{.Normalized.Name}}Log(confidence evm.ConfidenceLevel, filters []{{.Normalized.Name}}) (sdk.Trigger[*evm.Log, *evm.Log], error) {
event := c.ABI.Events["{{.Normalized.Name}}"]
topics, err := c.Codec.Encode{{.Normalized.Name}}Topics(event, filters)
if err != nil {
return nil, fmt.Errorf("failed to encode topics for {{.Normalized.Name}}: %w", err)
}

return evm.LogTrigger(&evm.FilterLogTriggerRequest{
Addresses: [][]byte{c.Address},
Topics: topics,
Confidence: confidence,
}), nil
}

func (c *{{$contract.Type}}) RegisterLogTracking{{.Normalized.Name}}(runtime sdk.Runtime, options *bindings.LogTrackingOptions[{{.Normalized.Name}}]) error {
bindings.ValidateLogTrackingOptions[{{.Normalized.Name}}](options)
topics, err := c.Codec.Encode{{.Normalized.Name}}Topics(c.ABI.Events["{{.Normalized.Name}}"], options.Filters)
if err != nil {
return fmt.Errorf("failed to encode topics for {{.Normalized.Name}}: %w", err)
}
padded := bindings.PadTopics(topics)
c.evmClient.RegisterLogTracking(runtime, &evm.RegisterLogTrackingRequest{
Filter: &evm.LPFilter{
Name: "{{.Normalized.Name}}-" + common.Bytes2Hex(c.Address),
Addresses: [][]byte{c.Address},
EventSigs: [][]byte{c.Codec.{{.Normalized.Name}}LogHash()},
MaxLogsKept: options.MaxLogsKept,
Name: "{{.Normalized.Name}}-" + common.Bytes2Hex(c.Address),
Addresses: [][]byte{c.Address},
EventSigs: [][]byte{c.Codec.{{.Normalized.Name}}LogHash()},
MaxLogsKept: options.MaxLogsKept,
RetentionTime: options.RetentionTime,
LogsPerBlock: options.LogsPerBlock,
Topic2: options.Topic2,
Topic3: options.Topic3,
Topic4: options.Topic4,
LogsPerBlock: options.LogsPerBlock,
Topic2: padded[1].Values,
Topic3: padded[2].Values,
Topic4: padded[3].Values,
},
})
return nil
}

func (c *{{$contract.Type}}) UnregisterLogTracking{{.Normalized.Name}}(runtime sdk.Runtime) {
Expand Down
10 changes: 10 additions & 0 deletions pkg/bindings/testdata/DataStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ contract DataStorage {
// Event emitted when data is stored
event DataStored(address indexed sender, string key, string value);

event DynamicEvent(
string key,
UserData indexed userData,
string sender,
bytes indexed metadata,
bytes[] indexed metadataArray
);

// New event emitted by a different method
event AccessLogged(address indexed caller, string message);

event NoFields();

// Custom error for when a key is not found
error DataNotFound(address requester, string key, string reason);
// duplicate error for testing
Expand Down
2 changes: 1 addition & 1 deletion pkg/bindings/testdata/DataStorage_combined.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"contracts":{"pkg/bindings/testdata/DataStorage.sol:DataStorage":{"abi":[{"inputs":[{"internalType":"address","name":"requester","type":"address"},{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"reason","type":"string"}],"name":"DataNotFound","type":"error"},{"inputs":[{"internalType":"address","name":"requester","type":"address"},{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"reason","type":"string"}],"name":"DataNotFound2","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"caller","type":"address"},{"indexed":false,"internalType":"string","name":"message","type":"string"}],"name":"AccessLogged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"string","name":"key","type":"string"},{"indexed":false,"internalType":"string","name":"value","type":"string"}],"name":"DataStored","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"message","type":"string"}],"name":"logAccess","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"metadata","type":"bytes"},{"internalType":"bytes","name":"payload","type":"bytes"}],"name":"onReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"string","name":"key","type":"string"}],"name":"readData","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"name":"storeData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"internalType":"struct DataStorage.UserData","name":"userData","type":"tuple"}],"name":"storeUserData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"newValue","type":"string"}],"name":"updateData","outputs":[{"internalType":"string","name":"oldValue","type":"string"}],"stateMutability":"nonpayable","type":"function"}]}},"version":"0.8.30+commit.73712a01.Darwin.appleclang"}
{"contracts":{"pkg/bindings/testdata/DataStorage.sol:DataStorage":{"abi":[{"inputs":[{"internalType":"address","name":"requester","type":"address"},{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"reason","type":"string"}],"name":"DataNotFound","type":"error"},{"inputs":[{"internalType":"address","name":"requester","type":"address"},{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"reason","type":"string"}],"name":"DataNotFound2","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"caller","type":"address"},{"indexed":false,"internalType":"string","name":"message","type":"string"}],"name":"AccessLogged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"string","name":"key","type":"string"},{"indexed":false,"internalType":"string","name":"value","type":"string"}],"name":"DataStored","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"key","type":"string"},{"components":[{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"indexed":true,"internalType":"struct DataStorage.UserData","name":"userData","type":"tuple"},{"indexed":false,"internalType":"string","name":"sender","type":"string"},{"indexed":true,"internalType":"bytes","name":"metadata","type":"bytes"},{"indexed":true,"internalType":"bytes[]","name":"metadataArray","type":"bytes[]"}],"name":"DynamicEvent","type":"event"},{"anonymous":false,"inputs":[],"name":"NoFields","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"message","type":"string"}],"name":"logAccess","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"metadata","type":"bytes"},{"internalType":"bytes","name":"payload","type":"bytes"}],"name":"onReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"string","name":"key","type":"string"}],"name":"readData","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"name":"storeData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"internalType":"struct DataStorage.UserData","name":"userData","type":"tuple"}],"name":"storeUserData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"newValue","type":"string"}],"name":"updateData","outputs":[{"internalType":"string","name":"oldValue","type":"string"}],"stateMutability":"nonpayable","type":"function"}]}},"version":"0.8.30+commit.73712a01.Darwin.appleclang"}
Loading
Loading