diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 94eea2b4b3a..7e846dcd545 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -304,6 +304,7 @@ require ( github.com/smartcontractkit/chainlink-protos/orchestrator v0.3.2 // indirect github.com/smartcontractkit/chainlink-solana v1.1.1-0.20241204153209-c3a71b0eef99 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.1.1-0.20241202202529-2033490e77b8 // indirect + github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.13 // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20241009055228-33d0c0bf38de // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20241009055228-33d0c0bf38de // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index e312f248ceb..0c60e0596d7 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1158,6 +1158,8 @@ github.com/smartcontractkit/chainlink-solana v1.1.1-0.20241204153209-c3a71b0eef9 github.com/smartcontractkit/chainlink-solana v1.1.1-0.20241204153209-c3a71b0eef99/go.mod h1:p8aUDfJeley6oer7y+Ucd3edOtRlMTnWg3mN6rhaLWo= github.com/smartcontractkit/chainlink-starknet/relayer v0.1.1-0.20241202202529-2033490e77b8 h1:tNS7U9lrxkFvEuyxQv11HHOiV9LPDGC9wYEy+yM/Jv4= github.com/smartcontractkit/chainlink-starknet/relayer v0.1.1-0.20241202202529-2033490e77b8/go.mod h1:EBrEgcdIbwepqguClkv8Ohy7CbyWSJaE4EC9aBJlQK0= +github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.13 h1:T0kbw07Vb6xUyA9MIJZfErMgWseWi1zf7cYvRpoq7ug= +github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.13/go.mod h1:1CKUOzoK+Ga19WuhRH9pxZ+qUUnrlIx108VEA6qSzeQ= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12 h1:NzZGjaqez21I3DU7objl3xExTH4fxYvzTqar8DC6360= diff --git a/deployment/environment/memory/chain.go b/deployment/environment/memory/chain.go index cbb3e67df7a..58f71a83a8c 100644 --- a/deployment/environment/memory/chain.go +++ b/deployment/environment/memory/chain.go @@ -47,30 +47,7 @@ func GenerateChains(t *testing.T, numChains int, numUsers int) map[uint64]EVMCha chains := make(map[uint64]EVMChain) for i := 0; i < numChains; i++ { chainID := chainsel.TEST_90000001.EvmChainID + uint64(i) - key, err := crypto.GenerateKey() - require.NoError(t, err) - owner, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337)) - require.NoError(t, err) - genesis := types.GenesisAlloc{ - owner.From: {Balance: big.NewInt(0).Mul(big.NewInt(7000), big.NewInt(params.Ether))}} - // create a set of user keys - var users []*bind.TransactOpts - for j := 0; j < numUsers; j++ { - key, err := crypto.GenerateKey() - require.NoError(t, err) - user, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337)) - require.NoError(t, err) - users = append(users, user) - genesis[user.From] = types.Account{Balance: big.NewInt(0).Mul(big.NewInt(7000), big.NewInt(params.Ether))} - } - // there have to be enough initial funds on each chain to allocate for all the nodes that share the given chain in the test - backend := simulated.NewBackend(genesis, simulated.WithBlockGasLimit(50000000)) - backend.Commit() // ts will be now. - chains[chainID] = EVMChain{ - Backend: backend, - DeployerKey: owner, - Users: users, - } + chains[chainID] = evmChain(t, numUsers) } return chains } @@ -78,18 +55,34 @@ func GenerateChains(t *testing.T, numChains int, numUsers int) map[uint64]EVMCha func GenerateChainsWithIds(t *testing.T, chainIDs []uint64) map[uint64]EVMChain { chains := make(map[uint64]EVMChain) for _, chainID := range chainIDs { + chains[chainID] = evmChain(t, 1) + } + return chains +} + +func evmChain(t *testing.T, numUsers int) EVMChain { + key, err := crypto.GenerateKey() + require.NoError(t, err) + owner, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337)) + require.NoError(t, err) + genesis := types.GenesisAlloc{ + owner.From: {Balance: big.NewInt(0).Mul(big.NewInt(700000), big.NewInt(params.Ether))}} + // create a set of user keys + var users []*bind.TransactOpts + for j := 0; j < numUsers; j++ { key, err := crypto.GenerateKey() require.NoError(t, err) - owner, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337)) + user, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337)) require.NoError(t, err) - backend := simulated.NewBackend(types.GenesisAlloc{ - owner.From: {Balance: big.NewInt(0).Mul(big.NewInt(700000), big.NewInt(params.Ether))}}, - simulated.WithBlockGasLimit(10000000)) - backend.Commit() // Note initializes block timestamp to now(). - chains[chainID] = EVMChain{ - Backend: backend, - DeployerKey: owner, - } + users = append(users, user) + genesis[user.From] = types.Account{Balance: big.NewInt(0).Mul(big.NewInt(700000), big.NewInt(params.Ether))} + } + // there have to be enough initial funds on each chain to allocate for all the nodes that share the given chain in the test + backend := simulated.NewBackend(genesis, simulated.WithBlockGasLimit(50000000)) + backend.Commit() // ts will be now. + return EVMChain{ + Backend: backend, + DeployerKey: owner, + Users: users, } - return chains } diff --git a/deployment/keystone/changeset/accept_ownership.go b/deployment/keystone/changeset/accept_ownership.go index 7dffc5a70c4..662a4c2dcfa 100644 --- a/deployment/keystone/changeset/accept_ownership.go +++ b/deployment/keystone/changeset/accept_ownership.go @@ -5,6 +5,8 @@ import ( "github.com/ethereum/go-ethereum/common" + kslib "github.com/smartcontractkit/chainlink/deployment/keystone" + "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/common/changeset" ) @@ -23,39 +25,21 @@ func AcceptAllOwnershipsProposal(e deployment.Environment, req *AcceptAllOwnersh chain := e.Chains[chainSelector] addrBook := e.ExistingAddresses - capRegs, err := capRegistriesFromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, err - } - ocr3, err := ocr3FromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, err - } - forwarders, err := forwardersFromAddrBook(addrBook, chain) - if err != nil { - return deployment.ChangesetOutput{}, err - } - consumers, err := feedsConsumersFromAddrBook(addrBook, chain) + r, err := kslib.GetContractSets(e.Logger, &kslib.GetContractSetsRequest{ + Chains: map[uint64]deployment.Chain{ + req.ChainSelector: chain, + }, + AddressBook: addrBook, + }) if err != nil { return deployment.ChangesetOutput{}, err } - var addrsToTransfer []common.Address - for _, consumer := range consumers { - addrsToTransfer = append(addrsToTransfer, consumer.Address()) - } - for _, o := range ocr3 { - addrsToTransfer = append(addrsToTransfer, o.Address()) - } - for _, f := range forwarders { - addrsToTransfer = append(addrsToTransfer, f.Address()) - } - for _, c := range capRegs { - addrsToTransfer = append(addrsToTransfer, c.Address()) - } + contracts := r.ContractSets[chainSelector] + // Construct the configuration cfg := changeset.TransferToMCMSWithTimelockConfig{ ContractsByChain: map[uint64][]common.Address{ - chainSelector: addrsToTransfer, + chainSelector: contracts.TransferableContracts(), }, MinDelay: minDelay, } diff --git a/deployment/keystone/changeset/accept_ownership_test.go b/deployment/keystone/changeset/accept_ownership_test.go index 997f0b7e163..ec65ef920ac 100644 --- a/deployment/keystone/changeset/accept_ownership_test.go +++ b/deployment/keystone/changeset/accept_ownership_test.go @@ -38,10 +38,6 @@ func TestAcceptAllOwnership(t *testing.T) { Changeset: commonchangeset.WrapChangeSet(changeset.DeployForwarder), Config: registrySel, }, - { - Changeset: commonchangeset.WrapChangeSet(changeset.DeployFeedsConsumer), - Config: &changeset.DeployFeedsConsumerRequest{ChainSelector: registrySel}, - }, { Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), Config: map[uint64]types.MCMSWithTimelockConfig{ diff --git a/deployment/keystone/changeset/configure_contracts.go b/deployment/keystone/changeset/configure_contracts.go index d5bcb32243b..17635a42ed1 100644 --- a/deployment/keystone/changeset/configure_contracts.go +++ b/deployment/keystone/changeset/configure_contracts.go @@ -9,6 +9,25 @@ import ( kslib "github.com/smartcontractkit/chainlink/deployment/keystone" ) +var _ deployment.ChangeSet[InitialContractsCfg] = ConfigureInitialContractsChangeset + +type InitialContractsCfg struct { + RegistryChainSel uint64 + Dons []kslib.DonCapabilities + OCR3Config *kslib.OracleConfigWithSecrets +} + +func ConfigureInitialContractsChangeset(e deployment.Environment, cfg InitialContractsCfg) (deployment.ChangesetOutput, error) { + req := &kslib.ConfigureContractsRequest{ + Env: &e, + RegistryChainSel: cfg.RegistryChainSel, + Dons: cfg.Dons, + OCR3Config: cfg.OCR3Config, + } + return ConfigureInitialContracts(e.Logger, req) +} + +// Deprecated: Use ConfigureInitialContractsChangeset instead. func ConfigureInitialContracts(lggr logger.Logger, req *kslib.ConfigureContractsRequest) (deployment.ChangesetOutput, error) { if err := req.Validate(); err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to validate request: %w", err) diff --git a/deployment/keystone/changeset/deploy_forwarder.go b/deployment/keystone/changeset/deploy_forwarder.go index 2dc160dcf4c..5207d99aacd 100644 --- a/deployment/keystone/changeset/deploy_forwarder.go +++ b/deployment/keystone/changeset/deploy_forwarder.go @@ -9,16 +9,10 @@ import ( var _ deployment.ChangeSet[uint64] = DeployForwarder +// DeployForwarder deploys the KeystoneForwarder contract to all chains in the environment +// callers must merge the output addressbook with the existing one func DeployForwarder(env deployment.Environment, registryChainSel uint64) (deployment.ChangesetOutput, error) { lggr := env.Logger - // expect OCR3 to be deployed & capabilities registry - regAddrs, err := env.ExistingAddresses.AddressesForChain(registryChainSel) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("no addresses found for chain %d: %w", registryChainSel, err) - } - if len(regAddrs) != 2 { - return deployment.ChangesetOutput{}, fmt.Errorf("expected 2 addresses for chain %d, got %d", registryChainSel, len(regAddrs)) - } ab := deployment.NewMemoryAddressBook() for _, chain := range env.Chains { lggr.Infow("deploying forwarder", "chainSelector", chain.Selector) diff --git a/deployment/keystone/changeset/deploy_forwarder_test.go b/deployment/keystone/changeset/deploy_forwarder_test.go index 8d73134fc1d..b6d8ec8f753 100644 --- a/deployment/keystone/changeset/deploy_forwarder_test.go +++ b/deployment/keystone/changeset/deploy_forwarder_test.go @@ -10,7 +10,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/environment/memory" - kslb "github.com/smartcontractkit/chainlink/deployment/keystone" "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" ) @@ -24,43 +23,11 @@ func TestDeployForwarder(t *testing.T) { } env := memory.NewMemoryEnvironment(t, lggr, zapcore.DebugLevel, cfg) - var ( - ocrTV = deployment.NewTypeAndVersion(kslb.OCR3Capability, deployment.Version1_0_0) - crTV = deployment.NewTypeAndVersion(kslb.CapabilitiesRegistry, deployment.Version1_0_0) - ) - registrySel := env.AllChainSelectors()[0] - t.Run("err if no capabilities registry on registry chain", func(t *testing.T) { - m := make(map[uint64]map[string]deployment.TypeAndVersion) - m[registrySel] = map[string]deployment.TypeAndVersion{ - "0x0000000000000000000000000000000000000002": ocrTV, - } - env.ExistingAddresses = deployment.NewMemoryAddressBookFromMap(m) - // capabilities registry and ocr3 must be deployed on registry chain - _, err := changeset.DeployForwarder(env, registrySel) - require.Error(t, err) - }) - - t.Run("err if no ocr3 on registry chain", func(t *testing.T) { - m := make(map[uint64]map[string]deployment.TypeAndVersion) - m[registrySel] = map[string]deployment.TypeAndVersion{ - "0x0000000000000000000000000000000000000001": crTV, - } - env.ExistingAddresses = deployment.NewMemoryAddressBookFromMap(m) - // capabilities registry and ocr3 must be deployed on registry chain - _, err := changeset.DeployForwarder(env, registrySel) - require.Error(t, err) - }) t.Run("should deploy forwarder", func(t *testing.T) { ab := deployment.NewMemoryAddressBook() - // fake capabilities registry - err := ab.Save(registrySel, "0x0000000000000000000000000000000000000001", crTV) - require.NoError(t, err) - // fake ocr3 - err = ab.Save(registrySel, "0x0000000000000000000000000000000000000002", ocrTV) - require.NoError(t, err) // deploy forwarder env.ExistingAddresses = ab resp, err := changeset.DeployForwarder(env, registrySel) diff --git a/deployment/keystone/changeset/deploy_ocr3.go b/deployment/keystone/changeset/deploy_ocr3.go index 40d9e558584..fdf51e644be 100644 --- a/deployment/keystone/changeset/deploy_ocr3.go +++ b/deployment/keystone/changeset/deploy_ocr3.go @@ -1,10 +1,11 @@ package changeset import ( + "encoding/json" "fmt" + "io" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink/deployment" kslib "github.com/smartcontractkit/chainlink/deployment/keystone" ) @@ -27,12 +28,49 @@ func DeployOCR3(env deployment.Environment, registryChainSel uint64) (deployment return deployment.ChangesetOutput{AddressBook: ab}, nil } -func ConfigureOCR3Contract(lggr logger.Logger, env deployment.Environment, cfg kslib.ConfigureOCR3Config) (deployment.ChangesetOutput, error) { +var _ deployment.ChangeSet[ConfigureOCR3Config] = ConfigureOCR3Contract + +type ConfigureOCR3Config struct { + ChainSel uint64 + NodeIDs []string + OCR3Config *kslib.OracleConfigWithSecrets + DryRun bool + WriteGeneratedConfig io.Writer // if not nil, write the generated config to this writer as JSON [OCR2OracleConfig] + + UseMCMS bool +} - _, err := kslib.ConfigureOCR3ContractFromJD(&env, cfg) +func ConfigureOCR3Contract(env deployment.Environment, cfg ConfigureOCR3Config) (deployment.ChangesetOutput, error) { + resp, err := kslib.ConfigureOCR3ContractFromJD(&env, kslib.ConfigureOCR3Config{ + ChainSel: cfg.ChainSel, + NodeIDs: cfg.NodeIDs, + OCR3Config: cfg.OCR3Config, + DryRun: cfg.DryRun, + UseMCMS: cfg.UseMCMS, + }) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to configure OCR3Capability: %w", err) } + if w := cfg.WriteGeneratedConfig; w != nil { + b, err := json.MarshalIndent(&resp.OCR2OracleConfig, "", " ") + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to marshal response output: %w", err) + } + env.Logger.Infof("Generated OCR3 config: %s", string(b)) + n, err := w.Write(b) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to write response output: %w", err) + } + if n != len(b) { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to write all bytes") + } + } // does not create any new addresses - return deployment.ChangesetOutput{}, nil + var proposals []timelock.MCMSWithTimelockProposal + if cfg.UseMCMS { + proposals = append(proposals, *resp.Proposal) + } + return deployment.ChangesetOutput{ + Proposals: proposals, + }, nil } diff --git a/deployment/keystone/changeset/deploy_ocr3_test.go b/deployment/keystone/changeset/deploy_ocr3_test.go index d3fdf118f8b..0d49af68823 100644 --- a/deployment/keystone/changeset/deploy_ocr3_test.go +++ b/deployment/keystone/changeset/deploy_ocr3_test.go @@ -1,6 +1,8 @@ package changeset_test import ( + "bytes" + "encoding/json" "testing" "go.uber.org/zap/zapcore" @@ -8,8 +10,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/environment/memory" + kslib "github.com/smartcontractkit/chainlink/deployment/keystone" "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" ) @@ -37,3 +43,103 @@ func TestDeployOCR3(t *testing.T) { oaddrs, _ := resp.AddressBook.AddressesForChain(env.AllChainSelectors()[1]) assert.Len(t, oaddrs, 0) } + +func TestConfigureOCR3(t *testing.T) { + t.Parallel() + lggr := logger.Test(t) + + c := kslib.OracleConfigWithSecrets{ + OracleConfig: kslib.OracleConfig{ + MaxFaultyOracles: 1, + DeltaProgressMillis: 12345, + }, + OCRSecrets: deployment.XXXGenerateTestOCRSecrets(), + } + + t.Run("no mcms", func(t *testing.T) { + + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 1, + }) + + var wfNodes []string + for id, _ := range te.WFNodes { + wfNodes = append(wfNodes, id) + } + + w := &bytes.Buffer{} + cfg := changeset.ConfigureOCR3Config{ + ChainSel: te.RegistrySelector, + NodeIDs: wfNodes, + OCR3Config: &c, + WriteGeneratedConfig: w, + UseMCMS: false, + } + + csOut, err := changeset.ConfigureOCR3Contract(te.Env, cfg) + require.NoError(t, err) + var got kslib.OCR2OracleConfig + err = json.Unmarshal(w.Bytes(), &got) + require.NoError(t, err) + assert.Len(t, got.Signers, 4) + assert.Len(t, got.Transmitters, 4) + assert.Nil(t, csOut.Proposals) + }) + + t.Run("mcms", func(t *testing.T) { + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 1, + UseMCMS: true, + }) + + var wfNodes []string + for id, _ := range te.WFNodes { + wfNodes = append(wfNodes, id) + } + + w := &bytes.Buffer{} + cfg := changeset.ConfigureOCR3Config{ + ChainSel: te.RegistrySelector, + NodeIDs: wfNodes, + OCR3Config: &c, + WriteGeneratedConfig: w, + UseMCMS: true, + } + + csOut, err := changeset.ConfigureOCR3Contract(te.Env, cfg) + require.NoError(t, err) + var got kslib.OCR2OracleConfig + err = json.Unmarshal(w.Bytes(), &got) + require.NoError(t, err) + assert.Len(t, got.Signers, 4) + assert.Len(t, got.Transmitters, 4) + assert.NotNil(t, csOut.Proposals) + t.Logf("got: %v", csOut.Proposals[0]) + + contractSetsResp, err := kslib.GetContractSets(lggr, &kslib.GetContractSetsRequest{ + Chains: te.Env.Chains, + AddressBook: te.Env.ExistingAddresses, + }) + require.NoError(t, err) + var timelocks = map[uint64]*gethwrappers.RBACTimelock{ + te.RegistrySelector: contractSetsResp.ContractSets[te.RegistrySelector].Timelock, + } + // now apply the changeset such that the proposal is signed and execed + w2 := &bytes.Buffer{} + cfg.WriteGeneratedConfig = w2 + _, err = commonchangeset.ApplyChangesets(t, te.Env, timelocks, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.ConfigureOCR3Contract), + Config: cfg, + }, + }) + require.NoError(t, err) + }) + +} diff --git a/deployment/keystone/changeset/helpers_test.go b/deployment/keystone/changeset/helpers_test.go new file mode 100644 index 00000000000..85e69507009 --- /dev/null +++ b/deployment/keystone/changeset/helpers_test.go @@ -0,0 +1,419 @@ +package changeset_test + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "math/big" + "sort" + + "math" + "testing" + + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "github.com/smartcontractkit/chainlink/deployment" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" + "github.com/smartcontractkit/chainlink/deployment/keystone" + kslib "github.com/smartcontractkit/chainlink/deployment/keystone" + kschangeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" +) + +func TestSetupTestEnv(t *testing.T) { + t.Parallel() + ctx := tests.Context(t) + for _, useMCMS := range []bool{true, false} { + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 3, + UseMCMS: useMCMS, + }) + t.Run(fmt.Sprintf("set up test env using MCMS: %T", useMCMS), func(t *testing.T) { + require.NotNil(t, te.Env.ExistingAddresses) + require.Len(t, te.Env.Chains, 3) + require.NotEmpty(t, te.RegistrySelector) + require.NotNil(t, te.Env.Offchain) + r, err := te.Env.Offchain.ListNodes(ctx, &node.ListNodesRequest{}) + require.NoError(t, err) + require.Len(t, r.Nodes, 12) + }) + } +} + +type DonConfig struct { + N int +} + +func (c DonConfig) Validate() error { + if c.N < 4 { + return errors.New("N must be at least 4") + } + return nil +} + +// TODO: separate the config into different types; wf should expand to types of ocr keybundles; writer to target chains; ... +type WFDonConfig = DonConfig +type AssetDonConfig = DonConfig +type WriterDonConfig = DonConfig + +type TestConfig struct { + WFDonConfig + AssetDonConfig + WriterDonConfig + NumChains int + + UseMCMS bool +} + +func (c TestConfig) Validate() error { + if err := c.WFDonConfig.Validate(); err != nil { + return err + } + if err := c.AssetDonConfig.Validate(); err != nil { + return err + } + if err := c.WriterDonConfig.Validate(); err != nil { + return err + } + if c.NumChains < 1 { + return errors.New("NumChains must be at least 1") + } + return nil +} + +type TestEnv struct { + Env deployment.Environment + RegistrySelector uint64 + + WFNodes map[string]memory.Node + CWNodes map[string]memory.Node + AssetNodes map[string]memory.Node +} + +// SetupTestEnv sets up a keystone test environment with the given configuration +func SetupTestEnv(t *testing.T, c TestConfig) TestEnv { + require.NoError(t, c.Validate()) + lggr := logger.Test(t) + ctx := tests.Context(t) + chains, _ := memory.NewMemoryChains(t, c.NumChains, 1) + registryChainSel := registryChain(t, chains) + // note that all the nodes require TOML configuration of the cap registry address + // and writers need forwarder address as TOML config + // we choose to use changesets to deploy the initial contracts because that's how it's done in the real world + // this requires a initial environment to house the address book + e := deployment.Environment{ + Logger: lggr, + Chains: chains, + ExistingAddresses: deployment.NewMemoryAddressBook(), + } + e, err := commonchangeset.ApplyChangesets(t, e, nil, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(kschangeset.DeployCapabilityRegistry), + Config: registryChainSel, + }, + { + Changeset: commonchangeset.WrapChangeSet(kschangeset.DeployOCR3), + Config: registryChainSel, + }, + { + Changeset: commonchangeset.WrapChangeSet(kschangeset.DeployForwarder), + Config: registryChainSel, + }, + }) + require.NoError(t, err) + require.NotNil(t, e) + require.Len(t, e.Chains, c.NumChains) + validateInitialChainState(t, e, registryChainSel) + // now that we have the initial contracts deployed, we can configure the nodes with the addresses + // TODO: configure the nodes with the correct override functions + crConfig := deployment.CapabilityRegistryConfig{ + EVMChainID: registryChainSel, + Contract: [20]byte{}, + } + + wfChains := map[uint64]deployment.Chain{} + wfChains[registryChainSel] = chains[registryChainSel] + wfNodes := memory.NewNodes(t, zapcore.InfoLevel, wfChains, c.WFDonConfig.N, 0, crConfig) + require.Len(t, wfNodes, c.WFDonConfig.N) + + writerChains := map[uint64]deployment.Chain{} + maps.Copy(writerChains, chains) + cwNodes := memory.NewNodes(t, zapcore.InfoLevel, writerChains, c.WriterDonConfig.N, 0, crConfig) + require.Len(t, cwNodes, c.WriterDonConfig.N) + + assetChains := map[uint64]deployment.Chain{} + assetChains[registryChainSel] = chains[registryChainSel] + assetNodes := memory.NewNodes(t, zapcore.InfoLevel, assetChains, c.AssetDonConfig.N, 0, crConfig) + require.Len(t, assetNodes, c.AssetDonConfig.N) + + // TODO: partition nodes into multiple nops + + wfDon := keystone.DonCapabilities{ + Name: keystone.WFDonName, + Nops: []keystone.NOP{ + { + Name: "nop 1", + Nodes: maps.Keys(wfNodes), + }, + }, + Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.OCR3Cap}, + } + cwDon := keystone.DonCapabilities{ + Name: keystone.TargetDonName, + Nops: []keystone.NOP{ + { + Name: "nop 2", + Nodes: maps.Keys(cwNodes), + }, + }, + Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.WriteChainCap}, + } + assetDon := keystone.DonCapabilities{ + Name: keystone.StreamDonName, + Nops: []keystone.NOP{ + { + Name: "nop 3", + Nodes: maps.Keys(assetNodes), + }, + }, + Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.StreamTriggerCap}, + } + + allChains := make(map[uint64]deployment.Chain) + maps.Copy(allChains, chains) + + allNodes := make(map[string]memory.Node) + maps.Copy(allNodes, wfNodes) + maps.Copy(allNodes, cwNodes) + maps.Copy(allNodes, assetNodes) + env := memory.NewMemoryEnvironmentFromChainsNodes(func() context.Context { return ctx }, lggr, allChains, allNodes) + // set the env addresses to the deployed addresses that were created prior to configuring the nodes + err = env.ExistingAddresses.Merge(e.ExistingAddresses) + require.NoError(t, err) + + var ocr3Config = keystone.OracleConfigWithSecrets{ + OracleConfig: keystone.OracleConfig{ + MaxFaultyOracles: len(wfNodes) / 3, + }, + OCRSecrets: deployment.XXXGenerateTestOCRSecrets(), + } + var allDons = []keystone.DonCapabilities{wfDon, cwDon, assetDon} + + _, err = kschangeset.ConfigureInitialContractsChangeset(env, kschangeset.InitialContractsCfg{ + RegistryChainSel: registryChainSel, + Dons: allDons, + OCR3Config: &ocr3Config, + }) + require.NoError(t, err) + // TODO: KS-rm_deploy_opt + //require.Nil(t, csOut.AddressBook, "no new addresses should be created in configure initial contracts") + //require.NoError(t, env.ExistingAddresses.Merge(csOut.AddressBook)) + + req := &keystone.GetContractSetsRequest{ + Chains: env.Chains, + AddressBook: env.ExistingAddresses, + } + + contractSetsResp, err := keystone.GetContractSets(lggr, req) + require.NoError(t, err) + require.Len(t, contractSetsResp.ContractSets, len(env.Chains)) + // check the registry + gotRegistry := contractSetsResp.ContractSets[registryChainSel].CapabilitiesRegistry + require.NotNil(t, gotRegistry) + // validate the registry + // check the nodes + gotNodes, err := gotRegistry.GetNodes(nil) + require.NoError(t, err) + require.Len(t, gotNodes, len(allNodes)) + validateNodes(t, gotRegistry, wfNodes, expectedHashedCapabilities(t, gotRegistry, wfDon)) + validateNodes(t, gotRegistry, cwNodes, expectedHashedCapabilities(t, gotRegistry, cwDon)) + validateNodes(t, gotRegistry, assetNodes, expectedHashedCapabilities(t, gotRegistry, assetDon)) + + // check the dons + validateDon(t, gotRegistry, wfNodes, wfDon) + validateDon(t, gotRegistry, cwNodes, cwDon) + validateDon(t, gotRegistry, assetNodes, assetDon) + + if c.UseMCMS { + // TODO: mcms on all the chains, currently only on the registry chain. need to fix this for forwarders + t.Logf("Enabling MCMS registry chain %d", registryChainSel) + // deploy, configure and xfer ownership of MCMS + env, err = commonchangeset.ApplyChangesets(t, env, nil, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), + Config: map[uint64]commontypes.MCMSWithTimelockConfig{ + registryChainSel: { + Canceller: commonchangeset.SingleGroupMCMS(t), + Bypasser: commonchangeset.SingleGroupMCMS(t), + Proposer: commonchangeset.SingleGroupMCMS(t), + TimelockExecutors: env.AllDeployerKeys(), + TimelockMinDelay: big.NewInt(0), + }, + }, + }, + }) + require.NoError(t, err) + // extract the MCMS address + r, err := kslib.GetContractSets(lggr, &kslib.GetContractSetsRequest{ + Chains: map[uint64]deployment.Chain{ + registryChainSel: env.Chains[registryChainSel], + }, + AddressBook: env.ExistingAddresses, + }) + require.NoError(t, err) + mcms := r.ContractSets[registryChainSel].MCMSWithTimelockState + require.NotNil(t, mcms) + // transfer ownership of all contracts to the MCMS + env, err = commonchangeset.ApplyChangesets(t, env, map[uint64]*gethwrappers.RBACTimelock{registryChainSel: mcms.Timelock}, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(kschangeset.AcceptAllOwnershipsProposal), + Config: &kschangeset.AcceptAllOwnershipRequest{ + ChainSelector: registryChainSel, + MinDelay: 0, + }, + }, + }) + require.NoError(t, err) + // ensure the MCMS is deployed + req = &keystone.GetContractSetsRequest{ + Chains: env.Chains, + AddressBook: env.ExistingAddresses, + } + contractSetsResp, err = keystone.GetContractSets(lggr, req) + require.NoError(t, err) + require.Len(t, contractSetsResp.ContractSets, len(env.Chains)) + // check the mcms contract on registry chain + gotMCMS := contractSetsResp.ContractSets[registryChainSel].MCMSWithTimelockState + require.NoError(t, gotMCMS.Validate()) + } + return TestEnv{ + Env: env, + RegistrySelector: registryChainSel, + WFNodes: wfNodes, + CWNodes: cwNodes, + AssetNodes: assetNodes, + } +} + +func registryChain(t *testing.T, chains map[uint64]deployment.Chain) uint64 { + var registryChainSel uint64 = math.MaxUint64 + for sel := range chains { + if sel < registryChainSel { + registryChainSel = sel + } + } + return registryChainSel +} + +// validateInitialChainState checks that the initial chain state +// has the expected contracts deployed +func validateInitialChainState(t *testing.T, env deployment.Environment, registryChainSel uint64) { + ad := env.ExistingAddresses + // all contracts on registry chain + registryChainAddrs, err := ad.AddressesForChain(registryChainSel) + require.NoError(t, err) + require.Len(t, registryChainAddrs, 3) // registry, ocr3, forwarder + // only forwarder on non-home chain + for sel := range env.Chains { + chainAddrs, err := ad.AddressesForChain(sel) + require.NoError(t, err) + if sel != registryChainSel { + require.Len(t, chainAddrs, 1) + } else { + require.Len(t, chainAddrs, 3) + } + containsForwarder := false + for _, tv := range chainAddrs { + if tv.Type == keystone.KeystoneForwarder { + containsForwarder = true + break + } + } + require.True(t, containsForwarder, "no forwarder found in %v on chain %d for target don", chainAddrs, sel) + } +} + +// validateNodes checks that the nodes exist and have the expected capabilities +func validateNodes(t *testing.T, gotRegistry *kcr.CapabilitiesRegistry, nodes map[string]memory.Node, expectedHashedCaps [][32]byte) { + gotNodes, err := gotRegistry.GetNodesByP2PIds(nil, p2pIDs(t, maps.Keys(nodes))) + require.NoError(t, err) + require.Len(t, gotNodes, len(nodes)) + for _, n := range gotNodes { + require.Equal(t, expectedHashedCaps, n.HashedCapabilityIds) + } +} + +// validateDon checks that the don exists and has the expected capabilities +func validateDon(t *testing.T, gotRegistry *kcr.CapabilitiesRegistry, nodes map[string]memory.Node, don kslib.DonCapabilities) { + gotDons, err := gotRegistry.GetDONs(nil) + require.NoError(t, err) + wantP2PID := sortedHash(p2pIDs(t, maps.Keys(nodes))) + found := false + for _, have := range gotDons { + gotP2PID := sortedHash(have.NodeP2PIds) + if gotP2PID == wantP2PID { + found = true + gotCapIDs := capIDs(t, have.CapabilityConfigurations) + require.Equal(t, expectedHashedCapabilities(t, gotRegistry, don), gotCapIDs) + break + } + } + require.True(t, found, "don not found in registry") +} + +func capIDs(t *testing.T, cfgs []kcr.CapabilitiesRegistryCapabilityConfiguration) [][32]byte { + var out [][32]byte + for _, cfg := range cfgs { + out = append(out, cfg.CapabilityId) + } + return out +} + +func ptr[T any](t T) *T { + return &t +} + +func p2pIDs(t *testing.T, vals []string) [][32]byte { + var out [][32]byte + for _, v := range vals { + id, err := p2pkey.MakePeerID(v) + require.NoError(t, err) + out = append(out, id) + } + return out +} + +func expectedHashedCapabilities(t *testing.T, registry *kcr.CapabilitiesRegistry, don kslib.DonCapabilities) [][32]byte { + out := make([][32]byte, len(don.Capabilities)) + var err error + for i, cap := range don.Capabilities { + out[i], err = registry.GetHashedCapabilityId(nil, cap.LabelledName, cap.Version) + require.NoError(t, err) + } + return out +} + +func sortedHash(p2pids [][32]byte) string { + sha256Hash := sha256.New() + sort.Slice(p2pids, func(i, j int) bool { + return bytes.Compare(p2pids[i][:], p2pids[j][:]) < 0 + }) + for _, id := range p2pids { + sha256Hash.Write(id[:]) + } + return hex.EncodeToString(sha256Hash.Sum(nil)) +} diff --git a/deployment/keystone/changeset/internal/test/utils.go b/deployment/keystone/changeset/internal/test/utils.go index df01266043a..6fe4a8f4a2e 100644 --- a/deployment/keystone/changeset/internal/test/utils.go +++ b/deployment/keystone/changeset/internal/test/utils.go @@ -158,7 +158,6 @@ func addDons(t *testing.T, lggr logger.Logger, chain deployment.Chain, registry cc.Config = defaultCapConfig(t, ccfg.Capability) } var exists bool - //var cc kcr.CapabilitiesRegistryCapabilityConfiguration{} cc.CapabilityId, exists = capCache.Get(ccfg.Capability) require.True(t, exists, "capability not found in cache %v", ccfg.Capability) capConfigs = append(capConfigs, cc) diff --git a/deployment/keystone/deploy.go b/deployment/keystone/deploy.go index 374f7f06460..2aa26312eae 100644 --- a/deployment/keystone/deploy.go +++ b/deployment/keystone/deploy.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" "golang.org/x/exp/maps" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink/deployment" "google.golang.org/protobuf/proto" @@ -40,6 +41,7 @@ type ConfigureContractsRequest struct { Dons []DonCapabilities // externally sourced based on the environment OCR3Config *OracleConfigWithSecrets // TODO: probably should be a map of don to config; but currently we only have one wf don therefore one config + // TODO rm this option; unused DoContractDeploy bool // if false, the contracts are assumed to be deployed and the address book is used } @@ -75,6 +77,7 @@ func ConfigureContracts(ctx context.Context, lggr logger.Logger, req ConfigureCo } addrBook := req.Env.ExistingAddresses + // TODO: KS-rm_deploy_opt remove this option; it's not used if req.DoContractDeploy { contractDeployCS, err := DeployContracts(req.Env, req.RegistryChainSel) if err != nil { @@ -320,6 +323,7 @@ func ConfigureForwardContracts(env *deployment.Environment, dons []RegisteredDon return nil } +// Depreciated: use changeset.ConfigureOCR3Contract instead // ocr3 contract on the registry chain for the wf dons func ConfigureOCR3Contract(env *deployment.Environment, chainSel uint64, dons []RegisteredDon, addrBook deployment.AddressBook, cfg *OracleConfigWithSecrets) error { registryChain, ok := env.Chains[chainSel] @@ -350,10 +354,11 @@ func ConfigureOCR3Contract(env *deployment.Environment, chainSel uint64, dons [] } _, err := configureOCR3contract(configureOCR3Request{ - cfg: cfg, - chain: registryChain, - contract: contract, - nodes: don.Nodes, + cfg: cfg, + chain: registryChain, + contract: contract, + nodes: don.Nodes, + contractSet: &contracts, }) if err != nil { return fmt.Errorf("failed to configure OCR3 contract for don %s: %w", don.Name, err) @@ -364,6 +369,7 @@ func ConfigureOCR3Contract(env *deployment.Environment, chainSel uint64, dons [] type ConfigureOCR3Resp struct { OCR2OracleConfig + Proposal *timelock.MCMSWithTimelockProposal } type ConfigureOCR3Config struct { @@ -371,8 +377,11 @@ type ConfigureOCR3Config struct { NodeIDs []string OCR3Config *OracleConfigWithSecrets DryRun bool + + UseMCMS bool } +// Depreciated: use changeset.ConfigureOCR3Contract instead func ConfigureOCR3ContractFromJD(env *deployment.Environment, cfg ConfigureOCR3Config) (*ConfigureOCR3Resp, error) { prefix := "" if cfg.DryRun { @@ -403,17 +412,20 @@ func ConfigureOCR3ContractFromJD(env *deployment.Environment, cfg ConfigureOCR3C return nil, err } r, err := configureOCR3contract(configureOCR3Request{ - cfg: cfg.OCR3Config, - chain: registryChain, - contract: contract, - nodes: nodes, - dryRun: cfg.DryRun, + cfg: cfg.OCR3Config, + chain: registryChain, + contract: contract, + nodes: nodes, + dryRun: cfg.DryRun, + contractSet: &contracts, + useMCMS: cfg.UseMCMS, }) if err != nil { return nil, err } return &ConfigureOCR3Resp{ OCR2OracleConfig: r.ocrConfig, + Proposal: r.proposal, }, nil } diff --git a/deployment/keystone/deploy_test.go b/deployment/keystone/deploy_test.go index b02497c22fa..fd59f3007fd 100644 --- a/deployment/keystone/deploy_test.go +++ b/deployment/keystone/deploy_test.go @@ -1,7 +1,6 @@ package keystone_test import ( - "context" "encoding/json" "fmt" "os" @@ -22,192 +21,9 @@ import ( kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/stretchr/testify/assert" - "github.com/test-go/testify/require" - "go.uber.org/zap/zapcore" - "golang.org/x/exp/maps" + "github.com/stretchr/testify/require" ) -func TestDeploy(t *testing.T) { - lggr := logger.Test(t) - ctx := tests.Context(t) - - // sepolia; all nodes are on the this chain - sepoliaChainId := uint64(11155111) - sepoliaArbitrumChainId := uint64(421614) - - sepoliaChainSel, err := chainsel.SelectorFromChainId(sepoliaChainId) - require.NoError(t, err) - // sepoliaArbitrumChainSel, err := chainsel.SelectorFromChainId(sepoliaArbitrumChainId) - // require.NoError(t, err) - // aptos-testnet - aptosChainSel := chainsel.AptosChainIdToChainSelector()[2] - - crConfig := deployment.CapabilityRegistryConfig{ - EVMChainID: sepoliaChainId, - Contract: [20]byte{}, - } - - evmChains := memory.NewMemoryChainsWithChainIDs(t, []uint64{sepoliaChainId, sepoliaArbitrumChainId}) - aptosChain := memory.NewMemoryChain(t, aptosChainSel) - - wfChains := map[uint64]deployment.Chain{} - wfChains[sepoliaChainSel] = evmChains[sepoliaChainSel] - wfChains[aptosChainSel] = aptosChain - wfNodes := memory.NewNodes(t, zapcore.InfoLevel, wfChains, 4, 0, crConfig) - require.Len(t, wfNodes, 4) - - cwNodes := memory.NewNodes(t, zapcore.InfoLevel, evmChains, 4, 0, crConfig) - - assetChains := map[uint64]deployment.Chain{} - assetChains[sepoliaChainSel] = evmChains[sepoliaChainSel] - assetNodes := memory.NewNodes(t, zapcore.InfoLevel, assetChains, 4, 0, crConfig) - require.Len(t, assetNodes, 4) - - // TODO: partition nodes into multiple nops - - wfDon := keystone.DonCapabilities{ - Name: keystone.WFDonName, - Nops: []keystone.NOP{ - { - Name: "nop 1", - Nodes: maps.Keys(wfNodes), - }, - }, - Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.OCR3Cap}, - } - cwDon := keystone.DonCapabilities{ - Name: keystone.TargetDonName, - Nops: []keystone.NOP{ - { - Name: "nop 2", - Nodes: maps.Keys(cwNodes), - }, - }, - Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.WriteChainCap}, - } - assetDon := keystone.DonCapabilities{ - Name: keystone.StreamDonName, - Nops: []keystone.NOP{ - { - Name: "nop 3", - Nodes: maps.Keys(assetNodes), - }, - }, - Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.StreamTriggerCap}, - } - - allChains := make(map[uint64]deployment.Chain) - maps.Copy(allChains, evmChains) - // allChains[aptosChainSel] = aptosChain - - allNodes := make(map[string]memory.Node) - maps.Copy(allNodes, wfNodes) - maps.Copy(allNodes, cwNodes) - maps.Copy(allNodes, assetNodes) - env := memory.NewMemoryEnvironmentFromChainsNodes(func() context.Context { return ctx }, lggr, allChains, allNodes) - - var ocr3Config = keystone.OracleConfigWithSecrets{ - OracleConfig: keystone.OracleConfig{ - MaxFaultyOracles: len(wfNodes) / 3, - }, - OCRSecrets: deployment.XXXGenerateTestOCRSecrets(), - } - - // explicitly deploy the contracts - cs, err := keystone.DeployContracts(&env, sepoliaChainSel) - require.NoError(t, err) - env.ExistingAddresses = cs.AddressBook - deployReq := keystone.ConfigureContractsRequest{ - RegistryChainSel: sepoliaChainSel, - Env: &env, - OCR3Config: &ocr3Config, - Dons: []keystone.DonCapabilities{wfDon, cwDon, assetDon}, - DoContractDeploy: false, - } - deployResp, err := keystone.ConfigureContracts(ctx, lggr, deployReq) - require.NoError(t, err) - ad := deployResp.Changeset.AddressBook - addrs, err := ad.Addresses() - require.NoError(t, err) - lggr.Infow("Deployed Keystone contracts", "address book", addrs) - - // all contracts on home chain - homeChainAddrs, err := ad.AddressesForChain(sepoliaChainSel) - require.NoError(t, err) - require.Len(t, homeChainAddrs, 3) - // only forwarder on non-home chain - for sel := range env.Chains { - chainAddrs, err := ad.AddressesForChain(sel) - require.NoError(t, err) - if sel != sepoliaChainSel { - require.Len(t, chainAddrs, 1) - } else { - require.Len(t, chainAddrs, 3) - } - containsForwarder := false - for _, tv := range chainAddrs { - if tv.Type == keystone.KeystoneForwarder { - containsForwarder = true - break - } - } - require.True(t, containsForwarder, "no forwarder found in %v on chain %d for target don", chainAddrs, sel) - } - req := &keystone.GetContractSetsRequest{ - Chains: env.Chains, - AddressBook: ad, - } - - contractSetsResp, err := keystone.GetContractSets(lggr, req) - require.NoError(t, err) - require.Len(t, contractSetsResp.ContractSets, len(env.Chains)) - // check the registry - regChainContracts, ok := contractSetsResp.ContractSets[sepoliaChainSel] - require.True(t, ok) - gotRegistry := regChainContracts.CapabilitiesRegistry - require.NotNil(t, gotRegistry) - // contract reads - gotDons, err := gotRegistry.GetDONs(&bind.CallOpts{}) - if err != nil { - err = keystone.DecodeErr(kcr.CapabilitiesRegistryABI, err) - require.Fail(t, fmt.Sprintf("failed to get Dons from registry at %s: %s", gotRegistry.Address().String(), err)) - } - require.NoError(t, err) - assert.Len(t, gotDons, len(deployReq.Dons)) - - for n, info := range deployResp.DonInfos { - found := false - for _, gdon := range gotDons { - if gdon.Id == info.Id { - found = true - assert.EqualValues(t, info, gdon) - break - } - } - require.True(t, found, "don %s not found in registry", n) - } - // check the forwarder - for _, cs := range contractSetsResp.ContractSets { - forwarder := cs.Forwarder - require.NotNil(t, forwarder) - // any read to ensure that the contract is deployed correctly - _, err := forwarder.Owner(&bind.CallOpts{}) - require.NoError(t, err) - // TODO expand this test; there is no get method on the forwarder so unclear how to test it - } - // check the ocr3 contract - for chainSel, cs := range contractSetsResp.ContractSets { - if chainSel != sepoliaChainSel { - require.Nil(t, cs.OCR3) - continue - } - require.NotNil(t, cs.OCR3) - // any read to ensure that the contract is deployed correctly - _, err := cs.OCR3.LatestConfigDetails(&bind.CallOpts{}) - require.NoError(t, err) - } -} - // TODO: Deprecated, remove everything below that leverages CLO func nodeOperatorsToIDs(t *testing.T, nops []*models.NodeOperator) (nodeIDs []keystone.NOP) { diff --git a/deployment/keystone/ocr3config.go b/deployment/keystone/ocr3config.go index a281a69ed8a..c4f8714b113 100644 --- a/deployment/keystone/ocr3config.go +++ b/deployment/keystone/ocr3config.go @@ -8,6 +8,8 @@ import ( "encoding/json" "errors" "fmt" + "math/big" + "strings" "time" "github.com/ethereum/go-ethereum/common" @@ -16,7 +18,11 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" kocr3 "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/ocr3_capability" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" @@ -107,6 +113,44 @@ func (c OCR2OracleConfig) MarshalJSON() ([]byte, error) { return json.Marshal(alias) } +func (c *OCR2OracleConfig) UnmarshalJSON(data []byte) error { + type aliasT struct { + Signers []string + Transmitters []string + F uint8 + OnchainConfig string + OffchainConfigVersion uint64 + OffchainConfig string + } + var alias aliasT + err := json.Unmarshal(data, &alias) + if err != nil { + return fmt.Errorf("failed to unmarshal OCR2OracleConfig alias: %w", err) + } + c.F = alias.F + c.OffchainConfigVersion = alias.OffchainConfigVersion + c.Signers = make([][]byte, len(alias.Signers)) + for i, signer := range alias.Signers { + c.Signers[i], err = hex.DecodeString(strings.TrimPrefix(signer, "0x")) + if err != nil { + return fmt.Errorf("failed to decode signer: %w", err) + } + } + c.Transmitters = make([]common.Address, len(alias.Transmitters)) + for i, transmitter := range alias.Transmitters { + c.Transmitters[i] = common.HexToAddress(transmitter) + } + c.OnchainConfig, err = hex.DecodeString(strings.TrimPrefix(alias.OnchainConfig, "0x")) + if err != nil { + return fmt.Errorf("failed to decode onchain config: %w", err) + } + c.OffchainConfig, err = hex.DecodeString(strings.TrimPrefix(alias.OffchainConfig, "0x")) + if err != nil { + return fmt.Errorf("failed to decode offchain config: %w", err) + } + return nil +} + func GenerateOCR3Config(cfg OracleConfigWithSecrets, nca []NodeKeys) (OCR2OracleConfig, error) { onchainPubKeys := [][]byte{} allPubKeys := map[string]any{} @@ -245,6 +289,9 @@ type configureOCR3Request struct { contract *kocr3.OCR3Capability nodes []deployment.Node dryRun bool + + useMCMS bool + contractSet *ContractSet } func (r configureOCR3Request) generateOCR3Config() (OCR2OracleConfig, error) { @@ -254,6 +301,7 @@ func (r configureOCR3Request) generateOCR3Config() (OCR2OracleConfig, error) { type configureOCR3Response struct { ocrConfig OCR2OracleConfig + proposal *timelock.MCMSWithTimelockProposal } func configureOCR3contract(req configureOCR3Request) (*configureOCR3Response, error) { @@ -265,9 +313,15 @@ func configureOCR3contract(req configureOCR3Request) (*configureOCR3Response, er return nil, fmt.Errorf("failed to generate OCR3 config: %w", err) } if req.dryRun { - return &configureOCR3Response{ocrConfig}, nil + return &configureOCR3Response{ocrConfig, nil}, nil } - tx, err := req.contract.SetConfig(req.chain.DeployerKey, + + txOpt := req.chain.DeployerKey + if req.useMCMS { + txOpt = deployment.SimTransactOpts() + } + + tx, err := req.contract.SetConfig(txOpt, ocrConfig.Signers, ocrConfig.Transmitters, ocrConfig.F, @@ -277,12 +331,45 @@ func configureOCR3contract(req configureOCR3Request) (*configureOCR3Response, er ) if err != nil { err = DecodeErr(kocr3.OCR3CapabilityABI, err) - return nil, fmt.Errorf("failed to call SetConfig for OCR3 contract %s: %w", req.contract.Address().String(), err) + return nil, fmt.Errorf("failed to call SetConfig for OCR3 contract %s using mcms: %T: %w", req.contract.Address().String(), req.useMCMS, err) } - _, err = req.chain.Confirm(tx) - if err != nil { - err = DecodeErr(kocr3.OCR3CapabilityABI, err) - return nil, fmt.Errorf("failed to confirm SetConfig for OCR3 contract %s: %w", req.contract.Address().String(), err) + + var proposal *timelock.MCMSWithTimelockProposal + if !req.useMCMS { + _, err = req.chain.Confirm(tx) + if err != nil { + err = DecodeErr(kocr3.OCR3CapabilityABI, err) + return nil, fmt.Errorf("failed to confirm SetConfig for OCR3 contract %s: %w", req.contract.Address().String(), err) + } + } else { + ops := timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(req.chain.Selector), + Batch: []mcms.Operation{ + { + To: req.contract.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + } + timelocksPerChain := map[uint64]common.Address{ + req.chain.Selector: req.contractSet.Timelock.Address(), + } + proposerMCMSes := map[uint64]*gethwrappers.ManyChainMultiSig{ + req.chain.Selector: req.contractSet.ProposerMcm, + } + + proposal, err = proposalutils.BuildProposalFromBatches( + timelocksPerChain, + proposerMCMSes, + []timelock.BatchChainOperation{ops}, + "proposal to set ocr3 config", + 0, + ) + if err != nil { + return nil, fmt.Errorf("failed to build proposal: %w", err) + } } - return &configureOCR3Response{ocrConfig}, nil + + return &configureOCR3Response{ocrConfig, proposal}, nil } diff --git a/deployment/keystone/state.go b/deployment/keystone/state.go index 68f2ab97a5d..1e6ffdd895f 100644 --- a/deployment/keystone/state.go +++ b/deployment/keystone/state.go @@ -8,6 +8,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/deployment" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" common_v1_0 "github.com/smartcontractkit/chainlink/deployment/common/view/v1_0" "github.com/smartcontractkit/chainlink/deployment/keystone/view" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" @@ -25,11 +26,26 @@ type GetContractSetsResponse struct { } type ContractSet struct { + commonchangeset.MCMSWithTimelockState OCR3 *ocr3_capability.OCR3Capability Forwarder *forwarder.KeystoneForwarder CapabilitiesRegistry *capabilities_registry.CapabilitiesRegistry } +func (cs ContractSet) TransferableContracts() []common.Address { + var out []common.Address + if cs.OCR3 != nil { + out = append(out, cs.OCR3.Address()) + } + if cs.Forwarder != nil { + out = append(out, cs.Forwarder.Address()) + } + if cs.CapabilitiesRegistry != nil { + out = append(out, cs.CapabilitiesRegistry.Address()) + } + return out +} + func (cs ContractSet) View() (view.KeystoneChainView, error) { out := view.NewKeystoneChainView() if cs.CapabilitiesRegistry != nil { @@ -62,6 +78,11 @@ func GetContractSets(lggr logger.Logger, req *GetContractSetsRequest) (*GetContr func loadContractSet(lggr logger.Logger, chain deployment.Chain, addresses map[string]deployment.TypeAndVersion) (*ContractSet, error) { var out ContractSet + mcmsWithTimelock, err := commonchangeset.LoadMCMSWithTimelockState(chain, addresses) + if err != nil { + return nil, fmt.Errorf("failed to load mcms contract: %w", err) + } + out.MCMSWithTimelockState = *mcmsWithTimelock for addr, tv := range addresses { // todo handle versions