diff --git a/core/services/gateway/handlers/functions/allowlist/allowlist.go b/core/services/gateway/handlers/functions/allowlist/allowlist.go index cd92e0c984f..5a9d582ab84 100644 --- a/core/services/gateway/handlers/functions/allowlist/allowlist.go +++ b/core/services/gateway/handlers/functions/allowlist/allowlist.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "fmt" "math/big" + "regexp" + "strings" "sync" "sync/atomic" "time" @@ -23,9 +25,10 @@ import ( ) const ( - defaultStoredAllowlistBatchSize = 1000 - defaultOnchainAllowlistBatchSize = 100 - defaultFetchingDelayInRangeSec = 1 + defaultStoredAllowlistBatchSize = 1000 + defaultOnchainAllowlistBatchSize = 100 + defaultFetchingDelayInRangeSec = 1 + tosContractMinBatchProcessingVersion = "1.1.0" ) type OnchainAllowlistConfig struct { @@ -38,8 +41,6 @@ type OnchainAllowlistConfig struct { UpdateTimeoutSec uint `json:"updateTimeoutSec"` StoredAllowlistBatchSize uint `json:"storedAllowlistBatchSize"` OnchainAllowlistBatchSize uint `json:"onchainAllowlistBatchSize"` - // StoreAllowedSendersEnabled is a feature flag that enables storing in db a copy of the allowlist. - StoreAllowedSendersEnabled bool `json:"storeAllowedSendersEnabled"` // FetchingDelayInRangeSec prevents RPC client being rate limited when fetching the allowlist in ranges. FetchingDelayInRangeSec uint `json:"fetchingDelayInRangeSec"` } @@ -210,7 +211,21 @@ func (a *onchainAllowlist) updateFromContractV1(ctx context.Context, blockNum *b } var allowedSenderList []common.Address - if !a.config.StoreAllowedSendersEnabled { + typeAndVersion, err := tosContract.TypeAndVersion(&bind.CallOpts{ + Pending: false, + BlockNumber: blockNum, + Context: ctx, + }) + if err != nil { + return errors.Wrap(err, "failed to fetch the tos contract type and version") + } + + version, err := extractContractVersion(typeAndVersion) + if err != nil { + return errors.Wrap(err, "failed to extract version") + } + + if version < strings.ReplaceAll(tosContractMinBatchProcessingVersion, ".", "") { allowedSenderList, err = tosContract.GetAllAllowedSenders(&bind.CallOpts{ Pending: false, BlockNumber: blockNum, @@ -355,3 +370,14 @@ func (a *onchainAllowlist) loadStoredAllowedSenderList() { a.update(allowedList) } + +func extractContractVersion(str string) (string, error) { + pattern := `v(\d+).(\d+).(\d+)` + re := regexp.MustCompile(pattern) + + match := re.FindStringSubmatch(str) + if len(match) != 4 { + return "", fmt.Errorf("version not found in string: %s", str) + } + return match[1] + match[2] + match[3], nil +} diff --git a/core/services/gateway/handlers/functions/allowlist/allowlist_test.go b/core/services/gateway/handlers/functions/allowlist/allowlist_test.go index 103ed412433..99de4a0b504 100644 --- a/core/services/gateway/handlers/functions/allowlist/allowlist_test.go +++ b/core/services/gateway/handlers/functions/allowlist/allowlist_test.go @@ -7,7 +7,9 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/onsi/gomega" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -18,58 +20,105 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/functions/allowlist" amocks "github.com/smartcontractkit/chainlink/v2/core/services/gateway/handlers/functions/allowlist/mocks" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" ) const ( - addr1 = "9ed925d8206a4f88a2f643b28b3035b315753cd6" - addr2 = "ea6721ac65bced841b8ec3fc5fedea6141a0ade4" - addr3 = "84689acc87ff22841b8ec378300da5e141a99911" + addr1 = "9ed925d8206a4f88a2f643b28b3035b315753cd6" + addr2 = "ea6721ac65bced841b8ec3fc5fedea6141a0ade4" + addr3 = "84689acc87ff22841b8ec378300da5e141a99911" + ToSContractV100 = "Functions Terms of Service Allow List v1.0.0" + ToSContractV110 = "Functions Terms of Service Allow List v1.1.0" ) -func sampleEncodedAllowlist(t *testing.T) []byte { - abiEncodedAddresses := - "0000000000000000000000000000000000000000000000000000000000000020" + - "0000000000000000000000000000000000000000000000000000000000000002" + - "000000000000000000000000" + addr1 + - "000000000000000000000000" + addr2 - rawData, err := hex.DecodeString(abiEncodedAddresses) - require.NoError(t, err) - return rawData -} - -func TestAllowlist_UpdateAndCheck(t *testing.T) { +func TestUpdateAndCheck(t *testing.T) { t.Parallel() - client := mocks.NewClient(t) - client.On("LatestBlockHeight", mock.Anything).Return(big.NewInt(42), nil) - client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(sampleEncodedAllowlist(t), nil) - config := allowlist.OnchainAllowlistConfig{ - ContractVersion: 1, - ContractAddress: common.Address{}, - BlockConfirmations: 1, - } + t.Run("OK-with_ToS_V1.0.0", func(t *testing.T) { + client := mocks.NewClient(t) + client.On("LatestBlockHeight", mock.Anything).Return(big.NewInt(42), nil) - orm := amocks.NewORM(t) - orm.On("PurgeAllowedSenders").Times(1).Return(nil) - orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + addr := common.HexToAddress("0x0000000000000000000000000000000000000020") + typeAndVersionResponse, err := encodeTypeAndVersionResponse(ToSContractV100) + require.NoError(t, err) - allowlist, err := allowlist.NewOnchainAllowlist(client, config, orm, logger.TestLogger(t)) - require.NoError(t, err) + client.On("CallContract", mock.Anything, ethereum.CallMsg{ // typeAndVersion + To: &addr, + Data: hexutil.MustDecode("0x181f5a77"), + }, mock.Anything).Return(typeAndVersionResponse, nil) - err = allowlist.Start(testutils.Context(t)) - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, allowlist.Close()) + client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(sampleEncodedAllowlist(t), nil) + + config := allowlist.OnchainAllowlistConfig{ + ContractVersion: 1, + ContractAddress: common.Address{}, + BlockConfirmations: 1, + } + + orm := amocks.NewORM(t) + orm.On("PurgeAllowedSenders").Times(1).Return(nil) + orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + + allowlist, err := allowlist.NewOnchainAllowlist(client, config, orm, logger.TestLogger(t)) + require.NoError(t, err) + + err = allowlist.Start(testutils.Context(t)) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, allowlist.Close()) + }) + + require.NoError(t, allowlist.UpdateFromContract(testutils.Context(t))) + require.False(t, allowlist.Allow(common.Address{})) + require.True(t, allowlist.Allow(common.HexToAddress(addr1))) + require.True(t, allowlist.Allow(common.HexToAddress(addr2))) + require.False(t, allowlist.Allow(common.HexToAddress(addr3))) }) - require.NoError(t, allowlist.UpdateFromContract(testutils.Context(t))) - require.False(t, allowlist.Allow(common.Address{})) - require.True(t, allowlist.Allow(common.HexToAddress(addr1))) - require.True(t, allowlist.Allow(common.HexToAddress(addr2))) - require.False(t, allowlist.Allow(common.HexToAddress(addr3))) + t.Run("OK-with_ToS_V1.1.0", func(t *testing.T) { + client := mocks.NewClient(t) + client.On("LatestBlockHeight", mock.Anything).Return(big.NewInt(42), nil) + + typeAndVersionResponse, err := encodeTypeAndVersionResponse(ToSContractV110) + require.NoError(t, err) + + addr := common.HexToAddress("0x0000000000000000000000000000000000000020") + client.On("CallContract", mock.Anything, ethereum.CallMsg{ // typeAndVersion + To: &addr, + Data: hexutil.MustDecode("0x181f5a77"), + }, mock.Anything).Return(typeAndVersionResponse, nil) + + client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(sampleEncodedAllowlist(t), nil) + + config := allowlist.OnchainAllowlistConfig{ + ContractVersion: 1, + ContractAddress: common.Address{}, + BlockConfirmations: 1, + } + + orm := amocks.NewORM(t) + orm.On("DeleteAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + + allowlist, err := allowlist.NewOnchainAllowlist(client, config, orm, logger.TestLogger(t)) + require.NoError(t, err) + + err = allowlist.Start(testutils.Context(t)) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, allowlist.Close()) + }) + + require.NoError(t, allowlist.UpdateFromContract(testutils.Context(t))) + require.False(t, allowlist.Allow(common.Address{})) + require.True(t, allowlist.Allow(common.HexToAddress(addr1))) + require.True(t, allowlist.Allow(common.HexToAddress(addr2))) + require.False(t, allowlist.Allow(common.HexToAddress(addr3))) + }) } -func TestAllowlist_UnsupportedVersion(t *testing.T) { +func TestUnsupportedVersion(t *testing.T) { t.Parallel() client := mocks.NewClient(t) @@ -84,67 +133,132 @@ func TestAllowlist_UnsupportedVersion(t *testing.T) { require.Error(t, err) } -func TestAllowlist_UpdatePeriodically(t *testing.T) { +func TestUpdatePeriodically(t *testing.T) { t.Parallel() - ctx, cancel := context.WithCancel(testutils.Context(t)) - client := mocks.NewClient(t) - client.On("LatestBlockHeight", mock.Anything).Return(big.NewInt(42), nil) - client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - cancel() - }).Return(sampleEncodedAllowlist(t), nil) - config := allowlist.OnchainAllowlistConfig{ - ContractAddress: common.Address{}, - ContractVersion: 1, - BlockConfirmations: 1, - UpdateFrequencySec: 2, - UpdateTimeoutSec: 1, - } + t.Run("OK-with_ToS_V1.0.0", func(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + client := mocks.NewClient(t) + client.On("LatestBlockHeight", mock.Anything).Return(big.NewInt(42), nil) - orm := amocks.NewORM(t) - orm.On("PurgeAllowedSenders").Times(1).Return(nil) - orm.On("GetAllowedSenders", uint(0), uint(1000)).Return([]common.Address{}, nil) - orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + addr := common.HexToAddress("0x0000000000000000000000000000000000000020") + typeAndVersionResponse, err := encodeTypeAndVersionResponse(ToSContractV100) + require.NoError(t, err) - allowlist, err := allowlist.NewOnchainAllowlist(client, config, orm, logger.TestLogger(t)) - require.NoError(t, err) + client.On("CallContract", mock.Anything, ethereum.CallMsg{ // typeAndVersion + To: &addr, + Data: hexutil.MustDecode("0x181f5a77"), + }, mock.Anything).Return(typeAndVersionResponse, nil) - err = allowlist.Start(ctx) - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, allowlist.Close()) + client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + cancel() + }).Return(sampleEncodedAllowlist(t), nil) + config := allowlist.OnchainAllowlistConfig{ + ContractAddress: common.Address{}, + ContractVersion: 1, + BlockConfirmations: 1, + UpdateFrequencySec: 2, + UpdateTimeoutSec: 1, + } + + orm := amocks.NewORM(t) + orm.On("PurgeAllowedSenders").Times(1).Return(nil) + orm.On("GetAllowedSenders", uint(0), uint(1000)).Return([]common.Address{}, nil) + orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + + allowlist, err := allowlist.NewOnchainAllowlist(client, config, orm, logger.TestLogger(t)) + require.NoError(t, err) + + err = allowlist.Start(ctx) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, allowlist.Close()) + }) + + gomega.NewGomegaWithT(t).Eventually(func() bool { + return allowlist.Allow(common.HexToAddress(addr1)) && !allowlist.Allow(common.HexToAddress(addr3)) + }, testutils.WaitTimeout(t), time.Second).Should(gomega.BeTrue()) }) - gomega.NewGomegaWithT(t).Eventually(func() bool { - return allowlist.Allow(common.HexToAddress(addr1)) && !allowlist.Allow(common.HexToAddress(addr3)) - }, testutils.WaitTimeout(t), time.Second).Should(gomega.BeTrue()) + t.Run("OK-with_ToS_V1.1.0", func(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + client := mocks.NewClient(t) + client.On("LatestBlockHeight", mock.Anything).Return(big.NewInt(42), nil) + + addr := common.HexToAddress("0x0000000000000000000000000000000000000020") + typeAndVersionResponse, err := encodeTypeAndVersionResponse(ToSContractV110) + require.NoError(t, err) + + client.On("CallContract", mock.Anything, ethereum.CallMsg{ // typeAndVersion + To: &addr, + Data: hexutil.MustDecode("0x181f5a77"), + }, mock.Anything).Return(typeAndVersionResponse, nil) + + client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + cancel() + }).Return(sampleEncodedAllowlist(t), nil) + config := allowlist.OnchainAllowlistConfig{ + ContractAddress: common.Address{}, + ContractVersion: 1, + BlockConfirmations: 1, + UpdateFrequencySec: 2, + UpdateTimeoutSec: 1, + } + + orm := amocks.NewORM(t) + orm.On("DeleteAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + orm.On("GetAllowedSenders", uint(0), uint(1000)).Return([]common.Address{}, nil) + orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + + allowlist, err := allowlist.NewOnchainAllowlist(client, config, orm, logger.TestLogger(t)) + require.NoError(t, err) + + err = allowlist.Start(ctx) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, allowlist.Close()) + }) + + gomega.NewGomegaWithT(t).Eventually(func() bool { + return allowlist.Allow(common.HexToAddress(addr1)) && !allowlist.Allow(common.HexToAddress(addr3)) + }, testutils.WaitTimeout(t), time.Second).Should(gomega.BeTrue()) + }) } -func TestAllowlist_UpdateFromContract(t *testing.T) { +func TestUpdateFromContract(t *testing.T) { t.Parallel() - t.Run("OK-iterate_over_list_of_allowed_senders", func(t *testing.T) { + t.Run("OK-fetch_complete_list_of_allowed_senders", func(t *testing.T) { ctx, cancel := context.WithCancel(testutils.Context(t)) client := mocks.NewClient(t) client.On("LatestBlockHeight", mock.Anything).Return(big.NewInt(42), nil) + + addr := common.HexToAddress("0x0000000000000000000000000000000000000020") + typeAndVersionResponse, err := encodeTypeAndVersionResponse(ToSContractV100) + require.NoError(t, err) + + client.On("CallContract", mock.Anything, ethereum.CallMsg{ // typeAndVersion + To: &addr, + Data: hexutil.MustDecode("0x181f5a77"), + }, mock.Anything).Return(typeAndVersionResponse, nil) + client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { cancel() }).Return(sampleEncodedAllowlist(t), nil) config := allowlist.OnchainAllowlistConfig{ - ContractAddress: common.HexToAddress(addr3), - ContractVersion: 1, - BlockConfirmations: 1, - UpdateFrequencySec: 2, - UpdateTimeoutSec: 1, - StoredAllowlistBatchSize: 2, - OnchainAllowlistBatchSize: 16, - StoreAllowedSendersEnabled: true, - FetchingDelayInRangeSec: 0, + ContractAddress: common.HexToAddress(addr3), + ContractVersion: 1, + BlockConfirmations: 1, + UpdateFrequencySec: 2, + UpdateTimeoutSec: 1, + StoredAllowlistBatchSize: 2, + OnchainAllowlistBatchSize: 16, + FetchingDelayInRangeSec: 0, } orm := amocks.NewORM(t) - orm.On("DeleteAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(2).Return(nil) - orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(2).Return(nil) + orm.On("PurgeAllowedSenders").Times(1).Return(nil) + orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) allowlist, err := allowlist.NewOnchainAllowlist(client, config, orm, logger.TestLogger(t)) require.NoError(t, err) @@ -157,28 +271,37 @@ func TestAllowlist_UpdateFromContract(t *testing.T) { }, testutils.WaitTimeout(t), time.Second).Should(gomega.BeTrue()) }) - t.Run("OK-fetch_complete_list_of_allowed_senders", func(t *testing.T) { + t.Run("OK-iterate_over_list_of_allowed_senders", func(t *testing.T) { ctx, cancel := context.WithCancel(testutils.Context(t)) client := mocks.NewClient(t) client.On("LatestBlockHeight", mock.Anything).Return(big.NewInt(42), nil) + + addr := common.HexToAddress("0x0000000000000000000000000000000000000020") + typeAndVersionResponse, err := encodeTypeAndVersionResponse(ToSContractV110) + require.NoError(t, err) + + client.On("CallContract", mock.Anything, ethereum.CallMsg{ // typeAndVersion + To: &addr, + Data: hexutil.MustDecode("0x181f5a77"), + }, mock.Anything).Return(typeAndVersionResponse, nil) + client.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { cancel() }).Return(sampleEncodedAllowlist(t), nil) config := allowlist.OnchainAllowlistConfig{ - ContractAddress: common.HexToAddress(addr3), - ContractVersion: 1, - BlockConfirmations: 1, - UpdateFrequencySec: 2, - UpdateTimeoutSec: 1, - StoredAllowlistBatchSize: 2, - OnchainAllowlistBatchSize: 16, - StoreAllowedSendersEnabled: false, - FetchingDelayInRangeSec: 0, + ContractAddress: common.HexToAddress(addr3), + ContractVersion: 1, + BlockConfirmations: 1, + UpdateFrequencySec: 2, + UpdateTimeoutSec: 1, + StoredAllowlistBatchSize: 2, + OnchainAllowlistBatchSize: 16, + FetchingDelayInRangeSec: 0, } orm := amocks.NewORM(t) - orm.On("PurgeAllowedSenders").Times(1).Return(nil) - orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(1).Return(nil) + orm.On("DeleteAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(2).Return(nil) + orm.On("CreateAllowedSenders", []common.Address{common.HexToAddress(addr1), common.HexToAddress(addr2)}).Times(2).Return(nil) allowlist, err := allowlist.NewOnchainAllowlist(client, config, orm, logger.TestLogger(t)) require.NoError(t, err) @@ -190,4 +313,38 @@ func TestAllowlist_UpdateFromContract(t *testing.T) { return allowlist.Allow(common.HexToAddress(addr1)) && !allowlist.Allow(common.HexToAddress(addr3)) }, testutils.WaitTimeout(t), time.Second).Should(gomega.BeTrue()) }) + +} + +func encodeTypeAndVersionResponse(typeAndVersion string) ([]byte, error) { + codecName := "my_codec" + evmEncoderConfig := `[{"Name":"typeAndVersion","Type":"string"}]` + codecConfig := types.CodecConfig{Configs: map[string]types.ChainCodecConfig{ + codecName: {TypeABI: evmEncoderConfig}, + }} + encoder, err := evm.NewCodec(codecConfig) + if err != nil { + return nil, err + } + + input := map[string]any{ + "typeAndVersion": typeAndVersion, + } + typeAndVersionResponse, err := encoder.Encode(context.Background(), input, codecName) + if err != nil { + return nil, err + } + + return typeAndVersionResponse, nil +} + +func sampleEncodedAllowlist(t *testing.T) []byte { + abiEncodedAddresses := + "0000000000000000000000000000000000000000000000000000000000000020" + + "0000000000000000000000000000000000000000000000000000000000000002" + + "000000000000000000000000" + addr1 + + "000000000000000000000000" + addr2 + rawData, err := hex.DecodeString(abiEncodedAddresses) + require.NoError(t, err) + return rawData }