-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Syncer #13427
Merged
Syncer #13427
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
efe2e1f
[KS-211] Implement basic syncer
cedric-cordenier 674cee9
Rename config entry Capabilities.Registry -> Capabilities.ExternalReg…
cedric-cordenier 8b9ae5d
Drop --gen-go-grpc options as they aren't needed
cedric-cordenier 58e45e8
Fully-qualified proto name
cedric-cordenier 6fa5bd4
Some more comments
cedric-cordenier 411516a
Correctly set ExternalRegistry defaults
cedric-cordenier 544eca6
Add logging
cedric-cordenier dada8b3
Unpad signatures
cedric-cordenier 24899cb
Fix tests
cedric-cordenier 8a33928
Add changeset
cedric-cordenier 3ab9a57
Correctly close syncer in tests
cedric-cordenier 1d9a726
Client: remoteDON -> myDON
cedric-cordenier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"chainlink": minor | ||
--- | ||
|
||
#internal Add RegistrySyncer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package capabilities | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
|
||
"github.com/smartcontractkit/chainlink-common/pkg/types" | ||
kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/keystone_capability_registry" | ||
p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" | ||
evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" | ||
) | ||
|
||
type remoteRegistryReader struct { | ||
r types.ContractReader | ||
} | ||
|
||
var _ reader = (*remoteRegistryReader)(nil) | ||
|
||
type hashedCapabilityID [32]byte | ||
type donID uint32 | ||
|
||
type state struct { | ||
IDsToDONs map[donID]kcr.CapabilityRegistryDONInfo | ||
IDsToNodes map[p2ptypes.PeerID]kcr.CapabilityRegistryNodeInfo | ||
IDsToCapabilities map[hashedCapabilityID]kcr.CapabilityRegistryCapability | ||
} | ||
|
||
func (r *remoteRegistryReader) state(ctx context.Context) (state, error) { | ||
dons := []kcr.CapabilityRegistryDONInfo{} | ||
err := r.r.GetLatestValue(ctx, "capabilityRegistry", "getDONs", nil, &dons) | ||
if err != nil { | ||
return state{}, err | ||
} | ||
|
||
idsToDONs := map[donID]kcr.CapabilityRegistryDONInfo{} | ||
for _, d := range dons { | ||
idsToDONs[donID(d.Id)] = d | ||
} | ||
|
||
caps := kcr.GetCapabilities{} | ||
err = r.r.GetLatestValue(ctx, "capabilityRegistry", "getCapabilities", nil, &caps) | ||
if err != nil { | ||
return state{}, err | ||
} | ||
|
||
idsToCapabilities := map[hashedCapabilityID]kcr.CapabilityRegistryCapability{} | ||
for i, c := range caps.Capabilities { | ||
idsToCapabilities[caps.HashedCapabilityIds[i]] = c | ||
} | ||
|
||
nodes := &kcr.GetNodes{} | ||
err = r.r.GetLatestValue(ctx, "capabilityRegistry", "getNodes", nil, &nodes) | ||
if err != nil { | ||
return state{}, err | ||
} | ||
|
||
idsToNodes := map[p2ptypes.PeerID]kcr.CapabilityRegistryNodeInfo{} | ||
for _, node := range nodes.NodeInfo { | ||
idsToNodes[node.P2pId] = node | ||
} | ||
|
||
return state{IDsToDONs: idsToDONs, IDsToCapabilities: idsToCapabilities, IDsToNodes: idsToNodes}, nil | ||
} | ||
|
||
type contractReaderFactory interface { | ||
NewContractReader(context.Context, []byte) (types.ContractReader, error) | ||
} | ||
|
||
func newRemoteRegistryReader(ctx context.Context, relayer contractReaderFactory, remoteRegistryAddress string) (*remoteRegistryReader, error) { | ||
contractReaderConfig := evmrelaytypes.ChainReaderConfig{ | ||
Contracts: map[string]evmrelaytypes.ChainContractReader{ | ||
"capabilityRegistry": { | ||
ContractABI: kcr.CapabilityRegistryABI, | ||
Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ | ||
"getDONs": { | ||
ChainSpecificName: "getDONs", | ||
}, | ||
"getCapabilities": { | ||
ChainSpecificName: "getCapabilities", | ||
}, | ||
"getNodes": { | ||
ChainSpecificName: "getNodes", | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
contractReaderConfigEncoded, err := json.Marshal(contractReaderConfig) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
cr, err := relayer.NewContractReader(ctx, contractReaderConfigEncoded) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
err = cr.Bind(ctx, []types.BoundContract{ | ||
{ | ||
Address: remoteRegistryAddress, | ||
Name: "capabilityRegistry", | ||
}, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &remoteRegistryReader{r: cr}, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
package capabilities | ||
|
||
import ( | ||
"context" | ||
"crypto/rand" | ||
"encoding/json" | ||
"fmt" | ||
"math/big" | ||
"testing" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/accounts/abi/bind" | ||
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/core" | ||
"github.com/ethereum/go-ethereum/eth/ethconfig" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/smartcontractkit/chainlink-common/pkg/types" | ||
evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" | ||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" | ||
kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/keystone_capability_registry" | ||
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils" | ||
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" | ||
"github.com/smartcontractkit/chainlink/v2/core/logger" | ||
p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" | ||
"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" | ||
evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" | ||
) | ||
|
||
var writeChainCapability = kcr.CapabilityRegistryCapability{ | ||
LabelledName: "write-chain", | ||
Version: "1.0.1", | ||
ResponseType: uint8(1), | ||
} | ||
|
||
func startNewChainWithRegistry(t *testing.T) (*kcr.CapabilityRegistry, common.Address, *bind.TransactOpts, *backends.SimulatedBackend) { | ||
owner := testutils.MustNewSimTransactor(t) | ||
|
||
oneEth, _ := new(big.Int).SetString("100000000000000000000", 10) | ||
gasLimit := ethconfig.Defaults.Miner.GasCeil * 2 // 60 M blocks | ||
|
||
simulatedBackend := backends.NewSimulatedBackend(core.GenesisAlloc{owner.From: { | ||
Balance: oneEth, | ||
}}, gasLimit) | ||
simulatedBackend.Commit() | ||
|
||
capabilityRegistryAddress, _, capabilityRegistry, err := kcr.DeployCapabilityRegistry(owner, simulatedBackend) | ||
require.NoError(t, err, "DeployCapabilityRegistry failed") | ||
|
||
fmt.Println("Deployed CapabilityRegistry at", capabilityRegistryAddress.Hex()) | ||
simulatedBackend.Commit() | ||
|
||
return capabilityRegistry, capabilityRegistryAddress, owner, simulatedBackend | ||
} | ||
|
||
type crFactory struct { | ||
lggr logger.Logger | ||
logPoller logpoller.LogPoller | ||
client evmclient.Client | ||
} | ||
|
||
func (c *crFactory) NewContractReader(ctx context.Context, cfg []byte) (types.ContractReader, error) { | ||
crCfg := &evmrelaytypes.ChainReaderConfig{} | ||
if err := json.Unmarshal(cfg, crCfg); err != nil { | ||
return nil, err | ||
} | ||
svc, err := evm.NewChainReaderService(ctx, c.lggr, c.logPoller, c.client, *crCfg) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return svc, svc.Start(ctx) | ||
} | ||
|
||
func newContractReaderFactory(t *testing.T, simulatedBackend *backends.SimulatedBackend) *crFactory { | ||
lggr := logger.TestLogger(t) | ||
client := evmclient.NewSimulatedBackendClient( | ||
t, | ||
simulatedBackend, | ||
testutils.SimulatedChainID, | ||
) | ||
db := pgtest.NewSqlxDB(t) | ||
lp := logpoller.NewLogPoller( | ||
logpoller.NewORM(testutils.SimulatedChainID, db, lggr), | ||
client, | ||
lggr, | ||
logpoller.Opts{ | ||
PollPeriod: 100 * time.Millisecond, | ||
FinalityDepth: 2, | ||
BackfillBatchSize: 3, | ||
RpcBatchSize: 2, | ||
KeepFinalizedBlocksDepth: 1000, | ||
}, | ||
) | ||
return &crFactory{ | ||
lggr: lggr, | ||
client: client, | ||
logPoller: lp, | ||
} | ||
} | ||
|
||
func randomWord() [32]byte { | ||
word := make([]byte, 32) | ||
_, err := rand.Read(word) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return [32]byte(word) | ||
} | ||
|
||
func TestReader_Integration(t *testing.T) { | ||
ctx := testutils.Context(t) | ||
reg, regAddress, owner, sim := startNewChainWithRegistry(t) | ||
|
||
_, err := reg.AddCapabilities(owner, []kcr.CapabilityRegistryCapability{writeChainCapability}) | ||
require.NoError(t, err, "AddCapability failed for %s", writeChainCapability.LabelledName) | ||
sim.Commit() | ||
|
||
cid, err := reg.GetHashedCapabilityId(&bind.CallOpts{}, writeChainCapability.LabelledName, writeChainCapability.Version) | ||
require.NoError(t, err) | ||
|
||
_, err = reg.AddNodeOperators(owner, []kcr.CapabilityRegistryNodeOperator{ | ||
{ | ||
Admin: owner.From, | ||
Name: "TEST_NOP", | ||
}, | ||
}) | ||
require.NoError(t, err) | ||
|
||
nodeSet := [][32]byte{ | ||
randomWord(), | ||
randomWord(), | ||
randomWord(), | ||
} | ||
|
||
nodes := []kcr.CapabilityRegistryNodeInfo{ | ||
{ | ||
// The first NodeOperatorId has id 1 since the id is auto-incrementing. | ||
NodeOperatorId: uint32(1), | ||
Signer: randomWord(), | ||
P2pId: nodeSet[0], | ||
HashedCapabilityIds: [][32]byte{cid}, | ||
}, | ||
{ | ||
// The first NodeOperatorId has id 1 since the id is auto-incrementing. | ||
NodeOperatorId: uint32(1), | ||
Signer: randomWord(), | ||
P2pId: nodeSet[1], | ||
HashedCapabilityIds: [][32]byte{cid}, | ||
}, | ||
{ | ||
// The first NodeOperatorId has id 1 since the id is auto-incrementing. | ||
NodeOperatorId: uint32(1), | ||
Signer: randomWord(), | ||
P2pId: nodeSet[2], | ||
HashedCapabilityIds: [][32]byte{cid}, | ||
}, | ||
} | ||
_, err = reg.AddNodes(owner, nodes) | ||
require.NoError(t, err) | ||
|
||
cfgs := []kcr.CapabilityRegistryCapabilityConfiguration{ | ||
{ | ||
CapabilityId: cid, | ||
Config: []byte(`{"hello": "world"}`), | ||
}, | ||
} | ||
_, err = reg.AddDON( | ||
owner, | ||
nodeSet, | ||
cfgs, | ||
true, | ||
true, | ||
1, | ||
) | ||
sim.Commit() | ||
|
||
require.NoError(t, err) | ||
|
||
factory := newContractReaderFactory(t, sim) | ||
reader, err := newRemoteRegistryReader(ctx, factory, regAddress.Hex()) | ||
require.NoError(t, err) | ||
|
||
s, err := reader.state(ctx) | ||
require.NoError(t, err) | ||
assert.Len(t, s.IDsToCapabilities, 1) | ||
|
||
gotCap := s.IDsToCapabilities[cid] | ||
assert.Equal(t, writeChainCapability, gotCap) | ||
|
||
assert.Len(t, s.IDsToDONs, 1) | ||
assert.Equal(t, kcr.CapabilityRegistryDONInfo{ | ||
Id: 1, // initial Id | ||
ConfigCount: 1, // initial Count | ||
IsPublic: true, | ||
AcceptsWorkflows: true, | ||
F: 1, | ||
NodeP2PIds: nodeSet, | ||
CapabilityConfigurations: cfgs, | ||
}, s.IDsToDONs[1]) | ||
|
||
assert.Len(t, s.IDsToNodes, 3) | ||
assert.Equal(t, map[p2ptypes.PeerID]kcr.CapabilityRegistryNodeInfo{ | ||
nodeSet[0]: nodes[0], | ||
nodeSet[1]: nodes[1], | ||
nodeSet[2]: nodes[2], | ||
}, s.IDsToNodes) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, why isn't this part of the
syncer.go
file? Is it to keep the file size smaller or some go pattern?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean this type assertion specifically? It's located here because it makes assertions about the reader abstraction and the interface it satisfies. Separating these out also makes it easier to mock the reader so we don't have to set up a chain and make the relevant registry calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I should've been clearer 🙈 I was wondering why the reader.go was split from the syncer.go. After reviewing the whole PR, it might make sense for the reader to do the reading and state augmentation though and the syncer being a simple service that manages the reader 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the reason why it's separate is so that the syncer can accept a mocked reader rather than a concrete implementation based on ChainReader (which therefore depends on a mocked chain + all of the state that comes with it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense