diff --git a/integration-tests/deployment/ccip/add_chain.go b/integration-tests/deployment/ccip/add_chain.go new file mode 100644 index 00000000000..bc997f0dc5e --- /dev/null +++ b/integration-tests/deployment/ccip/add_chain.go @@ -0,0 +1,170 @@ +package ccipdeployment + +import ( + "math/big" + + "github.com/smartcontractkit/ccip-owner-contracts/tools/proposal/mcms" + "github.com/smartcontractkit/ccip-owner-contracts/tools/proposal/timelock" + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-ccip/chainconfig" + "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" +) + +// NewChainInboundProposal generates a proposal +// to connect the new chain to the existing chains. +func NewChainInboundProposal( + e deployment.Environment, + state CCIPOnChainState, + homeChainSel uint64, + newChainSel uint64, + sources []uint64, +) (*timelock.MCMSWithTimelockProposal, error) { + // Generate proposal which enables new destination (from test router) on all source chains. + var batches []timelock.BatchChainOperation + metaDataPerChain := make(map[mcms.ChainIdentifier]timelock.MCMSWithTimelockChainMetadata) + for _, source := range sources { + chain, _ := chainsel.ChainBySelector(source) + enableOnRampDest, err := state.Chains[source].OnRamp.ApplyDestChainConfigUpdates(SimTransactOpts(), []onramp.OnRampDestChainConfigArgs{ + { + DestChainSelector: newChainSel, + Router: state.Chains[source].TestRouter.Address(), + }, + }) + if err != nil { + return nil, err + } + enableFeeQuoterDest, err := state.Chains[source].FeeQuoter.ApplyDestChainConfigUpdates( + SimTransactOpts(), + []fee_quoter.FeeQuoterDestChainConfigArgs{ + { + DestChainSelector: newChainSel, + DestChainConfig: defaultFeeQuoterDestChainConfig(), + }, + }) + if err != nil { + return nil, err + } + initialPrices, err := state.Chains[source].FeeQuoter.UpdatePrices( + SimTransactOpts(), + fee_quoter.InternalPriceUpdates{ + TokenPriceUpdates: []fee_quoter.InternalTokenPriceUpdate{}, + GasPriceUpdates: []fee_quoter.InternalGasPriceUpdate{ + { + DestChainSelector: newChainSel, + // TODO: parameterize + UsdPerUnitGas: big.NewInt(2e12), + }, + }}) + if err != nil { + return nil, err + } + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chain.Selector), + Batch: []mcms.Operation{ + { + // Enable the source in on ramp + To: state.Chains[source].OnRamp.Address(), + Data: enableOnRampDest.Data(), + Value: big.NewInt(0), + }, + { + // Set initial dest prices to unblock testing. + To: state.Chains[source].FeeQuoter.Address(), + Data: initialPrices.Data(), + Value: big.NewInt(0), + }, + { + To: state.Chains[source].FeeQuoter.Address(), + Data: enableFeeQuoterDest.Data(), + Value: big.NewInt(0), + }, + }, + }) + metaDataPerChain[mcms.ChainIdentifier(chain.Selector)] = timelock.MCMSWithTimelockChainMetadata{ + ChainMetadata: mcms.ChainMetadata{ + NonceOffset: 0, + MCMAddress: state.Chains[source].Mcm.Address(), + }, + TimelockAddress: state.Chains[source].Timelock.Address(), + } + } + + // Home chain new don. + // - Add new DONs for destination to home chain + nodes, err := deployment.NodeInfo(e.NodeIDs, e.Offchain) + if err != nil { + return nil, err + } + encodedExtraChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ + GasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(1000), + DAGasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(0), + OptimisticConfirmations: 1, + }) + if err != nil { + return nil, err + } + chainConfig := SetupConfigInfo(newChainSel, nodes.NonBootstraps().PeerIDs(), + nodes.DefaultF(), encodedExtraChainConfig) + addChain, err := state.Chains[homeChainSel].CCIPConfig.ApplyChainConfigUpdates(SimTransactOpts(), nil, []ccip_config.CCIPConfigTypesChainConfigInfo{ + chainConfig, + }) + if err != nil { + return nil, err + } + + newDONArgs, err := BuildAddDONArgs(e.Logger, state.Chains[newChainSel].OffRamp, e.Chains[newChainSel], nodes.NonBootstraps()) + if err != nil { + return nil, err + } + addDON, err := state.Chains[homeChainSel].CapabilityRegistry.AddDON(SimTransactOpts(), + nodes.NonBootstraps().PeerIDs(), []capabilities_registry.CapabilitiesRegistryCapabilityConfiguration{ + { + CapabilityId: CCIPCapabilityID, + Config: newDONArgs, + }, + }, false, false, nodes.NonBootstraps().DefaultF()) + if err != nil { + return nil, err + } + homeChain, _ := chainsel.ChainBySelector(homeChainSel) + metaDataPerChain[mcms.ChainIdentifier(homeChain.Selector)] = timelock.MCMSWithTimelockChainMetadata{ + ChainMetadata: mcms.ChainMetadata{ + NonceOffset: 0, + MCMAddress: state.Chains[homeChainSel].Mcm.Address(), + }, + TimelockAddress: state.Chains[homeChainSel].Timelock.Address(), + } + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(homeChain.Selector), + Batch: []mcms.Operation{ + { + // Add the chain first, don needs it to be there. + To: state.Chains[homeChainSel].CCIPConfig.Address(), + Data: addChain.Data(), + Value: big.NewInt(0), + }, + { + To: state.Chains[homeChainSel].CapabilityRegistry.Address(), + Data: addDON.Data(), + Value: big.NewInt(0), + }, + }, + }) + return timelock.NewMCMSWithTimelockProposal( + "1", + 2004259681, // TODO: should be parameterized and based on current block timestamp. + []mcms.Signature{}, + false, + metaDataPerChain, + "blah", // TODO + batches, + timelock.Schedule, + "0s", // TODO: Should be parameterized. + ) +} diff --git a/integration-tests/deployment/ccip/add_chain_test.go b/integration-tests/deployment/ccip/add_chain_test.go new file mode 100644 index 00000000000..a484bda0f2d --- /dev/null +++ b/integration-tests/deployment/ccip/add_chain_test.go @@ -0,0 +1,157 @@ +package ccipdeployment + +import ( + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestAddChainInbound(t *testing.T) { + // 4 chains where the 4th is added after initial deployment. + e := NewEnvironmentWithCRAndJobs(t, logger.TestLogger(t), 4) + require.Equal(t, len(e.Nodes), 5) + state, err := LoadOnchainState(e.Env, e.Ab) + require.NoError(t, err) + // Take first non-home chain as the new chain. + newChain := e.Env.AllChainSelectorsExcluding([]uint64{e.HomeChainSel})[0] + // We deploy to the rest. + initialDeploy := e.Env.AllChainSelectorsExcluding([]uint64{newChain}) + + ab, err := DeployCCIPContracts(e.Env, DeployCCIPContractConfig{ + HomeChainSel: e.HomeChainSel, + ChainsToDeploy: initialDeploy, + CCIPOnChainState: state, + }) + require.NoError(t, err) + require.NoError(t, e.Ab.Merge(ab)) + state, err = LoadOnchainState(e.Env, e.Ab) + require.NoError(t, err) + + // Connect all the existing lanes. + for _, source := range initialDeploy { + for _, dest := range initialDeploy { + if source != dest { + require.NoError(t, AddLane(e.Env, state, source, dest)) + } + } + } + + // Deploy contracts to new chain + newAddresses, err := DeployChainContracts(e.Env, e.Env.Chains[newChain], deployment.NewMemoryAddressBook()) + require.NoError(t, err) + require.NoError(t, e.Ab.Merge(newAddresses)) + state, err = LoadOnchainState(e.Env, e.Ab) + require.NoError(t, err) + + // Transfer onramp/fq ownership to timelock. + // Enable the new dest on the test router. + for _, source := range initialDeploy { + tx, err := state.Chains[source].OnRamp.TransferOwnership(e.Env.Chains[source].DeployerKey, state.Chains[source].Timelock.Address()) + require.NoError(t, err) + _, err = deployment.ConfirmIfNoError(e.Env.Chains[source], tx, err) + require.NoError(t, err) + tx, err = state.Chains[source].FeeQuoter.TransferOwnership(e.Env.Chains[source].DeployerKey, state.Chains[source].Timelock.Address()) + require.NoError(t, err) + _, err = deployment.ConfirmIfNoError(e.Env.Chains[source], tx, err) + require.NoError(t, err) + tx, err = state.Chains[source].TestRouter.ApplyRampUpdates(e.Env.Chains[source].DeployerKey, []router.RouterOnRamp{ + { + DestChainSelector: newChain, + OnRamp: state.Chains[source].OnRamp.Address(), + }, + }, nil, nil) + _, err = deployment.ConfirmIfNoError(e.Env.Chains[source], tx, err) + require.NoError(t, err) + } + // Transfer CR contract ownership + tx, err := state.Chains[e.HomeChainSel].CapabilityRegistry.TransferOwnership(e.Env.Chains[e.HomeChainSel].DeployerKey, state.Chains[e.HomeChainSel].Timelock.Address()) + require.NoError(t, err) + _, err = deployment.ConfirmIfNoError(e.Env.Chains[e.HomeChainSel], tx, err) + require.NoError(t, err) + tx, err = state.Chains[e.HomeChainSel].CCIPConfig.TransferOwnership(e.Env.Chains[e.HomeChainSel].DeployerKey, state.Chains[e.HomeChainSel].Timelock.Address()) + require.NoError(t, err) + _, err = deployment.ConfirmIfNoError(e.Env.Chains[e.HomeChainSel], tx, err) + require.NoError(t, err) + + acceptOwnershipProposal, err := GenerateAcceptOwnershipProposal(state, e.HomeChainSel, initialDeploy) + require.NoError(t, err) + acceptOwnershipExec := SignProposal(t, e.Env, acceptOwnershipProposal) + // Apply the accept ownership proposal to all the chains. + for _, sel := range initialDeploy { + ExecuteProposal(t, e.Env, acceptOwnershipExec, state, sel) + } + for _, chain := range initialDeploy { + owner, err2 := state.Chains[chain].OnRamp.Owner(nil) + require.NoError(t, err2) + require.Equal(t, state.Chains[chain].Timelock.Address(), owner) + } + cfgOwner, err := state.Chains[e.HomeChainSel].CCIPConfig.Owner(nil) + require.NoError(t, err) + crOwner, err := state.Chains[e.HomeChainSel].CapabilityRegistry.Owner(nil) + require.NoError(t, err) + require.Equal(t, state.Chains[e.HomeChainSel].Timelock.Address(), cfgOwner) + require.Equal(t, state.Chains[e.HomeChainSel].Timelock.Address(), crOwner) + + // Generate and sign inbound proposal to new 4th chain. + chainInboundProposal, err := NewChainInboundProposal(e.Env, state, e.HomeChainSel, newChain, initialDeploy) + require.NoError(t, err) + chainInboundExec := SignProposal(t, e.Env, chainInboundProposal) + for _, sel := range initialDeploy { + ExecuteProposal(t, e.Env, chainInboundExec, state, sel) + } + + // Now configure the new chain using deployer key (not transferred to timelock yet). + var offRampEnables []offramp.OffRampSourceChainConfigArgs + for _, source := range initialDeploy { + offRampEnables = append(offRampEnables, offramp.OffRampSourceChainConfigArgs{ + Router: state.Chains[newChain].Router.Address(), + SourceChainSelector: source, + IsEnabled: true, + OnRamp: common.LeftPadBytes(state.Chains[source].OnRamp.Address().Bytes(), 32), + }) + } + tx, err = state.Chains[newChain].OffRamp.ApplySourceChainConfigUpdates(e.Env.Chains[newChain].DeployerKey, offRampEnables) + require.NoError(t, err) + _, err = deployment.ConfirmIfNoError(e.Env.Chains[newChain], tx, err) + require.NoError(t, err) + // Set the OCR3 config on new 4th chain to enable the plugin. + latestDON, err := LatestCCIPDON(state.Chains[e.HomeChainSel].CapabilityRegistry) + require.NoError(t, err) + ocrConfigs, err := BuildSetOCR3ConfigArgs(latestDON.Id, state.Chains[e.HomeChainSel].CCIPConfig) + require.NoError(t, err) + tx, err = state.Chains[newChain].OffRamp.SetOCR3Configs(e.Env.Chains[newChain].DeployerKey, ocrConfigs) + require.NoError(t, err) + _, err = deployment.ConfirmIfNoError(e.Env.Chains[newChain], tx, err) + require.NoError(t, err) + + // Assert the inbound lanes to the new chain are wired correctly. + state, err = LoadOnchainState(e.Env, e.Ab) + require.NoError(t, err) + for _, chain := range initialDeploy { + cfg, err2 := state.Chains[chain].OnRamp.GetDestChainConfig(nil, newChain) + require.NoError(t, err2) + assert.Equal(t, cfg.Router, state.Chains[chain].TestRouter.Address()) + fqCfg, err2 := state.Chains[chain].FeeQuoter.GetDestChainConfig(nil, newChain) + require.NoError(t, err2) + assert.True(t, fqCfg.IsEnabled) + s, err2 := state.Chains[newChain].OffRamp.GetSourceChainConfig(nil, chain) + require.NoError(t, err2) + assert.Equal(t, common.LeftPadBytes(state.Chains[chain].OnRamp.Address().Bytes(), 32), s.OnRamp) + } + // Ensure job related logs are up to date. + time.Sleep(30 * time.Second) + require.NoError(t, ReplayAllLogs(e.Nodes, e.Env.Chains)) + + // TODO: Send via all inbound lanes and use parallel helper + // Now that the proposal has been executed we expect to be able to send traffic to this new 4th chain. + seqNr := SendRequest(t, e.Env, state, initialDeploy[0], newChain, true) + ConfirmExecution(t, e.Env.Chains[initialDeploy[0]], e.Env.Chains[newChain], state.Chains[newChain].OffRamp, seqNr) +} diff --git a/integration-tests/deployment/ccip/add_lane.go b/integration-tests/deployment/ccip/add_lane.go index 7ea757f03ac..0e7c01405b1 100644 --- a/integration-tests/deployment/ccip/add_lane.go +++ b/integration-tests/deployment/ccip/add_lane.go @@ -7,8 +7,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/integration-tests/deployment" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" ) @@ -18,13 +19,13 @@ func AddLane(e deployment.Environment, state CCIPOnChainState, from, to uint64) tx, err := state.Chains[from].Router.ApplyRampUpdates(e.Chains[from].DeployerKey, []router.RouterOnRamp{ { DestChainSelector: to, - OnRamp: state.Chains[from].EvmOnRampV160.Address(), + OnRamp: state.Chains[from].OnRamp.Address(), }, }, []router.RouterOffRamp{}, []router.RouterOffRamp{}) if _, err := deployment.ConfirmIfNoError(e.Chains[from], tx, err); err != nil { return err } - tx, err = state.Chains[from].EvmOnRampV160.ApplyDestChainConfigUpdates(e.Chains[from].DeployerKey, + tx, err = state.Chains[from].OnRamp.ApplyDestChainConfigUpdates(e.Chains[from].DeployerKey, []onramp.OnRampDestChainConfigArgs{ { DestChainSelector: to, @@ -35,7 +36,7 @@ func AddLane(e deployment.Environment, state CCIPOnChainState, from, to uint64) return err } - _, err = state.Chains[from].PriceRegistry.UpdatePrices( + _, err = state.Chains[from].FeeQuoter.UpdatePrices( e.Chains[from].DeployerKey, fee_quoter.InternalPriceUpdates{ TokenPriceUpdates: []fee_quoter.InternalTokenPriceUpdate{ { @@ -57,25 +58,25 @@ func AddLane(e deployment.Environment, state CCIPOnChainState, from, to uint64) return err } - // Enable dest in price registry - tx, err = state.Chains[from].PriceRegistry.ApplyDestChainConfigUpdates(e.Chains[from].DeployerKey, + // Enable dest in fee quoter + tx, err = state.Chains[from].FeeQuoter.ApplyDestChainConfigUpdates(e.Chains[from].DeployerKey, []fee_quoter.FeeQuoterDestChainConfigArgs{ { DestChainSelector: to, - DestChainConfig: defaultPriceRegistryDestChainConfig(), + DestChainConfig: defaultFeeQuoterDestChainConfig(), }, }) if _, err := deployment.ConfirmIfNoError(e.Chains[from], tx, err); err != nil { return err } - tx, err = state.Chains[to].EvmOffRampV160.ApplySourceChainConfigUpdates(e.Chains[to].DeployerKey, + tx, err = state.Chains[to].OffRamp.ApplySourceChainConfigUpdates(e.Chains[to].DeployerKey, []offramp.OffRampSourceChainConfigArgs{ { Router: state.Chains[to].Router.Address(), SourceChainSelector: from, IsEnabled: true, - OnRamp: common.LeftPadBytes(state.Chains[from].EvmOnRampV160.Address().Bytes(), 32), + OnRamp: common.LeftPadBytes(state.Chains[from].OnRamp.Address().Bytes(), 32), }, }) if _, err := deployment.ConfirmIfNoError(e.Chains[to], tx, err); err != nil { @@ -84,14 +85,14 @@ func AddLane(e deployment.Environment, state CCIPOnChainState, from, to uint64) tx, err = state.Chains[to].Router.ApplyRampUpdates(e.Chains[to].DeployerKey, []router.RouterOnRamp{}, []router.RouterOffRamp{}, []router.RouterOffRamp{ { SourceChainSelector: from, - OffRamp: state.Chains[to].EvmOffRampV160.Address(), + OffRamp: state.Chains[to].OffRamp.Address(), }, }) _, err = deployment.ConfirmIfNoError(e.Chains[to], tx, err) return err } -func defaultPriceRegistryDestChainConfig() fee_quoter.FeeQuoterDestChainConfig { +func defaultFeeQuoterDestChainConfig() fee_quoter.FeeQuoterDestChainConfig { // https://github.com/smartcontractkit/ccip/blob/c4856b64bd766f1ddbaf5d13b42d3c4b12efde3a/contracts/src/v0.8/ccip/libraries/Internal.sol#L337-L337 /* ```Solidity @@ -112,9 +113,10 @@ func defaultPriceRegistryDestChainConfig() fee_quoter.FeeQuoterDestChainConfig { DestGasPerDataAvailabilityByte: 100, DestDataAvailabilityMultiplierBps: 1, DefaultTokenDestGasOverhead: 125_000, - DefaultTxGasLimit: 200_000, - GasMultiplierWeiPerEth: 1, - NetworkFeeUSDCents: 1, - ChainFamilySelector: [4]byte(evmFamilySelector), + //DefaultTokenDestBytesOverhead: 32, + DefaultTxGasLimit: 200_000, + GasMultiplierWeiPerEth: 1, + NetworkFeeUSDCents: 1, + ChainFamilySelector: [4]byte(evmFamilySelector), } } diff --git a/integration-tests/deployment/ccip/add_lane_test.go b/integration-tests/deployment/ccip/add_lane_test.go index 567f5ca6856..77b82348e4a 100644 --- a/integration-tests/deployment/ccip/add_lane_test.go +++ b/integration-tests/deployment/ccip/add_lane_test.go @@ -1 +1,57 @@ package ccipdeployment + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +// TestAddLane covers the workflow of adding a lane +// between existing supported chains in CCIP. +func TestAddLane(t *testing.T) { + // TODO: The offchain code doesn't yet support partial lane + // enablement, need to address then re-enable this test. + t.Skip() + e := NewEnvironmentWithCRAndJobs(t, logger.TestLogger(t), 3) + // Here we have CR + nodes set up, but no CCIP contracts deployed. + state, err := LoadOnchainState(e.Env, e.Ab) + require.NoError(t, err) + // Set up CCIP contracts and a DON per chain. + ab, err := DeployCCIPContracts(e.Env, DeployCCIPContractConfig{ + HomeChainSel: e.HomeChainSel, + CCIPOnChainState: state, + }) + require.NoError(t, err) + require.NoError(t, e.Ab.Merge(ab)) + + // We expect no lanes available on any chain. + state, err = LoadOnchainState(e.Env, e.Ab) + require.NoError(t, err) + for _, chain := range state.Chains { + offRamps, err := chain.Router.GetOffRamps(nil) + require.NoError(t, err) + require.Len(t, offRamps, 0) + } + + // Add one lane and send traffic. + from, to := e.Env.AllChainSelectors()[0], e.Env.AllChainSelectors()[1] + require.NoError(t, AddLane(e.Env, state, from, to)) + + for sel, chain := range state.Chains { + offRamps, err := chain.Router.GetOffRamps(nil) + require.NoError(t, err) + if sel == to { + require.Len(t, offRamps, 1) + } else { + require.Len(t, offRamps, 0) + } + } + seqNum := SendRequest(t, e.Env, state, from, to, false) + require.Equal(t, uint64(1), seqNum) + ConfirmExecution(t, e.Env.Chains[from], e.Env.Chains[to], state.Chains[to].OffRamp, seqNum) + + // TODO: Add a second lane, then disable the first and + // ensure we can send on the second but not the first. +} diff --git a/integration-tests/deployment/ccip/changeset/1_cap_reg.go b/integration-tests/deployment/ccip/changeset/1_cap_reg.go index 1929aede02f..58634916235 100644 --- a/integration-tests/deployment/ccip/changeset/1_cap_reg.go +++ b/integration-tests/deployment/ccip/changeset/1_cap_reg.go @@ -1,6 +1,8 @@ package changeset import ( + "github.com/smartcontractkit/ccip-owner-contracts/tools/proposal/timelock" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" ccipdeployment "github.com/smartcontractkit/chainlink/integration-tests/deployment/ccip" ) @@ -14,7 +16,7 @@ func Apply0001(env deployment.Environment, homeChainSel uint64) (deployment.Chan return deployment.ChangesetOutput{}, err } return deployment.ChangesetOutput{ - Proposals: []deployment.Proposal{}, + Proposals: []timelock.MCMSWithTimelockProposal{}, AddressBook: ab, JobSpecs: nil, }, nil diff --git a/integration-tests/deployment/ccip/changeset/2_initial_deploy.go b/integration-tests/deployment/ccip/changeset/2_initial_deploy.go index b20ffb2d4ac..93c408bf45a 100644 --- a/integration-tests/deployment/ccip/changeset/2_initial_deploy.go +++ b/integration-tests/deployment/ccip/changeset/2_initial_deploy.go @@ -1,6 +1,8 @@ package changeset import ( + "github.com/smartcontractkit/ccip-owner-contracts/tools/proposal/timelock" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" ccipdeployment "github.com/smartcontractkit/chainlink/integration-tests/deployment/ccip" @@ -14,18 +16,14 @@ func Apply0002(env deployment.Environment, c ccipdeployment.DeployCCIPContractCo ab, err := ccipdeployment.DeployCCIPContracts(env, c) if err != nil { env.Logger.Errorw("Failed to deploy CCIP contracts", "err", err, "addresses", ab) - return deployment.ChangesetOutput{}, err + return deployment.ChangesetOutput{}, deployment.MaybeDataErr(err) } js, err := ccipdeployment.NewCCIPJobSpecs(env.NodeIDs, env.Offchain) if err != nil { return deployment.ChangesetOutput{}, err } - proposal, err := ccipdeployment.GenerateAcceptOwnershipProposal(env, env.AllChainSelectors(), ab) - if err != nil { - return deployment.ChangesetOutput{}, err - } return deployment.ChangesetOutput{ - Proposals: []deployment.Proposal{proposal}, + Proposals: []timelock.MCMSWithTimelockProposal{}, AddressBook: ab, // Mapping of which nodes get which jobs. JobSpecs: js, diff --git a/integration-tests/deployment/ccip/changeset/2_initial_deploy_test.go b/integration-tests/deployment/ccip/changeset/2_initial_deploy_test.go index 8098ebaef21..fa0fbb9141b 100644 --- a/integration-tests/deployment/ccip/changeset/2_initial_deploy_test.go +++ b/integration-tests/deployment/ccip/changeset/2_initial_deploy_test.go @@ -8,27 +8,22 @@ import ( "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/stretchr/testify/require" "github.com/smartcontractkit/chainlink/integration-tests/deployment" "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" + ccipdeployment "github.com/smartcontractkit/chainlink/integration-tests/deployment/ccip" jobv1 "github.com/smartcontractkit/chainlink/integration-tests/deployment/jd/job/v1" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" - - ccipdeployment "github.com/smartcontractkit/chainlink/integration-tests/deployment/ccip" - "github.com/smartcontractkit/chainlink/integration-tests/deployment/memory" - "github.com/smartcontractkit/chainlink/v2/core/logger" ) func Test0002_InitialDeploy(t *testing.T) { lggr := logger.TestLogger(t) ctx := ccipdeployment.Context(t) - tenv := ccipdeployment.NewDeployedTestEnvironment(t, lggr) + tenv := ccipdeployment.NewEnvironmentWithCR(t, lggr, 3) e := tenv.Env nodes := tenv.Nodes chains := e.Chains @@ -38,7 +33,8 @@ func Test0002_InitialDeploy(t *testing.T) { // Apply migration output, err := Apply0002(tenv.Env, ccipdeployment.DeployCCIPContractConfig{ - HomeChainSel: tenv.HomeChainSel, + HomeChainSel: tenv.HomeChainSel, + ChainsToDeploy: tenv.Env.AllChainSelectors(), // Capreg/config already exist. CCIPOnChainState: state, }) @@ -48,7 +44,7 @@ func Test0002_InitialDeploy(t *testing.T) { require.NoError(t, err) // Ensure capreg logs are up to date. - require.NoError(t, ReplayAllLogs(nodes, chains)) + require.NoError(t, ccipdeployment.ReplayAllLogs(nodes, chains)) // Apply the jobs. for nodeID, jobs := range output.JobSpecs { @@ -67,9 +63,8 @@ func Test0002_InitialDeploy(t *testing.T) { time.Sleep(30 * time.Second) // Ensure job related logs are up to date. - require.NoError(t, ReplayAllLogs(nodes, chains)) + require.NoError(t, ccipdeployment.ReplayAllLogs(nodes, chains)) - // Send a request from every router // Add all lanes for source := range e.Chains { for dest := range e.Chains { @@ -80,47 +75,19 @@ func Test0002_InitialDeploy(t *testing.T) { } // Send a message from each chain to every other chain. - for src, srcChain := range e.Chains { + expectedSeqNum := make(map[uint64]uint64) + for src := range e.Chains { for dest := range e.Chains { if src == dest { continue } - msg := router.ClientEVM2AnyMessage{ - Receiver: common.LeftPadBytes(state.Chains[dest].Receiver.Address().Bytes(), 32), - Data: []byte("hello"), - TokenAmounts: nil, // TODO: no tokens for now - FeeToken: state.Chains[src].Weth9.Address(), - ExtraArgs: nil, // TODO: no extra args for now, falls back to default - } - fee, err := state.Chains[src].Router.GetFee( - &bind.CallOpts{Context: context.Background()}, dest, msg) - require.NoError(t, err, deployment.MaybeDataErr(err)) - tx, err := state.Chains[src].Weth9.Deposit(&bind.TransactOpts{ - From: e.Chains[src].DeployerKey.From, - Signer: e.Chains[src].DeployerKey.Signer, - Value: fee, - }) - require.NoError(t, err) - _, err = srcChain.Confirm(tx) - require.NoError(t, err) - - // TODO: should be able to avoid this by using native? - tx, err = state.Chains[src].Weth9.Approve(e.Chains[src].DeployerKey, - state.Chains[src].Router.Address(), fee) - require.NoError(t, err) - _, err = srcChain.Confirm(tx) - require.NoError(t, err) - - t.Logf("Sending CCIP request from chain selector %d to chain selector %d", - src, dest) - tx, err = state.Chains[src].Router.CcipSend(e.Chains[src].DeployerKey, dest, msg) - require.NoError(t, err) - _, err = srcChain.Confirm(tx) - require.NoError(t, err) + seqNum := ccipdeployment.SendRequest(t, e, state, src, dest, false) + expectedSeqNum[dest] = seqNum } } // Wait for all commit reports to land. + cStart := time.Now() var wg sync.WaitGroup for src, srcChain := range e.Chains { for dest, dstChain := range e.Chains { @@ -132,11 +99,13 @@ func Test0002_InitialDeploy(t *testing.T) { wg.Add(1) go func(src, dest uint64) { defer wg.Done() - waitForCommitWithInterval(t, srcChain, dstChain, state.Chains[dest].EvmOffRampV160, ccipocr3.SeqNumRange{1, 1}) + waitForCommitWithInterval(t, srcChain, dstChain, state.Chains[dest].OffRamp, + ccipocr3.SeqNumRange{ccipocr3.SeqNum(expectedSeqNum[dest]), ccipocr3.SeqNum(expectedSeqNum[dest])}) }(src, dest) } } wg.Wait() + cEnd := time.Now() // Wait for all exec reports to land for src, srcChain := range e.Chains { @@ -149,26 +118,19 @@ func Test0002_InitialDeploy(t *testing.T) { wg.Add(1) go func(src, dest deployment.Chain) { defer wg.Done() - waitForExecWithSeqNr(t, src, dest, state.Chains[dest.Selector].EvmOffRampV160, 1) + ccipdeployment.ConfirmExecution(t, + src, dest, state.Chains[dest.Selector].OffRamp, + expectedSeqNum[dest.Selector]) }(srcChain, dstChain) } } wg.Wait() - + eEnd := time.Now() + t.Log("Commit time:", cEnd.Sub(cStart)) + t.Log("Exec time:", eEnd.Sub(cEnd)) // TODO: Apply the proposal. } -func ReplayAllLogs(nodes map[string]memory.Node, chains map[uint64]deployment.Chain) error { - for _, node := range nodes { - for sel := range chains { - if err := node.ReplayLogs(map[uint64]uint64{sel: 1}); err != nil { - return err - } - } - } - return nil -} - func waitForCommitWithInterval( t *testing.T, src deployment.Chain, @@ -211,34 +173,3 @@ func waitForCommitWithInterval( } } } - -func waitForExecWithSeqNr(t *testing.T, - source, dest deployment.Chain, - offramp *offramp.OffRamp, - expectedSeqNr uint64) { - tick := time.NewTicker(5 * time.Second) - defer tick.Stop() - for range tick.C { - // TODO: Clean this up - source.Client.(*backends.SimulatedBackend).Commit() - dest.Client.(*backends.SimulatedBackend).Commit() - scc, err := offramp.GetSourceChainConfig(nil, source.Selector) - require.NoError(t, err) - t.Logf("Waiting for ExecutionStateChanged on chain %d from chain %d with expected sequence number %d, current onchain minSeqNr: %d", - dest.Selector, source.Selector, expectedSeqNr, scc.MinSeqNr) - iter, err := offramp.FilterExecutionStateChanged(nil, - []uint64{source.Selector}, []uint64{expectedSeqNr}, nil) - require.NoError(t, err) - var count int - for iter.Next() { - if iter.Event.SequenceNumber == expectedSeqNr && iter.Event.SourceChainSelector == source.Selector { - count++ - } - } - if count == 1 { - t.Logf("Received ExecutionStateChanged on chain %d from chain %d with expected sequence number %d", - dest.Selector, source.Selector, expectedSeqNr) - return - } - } -} diff --git a/integration-tests/deployment/ccip/deploy.go b/integration-tests/deployment/ccip/deploy.go index eb2ddae2933..72ab5d7d6ee 100644 --- a/integration-tests/deployment/ccip/deploy.go +++ b/integration-tests/deployment/ccip/deploy.go @@ -1,20 +1,24 @@ package ccipdeployment import ( + "crypto/ecdsa" "fmt" "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers" + "github.com/ethereum/go-ethereum/crypto" + "github.com/smartcontractkit/ccip-owner-contracts/tools/configwrappers" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/tools/gethwrappers" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" "github.com/smartcontractkit/chainlink/integration-tests/deployment" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/maybe_revert_message_receiver" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/nonce_manager" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" @@ -36,16 +40,28 @@ var ( Router deployment.ContractType = "Router" TokenAdminRegistry deployment.ContractType = "TokenAdminRegistry" NonceManager deployment.ContractType = "NonceManager" - PriceRegistry deployment.ContractType = "PriceRegistry" + FeeQuoter deployment.ContractType = "FeeQuoter" ManyChainMultisig deployment.ContractType = "ManyChainMultiSig" CCIPConfig deployment.ContractType = "CCIPConfig" RBACTimelock deployment.ContractType = "RBACTimelock" OnRamp deployment.ContractType = "OnRamp" OffRamp deployment.ContractType = "OffRamp" - CCIPReceiver deployment.ContractType = "CCIPReceiver" CapabilitiesRegistry deployment.ContractType = "CapabilitiesRegistry" + // Note test router maps to a regular router contract. + TestRouter deployment.ContractType = "TestRouter" + CCIPReceiver deployment.ContractType = "CCIPReceiver" + + TestXXXMCMSSigner *ecdsa.PrivateKey ) +func init() { + key, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + TestXXXMCMSSigner = key +} + type Contracts interface { *capabilities_registry.CapabilitiesRegistry | *rmn_proxy_contract.RMNProxyContract | @@ -101,7 +117,8 @@ func deployContract[C Contracts]( } type DeployCCIPContractConfig struct { - HomeChainSel uint64 + HomeChainSel uint64 + ChainsToDeploy []uint64 // Existing contracts which we want to skip deployment // Leave empty if we want to deploy everything // TODO: Add skips to deploy function. @@ -128,17 +145,23 @@ func DeployCCIPContracts(e deployment.Environment, c DeployCCIPContractConfig) ( e.Logger.Errorw("Failed to get hashed capability id", "err", err) return ab, err } + if cr != CCIPCapabilityID { + return ab, fmt.Errorf("Capability registry does not support CCIP %s %s", hexutil.Encode(cr[:]), hexutil.Encode(CCIPCapabilityID[:])) + } // Signal to CR that our nodes support CCIP capability. if err := AddNodes( c.Chains[c.HomeChainSel].CapabilityRegistry, e.Chains[c.HomeChainSel], - nodes.PeerIDs(c.HomeChainSel), // Doesn't actually matter which sel here - [][32]byte{cr}, + nodes.NonBootstraps().PeerIDs(), ); err != nil { return ab, err } - for _, chain := range e.Chains { + for _, chainSel := range c.ChainsToDeploy { + chain, ok := e.Chains[chainSel] + if !ok { + return ab, fmt.Errorf("Chain %d not found", chainSel) + } ab, err = DeployChainContracts(e, chain, ab) if err != nil { return ab, err @@ -153,47 +176,28 @@ func DeployCCIPContracts(e deployment.Environment, c DeployCCIPContractConfig) ( e.Logger.Errorw("Failed to load chain state", "err", err) return ab, err } - // Enable ramps on price registry/nonce manager - tx, err := chainState.PriceRegistry.ApplyAuthorizedCallerUpdates(chain.DeployerKey, fee_quoter.AuthorizedCallersAuthorizedCallerArgs{ - // TODO: We enable the deployer initially to set prices - AddedCallers: []common.Address{chainState.EvmOffRampV160.Address(), chain.DeployerKey.From}, - }) - if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { - e.Logger.Errorw("Failed to confirm price registry authorized caller update", "err", err) - return ab, err - } - - tx, err = chainState.NonceManager.ApplyAuthorizedCallerUpdates(chain.DeployerKey, nonce_manager.AuthorizedCallersAuthorizedCallerArgs{ - AddedCallers: []common.Address{chainState.EvmOffRampV160.Address(), chainState.EvmOnRampV160.Address()}, - }) - if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { - e.Logger.Errorw("Failed to update nonce manager with ramps", "err", err) - return ab, err - } + // TODO: Do we want to extract this? // Add chain config for each chain. - _, err = AddChainConfig(e.Logger, + _, err = AddChainConfig( + e.Logger, e.Chains[c.HomeChainSel], c.Chains[c.HomeChainSel].CCIPConfig, chain.Selector, - nodes.PeerIDs(chain.Selector), - uint8(len(nodes)/3)) + nodes.NonBootstraps().PeerIDs()) if err != nil { return ab, err } - // For each chain, we create a DON on the home chain. - if err := AddDON(e.Logger, - cr, + // For each chain, we create a DON on the home chain (2 OCR instances) + if err := AddDON( + e.Logger, c.Chains[c.HomeChainSel].CapabilityRegistry, c.Chains[c.HomeChainSel].CCIPConfig, - chainState.EvmOffRampV160, + chainState.OffRamp, chain, e.Chains[c.HomeChainSel], - uint8(len(nodes)/3), - nodes.BootstrapPeerIDs(chain.Selector)[0], - nodes.PeerIDs(chain.Selector), - nodes, + nodes.NonBootstraps(), ); err != nil { e.Logger.Errorw("Failed to add DON", "err", err) return ab, err @@ -203,7 +207,11 @@ func DeployCCIPContracts(e deployment.Environment, c DeployCCIPContractConfig) ( return ab, nil } -func DeployChainContracts(e deployment.Environment, chain deployment.Chain, ab deployment.AddressBook) (deployment.AddressBook, error) { +func DeployChainContracts( + e deployment.Environment, + chain deployment.Chain, + ab deployment.AddressBook, +) (deployment.AddressBook, error) { ccipReceiver, err := deployContract(e.Logger, chain, ab, func(chain deployment.Chain) ContractDeploy[*maybe_revert_message_receiver.MaybeRevertMessageReceiver] { receiverAddr, tx, receiver, err2 := maybe_revert_message_receiver.DeployMaybeRevertMessageReceiver( @@ -253,16 +261,37 @@ func DeployChainContracts(e deployment.Environment, chain deployment.Chain, ab d e.Logger.Errorw("Failed to deploy mcm", "err", err) return ab, err } - // TODO: Address soon + // TODO: Parameterize this. e.Logger.Infow("deployed mcm", "addr", mcm.Address) + publicKey := TestXXXMCMSSigner.Public().(*ecdsa.PublicKey) + // Convert the public key to an Ethereum address + address := crypto.PubkeyToAddress(*publicKey) + c, err := configwrappers.NewConfig(1, []common.Address{address}, []configwrappers.Config{}) + if err != nil { + e.Logger.Errorw("Failed to create config", "err", err) + return ab, err + } + groupQuorums, groupParents, signerAddresses, signerGroups := c.ExtractSetConfigInputs() + mcmsTx, err := mcm.Contract.SetConfig(chain.DeployerKey, + signerAddresses, + signerGroups, // Signer 1 is int group 0 (root group) with quorum 1. + groupQuorums, + groupParents, + false, + ) + if _, err := deployment.ConfirmIfNoError(chain, mcmsTx, err); err != nil { + e.Logger.Errorw("Failed to confirm mcm config", "err", err) + return ab, err + } - _, err = deployContract(e.Logger, chain, ab, + timelock, err := deployContract(e.Logger, chain, ab, func(chain deployment.Chain) ContractDeploy[*owner_helpers.RBACTimelock] { timelock, tx, cc, err2 := owner_helpers.DeployRBACTimelock( chain.DeployerKey, chain.Client, big.NewInt(0), // minDelay mcm.Address, + // TODO: Actual MCM groups need to be parameterized. []common.Address{mcm.Address}, // proposers []common.Address{chain.DeployerKey.From}, //executors []common.Address{mcm.Address}, // cancellers @@ -295,6 +324,8 @@ func DeployChainContracts(e deployment.Environment, chain deployment.Chain, ab d } e.Logger.Infow("deployed rmnProxy", "addr", rmnProxy.Address) + // TODO: Need general configuration for using pre-existing weth9 + // link tokens. weth9, err := deployContract(e.Logger, chain, ab, func(chain deployment.Chain) ContractDeploy[*weth9.WETH9] { weth9Addr, tx, weth9c, err2 := weth9.DeployWETH9( @@ -347,6 +378,24 @@ func DeployChainContracts(e deployment.Environment, chain deployment.Chain, ab d } e.Logger.Infow("deployed router", "addr", routerContract) + testRouterContract, err := deployContract(e.Logger, chain, ab, + func(chain deployment.Chain) ContractDeploy[*router.Router] { + routerAddr, tx, routerC, err2 := router.DeployRouter( + chain.DeployerKey, + chain.Client, + weth9.Address, + rmnProxy.Address, + ) + return ContractDeploy[*router.Router]{ + routerAddr, routerC, tx, deployment.NewTypeAndVersion(TestRouter, deployment.Version1_2_0), err2, + } + }) + if err != nil { + e.Logger.Errorw("Failed to deploy test router", "err", err) + return ab, err + } + e.Logger.Infow("deployed test router", "addr", testRouterContract.Address) + tokenAdminRegistry, err := deployContract(e.Logger, chain, ab, func(chain deployment.Chain) ContractDeploy[*token_admin_registry.TokenAdminRegistry] { tokenAdminRegistryAddr, tx, tokenAdminRegistry, err2 := token_admin_registry.DeployTokenAdminRegistry( @@ -388,7 +437,7 @@ func DeployChainContracts(e deployment.Environment, chain deployment.Chain, ab d LinkToken: linkToken.Address, StalenessThreshold: uint32(24 * 60 * 60), }, - []common.Address{}, // ramps added after + []common.Address{timelock.Address}, // timelock should be able to update, ramps added after []common.Address{weth9.Address, linkToken.Address}, // fee tokens []fee_quoter.FeeQuoterTokenPriceFeedUpdate{}, []fee_quoter.FeeQuoterTokenTransferFeeConfigArgs{}, // TODO: tokens @@ -405,11 +454,11 @@ func DeployChainContracts(e deployment.Environment, chain deployment.Chain, ab d []fee_quoter.FeeQuoterDestChainConfigArgs{}, ) return ContractDeploy[*fee_quoter.FeeQuoter]{ - prAddr, pr, tx, deployment.NewTypeAndVersion(PriceRegistry, deployment.Version1_6_0_dev), err2, + prAddr, pr, tx, deployment.NewTypeAndVersion(FeeQuoter, deployment.Version1_6_0_dev), err2, } }) if err != nil { - e.Logger.Errorw("Failed to deploy price registry", "err", err) + e.Logger.Errorw("Failed to deploy fee quoter", "err", err) return ab, err } @@ -468,5 +517,24 @@ func DeployChainContracts(e deployment.Environment, chain deployment.Chain, ab d return ab, err } e.Logger.Infow("deployed offramp", "addr", offRamp) + + // Basic wiring is always needed. + tx, err := feeQuoter.Contract.ApplyAuthorizedCallerUpdates(chain.DeployerKey, fee_quoter.AuthorizedCallersAuthorizedCallerArgs{ + // TODO: We enable the deployer initially to set prices + // Should be removed after. + AddedCallers: []common.Address{offRamp.Contract.Address(), chain.DeployerKey.From}, + }) + if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { + e.Logger.Errorw("Failed to confirm fee quoter authorized caller update", "err", err) + return ab, err + } + + tx, err = nonceManager.Contract.ApplyAuthorizedCallerUpdates(chain.DeployerKey, nonce_manager.AuthorizedCallersAuthorizedCallerArgs{ + AddedCallers: []common.Address{offRamp.Contract.Address(), onRamp.Contract.Address()}, + }) + if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { + e.Logger.Errorw("Failed to update nonce manager with ramps", "err", err) + return ab, err + } return ab, nil } diff --git a/integration-tests/deployment/ccip/deploy_home_chain.go b/integration-tests/deployment/ccip/deploy_home_chain.go index 1fe6bd5d562..ec078e4a9db 100644 --- a/integration-tests/deployment/ccip/deploy_home_chain.go +++ b/integration-tests/deployment/ccip/deploy_home_chain.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "errors" - "sort" + "fmt" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -17,9 +17,9 @@ import ( commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink/integration-tests/deployment" cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ocr3_config_encoder" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" @@ -52,6 +52,18 @@ const ( MaxDurationShouldTransmitAcceptedReport = 10 * time.Second ) +var ( + CCIPCapabilityID = utils.Keccak256Fixed(MustABIEncode(`[{"type": "string"}, {"type": "string"}]`, CapabilityLabelledName, CapabilityVersion)) +) + +func MustABIEncode(abiString string, args ...interface{}) []byte { + encoded, err := utils.ABIEncode(abiString, args...) + if err != nil { + panic(err) + } + return encoded +} + func DeployCapReg(lggr logger.Logger, chains map[uint64]deployment.Chain, chainSel uint64) (deployment.AddressBook, common.Address, error) { ab := deployment.NewMemoryAddressBook() chain := chains[chainSel] @@ -69,6 +81,7 @@ func DeployCapReg(lggr logger.Logger, chains map[uint64]deployment.Chain, chainS lggr.Errorw("Failed to deploy capreg", "err", err) return ab, common.Address{}, err } + lggr.Infow("deployed capreg", "addr", capReg.Address) ccipConfig, err := deployContract( lggr, chain, ab, @@ -115,27 +128,18 @@ func DeployCapReg(lggr logger.Logger, chains map[uint64]deployment.Chain, chainS return ab, capReg.Address, nil } -func sortP2PIDS(p2pIDs [][32]byte) { - sort.Slice(p2pIDs, func(i, j int) bool { - return bytes.Compare(p2pIDs[i][:], p2pIDs[j][:]) < 0 - }) -} - func AddNodes( capReg *capabilities_registry.CapabilitiesRegistry, chain deployment.Chain, p2pIDs [][32]byte, - capabilityIDs [][32]byte, ) error { - // Need to sort, otherwise _checkIsValidUniqueSubset onChain will fail - sortP2PIDS(p2pIDs) var nodeParams []capabilities_registry.CapabilitiesRegistryNodeParams for _, p2pID := range p2pIDs { nodeParam := capabilities_registry.CapabilitiesRegistryNodeParams{ NodeOperatorId: NodeOperatorID, Signer: p2pID, // Not used in tests P2pId: p2pID, - HashedCapabilityIds: capabilityIDs, + HashedCapabilityIds: [][32]byte{CCIPCapabilityID}, } nodeParams = append(nodeParams, nodeParam) } @@ -164,10 +168,7 @@ func AddChainConfig( ccipConfig *ccip_config.CCIPConfig, chainSelector uint64, p2pIDs [][32]byte, - f uint8, ) (ccip_config.CCIPConfigTypesChainConfigInfo, error) { - // Need to sort, otherwise _checkIsValidUniqueSubset onChain will fail - sortP2PIDS(p2pIDs) // First Add ChainConfig that includes all p2pIDs as readers encodedExtraChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ GasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(1000), @@ -177,11 +178,10 @@ func AddChainConfig( if err != nil { return ccip_config.CCIPConfigTypesChainConfigInfo{}, err } - chainConfig := SetupConfigInfo(chainSelector, p2pIDs, f, encodedExtraChainConfig) - inputConfig := []ccip_config.CCIPConfigTypesChainConfigInfo{ + chainConfig := SetupConfigInfo(chainSelector, p2pIDs, uint8(len(p2pIDs)/3), encodedExtraChainConfig) + tx, err := ccipConfig.ApplyChainConfigUpdates(h.DeployerKey, nil, []ccip_config.CCIPConfigTypesChainConfigInfo{ chainConfig, - } - tx, err := ccipConfig.ApplyChainConfigUpdates(h.DeployerKey, nil, inputConfig) + }) if _, err := deployment.ConfirmIfNoError(h, tx, err); err != nil { return ccip_config.CCIPConfigTypesChainConfigInfo{}, err } @@ -189,20 +189,13 @@ func AddChainConfig( return chainConfig, nil } -func AddDON( +func BuildAddDONArgs( lggr logger.Logger, - ccipCapabilityID [32]byte, - capReg *capabilities_registry.CapabilitiesRegistry, - ccipConfig *ccip_config.CCIPConfig, offRamp *offramp.OffRamp, dest deployment.Chain, - home deployment.Chain, - f uint8, - bootstrapP2PID [32]byte, - p2pIDs [][32]byte, - nodes []deployment.Node, -) error { - sortP2PIDS(p2pIDs) + nodes deployment.Nodes, +) ([]byte, error) { + p2pIDs := nodes.PeerIDs() // Get OCR3 Config from helper var schedule []int var oracles []confighelper2.OracleIdentityExtra @@ -221,7 +214,7 @@ func AddDON( tabi, err := ocr3_config_encoder.IOCR3ConfigEncoderMetaData.GetAbi() if err != nil { - return err + return nil, err } // Add DON on capability registry contract @@ -246,7 +239,7 @@ func AddDON( }) } if err2 != nil { - return err2 + return nil, err2 } signers, transmitters, configF, _, offchainConfigVersion, offchainConfig, err2 := ocr3confighelper.ContractSetConfigArgsForTests( DeltaProgress, @@ -264,11 +257,11 @@ func AddDON( MaxDurationObservation, MaxDurationShouldAcceptAttestedReport, MaxDurationShouldTransmitAcceptedReport, - int(f), + int(nodes.DefaultF()), []byte{}, // empty OnChainConfig ) if err2 != nil { - return err2 + return nil, err2 } signersBytes := make([][]byte, len(signers)) @@ -280,7 +273,7 @@ func AddDON( for i, transmitter := range transmitters { parsed, err2 := common.ParseHexOrString(string(transmitter)) if err2 != nil { - return err2 + return nil, err2 } transmittersBytes[i] = parsed } @@ -298,77 +291,100 @@ func AddDON( }) } + // TODO: Can just use utils.ABIEncode directly here. encodedCall, err := tabi.Pack("exposeOCR3Config", ocr3Configs) if err != nil { - return err + return nil, err } // Trim first four bytes to remove function selector. encodedConfigs := encodedCall[4:] + return encodedConfigs, nil +} - tx, err := capReg.AddDON(home.DeployerKey, p2pIDs, []capabilities_registry.CapabilitiesRegistryCapabilityConfiguration{ - { - CapabilityId: ccipCapabilityID, - Config: encodedConfigs, - }, - }, false, false, f) - if _, err := deployment.ConfirmIfNoError(home, tx, err); err != nil { - return err - } - - latestBlock, err := home.Client.HeaderByNumber(context.Background(), nil) +func LatestCCIPDON(registry *capabilities_registry.CapabilitiesRegistry) (*capabilities_registry.CapabilitiesRegistryDONInfo, error) { + dons, err := registry.GetDONs(nil) if err != nil { - return err + return nil, err } - endBlock := latestBlock.Number.Uint64() - iter, err := capReg.FilterConfigSet(&bind.FilterOpts{ - Start: endBlock - 1, - End: &endBlock, - }, []uint32{}) - if err != nil { - return err - } - var donID uint32 - for iter.Next() { - donID = iter.Event.DonId - break - } - if donID == 0 { - return errors.New("failed to get donID") - } - - var signerAddresses []common.Address - for _, oracle := range oracles { - signerAddresses = append(signerAddresses, common.BytesToAddress(oracle.OnchainPublicKey)) - } - - var transmitterAddresses []common.Address - for _, oracle := range oracles { - transmitterAddresses = append(transmitterAddresses, common.HexToAddress(string(oracle.TransmitAccount))) + var ccipDON capabilities_registry.CapabilitiesRegistryDONInfo + for _, don := range dons { + if len(don.CapabilityConfigurations) == 1 && + don.CapabilityConfigurations[0].CapabilityId == CCIPCapabilityID && + don.Id > ccipDON.Id { + ccipDON = don + } } + return &ccipDON, nil +} - // get the config digest from the ccip config contract and set config on the offramp. +func BuildSetOCR3ConfigArgs( + donID uint32, + ccipConfig *ccip_config.CCIPConfig, +) ([]offramp.MultiOCR3BaseOCRConfigArgs, error) { var offrampOCR3Configs []offramp.MultiOCR3BaseOCRConfigArgs for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { ocrConfig, err2 := ccipConfig.GetOCRConfig(&bind.CallOpts{ Context: context.Background(), }, donID, uint8(pluginType)) if err2 != nil { - return err2 + return nil, err2 } if len(ocrConfig) != 1 { - return errors.New("expected exactly one OCR3 config") + return nil, errors.New("expected exactly one OCR3 config") + } + var signerAddresses []common.Address + for _, signer := range ocrConfig[0].Config.Signers { + signerAddresses = append(signerAddresses, common.BytesToAddress(signer)) + } + + var transmitterAddresses []common.Address + for _, transmitter := range ocrConfig[0].Config.Transmitters { + transmitterAddresses = append(transmitterAddresses, common.BytesToAddress(transmitter)) } offrampOCR3Configs = append(offrampOCR3Configs, offramp.MultiOCR3BaseOCRConfigArgs{ ConfigDigest: ocrConfig[0].ConfigDigest, OcrPluginType: uint8(pluginType), - F: f, + F: ocrConfig[0].Config.F, IsSignatureVerificationEnabled: pluginType == cctypes.PluginTypeCCIPCommit, Signers: signerAddresses, Transmitters: transmitterAddresses, }) } + return offrampOCR3Configs, nil +} + +func AddDON( + lggr logger.Logger, + capReg *capabilities_registry.CapabilitiesRegistry, + ccipConfig *ccip_config.CCIPConfig, + offRamp *offramp.OffRamp, + dest deployment.Chain, + home deployment.Chain, + nodes deployment.Nodes, +) error { + encodedConfigs, err := BuildAddDONArgs(lggr, offRamp, dest, nodes) + if err != nil { + return err + } + tx, err := capReg.AddDON(home.DeployerKey, nodes.PeerIDs(), []capabilities_registry.CapabilitiesRegistryCapabilityConfiguration{ + { + CapabilityId: CCIPCapabilityID, + Config: encodedConfigs, + }, + }, false, false, nodes.DefaultF()) + if _, err := deployment.ConfirmIfNoError(home, tx, err); err != nil { + return err + } + don, err := LatestCCIPDON(capReg) + if err != nil { + return err + } + offrampOCR3Configs, err := BuildSetOCR3ConfigArgs(don.Id, ccipConfig) + if err != nil { + return err + } tx, err = offRamp.SetOCR3Configs(dest.DeployerKey, offrampOCR3Configs) if _, err := deployment.ConfirmIfNoError(dest, tx, err); err != nil { @@ -376,25 +392,37 @@ func AddDON( } for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { - _, err = offRamp.LatestConfigDetails(&bind.CallOpts{ + ocrConfig, err := offRamp.LatestConfigDetails(&bind.CallOpts{ Context: context.Background(), }, uint8(pluginType)) if err != nil { - //return err - return deployment.MaybeDataErr(err) + return err } // TODO: assertions to be done as part of full state // resprentation validation CCIP-3047 - //require.Equalf(t, offrampOCR3Configs[pluginType].ConfigDigest, ocrConfig.ConfigInfo.ConfigDigest, "%s OCR3 config digest mismatch", pluginType.String()) - //require.Equalf(t, offrampOCR3Configs[pluginType].F, ocrConfig.ConfigInfo.F, "%s OCR3 config F mismatch", pluginType.String()) - //require.Equalf(t, offrampOCR3Configs[pluginType].IsSignatureVerificationEnabled, ocrConfig.ConfigInfo.IsSignatureVerificationEnabled, "%s OCR3 config signature verification mismatch", pluginType.String()) - //if pluginType == cctypes.PluginTypeCCIPCommit { - // // only commit will set signers, exec doesn't need them. - // require.Equalf(t, offrampOCR3Configs[pluginType].Signers, ocrConfig.Signers, "%s OCR3 config signers mismatch", pluginType.String()) - //} - //require.Equalf(t, offrampOCR3Configs[pluginType].TransmittersByEVMChainID, ocrConfig.TransmittersByEVMChainID, "%s OCR3 config transmitters mismatch", pluginType.String()) + if offrampOCR3Configs[pluginType].ConfigDigest != ocrConfig.ConfigInfo.ConfigDigest { + return fmt.Errorf("%s OCR3 config digest mismatch", pluginType.String()) + } + if offrampOCR3Configs[pluginType].F != ocrConfig.ConfigInfo.F { + return fmt.Errorf("%s OCR3 config F mismatch", pluginType.String()) + } + if offrampOCR3Configs[pluginType].IsSignatureVerificationEnabled != ocrConfig.ConfigInfo.IsSignatureVerificationEnabled { + return fmt.Errorf("%s OCR3 config signature verification mismatch", pluginType.String()) + } + if pluginType == cctypes.PluginTypeCCIPCommit { + // only commit will set signers, exec doesn't need them. + for i, signer := range offrampOCR3Configs[pluginType].Signers { + if !bytes.Equal(signer.Bytes(), ocrConfig.Signers[i].Bytes()) { + return fmt.Errorf("%s OCR3 config signer mismatch", pluginType.String()) + } + } + } + for i, transmitter := range offrampOCR3Configs[pluginType].Transmitters { + if !bytes.Equal(transmitter.Bytes(), ocrConfig.Transmitters[i].Bytes()) { + return fmt.Errorf("%s OCR3 config transmitter mismatch", pluginType.String()) + } + } } - lggr.Infof("set ocr3 config on the offramp, signers: %+v, transmitters: %+v", signerAddresses, transmitterAddresses) return nil } diff --git a/integration-tests/deployment/ccip/deploy_test.go b/integration-tests/deployment/ccip/deploy_test.go index 7bc56f82f76..e2963b84a3b 100644 --- a/integration-tests/deployment/ccip/deploy_test.go +++ b/integration-tests/deployment/ccip/deploy_test.go @@ -28,6 +28,7 @@ func TestDeployCCIPContracts(t *testing.T) { require.NoError(t, err) ab, err := DeployCCIPContracts(e, DeployCCIPContractConfig{ HomeChainSel: homeChain, + ChainsToDeploy: e.AllChainSelectors(), CCIPOnChainState: s, }) require.NoError(t, err) diff --git a/integration-tests/deployment/ccip/jobs.go b/integration-tests/deployment/ccip/jobs.go index c46ab7270f6..49fb1e95422 100644 --- a/integration-tests/deployment/ccip/jobs.go +++ b/integration-tests/deployment/ccip/jobs.go @@ -1,11 +1,7 @@ package ccipdeployment import ( - "context" - "fmt" - "github.com/smartcontractkit/chainlink/integration-tests/deployment" - nodev1 "github.com/smartcontractkit/chainlink/integration-tests/deployment/jd/node/v1" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/validate" "github.com/smartcontractkit/chainlink/v2/core/services/relay" ) @@ -13,67 +9,49 @@ import ( // In our case, the only address needed is the cap registry which is actually an env var. // and will pre-exist for our deployment. So the job specs only depend on the environment operators. func NewCCIPJobSpecs(nodeIds []string, oc deployment.OffchainClient) (map[string][]string, error) { + nodes, err := deployment.NodeInfo(nodeIds, oc) + if err != nil { + return nil, err + } // Generate a set of brand new job specs for CCIP for a specific environment // (including NOPs) and new addresses. // We want to assign one CCIP capability job to each node. And node with // an addr we'll list as bootstrapper. // Find the bootstrap nodes - bootstrapMp := make(map[string]struct{}) - for _, node := range nodeIds { - // TODO: Filter should accept multiple nodes - nodeChainConfigs, err := oc.ListNodeChainConfigs(context.Background(), &nodev1.ListNodeChainConfigsRequest{Filter: &nodev1.ListNodeChainConfigsRequest_Filter{ - NodeIds: []string{node}, - }}) - if err != nil { - return nil, err - } - for _, chainConfig := range nodeChainConfigs.ChainConfigs { - if chainConfig.Ocr2Config.IsBootstrap { - bootstrapMp[fmt.Sprintf("%s@%s", - // p2p_12D3... -> 12D3... - chainConfig.Ocr2Config.P2PKeyBundle.PeerId[4:], chainConfig.Ocr2Config.Multiaddr)] = struct{}{} - } - } - } - var bootstraps []string - for b := range bootstrapMp { - bootstraps = append(bootstraps, b) - } - nodesToJobSpecs := make(map[string][]string) - for _, node := range nodeIds { - // TODO: Filter should accept multiple. - nodeChainConfigs, err := oc.ListNodeChainConfigs(context.Background(), &nodev1.ListNodeChainConfigsRequest{Filter: &nodev1.ListNodeChainConfigsRequest_Filter{ - NodeIds: []string{node}, - }}) - if err != nil { - return nil, err - } - // only set P2PV2Bootstrappers in the job spec if the node is a plugin node. - var p2pV2Bootstrappers []string - for _, chainConfig := range nodeChainConfigs.ChainConfigs { - if !chainConfig.Ocr2Config.IsBootstrap { - p2pV2Bootstrappers = bootstraps - break - } + nodesToJobSpecs := make(map[string][]string) + for _, node := range nodes { + var spec string + var err error + if !node.IsBootstrap { + spec, err = validate.NewCCIPSpecToml(validate.SpecArgs{ + P2PV2Bootstrappers: nodes.BootstrapLocators(), + CapabilityVersion: CapabilityVersion, + CapabilityLabelledName: CapabilityLabelledName, + OCRKeyBundleIDs: map[string]string{ + // TODO: Validate that that all EVM chains are using the same keybundle. + relay.NetworkEVM: node.FirstOCRKeybundle().KeyBundleID, + }, + P2PKeyID: node.PeerID.String(), + RelayConfigs: nil, + PluginConfig: map[string]any{}, + }) + } else { + spec, err = validate.NewCCIPSpecToml(validate.SpecArgs{ + P2PV2Bootstrappers: []string{}, // Intentionally empty for bootstraps. + CapabilityVersion: CapabilityVersion, + CapabilityLabelledName: CapabilityLabelledName, + OCRKeyBundleIDs: map[string]string{}, + // TODO: validate that all EVM chains are using the same keybundle + P2PKeyID: node.PeerID.String(), + RelayConfigs: nil, + PluginConfig: map[string]any{}, + }) } - spec, err := validate.NewCCIPSpecToml(validate.SpecArgs{ - P2PV2Bootstrappers: p2pV2Bootstrappers, - CapabilityVersion: CapabilityVersion, - CapabilityLabelledName: CapabilityLabelledName, - OCRKeyBundleIDs: map[string]string{ - // TODO: Validate that that all EVM chains are using the same keybundle. - relay.NetworkEVM: nodeChainConfigs.ChainConfigs[0].Ocr2Config.OcrKeyBundle.BundleId, - }, - // TODO: validate that all EVM chains are using the same keybundle - P2PKeyID: nodeChainConfigs.ChainConfigs[0].Ocr2Config.P2PKeyBundle.PeerId, - RelayConfigs: nil, - PluginConfig: map[string]any{}, - }) if err != nil { return nil, err } - nodesToJobSpecs[node] = append(nodesToJobSpecs[node], spec) + nodesToJobSpecs[node.NodeID] = append(nodesToJobSpecs[node.NodeID], spec) } return nodesToJobSpecs, nil } diff --git a/integration-tests/deployment/ccip/propose.go b/integration-tests/deployment/ccip/propose.go index 4fc38965eaa..eced93dde0f 100644 --- a/integration-tests/deployment/ccip/propose.go +++ b/integration-tests/deployment/ccip/propose.go @@ -1,14 +1,20 @@ package ccipdeployment import ( + "bytes" + "context" "math/big" - "time" + "testing" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers" + "github.com/ethereum/go-ethereum/crypto" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/tools/gethwrappers" + "github.com/smartcontractkit/ccip-owner-contracts/tools/proposal/mcms" + "github.com/smartcontractkit/ccip-owner-contracts/tools/proposal/timelock" chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/integration-tests/deployment" ) @@ -17,48 +23,160 @@ import ( func SimTransactOpts() *bind.TransactOpts { return &bind.TransactOpts{Signer: func(address common.Address, transaction *types.Transaction) (*types.Transaction, error) { return transaction, nil - }, From: common.HexToAddress("0x0"), NoSend: true, GasLimit: 200_000} + }, From: common.HexToAddress("0x0"), NoSend: true, GasLimit: 1_000_000} +} + +func SignProposal(t *testing.T, env deployment.Environment, proposal *timelock.MCMSWithTimelockProposal) *mcms.Executor { + executorClients := make(map[mcms.ChainIdentifier]mcms.ContractDeployBackend) + for _, chain := range env.Chains { + chainselc, exists := chainsel.ChainBySelector(chain.Selector) + require.True(t, exists) + chainSel := mcms.ChainIdentifier(chainselc.Selector) + executorClients[chainSel] = chain.Client + } + realProposal, err := proposal.ToMCMSOnlyProposal() + require.NoError(t, err) + executor, err := realProposal.ToExecutor(executorClients) + require.NoError(t, err) + payload, err := executor.SigningHash() + require.NoError(t, err) + // Sign the payload + sig, err := crypto.Sign(payload.Bytes(), TestXXXMCMSSigner) + require.NoError(t, err) + mcmSig, err := mcms.NewSignatureFromBytes(sig) + require.NoError(t, err) + executor.Proposal.Signatures = append(executor.Proposal.Signatures, mcmSig) + require.NoError(t, executor.Proposal.Validate()) + return executor +} + +func ExecuteProposal(t *testing.T, env deployment.Environment, executor *mcms.Executor, + state CCIPOnChainState, sel uint64) { + // Set the root. + tx, err2 := executor.SetRootOnChain(env.Chains[sel].DeployerKey, mcms.ChainIdentifier(sel)) + require.NoError(t, err2) + _, err2 = env.Chains[sel].Confirm(tx) + require.NoError(t, err2) + + // TODO: This sort of helper probably should move to the MCMS lib. + // Execute all the transactions in the proposal which are for this chain. + for _, chainOp := range executor.Operations[mcms.ChainIdentifier(sel)] { + for idx, op := range executor.ChainAgnosticOps { + if bytes.Equal(op.Data, chainOp.Data) && op.To == chainOp.To { + opTx, err3 := executor.ExecuteOnChain(env.Chains[sel].DeployerKey, idx) + require.NoError(t, err3) + block, err3 := env.Chains[sel].Confirm(opTx) + require.NoError(t, err3) + t.Log("executed", chainOp) + it, err3 := state.Chains[sel].Timelock.FilterCallScheduled(&bind.FilterOpts{ + Start: block, + End: &block, + Context: context.Background(), + }, nil, nil) + require.NoError(t, err3) + var calls []owner_helpers.RBACTimelockCall + var pred, salt [32]byte + for it.Next() { + // Note these are the same for the whole batch, can overwrite + pred = it.Event.Predecessor + salt = it.Event.Salt + t.Log("scheduled", it.Event) + calls = append(calls, owner_helpers.RBACTimelockCall{ + Target: it.Event.Target, + Data: it.Event.Data, + Value: it.Event.Value, + }) + } + tx, err := state.Chains[sel].Timelock.ExecuteBatch( + env.Chains[sel].DeployerKey, calls, pred, salt) + require.NoError(t, err) + _, err = env.Chains[sel].Confirm(tx) + require.NoError(t, err) + } + } + } } func GenerateAcceptOwnershipProposal( - e deployment.Environment, + state CCIPOnChainState, + homeChain uint64, chains []uint64, - ab deployment.AddressBook, -) (deployment.Proposal, error) { - state, err := LoadOnchainState(e, ab) - if err != nil { - return deployment.Proposal{}, err - } - // TODO: Just onramp as an example - var ops []owner_helpers.ManyChainMultiSigOp +) (*timelock.MCMSWithTimelockProposal, error) { + // TODO: Accept rest of contracts + var batches []timelock.BatchChainOperation + metaDataPerChain := make(map[mcms.ChainIdentifier]timelock.MCMSWithTimelockChainMetadata) for _, sel := range chains { - opCount, err := state.Chains[sel].Mcm.GetOpCount(nil) + chain, _ := chainsel.ChainBySelector(sel) + acceptOnRamp, err := state.Chains[sel].OnRamp.AcceptOwnership(SimTransactOpts()) if err != nil { - return deployment.Proposal{}, err + return nil, err } - - txData, err := state.Chains[sel].EvmOnRampV160.AcceptOwnership(SimTransactOpts()) + acceptFeeQuoter, err := state.Chains[sel].FeeQuoter.AcceptOwnership(SimTransactOpts()) if err != nil { - return deployment.Proposal{}, err + return nil, err } - evmID, err := chainsel.ChainIdFromSelector(sel) - if err != nil { - return deployment.Proposal{}, err + chainSel := mcms.ChainIdentifier(chain.Selector) + metaDataPerChain[chainSel] = timelock.MCMSWithTimelockChainMetadata{ + ChainMetadata: mcms.ChainMetadata{ + NonceOffset: 0, + MCMAddress: state.Chains[sel].Mcm.Address(), + }, + TimelockAddress: state.Chains[sel].Timelock.Address(), } - ops = append(ops, owner_helpers.ManyChainMultiSigOp{ - ChainId: big.NewInt(int64(evmID)), - MultiSig: state.Chains[sel].McmsAddr, - Nonce: opCount, - To: state.Chains[sel].EvmOnRampV160.Address(), - Value: big.NewInt(0), - Data: txData.Data(), + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: chainSel, + Batch: []mcms.Operation{ + { + To: state.Chains[sel].OnRamp.Address(), + Data: acceptOnRamp.Data(), + Value: big.NewInt(0), + }, + { + To: state.Chains[sel].FeeQuoter.Address(), + Data: acceptFeeQuoter.Data(), + Value: big.NewInt(0), + }, + }, }) } - // TODO: Real valid until. - return deployment.Proposal{ValidUntil: uint32(time.Now().Unix()), Ops: ops}, nil -} - -func ApplyProposal(env deployment.Environment, p deployment.Proposal, state CCIPOnChainState) error { - // TODO - return nil + acceptCR, err := state.Chains[homeChain].CapabilityRegistry.AcceptOwnership(SimTransactOpts()) + if err != nil { + return nil, err + } + acceptCCIPConfig, err := state.Chains[homeChain].CCIPConfig.AcceptOwnership(SimTransactOpts()) + if err != nil { + return nil, err + } + homeChainID := mcms.ChainIdentifier(homeChain) + metaDataPerChain[homeChainID] = timelock.MCMSWithTimelockChainMetadata{ + ChainMetadata: mcms.ChainMetadata{ + NonceOffset: 0, + MCMAddress: state.Chains[homeChain].Mcm.Address(), + }, + TimelockAddress: state.Chains[homeChain].Timelock.Address(), + } + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: homeChainID, + Batch: []mcms.Operation{ + { + To: state.Chains[homeChain].CapabilityRegistry.Address(), + Data: acceptCR.Data(), + Value: big.NewInt(0), + }, + { + To: state.Chains[homeChain].CCIPConfig.Address(), + Data: acceptCCIPConfig.Data(), + Value: big.NewInt(0), + }, + }, + }) + return timelock.NewMCMSWithTimelockProposal( + "1", + 2004259681, // TODO + []mcms.Signature{}, + false, + metaDataPerChain, + "blah", // TODO + batches, + timelock.Schedule, "0s") } diff --git a/integration-tests/deployment/ccip/state.go b/integration-tests/deployment/ccip/state.go index f129650b30b..50ed396d9d8 100644 --- a/integration-tests/deployment/ccip/state.go +++ b/integration-tests/deployment/ccip/state.go @@ -7,13 +7,15 @@ import ( "github.com/pkg/errors" chainsel "github.com/smartcontractkit/chain-selectors" - owner_wrappers "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers" - "github.com/smartcontractkit/chainlink/integration-tests/deployment" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_config" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/maybe_revert_message_receiver" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/burn_mint_erc677" + + owner_wrappers "github.com/smartcontractkit/ccip-owner-contracts/tools/gethwrappers" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/nonce_manager" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/onramp" @@ -22,14 +24,12 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_admin_registry" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/weth9" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/burn_mint_erc677" ) type CCIPChainState struct { - EvmOnRampV160 *onramp.OnRamp - EvmOffRampV160 *offramp.OffRamp - PriceRegistry *fee_quoter.FeeQuoter + OnRamp *onramp.OnRamp + OffRamp *offramp.OffRamp + FeeQuoter *fee_quoter.FeeQuoter ArmProxy *rmn_proxy_contract.RMNProxyContract NonceManager *nonce_manager.NonceManager TokenAdminRegistry *token_admin_registry.TokenAdminRegistry @@ -42,12 +42,11 @@ type CCIPChainState struct { CapabilityRegistry *capabilities_registry.CapabilitiesRegistry CCIPConfig *ccip_config.CCIPConfig Mcm *owner_wrappers.ManyChainMultiSig - // TODO: remove once we have Address() on wrappers - McmsAddr common.Address - Timelock *owner_wrappers.RBACTimelock + Timelock *owner_wrappers.RBACTimelock // Test contracts - Receiver *maybe_revert_message_receiver.MaybeRevertMessageReceiver + Receiver *maybe_revert_message_receiver.MaybeRevertMessageReceiver + TestRouter *router.Router } // Onchain state always derivable from an address book. @@ -140,6 +139,24 @@ func (s CCIPOnChainState) Snapshot(chains []uint64) (CCIPSnapShot, error) { AuthorizedCallers: authorizedCallers, } } + if nm != nil { + authorizedCallers, err := nm.GetAllAuthorizedCallers(nil) + if err != nil { + return snapshot, err + } + tv, err := nm.TypeAndVersion(nil) + if err != nil { + return snapshot, err + } + c.NonceManager = NonceManagerView{ + Contract: Contract{ + TypeAndVersion: tv, + Address: nm.Address(), + }, + // TODO: these can be resolved using an address book + AuthorizedCallers: authorizedCallers, + } + } snapshot.Chains[chainName] = c } return snapshot, nil @@ -189,7 +206,6 @@ func LoadChainState(chain deployment.Chain, addresses map[string]deployment.Type return state, err } state.Mcm = mcms - state.McmsAddr = common.HexToAddress(address) case deployment.NewTypeAndVersion(CapabilitiesRegistry, deployment.Version1_0_0).String(): cr, err := capabilities_registry.NewCapabilitiesRegistry(common.HexToAddress(address), chain.Client) if err != nil { @@ -201,13 +217,13 @@ func LoadChainState(chain deployment.Chain, addresses map[string]deployment.Type if err != nil { return state, err } - state.EvmOnRampV160 = onRampC + state.OnRamp = onRampC case deployment.NewTypeAndVersion(OffRamp, deployment.Version1_6_0_dev).String(): offRamp, err := offramp.NewOffRamp(common.HexToAddress(address), chain.Client) if err != nil { return state, err } - state.EvmOffRampV160 = offRamp + state.OffRamp = offRamp case deployment.NewTypeAndVersion(ARMProxy, deployment.Version1_0_0).String(): armProxy, err := rmn_proxy_contract.NewRMNProxyContract(common.HexToAddress(address), chain.Client) if err != nil { @@ -244,12 +260,18 @@ func LoadChainState(chain deployment.Chain, addresses map[string]deployment.Type return state, err } state.Router = r - case deployment.NewTypeAndVersion(PriceRegistry, deployment.Version1_6_0_dev).String(): - pr, err := fee_quoter.NewFeeQuoter(common.HexToAddress(address), chain.Client) + case deployment.NewTypeAndVersion(TestRouter, deployment.Version1_2_0).String(): + r, err := router.NewRouter(common.HexToAddress(address), chain.Client) + if err != nil { + return state, err + } + state.TestRouter = r + case deployment.NewTypeAndVersion(FeeQuoter, deployment.Version1_6_0_dev).String(): + fq, err := fee_quoter.NewFeeQuoter(common.HexToAddress(address), chain.Client) if err != nil { return state, err } - state.PriceRegistry = pr + state.FeeQuoter = fq case deployment.NewTypeAndVersion(LinkToken, deployment.Version1_0_0).String(): lt, err := burn_mint_erc677.NewBurnMintERC677(common.HexToAddress(address), chain.Client) if err != nil { diff --git a/integration-tests/deployment/ccip/test_helpers.go b/integration-tests/deployment/ccip/test_helpers.go index 2ec837c9eec..779b29a7496 100644 --- a/integration-tests/deployment/ccip/test_helpers.go +++ b/integration-tests/deployment/ccip/test_helpers.go @@ -2,12 +2,21 @@ package ccipdeployment import ( "context" + "sort" "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" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" + jobv1 "github.com/smartcontractkit/chainlink/integration-tests/deployment/jd/job/v1" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" "github.com/smartcontractkit/chainlink/integration-tests/deployment/memory" @@ -40,17 +49,21 @@ type DeployedTestEnvironment struct { // NewDeployedEnvironment creates a new CCIP environment // with capreg and nodes set up. -func NewDeployedTestEnvironment(t *testing.T, lggr logger.Logger) DeployedTestEnvironment { +func NewEnvironmentWithCR(t *testing.T, lggr logger.Logger, numChains int) DeployedTestEnvironment { ctx := Context(t) - chains := memory.NewMemoryChains(t, 3) - homeChainSel := uint64(0) - homeChainEVM := uint64(0) + chains := memory.NewMemoryChains(t, numChains) + // Lower chainSel is home chain. + var chainSels []uint64 // Say first chain is home chain. for chainSel := range chains { - homeChainEVM, _ = chainsel.ChainIdFromSelector(chainSel) - homeChainSel = chainSel - break + chainSels = append(chainSels, chainSel) } + sort.Slice(chainSels, func(i, j int) bool { + return chainSels[i] < chainSels[j] + }) + // Take lowest for determinism. + homeChainSel := chainSels[0] + homeChainEVM, _ := chainsel.ChainIdFromSelector(homeChainSel) ab, capReg, err := DeployCapReg(lggr, chains, homeChainSel) require.NoError(t, err) @@ -73,3 +86,107 @@ func NewDeployedTestEnvironment(t *testing.T, lggr logger.Logger) DeployedTestEn Nodes: nodes, } } + +func NewEnvironmentWithCRAndJobs(t *testing.T, lggr logger.Logger, numChains int) DeployedTestEnvironment { + ctx := Context(t) + e := NewEnvironmentWithCR(t, lggr, numChains) + jbs, err := NewCCIPJobSpecs(e.Env.NodeIDs, e.Env.Offchain) + require.NoError(t, err) + for nodeID, jobs := range jbs { + for _, job := range jobs { + // Note these auto-accept + _, err := e.Env.Offchain.ProposeJob(ctx, + &jobv1.ProposeJobRequest{ + NodeId: nodeID, + Spec: job, + }) + require.NoError(t, err) + } + } + // Wait for plugins to register filters? + // TODO: Investigate how to avoid. + time.Sleep(30 * time.Second) + + // Ensure job related logs are up to date. + require.NoError(t, ReplayAllLogs(e.Nodes, e.Env.Chains)) + return e +} + +func ReplayAllLogs(nodes map[string]memory.Node, chains map[uint64]deployment.Chain) error { + for _, node := range nodes { + for sel := range chains { + if err := node.ReplayLogs(map[uint64]uint64{sel: 1}); err != nil { + return err + } + } + } + return nil +} + +func SendRequest(t *testing.T, e deployment.Environment, state CCIPOnChainState, src, dest uint64, testRouter bool) uint64 { + msg := router.ClientEVM2AnyMessage{ + Receiver: common.LeftPadBytes(state.Chains[dest].Receiver.Address().Bytes(), 32), + Data: []byte("hello"), + TokenAmounts: nil, // TODO: no tokens for now + // Pay native. + FeeToken: common.HexToAddress("0x0"), + ExtraArgs: nil, // TODO: no extra args for now, falls back to default + } + router := state.Chains[src].Router + if testRouter { + router = state.Chains[src].TestRouter + } + fee, err := router.GetFee( + &bind.CallOpts{Context: context.Background()}, dest, msg) + require.NoError(t, err, deployment.MaybeDataErr(err)) + + t.Logf("Sending CCIP request from chain selector %d to chain selector %d", + src, dest) + e.Chains[src].DeployerKey.Value = fee + tx, err := router.CcipSend( + e.Chains[src].DeployerKey, + dest, + msg) + require.NoError(t, err) + blockNum, err := e.Chains[src].Confirm(tx) + require.NoError(t, err) + it, err := state.Chains[src].OnRamp.FilterCCIPMessageSent(&bind.FilterOpts{ + Start: blockNum, + End: &blockNum, + Context: context.Background(), + }, []uint64{dest}) + require.NoError(t, err) + require.True(t, it.Next()) + return it.Event.Message.Header.SequenceNumber +} + +func ConfirmExecution(t *testing.T, + source, dest deployment.Chain, + offramp *offramp.OffRamp, + expectedSeqNr uint64) { + tick := time.NewTicker(5 * time.Second) + defer tick.Stop() + for range tick.C { + // TODO: Clean this up + source.Client.(*backends.SimulatedBackend).Commit() + dest.Client.(*backends.SimulatedBackend).Commit() + scc, err := offramp.GetSourceChainConfig(nil, source.Selector) + require.NoError(t, err) + t.Logf("Waiting for ExecutionStateChanged on chain %d from chain %d with expected sequence number %d, current onchain minSeqNr: %d", + dest.Selector, source.Selector, expectedSeqNr, scc.MinSeqNr) + iter, err := offramp.FilterExecutionStateChanged(nil, + []uint64{source.Selector}, []uint64{expectedSeqNr}, nil) + require.NoError(t, err) + var count int + for iter.Next() { + if iter.Event.SequenceNumber == expectedSeqNr && iter.Event.SourceChainSelector == source.Selector { + count++ + } + } + if count == 1 { + t.Logf("Received ExecutionStateChanged on chain %d from chain %d with expected sequence number %d", + dest.Selector, source.Selector, expectedSeqNr) + return + } + } +} diff --git a/integration-tests/deployment/changeset.go b/integration-tests/deployment/changeset.go index d929022ed96..aeac3e8e722 100644 --- a/integration-tests/deployment/changeset.go +++ b/integration-tests/deployment/changeset.go @@ -1,28 +1,12 @@ package deployment import ( - owner_wrappers "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers" + "github.com/smartcontractkit/ccip-owner-contracts/tools/proposal/timelock" ) -// TODO: Move to real MCM structs once available. -type Proposal struct { - // keccak256(abi.encode(root, validUntil)) is what is signed by MCMS - // signers. - ValidUntil uint32 - // Leaves are the items in the proposal. - // Uses these to generate the root as well as display whats in the root. - // These Ops may be destined for distinct chains. - Ops []owner_wrappers.ManyChainMultiSigOp -} - -func (p Proposal) String() string { - // TODO - return "" -} - // Services as input to CI/Async tasks type ChangesetOutput struct { JobSpecs map[string][]string - Proposals []Proposal + Proposals []timelock.MCMSWithTimelockProposal AddressBook AddressBook } diff --git a/integration-tests/deployment/environment.go b/integration-tests/deployment/environment.go index 5dfa1cd24e4..692d7744b12 100644 --- a/integration-tests/deployment/environment.go +++ b/integration-tests/deployment/environment.go @@ -1,10 +1,12 @@ package deployment import ( + "bytes" "context" "errors" "fmt" "math/big" + "sort" "strconv" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -28,6 +30,7 @@ type OnchainClient interface { // For EVM specifically we can use existing geth interface // to abstract chain clients. bind.ContractBackend + bind.DeployBackend } type OffchainClient interface { @@ -62,6 +65,23 @@ func (e Environment) AllChainSelectors() []uint64 { return selectors } +func (e Environment) AllChainSelectorsExcluding(excluding []uint64) []uint64 { + var selectors []uint64 + for sel := range e.Chains { + excluded := false + for _, toExclude := range excluding { + if sel == toExclude { + excluded = true + } + } + if excluded { + continue + } + selectors = append(selectors, sel) + } + return selectors +} + func ConfirmIfNoError(chain Chain, tx *types.Transaction, err error) (uint64, error) { if err != nil { //revive:disable @@ -100,42 +120,68 @@ type OCRConfig struct { PeerID p2pkey.PeerID TransmitAccount types2.Account ConfigEncryptionPublicKey types3.ConfigEncryptionPublicKey - IsBootstrap bool - MultiAddr string // TODO: type + KeyBundleID string } +// Nodes includes is a group CL nodes. type Nodes []Node -func (n Nodes) PeerIDs(chainSel uint64) [][32]byte { +// PeerIDs returns peerIDs in a sorted list +func (n Nodes) PeerIDs() [][32]byte { var peerIDs [][32]byte for _, node := range n { - cfg := node.SelToOCRConfig[chainSel] - // NOTE: Assume same peerID for all chains. - // Might make sense to change proto as peerID is 1-1 with node? - peerIDs = append(peerIDs, cfg.PeerID) + peerIDs = append(peerIDs, node.PeerID) } + sort.Slice(peerIDs, func(i, j int) bool { + return bytes.Compare(peerIDs[i][:], peerIDs[j][:]) < 0 + }) return peerIDs } -func (n Nodes) BootstrapPeerIDs(chainSel uint64) [][32]byte { - var peerIDs [][32]byte +func (n Nodes) NonBootstraps() Nodes { + var nonBootstraps Nodes for _, node := range n { - cfg := node.SelToOCRConfig[chainSel] - if !cfg.IsBootstrap { + if node.IsBootstrap { continue } - peerIDs = append(peerIDs, cfg.PeerID) + nonBootstraps = append(nonBootstraps, node) } - return peerIDs + return nonBootstraps +} + +func (n Nodes) DefaultF() uint8 { + return uint8(len(n) / 3) +} + +func (n Nodes) BootstrapLocators() []string { + bootstrapMp := make(map[string]struct{}) + for _, node := range n { + if node.IsBootstrap { + bootstrapMp[fmt.Sprintf("%s@%s", + // p2p_12D3... -> 12D3... + node.PeerID.String()[4:], node.MultiAddr)] = struct{}{} + } + } + var locators []string + for b := range bootstrapMp { + locators = append(locators, b) + } + return locators } -// OffchainPublicKey types.OffchainPublicKey -// // For EVM-chains, this an *address*. -// OnchainPublicKey types.OnchainPublicKey -// PeerID string -// TransmitAccount types.Account type Node struct { + NodeID string SelToOCRConfig map[uint64]OCRConfig + PeerID p2pkey.PeerID + IsBootstrap bool + MultiAddr string +} + +func (n Node) FirstOCRKeybundle() OCRConfig { + for _, ocrConfig := range n.SelToOCRConfig { + return ocrConfig + } + return OCRConfig{} } func MustPeerIDFromString(s string) p2pkey.PeerID { @@ -150,20 +196,33 @@ func MustPeerIDFromString(s string) p2pkey.PeerID { // OCR config for example. func NodeInfo(nodeIDs []string, oc OffchainClient) (Nodes, error) { var nodes []Node - for _, node := range nodeIDs { + for _, nodeID := range nodeIDs { // TODO: Filter should accept multiple nodes nodeChainConfigs, err := oc.ListNodeChainConfigs(context.Background(), &nodev1.ListNodeChainConfigsRequest{Filter: &nodev1.ListNodeChainConfigsRequest_Filter{ - NodeIds: []string{node}, + NodeIds: []string{nodeID}, }}) if err != nil { return nil, err } selToOCRConfig := make(map[uint64]OCRConfig) + bootstrap := false + var peerID p2pkey.PeerID + var multiAddr string for _, chainConfig := range nodeChainConfigs.ChainConfigs { if chainConfig.Chain.Type == nodev1.ChainType_CHAIN_TYPE_SOLANA { // Note supported for CCIP yet. continue } + // NOTE: Assume same peerID/multiAddr for all chains. + // Might make sense to change proto as peerID/multiAddr is 1-1 with nodeID? + peerID = MustPeerIDFromString(chainConfig.Ocr2Config.P2PKeyBundle.PeerId) + multiAddr = chainConfig.Ocr2Config.Multiaddr + if chainConfig.Ocr2Config.IsBootstrap { + // NOTE: Assume same peerID for all chains. + // Might make sense to change proto as peerID is 1-1 with nodeID? + bootstrap = true + break + } evmChainID, err := strconv.Atoi(chainConfig.Chain.Id) if err != nil { return nil, err @@ -186,12 +245,15 @@ func NodeInfo(nodeIDs []string, oc OffchainClient) (Nodes, error) { PeerID: MustPeerIDFromString(chainConfig.Ocr2Config.P2PKeyBundle.PeerId), TransmitAccount: types2.Account(chainConfig.AccountAddress), ConfigEncryptionPublicKey: cpk, - IsBootstrap: chainConfig.Ocr2Config.IsBootstrap, - MultiAddr: chainConfig.Ocr2Config.Multiaddr, + KeyBundleID: chainConfig.Ocr2Config.OcrKeyBundle.BundleId, } } nodes = append(nodes, Node{ + NodeID: nodeID, SelToOCRConfig: selToOCRConfig, + IsBootstrap: bootstrap, + PeerID: peerID, + MultiAddr: multiAddr, }) } diff --git a/integration-tests/go.mod b/integration-tests/go.mod index a260dbba6e6..8d21fff89ca 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -33,7 +33,7 @@ require ( github.com/segmentio/ksuid v1.0.4 github.com/shopspring/decimal v1.4.0 github.com/slack-go/slack v0.12.2 - github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685 + github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240910151738-3f318badcfb5 github.com/smartcontractkit/chain-selectors v1.0.23 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240911145028-d346e3ace978 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 1ba9feb496f..5d75436c1e6 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1417,8 +1417,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= -github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685 h1:jakAsdhDxV4cMgRAcSvHraXjyePi8umG5SEUTGFvuy8= -github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685/go.mod h1:p7L/xNEQpHDdZtgFA6/FavuZHqvV3kYhQysxBywmq1k= +github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240910151738-3f318badcfb5 h1:m0HuGuVdRHqBBkHJpSR/QBV7gtLB+hFkXZQ9tEkjdzo= +github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240910151738-3f318badcfb5/go.mod h1:N60/wwocvZ5A3RGmYaMWo0fPFa5tTMlhI9lJ22DRktM= github.com/smartcontractkit/chain-selectors v1.0.23 h1:D2Eaex4Cw/O7Lg3tX6WklOqnjjIQAEBnutCtksPzVDY= github.com/smartcontractkit/chain-selectors v1.0.23/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8=