Skip to content

Commit

Permalink
[CAPPL-19] Gateway Connector service wrapper (#14356)
Browse files Browse the repository at this point in the history
* service wrapper

* PR updates: simplify connector into signer and consolidate files

* keep signer as handler with empty methods for now

* lint

* PR comments except for unit test

* package lint

* unit test v1

* fix unit tests

* PR Review: fix config, separate tests

* PR review: clean up pointers, service wrapper generation.

* fix mocks return args issue

* add logs to figure out Sign issue

* fix keys issues

* typos

* remove redundant named import

* update order of deps to try to fix strange lint error

* remove handlegatewaymessage

* change name to gwcommon

* remove unused funcs and clean up var reuse

* add clock to servicewrapper

* fix bug in configs

* fix Ready and healthreport

* remove connectorSigner struct

* remove privateKey from constructor, keep initialization in Start

* lint

---------

Co-authored-by: Bolek <[email protected]>
  • Loading branch information
DavidOrchard and bolekk authored Sep 11, 2024
1 parent 356c70c commit 51cb378
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 0 deletions.
114 changes: 114 additions & 0 deletions core/capabilities/gateway_connector/service_wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package gatewayconnector

import (
"context"
"crypto/ecdsa"
"errors"
"math/big"
"slices"

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

"github.com/smartcontractkit/chainlink-common/pkg/services"
"github.com/smartcontractkit/chainlink/v2/core/config"
"github.com/smartcontractkit/chainlink/v2/core/logger"
gwcommon "github.com/smartcontractkit/chainlink/v2/core/services/gateway/common"
"github.com/smartcontractkit/chainlink/v2/core/services/gateway/connector"
"github.com/smartcontractkit/chainlink/v2/core/services/gateway/network"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey"
)

type ServiceWrapper struct {
services.StateMachine

config config.GatewayConnector
keystore keystore.Eth
connector connector.GatewayConnector
signerKey *ecdsa.PrivateKey
lggr logger.Logger
clock clockwork.Clock
}

func translateConfigs(f config.GatewayConnector) connector.ConnectorConfig {
r := connector.ConnectorConfig{}
r.NodeAddress = f.NodeAddress()
r.DonId = f.DonID()

if len(f.Gateways()) != 0 {
r.Gateways = make([]connector.ConnectorGatewayConfig, len(f.Gateways()))
for index, element := range f.Gateways() {
r.Gateways[index] = connector.ConnectorGatewayConfig{Id: element.ID(), URL: element.URL()}
}
}

r.WsClientConfig = network.WebSocketClientConfig{HandshakeTimeoutMillis: f.WSHandshakeTimeoutMillis()}
r.AuthMinChallengeLen = f.AuthMinChallengeLen()
r.AuthTimestampToleranceSec = f.AuthTimestampToleranceSec()
return r
}

// NOTE: this wrapper is needed to make sure that our services are started after Keystore.
func NewGatewayConnectorServiceWrapper(config config.GatewayConnector, keystore keystore.Eth, clock clockwork.Clock, lggr logger.Logger) *ServiceWrapper {
return &ServiceWrapper{
config: config,
keystore: keystore,
clock: clock,
lggr: lggr,
}
}

func (e *ServiceWrapper) Start(ctx context.Context) error {
return e.StartOnce("GatewayConnectorServiceWrapper", func() error {
conf := e.config
nodeAddress := conf.NodeAddress()
chainID, _ := new(big.Int).SetString(conf.ChainIDForNodeKey(), 0)
enabledKeys, err := e.keystore.EnabledKeysForChain(ctx, chainID)
if err != nil {
return err
}
if len(enabledKeys) == 0 {
return errors.New("no available keys found")
}
configuredNodeAddress := common.HexToAddress(nodeAddress)
idx := slices.IndexFunc(enabledKeys, func(key ethkey.KeyV2) bool { return key.Address == configuredNodeAddress })

if idx == -1 {
return errors.New("key for configured node address not found")
}
e.signerKey = enabledKeys[idx].ToEcdsaPrivKey()
if enabledKeys[idx].ID() != nodeAddress {
return errors.New("node address mismatch")
}

translated := translateConfigs(conf)
e.connector, err = connector.NewGatewayConnector(&translated, e, e.clock, e.lggr)
if err != nil {
return err
}
return e.connector.Start(ctx)
})
}

func (e *ServiceWrapper) Sign(data ...[]byte) ([]byte, error) {
return gwcommon.SignData(e.signerKey, data...)
}

func (e *ServiceWrapper) Close() error {
return e.StopOnce("GatewayConnectorServiceWrapper", func() (err error) {
return e.connector.Close()
})
}

func (e *ServiceWrapper) HealthReport() map[string]error {
return map[string]error{e.Name(): e.Healthy()}
}

func (e *ServiceWrapper) Name() string {
return "GatewayConnectorServiceWrapper"
}

func (e *ServiceWrapper) GetGatewayConnector() connector.GatewayConnector {
return e.connector
}
80 changes: 80 additions & 0 deletions core/capabilities/gateway_connector/service_wrapper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package gatewayconnector

import (
"crypto/ecdsa"
"testing"

"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink/v2/core/config/toml"
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/chainlink"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey"
ksmocks "github.com/smartcontractkit/chainlink/v2/core/services/keystore/mocks"
)

func generateWrapper(t *testing.T, privateKey *ecdsa.PrivateKey, keystoreKey *ecdsa.PrivateKey) (*ServiceWrapper, error) {
logger := logger.TestLogger(t)
privateKeyV2 := ethkey.FromPrivateKey(privateKey)
addr := privateKeyV2.Address
keystoreKeyV2 := ethkey.FromPrivateKey(keystoreKey)

config, err := chainlink.GeneralConfigOpts{
Config: chainlink.Config{
Core: toml.Core{
Capabilities: toml.Capabilities{
GatewayConnector: toml.GatewayConnector{
ChainIDForNodeKey: ptr("1"),
NodeAddress: ptr(addr.Hex()),
DonID: ptr("5"),
WSHandshakeTimeoutMillis: ptr[uint32](100),
AuthMinChallengeLen: ptr[int](0),
AuthTimestampToleranceSec: ptr[uint32](10),
Gateways: []toml.ConnectorGateway{{ID: ptr("example_gateway"), URL: ptr("wss://localhost:8081/node")}},
},
},
},
},
}.New()
ethKeystore := ksmocks.NewEth(t)
ethKeystore.On("EnabledKeysForChain", mock.Anything, mock.Anything).Return([]ethkey.KeyV2{keystoreKeyV2}, nil)
gc := config.Capabilities().GatewayConnector()
wrapper := NewGatewayConnectorServiceWrapper(gc, ethKeystore, clockwork.NewFakeClock(), logger)
require.NoError(t, err)
return wrapper, err
}

func TestGatewayConnectorServiceWrapper_CleanStartClose(t *testing.T) {
t.Parallel()

key, _ := testutils.NewPrivateKeyAndAddress(t)
wrapper, err := generateWrapper(t, key, key)
require.NoError(t, err)

ctx := testutils.Context(t)
err = wrapper.Start(ctx)
require.NoError(t, err)

t.Cleanup(func() {
assert.NoError(t, wrapper.Close())
})
}

func TestGatewayConnectorServiceWrapper_NonexistentKey(t *testing.T) {
t.Parallel()

key, _ := testutils.NewPrivateKeyAndAddress(t)
keystoreKey, _ := testutils.NewPrivateKeyAndAddress(t)
wrapper, err := generateWrapper(t, key, keystoreKey)
require.NoError(t, err)

ctx := testutils.Context(t)
err = wrapper.Start(ctx)
require.Error(t, err)
}

func ptr[T any](t T) *T { return &t }

0 comments on commit 51cb378

Please sign in to comment.