diff --git a/deployment/ccip/changeset/accept_ownership_test.go b/deployment/ccip/changeset/accept_ownership_test.go index 9b71e0ad5cb..f74556b6600 100644 --- a/deployment/ccip/changeset/accept_ownership_test.go +++ b/deployment/ccip/changeset/accept_ownership_test.go @@ -70,6 +70,8 @@ func genTestTransferOwnershipConfig( state.Chains[chain].FeeQuoter.Address(), state.Chains[chain].NonceManager.Address(), state.Chains[chain].RMNRemote.Address(), + state.Chains[chain].TestRouter.Address(), + state.Chains[chain].Router.Address(), } } diff --git a/deployment/ccip/changeset/cs_add_chain.go b/deployment/ccip/changeset/cs_add_chain.go deleted file mode 100644 index ddb6e61d5ba..00000000000 --- a/deployment/ccip/changeset/cs_add_chain.go +++ /dev/null @@ -1,173 +0,0 @@ -package changeset - -import ( - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-ccip/chainconfig" - "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" - - "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/v2/core/gethwrappers/ccip/generated/ccip_home" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/onramp" -) - -var _ deployment.ChangeSet[ChainInboundChangesetConfig] = NewChainInboundChangeset - -type ChainInboundChangesetConfig struct { - HomeChainSelector uint64 - NewChainSelector uint64 - SourceChainSelectors []uint64 -} - -func (c ChainInboundChangesetConfig) Validate() error { - if c.HomeChainSelector == 0 { - return fmt.Errorf("HomeChainSelector must be set") - } - if c.NewChainSelector == 0 { - return fmt.Errorf("NewChainSelector must be set") - } - if len(c.SourceChainSelectors) == 0 { - return fmt.Errorf("SourceChainSelectors must be set") - } - return nil -} - -// NewChainInboundChangeset generates a proposal -// to connect the new chain to the existing chains. -func NewChainInboundChangeset( - e deployment.Environment, - cfg ChainInboundChangesetConfig, -) (deployment.ChangesetOutput, error) { - if err := cfg.Validate(); err != nil { - return deployment.ChangesetOutput{}, err - } - - state, err := LoadOnchainState(e) - if err != nil { - return deployment.ChangesetOutput{}, err - } - // Generate proposal which enables new destination (from test router) on all source chains. - var batches []timelock.BatchChainOperation - for _, source := range cfg.SourceChainSelectors { - enableOnRampDest, err := state.Chains[source].OnRamp.ApplyDestChainConfigUpdates(deployment.SimTransactOpts(), []onramp.OnRampDestChainConfigArgs{ - { - DestChainSelector: cfg.NewChainSelector, - Router: state.Chains[source].TestRouter.Address(), - }, - }) - if err != nil { - return deployment.ChangesetOutput{}, err - } - enableFeeQuoterDest, err := state.Chains[source].FeeQuoter.ApplyDestChainConfigUpdates( - deployment.SimTransactOpts(), - []fee_quoter.FeeQuoterDestChainConfigArgs{ - { - DestChainSelector: cfg.NewChainSelector, - DestChainConfig: DefaultFeeQuoterDestChainConfig(), - }, - }) - if err != nil { - return deployment.ChangesetOutput{}, err - } - batches = append(batches, timelock.BatchChainOperation{ - ChainIdentifier: mcms.ChainIdentifier(source), - Batch: []mcms.Operation{ - { - // Enable the source in on ramp - To: state.Chains[source].OnRamp.Address(), - Data: enableOnRampDest.Data(), - Value: big.NewInt(0), - }, - { - To: state.Chains[source].FeeQuoter.Address(), - Data: enableFeeQuoterDest.Data(), - Value: big.NewInt(0), - }, - }, - }) - } - - addChainOp, err := applyChainConfigUpdatesOp(e, state, cfg.HomeChainSelector, []uint64{cfg.NewChainSelector}) - if err != nil { - return deployment.ChangesetOutput{}, err - } - - batches = append(batches, timelock.BatchChainOperation{ - ChainIdentifier: mcms.ChainIdentifier(cfg.HomeChainSelector), - Batch: []mcms.Operation{ - addChainOp, - }, - }) - - var ( - timelocksPerChain = make(map[uint64]common.Address) - proposerMCMSes = make(map[uint64]*gethwrappers.ManyChainMultiSig) - ) - for _, chain := range append(cfg.SourceChainSelectors, cfg.HomeChainSelector) { - timelocksPerChain[chain] = state.Chains[chain].Timelock.Address() - proposerMCMSes[chain] = state.Chains[chain].ProposerMcm - } - prop, err := proposalutils.BuildProposalFromBatches( - timelocksPerChain, - proposerMCMSes, - batches, - "proposal to set new chains", - 0, - ) - if err != nil { - return deployment.ChangesetOutput{}, err - } - - return deployment.ChangesetOutput{ - Proposals: []timelock.MCMSWithTimelockProposal{*prop}, - }, nil -} - -func applyChainConfigUpdatesOp( - e deployment.Environment, - state CCIPOnChainState, - homeChainSel uint64, - chains []uint64, -) (mcms.Operation, error) { - nodes, err := deployment.NodeInfo(e.NodeIDs, e.Offchain) - if err != nil { - return mcms.Operation{}, err - } - encodedExtraChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ - GasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(1000), - DAGasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(0), - OptimisticConfirmations: 1, - }) - if err != nil { - return mcms.Operation{}, err - } - var chainConfigUpdates []ccip_home.CCIPHomeChainConfigArgs - for _, chainSel := range chains { - chainConfig := setupConfigInfo(chainSel, nodes.NonBootstraps().PeerIDs(), - nodes.DefaultF(), encodedExtraChainConfig) - chainConfigUpdates = append(chainConfigUpdates, chainConfig) - } - - addChain, err := state.Chains[homeChainSel].CCIPHome.ApplyChainConfigUpdates( - deployment.SimTransactOpts(), - nil, - chainConfigUpdates, - ) - if err != nil { - return mcms.Operation{}, err - } - return mcms.Operation{ - To: state.Chains[homeChainSel].CCIPHome.Address(), - Data: addChain.Data(), - Value: big.NewInt(0), - }, nil -} diff --git a/deployment/ccip/changeset/cs_add_chain_test.go b/deployment/ccip/changeset/cs_add_chain_test.go deleted file mode 100644 index a70ea814881..00000000000 --- a/deployment/ccip/changeset/cs_add_chain_test.go +++ /dev/null @@ -1,335 +0,0 @@ -package changeset - -import ( - "testing" - "time" - - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" - commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" - "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" - commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" - - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/rmn_home" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" - commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" - - "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" - - "github.com/smartcontractkit/chainlink/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) { - t.Skipf("Skipping test as it is running into timeout issues, move the test into integration in-memory tests") - t.Parallel() - // 4 chains where the 4th is added after initial deployment. - e := NewMemoryEnvironment(t, - WithChains(4), - WithJobsOnly(), - ) - state, err := LoadOnchainState(e.Env) - 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}) - newAddresses := deployment.NewMemoryAddressBook() - err = deployPrerequisiteChainContracts(e.Env, newAddresses, initialDeploy, nil) - require.NoError(t, err) - require.NoError(t, e.Env.ExistingAddresses.Merge(newAddresses)) - - cfg := proposalutils.SingleGroupTimelockConfig(t) - e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, nil, []commonchangeset.ChangesetApplication{ - { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployLinkToken), - Config: initialDeploy, - }, - { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), - Config: map[uint64]commontypes.MCMSWithTimelockConfig{ - initialDeploy[0]: cfg, - initialDeploy[1]: cfg, - initialDeploy[2]: cfg, - }, - }, - }) - require.NoError(t, err) - newAddresses = deployment.NewMemoryAddressBook() - tokenConfig := NewTestTokenConfig(state.Chains[e.FeedChainSel].USDFeeds) - - chainConfig := make(map[uint64]CCIPOCRParams) - for _, chain := range initialDeploy { - chainConfig[chain] = DefaultOCRParams(e.FeedChainSel, nil, nil) - } - newChainCfg := NewChainsConfig{ - HomeChainSel: e.HomeChainSel, - FeedChainSel: e.FeedChainSel, - ChainConfigByChain: chainConfig, - } - e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, nil, []commonchangeset.ChangesetApplication{ - { - Changeset: commonchangeset.WrapChangeSet(DeployChainContracts), - Config: DeployChainContractsConfig{ - ChainSelectors: newChainCfg.Chains(), - HomeChainSelector: newChainCfg.HomeChainSel, - }, - }, - { - Changeset: commonchangeset.WrapChangeSet(ConfigureNewChains), - Config: newChainCfg, - }, - }) - require.NoError(t, err) - - state, err = LoadOnchainState(e.Env) - require.NoError(t, err) - - // Connect all the existing lanes. - for _, source := range initialDeploy { - for _, dest := range initialDeploy { - if source != dest { - require.NoError(t, AddLaneWithDefaultPricesAndFeeQuoterConfig(e.Env, state, source, dest, false)) - } - } - } - - rmnHomeAddress, err := deployment.SearchAddressBook(e.Env.ExistingAddresses, e.HomeChainSel, RMNHome) - require.NoError(t, err) - require.True(t, common.IsHexAddress(rmnHomeAddress)) - rmnHome, err := rmn_home.NewRMNHome(common.HexToAddress(rmnHomeAddress), e.Env.Chains[e.HomeChainSel].Client) - require.NoError(t, err) - - // Deploy contracts to new chain - e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, nil, []commonchangeset.ChangesetApplication{ - { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployLinkToken), - Config: []uint64{newChain}, - }, - { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), - Config: map[uint64]commontypes.MCMSWithTimelockConfig{ - newChain: cfg, - }, - }, - }) - require.NoError(t, err) - newAddresses = deployment.NewMemoryAddressBook() - - err = deployPrerequisiteChainContracts(e.Env, newAddresses, []uint64{newChain}, nil) - require.NoError(t, err) - require.NoError(t, e.Env.ExistingAddresses.Merge(newAddresses)) - newAddresses = deployment.NewMemoryAddressBook() - err = deployChainContracts(e.Env, - e.Env.Chains[newChain], newAddresses, rmnHome) - require.NoError(t, err) - require.NoError(t, e.Env.ExistingAddresses.Merge(newAddresses)) - state, err = LoadOnchainState(e.Env) - require.NoError(t, err) - - // configure the testrouter appropriately on each chain - for _, source := range initialDeploy { - 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 ownership to timelock - _, err = commonchangeset.ApplyChangesets(t, e.Env, map[uint64]*proposalutils.TimelockExecutionContracts{ - initialDeploy[0]: { - Timelock: state.Chains[initialDeploy[0]].Timelock, - CallProxy: state.Chains[initialDeploy[0]].CallProxy, - }, - initialDeploy[1]: { - Timelock: state.Chains[initialDeploy[1]].Timelock, - CallProxy: state.Chains[initialDeploy[1]].CallProxy, - }, - initialDeploy[2]: { - Timelock: state.Chains[initialDeploy[2]].Timelock, - CallProxy: state.Chains[initialDeploy[2]].CallProxy, - }, - }, []commonchangeset.ChangesetApplication{ - { - Changeset: commonchangeset.WrapChangeSet(commonchangeset.TransferToMCMSWithTimelock), - Config: genTestTransferOwnershipConfig(e, initialDeploy, state), - }, - { - Changeset: commonchangeset.WrapChangeSet(NewChainInboundChangeset), - Config: ChainInboundChangesetConfig{ - HomeChainSelector: e.HomeChainSel, - NewChainSelector: newChain, - SourceChainSelectors: initialDeploy, - }, - }, - }) - require.NoError(t, err) - - assertTimelockOwnership(t, e, initialDeploy, state) - - // TODO This currently is not working - Able to send the request here but request gets stuck in execution - // Send a new message and expect that this is delivered once the chain is completely set up as inbound - //TestSendRequest(t, e.Env, state, initialDeploy[0], newChain, true) - - _, err = commonchangeset.ApplyChangesets(t, e.Env, map[uint64]*proposalutils.TimelockExecutionContracts{ - e.HomeChainSel: { - Timelock: state.Chains[e.HomeChainSel].Timelock, - CallProxy: state.Chains[e.HomeChainSel].CallProxy, - }, - newChain: { - Timelock: state.Chains[newChain].Timelock, - CallProxy: state.Chains[newChain].CallProxy, - }, - }, []commonchangeset.ChangesetApplication{ - { - Changeset: commonchangeset.WrapChangeSet(AddDonAndSetCandidateChangeset), - Config: AddDonAndSetCandidateChangesetConfig{ - SetCandidateConfigBase: SetCandidateConfigBase{ - HomeChainSelector: e.HomeChainSel, - FeedChainSelector: e.FeedChainSel, - DONChainSelector: newChain, - PluginType: types.PluginTypeCCIPCommit, - CCIPOCRParams: DefaultOCRParams( - e.FeedChainSel, - tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[newChain].LinkToken, state.Chains[newChain].Weth9), - nil, - ), - MCMS: &MCMSConfig{ - MinDelay: 0, - }, - }, - }, - }, - { - Changeset: commonchangeset.WrapChangeSet(SetCandidateChangeset), - Config: SetCandidateChangesetConfig{ - SetCandidateConfigBase: SetCandidateConfigBase{ - HomeChainSelector: e.HomeChainSel, - FeedChainSelector: e.FeedChainSel, - DONChainSelector: newChain, - PluginType: types.PluginTypeCCIPExec, - CCIPOCRParams: DefaultOCRParams( - e.FeedChainSel, - tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[newChain].LinkToken, state.Chains[newChain].Weth9), - nil, - ), - MCMS: &MCMSConfig{ - MinDelay: 0, - }, - }, - }, - }, - { - Changeset: commonchangeset.WrapChangeSet(PromoteAllCandidatesChangeset), - Config: PromoteAllCandidatesChangesetConfig{ - HomeChainSelector: e.HomeChainSel, - DONChainSelector: newChain, - MCMS: &MCMSConfig{ - MinDelay: 0, - }, - }, - }, - }) - require.NoError(t, err) - - // verify if the configs are updated - require.NoError(t, ValidateCCIPHomeConfigSetUp( - e.Env.Logger, - state.Chains[e.HomeChainSel].CapabilityRegistry, - state.Chains[e.HomeChainSel].CCIPHome, - newChain, - )) - replayBlocks, err := LatestBlocksByChain(testcontext.Get(t), e.Env.Chains) - require.NoError(t, err) - - // 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 := internal.LatestCCIPDON(state.Chains[e.HomeChainSel].CapabilityRegistry) - require.NoError(t, err) - ocrConfigs, err := internal.BuildSetOCR3ConfigArgs(latestDON.Id, state.Chains[e.HomeChainSel].CCIPHome, newChain) - 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) - 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) - ReplayLogs(t, e.Env.Offchain, replayBlocks) - - // 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. - latesthdr, err := e.Env.Chains[newChain].Client.HeaderByNumber(testcontext.Get(t), nil) - require.NoError(t, err) - startBlock := latesthdr.Number.Uint64() - msgSentEvent := TestSendRequest(t, e.Env, state, initialDeploy[0], newChain, true, router.ClientEVM2AnyMessage{ - Receiver: common.LeftPadBytes(state.Chains[newChain].Receiver.Address().Bytes(), 32), - Data: []byte("hello world"), - TokenAmounts: nil, - FeeToken: common.HexToAddress("0x0"), - ExtraArgs: nil, - }) - require.NoError(t, - commonutils.JustError(ConfirmCommitWithExpectedSeqNumRange(t, e.Env.Chains[initialDeploy[0]], e.Env.Chains[newChain], state.Chains[newChain].OffRamp, &startBlock, cciptypes.SeqNumRange{ - cciptypes.SeqNum(1), - cciptypes.SeqNum(msgSentEvent.SequenceNumber), - }, true))) - require.NoError(t, - commonutils.JustError( - ConfirmExecWithSeqNrs( - t, - e.Env.Chains[initialDeploy[0]], - e.Env.Chains[newChain], - state.Chains[newChain].OffRamp, - &startBlock, - []uint64{msgSentEvent.SequenceNumber}, - ), - ), - ) - - linkAddress := state.Chains[newChain].LinkToken.Address() - feeQuoter := state.Chains[newChain].FeeQuoter - timestampedPrice, err := feeQuoter.GetTokenPrice(nil, linkAddress) - require.NoError(t, err) - require.Equal(t, MockLinkPrice, timestampedPrice.Value) -} diff --git a/deployment/ccip/changeset/cs_ccip_home.go b/deployment/ccip/changeset/cs_ccip_home.go index f1e860d9d28..1d8c6782b76 100644 --- a/deployment/ccip/changeset/cs_ccip_home.go +++ b/deployment/ccip/changeset/cs_ccip_home.go @@ -1,9 +1,12 @@ package changeset import ( - "context" + "bytes" + "encoding/hex" "fmt" "math/big" + "os" + "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -11,9 +14,17 @@ import ( "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" + "github.com/smartcontractkit/chainlink-ccip/chainconfig" + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-ccip/pluginconfig" + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/merklemulti" "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" + commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + commontypes "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_home" @@ -25,14 +36,82 @@ var ( _ deployment.ChangeSet[PromoteAllCandidatesChangesetConfig] = PromoteAllCandidatesChangeset _ deployment.ChangeSet[SetCandidateChangesetConfig] = SetCandidateChangeset _ deployment.ChangeSet[RevokeCandidateChangesetConfig] = RevokeCandidateChangeset + _ deployment.ChangeSet[UpdateChainConfigConfig] = UpdateChainConfig ) +type CCIPOCRParams struct { + OCRParameters commontypes.OCRParameters + // Note contains pointers to Arb feeds for prices + CommitOffChainConfig pluginconfig.CommitOffchainConfig + // Note ontains USDC config + ExecuteOffChainConfig pluginconfig.ExecuteOffchainConfig +} + +func (c CCIPOCRParams) Validate() error { + if err := c.OCRParameters.Validate(); err != nil { + return fmt.Errorf("invalid OCR parameters: %w", err) + } + if err := c.CommitOffChainConfig.Validate(); err != nil { + return fmt.Errorf("invalid commit off-chain config: %w", err) + } + if err := c.ExecuteOffChainConfig.Validate(); err != nil { + return fmt.Errorf("invalid execute off-chain config: %w", err) + } + return nil +} + +// DefaultOCRParams returns the default OCR parameters for a chain, +// except for a few values which must be parameterized (passed as arguments). +func DefaultOCRParams( + feedChainSel uint64, + tokenInfo map[ccipocr3.UnknownEncodedAddress]pluginconfig.TokenInfo, + tokenDataObservers []pluginconfig.TokenDataObserverConfig, +) CCIPOCRParams { + return CCIPOCRParams{ + OCRParameters: commontypes.OCRParameters{ + DeltaProgress: internal.DeltaProgress, + DeltaResend: internal.DeltaResend, + DeltaInitial: internal.DeltaInitial, + DeltaRound: internal.DeltaRound, + DeltaGrace: internal.DeltaGrace, + DeltaCertifiedCommitRequest: internal.DeltaCertifiedCommitRequest, + DeltaStage: internal.DeltaStage, + Rmax: internal.Rmax, + MaxDurationQuery: internal.MaxDurationQuery, + MaxDurationObservation: internal.MaxDurationObservation, + MaxDurationShouldAcceptAttestedReport: internal.MaxDurationShouldAcceptAttestedReport, + MaxDurationShouldTransmitAcceptedReport: internal.MaxDurationShouldTransmitAcceptedReport, + }, + ExecuteOffChainConfig: pluginconfig.ExecuteOffchainConfig{ + BatchGasLimit: internal.BatchGasLimit, + RelativeBoostPerWaitHour: internal.RelativeBoostPerWaitHour, + InflightCacheExpiry: *config.MustNewDuration(internal.InflightCacheExpiry), + RootSnoozeTime: *config.MustNewDuration(internal.RootSnoozeTime), + MessageVisibilityInterval: *config.MustNewDuration(internal.FirstBlockAge), + BatchingStrategyID: internal.BatchingStrategyID, + TokenDataObservers: tokenDataObservers, + }, + CommitOffChainConfig: pluginconfig.CommitOffchainConfig{ + RemoteGasPriceBatchWriteFrequency: *config.MustNewDuration(internal.RemoteGasPriceBatchWriteFrequency), + TokenPriceBatchWriteFrequency: *config.MustNewDuration(internal.TokenPriceBatchWriteFrequency), + TokenInfo: tokenInfo, + PriceFeedChainSelector: ccipocr3.ChainSelector(feedChainSel), + NewMsgScanBatchSize: merklemulti.MaxNumberTreeLeaves, + MaxReportTransmissionCheckAttempts: 5, + RMNEnabled: os.Getenv("ENABLE_RMN") == "true", // only enabled in manual test + RMNSignaturesTimeout: 30 * time.Minute, + MaxMerkleTreeSize: merklemulti.MaxNumberTreeLeaves, + SignObservationPrefix: "chainlink ccip 1.6 rmn observation", + }, + } +} + type PromoteAllCandidatesChangesetConfig struct { HomeChainSelector uint64 - // DONChainSelector is the chain selector of the DON that we want to promote the candidate config of. + // RemoteChainSelectors is the chain selector of the DONs that we want to promote the candidate config of. // Note that each (chain, ccip capability version) pair has a unique DON ID. - DONChainSelector uint64 + RemoteChainSelectors []uint64 // MCMS is optional MCMS configuration, if provided the changeset will generate an MCMS proposal. // If nil, the changeset will execute the commands directly using the deployer key @@ -40,65 +119,84 @@ type PromoteAllCandidatesChangesetConfig struct { MCMS *MCMSConfig } -func (p PromoteAllCandidatesChangesetConfig) Validate(e deployment.Environment, state CCIPOnChainState) (donID uint32, err error) { - if err := deployment.IsValidChainSelector(p.HomeChainSelector); err != nil { - return 0, fmt.Errorf("home chain selector invalid: %w", err) - } - if err := deployment.IsValidChainSelector(p.DONChainSelector); err != nil { - return 0, fmt.Errorf("don chain selector invalid: %w", err) +func (p PromoteAllCandidatesChangesetConfig) Validate(e deployment.Environment) ([]uint32, error) { + state, err := LoadOnchainState(e) + if err != nil { + return nil, err } - if len(e.NodeIDs) == 0 { - return 0, fmt.Errorf("NodeIDs must be set") + if err := deployment.IsValidChainSelector(p.HomeChainSelector); err != nil { + return nil, fmt.Errorf("home chain selector invalid: %w", err) } - if state.Chains[p.HomeChainSelector].CCIPHome == nil { - return 0, fmt.Errorf("CCIPHome contract does not exist") + homeChainState, exists := state.Chains[p.HomeChainSelector] + if !exists { + return nil, fmt.Errorf("home chain %d does not exist", p.HomeChainSelector) } - if state.Chains[p.HomeChainSelector].CapabilityRegistry == nil { - return 0, fmt.Errorf("CapabilityRegistry contract does not exist") - } - if state.Chains[p.DONChainSelector].OffRamp == nil { - // should not be possible, but a defensive check. - return 0, fmt.Errorf("OffRamp contract does not exist") + if err := commoncs.ValidateOwnership(e.GetContext(), p.MCMS != nil, e.Chains[p.HomeChainSelector].DeployerKey.From, homeChainState.Timelock.Address(), homeChainState.CapabilityRegistry); err != nil { + return nil, err } - donID, err = internal.DonIDForChain( - state.Chains[p.HomeChainSelector].CapabilityRegistry, - state.Chains[p.HomeChainSelector].CCIPHome, - p.DONChainSelector, - ) - if err != nil { - return 0, fmt.Errorf("fetch don id for chain: %w", err) - } - if donID == 0 { - return 0, fmt.Errorf("don doesn't exist in CR for chain %d", p.DONChainSelector) - } + var donIDs []uint32 + for _, chainSelector := range p.RemoteChainSelectors { + if err := deployment.IsValidChainSelector(chainSelector); err != nil { + return nil, fmt.Errorf("don chain selector invalid: %w", err) + } + chainState, exists := state.Chains[chainSelector] + if !exists { + return nil, fmt.Errorf("chain %d does not exist", chainSelector) + } + if chainState.OffRamp == nil { + // should not be possible, but a defensive check. + return nil, fmt.Errorf("OffRamp contract does not exist") + } - // Check that candidate digest and active digest are not both zero - this is enforced onchain. - commitConfigs, err := state.Chains[p.HomeChainSelector].CCIPHome.GetAllConfigs(&bind.CallOpts{ - Context: context.Background(), - }, donID, uint8(cctypes.PluginTypeCCIPCommit)) - if err != nil { - return 0, fmt.Errorf("fetching commit configs from cciphome: %w", err) - } + donID, err := internal.DonIDForChain( + state.Chains[p.HomeChainSelector].CapabilityRegistry, + state.Chains[p.HomeChainSelector].CCIPHome, + chainSelector, + ) + if err != nil { + return nil, fmt.Errorf("fetch don id for chain: %w", err) + } + if donID == 0 { + return nil, fmt.Errorf("don doesn't exist in CR for chain %d", p.RemoteChainSelectors) + } + // Check that candidate digest and active digest are not both zero - this is enforced onchain. + commitConfigs, err := state.Chains[p.HomeChainSelector].CCIPHome.GetAllConfigs(&bind.CallOpts{ + Context: e.GetContext(), + }, donID, uint8(cctypes.PluginTypeCCIPCommit)) + if err != nil { + return nil, fmt.Errorf("fetching commit configs from cciphome: %w", err) + } - execConfigs, err := state.Chains[p.HomeChainSelector].CCIPHome.GetAllConfigs(&bind.CallOpts{ - Context: context.Background(), - }, donID, uint8(cctypes.PluginTypeCCIPExec)) - if err != nil { - return 0, fmt.Errorf("fetching exec configs from cciphome: %w", err) - } + execConfigs, err := state.Chains[p.HomeChainSelector].CCIPHome.GetAllConfigs(&bind.CallOpts{ + Context: e.GetContext(), + }, donID, uint8(cctypes.PluginTypeCCIPExec)) + if err != nil { + return nil, fmt.Errorf("fetching exec configs from cciphome: %w", err) + } - if commitConfigs.ActiveConfig.ConfigDigest == [32]byte{} && - commitConfigs.CandidateConfig.ConfigDigest == [32]byte{} { - return 0, fmt.Errorf("commit active and candidate config digests are both zero") - } + if commitConfigs.ActiveConfig.ConfigDigest == [32]byte{} && + commitConfigs.CandidateConfig.ConfigDigest == [32]byte{} { + return nil, fmt.Errorf("commit active and candidate config digests are both zero") + } - if execConfigs.ActiveConfig.ConfigDigest == [32]byte{} && - execConfigs.CandidateConfig.ConfigDigest == [32]byte{} { - return 0, fmt.Errorf("exec active and candidate config digests are both zero") + if execConfigs.ActiveConfig.ConfigDigest == [32]byte{} && + execConfigs.CandidateConfig.ConfigDigest == [32]byte{} { + return nil, fmt.Errorf("exec active and candidate config digests are both zero") + } + donIDs = append(donIDs, donID) + } + if len(e.NodeIDs) == 0 { + return nil, fmt.Errorf("NodeIDs must be set") + } + if state.Chains[p.HomeChainSelector].CCIPHome == nil { + return nil, fmt.Errorf("CCIPHome contract does not exist") + } + if state.Chains[p.HomeChainSelector].CapabilityRegistry == nil { + return nil, fmt.Errorf("CapabilityRegistry contract does not exist") } - return donID, nil + return donIDs, nil } // PromoteAllCandidatesChangeset generates a proposal to call promoteCandidate on the CCIPHome through CapReg. @@ -110,14 +208,13 @@ func PromoteAllCandidatesChangeset( e deployment.Environment, cfg PromoteAllCandidatesChangesetConfig, ) (deployment.ChangesetOutput, error) { - state, err := LoadOnchainState(e) + donIDs, err := cfg.Validate(e) if err != nil { - return deployment.ChangesetOutput{}, err + return deployment.ChangesetOutput{}, fmt.Errorf("%w: %w", deployment.ErrInvalidConfig, err) } - - donID, err := cfg.Validate(e, state) + state, err := LoadOnchainState(e) if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("%w: %w", deployment.ErrInvalidConfig, err) + return deployment.ChangesetOutput{}, err } nodes, err := deployment.NodeInfo(e.NodeIDs, e.Offchain) @@ -132,17 +229,21 @@ func PromoteAllCandidatesChangeset( homeChain := e.Chains[cfg.HomeChainSelector] - promoteCandidateOps, err := promoteAllCandidatesForChainOps( - txOpts, - homeChain, - state.Chains[cfg.HomeChainSelector].CapabilityRegistry, - state.Chains[cfg.HomeChainSelector].CCIPHome, - nodes.NonBootstraps(), - donID, - cfg.MCMS != nil, - ) - if err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("generating promote candidate ops: %w", err) + var ops []mcms.Operation + for _, donID := range donIDs { + promoteCandidateOps, err := promoteAllCandidatesForChainOps( + txOpts, + homeChain, + state.Chains[cfg.HomeChainSelector].CapabilityRegistry, + state.Chains[cfg.HomeChainSelector].CCIPHome, + nodes.NonBootstraps(), + donID, + cfg.MCMS != nil, + ) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("generating promote candidate ops: %w", err) + } + ops = append(ops, promoteCandidateOps...) } // Disabled MCMS means that we already executed the txes, so just return early w/out the proposals. @@ -159,7 +260,7 @@ func PromoteAllCandidatesChangeset( }, []timelock.BatchChainOperation{{ ChainIdentifier: mcms.ChainIdentifier(cfg.HomeChainSelector), - Batch: promoteCandidateOps, + Batch: ops, }}, "promoteCandidate for commit and execution", cfg.MCMS.MinDelay, @@ -181,12 +282,12 @@ type SetCandidateConfigBase struct { HomeChainSelector uint64 FeedChainSelector uint64 - // DONChainSelector is the chain selector of the chain where the DON will be added. - DONChainSelector uint64 + // OCRConfigPerRemoteChainSelector is the chain selector of the chain where the DON will be added. + OCRConfigPerRemoteChainSelector map[uint64]CCIPOCRParams + // Only set one plugin at a time. TODO + // come back and allow both. PluginType types.PluginType - // Note that the PluginType field is used to determine which field in CCIPOCRParams is used. - CCIPOCRParams CCIPOCRParams // MCMS is optional MCMS configuration, if provided the changeset will generate an MCMS proposal. // If nil, the changeset will execute the commands directly using the deployer key @@ -201,8 +302,46 @@ func (s SetCandidateConfigBase) Validate(e deployment.Environment, state CCIPOnC if err := deployment.IsValidChainSelector(s.FeedChainSelector); err != nil { return fmt.Errorf("feed chain selector invalid: %w", err) } - if err := deployment.IsValidChainSelector(s.DONChainSelector); err != nil { - return fmt.Errorf("don chain selector invalid: %w", err) + homeChainState, exists := state.Chains[s.HomeChainSelector] + if !exists { + return fmt.Errorf("home chain %d does not exist", s.HomeChainSelector) + } + if err := commoncs.ValidateOwnership(e.GetContext(), s.MCMS != nil, e.Chains[s.HomeChainSelector].DeployerKey.From, homeChainState.Timelock.Address(), homeChainState.CapabilityRegistry); err != nil { + return err + } + + for chainSelector, params := range s.OCRConfigPerRemoteChainSelector { + if err := deployment.IsValidChainSelector(chainSelector); err != nil { + return fmt.Errorf("don chain selector invalid: %w", err) + } + if state.Chains[chainSelector].OffRamp == nil { + // should not be possible, but a defensive check. + return fmt.Errorf("OffRamp contract does not exist on don chain selector %d", chainSelector) + } + if s.PluginType != types.PluginTypeCCIPCommit && + s.PluginType != types.PluginTypeCCIPExec { + return fmt.Errorf("PluginType must be set to either CCIPCommit or CCIPExec") + } + + // no donID check since this config is used for both adding a new DON and updating an existing one. + // see AddDonAndSetCandidateChangesetConfig.Validate and SetCandidateChangesetConfig.Validate + // for these checks. + // check that chain config is set up for the new chain + chainConfig, err := state.Chains[s.HomeChainSelector].CCIPHome.GetChainConfig(nil, chainSelector) + if err != nil { + return fmt.Errorf("get all chain configs: %w", err) + } + // FChain should never be zero if a chain config is set in CCIPHome + if chainConfig.FChain == 0 { + return fmt.Errorf("chain config not set up for new chain %d", chainSelector) + } + err = params.Validate() + if err != nil { + return fmt.Errorf("invalid ccip ocr params: %w", err) + } + + // TODO: validate token config in the commit config, if commit is the plugin. + // TODO: validate gas config in the chain config in cciphome for this RemoteChainSelectors. } if len(e.NodeIDs) == 0 { return fmt.Errorf("nodeIDs must be set") @@ -213,37 +352,6 @@ func (s SetCandidateConfigBase) Validate(e deployment.Environment, state CCIPOnC if state.Chains[s.HomeChainSelector].CapabilityRegistry == nil { return fmt.Errorf("CapabilityRegistry contract does not exist") } - if state.Chains[s.DONChainSelector].OffRamp == nil { - // should not be possible, but a defensive check. - return fmt.Errorf("OffRamp contract does not exist on don chain selector %d", s.DONChainSelector) - } - if s.PluginType != types.PluginTypeCCIPCommit && - s.PluginType != types.PluginTypeCCIPExec { - return fmt.Errorf("PluginType must be set to either CCIPCommit or CCIPExec") - } - - // no donID check since this config is used for both adding a new DON and updating an existing one. - // see AddDonAndSetCandidateChangesetConfig.Validate and SetCandidateChangesetConfig.Validate - // for these checks. - - // check that chain config is set up for the new chain - chainConfig, err := state.Chains[s.HomeChainSelector].CCIPHome.GetChainConfig(nil, s.DONChainSelector) - if err != nil { - return fmt.Errorf("get all chain configs: %w", err) - } - - // FChain should never be zero if a chain config is set in CCIPHome - if chainConfig.FChain == 0 { - return fmt.Errorf("chain config not set up for new chain %d", s.DONChainSelector) - } - - err = s.CCIPOCRParams.Validate() - if err != nil { - return fmt.Errorf("invalid ccip ocr params: %w", err) - } - - // TODO: validate token config in the commit config, if commit is the plugin. - // TODO: validate gas config in the chain config in cciphome for this DONChainSelector. if e.OCRSecrets.IsEmpty() { return fmt.Errorf("OCR secrets must be set") @@ -265,17 +373,19 @@ func (a AddDonAndSetCandidateChangesetConfig) Validate(e deployment.Environment, return err } - // check if a DON already exists for this chain - donID, err := internal.DonIDForChain( - state.Chains[a.HomeChainSelector].CapabilityRegistry, - state.Chains[a.HomeChainSelector].CCIPHome, - a.DONChainSelector, - ) - if err != nil { - return fmt.Errorf("fetch don id for chain: %w", err) - } - if donID != 0 { - return fmt.Errorf("don already exists in CR for chain %d, it has id %d", a.DONChainSelector, donID) + for chainSelector := range a.OCRConfigPerRemoteChainSelector { + // check if a DON already exists for this chain + donID, err := internal.DonIDForChain( + state.Chains[a.HomeChainSelector].CapabilityRegistry, + state.Chains[a.HomeChainSelector].CCIPHome, + chainSelector, + ) + if err != nil { + return fmt.Errorf("fetch don id for chain: %w", err) + } + if donID != 0 { + return fmt.Errorf("don already exists in CR for chain %d, it has id %d", chainSelector, donID) + } } return nil @@ -314,43 +424,46 @@ func AddDonAndSetCandidateChangeset( if cfg.MCMS != nil { txOpts = deployment.SimTransactOpts() } + var donOps []mcms.Operation + for chainSelector, params := range cfg.OCRConfigPerRemoteChainSelector { + newDONArgs, err := internal.BuildOCR3ConfigForCCIPHome( + e.OCRSecrets, + state.Chains[chainSelector].OffRamp, + e.Chains[chainSelector], + nodes.NonBootstraps(), + state.Chains[cfg.HomeChainSelector].RMNHome.Address(), + params.OCRParameters, + params.CommitOffChainConfig, + params.ExecuteOffChainConfig, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } - newDONArgs, err := internal.BuildOCR3ConfigForCCIPHome( - e.OCRSecrets, - state.Chains[cfg.DONChainSelector].OffRamp, - e.Chains[cfg.DONChainSelector], - nodes.NonBootstraps(), - state.Chains[cfg.HomeChainSelector].RMNHome.Address(), - cfg.CCIPOCRParams.OCRParameters, - cfg.CCIPOCRParams.CommitOffChainConfig, - cfg.CCIPOCRParams.ExecuteOffChainConfig, - ) - if err != nil { - return deployment.ChangesetOutput{}, err - } - - latestDon, err := internal.LatestCCIPDON(state.Chains[cfg.HomeChainSelector].CapabilityRegistry) - if err != nil { - return deployment.ChangesetOutput{}, err - } + latestDon, err := internal.LatestCCIPDON(state.Chains[cfg.HomeChainSelector].CapabilityRegistry) + if err != nil { + return deployment.ChangesetOutput{}, err + } - pluginOCR3Config, ok := newDONArgs[cfg.PluginType] - if !ok { - return deployment.ChangesetOutput{}, fmt.Errorf("missing commit plugin in ocr3Configs") - } + pluginOCR3Config, ok := newDONArgs[cfg.PluginType] + if !ok { + return deployment.ChangesetOutput{}, fmt.Errorf("missing commit plugin in ocr3Configs") + } - expectedDonID := latestDon.Id + 1 - addDonOp, err := newDonWithCandidateOp( - txOpts, - e.Chains[cfg.HomeChainSelector], - expectedDonID, - pluginOCR3Config, - state.Chains[cfg.HomeChainSelector].CapabilityRegistry, - nodes.NonBootstraps(), - cfg.MCMS != nil, - ) - if err != nil { - return deployment.ChangesetOutput{}, err + expectedDonID := latestDon.Id + 1 + addDonOp, err := newDonWithCandidateOp( + txOpts, + e.Chains[cfg.HomeChainSelector], + expectedDonID, + pluginOCR3Config, + state.Chains[cfg.HomeChainSelector].CapabilityRegistry, + nodes.NonBootstraps(), + cfg.MCMS != nil, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + donOps = append(donOps, addDonOp) } if cfg.MCMS == nil { return deployment.ChangesetOutput{}, nil @@ -365,7 +478,7 @@ func AddDonAndSetCandidateChangeset( }, []timelock.BatchChainOperation{{ ChainIdentifier: mcms.ChainIdentifier(cfg.HomeChainSelector), - Batch: []mcms.Operation{addDonOp}, + Batch: donOps, }}, fmt.Sprintf("addDON on new Chain && setCandidate for plugin %s", cfg.PluginType.String()), cfg.MCMS.MinDelay, @@ -436,25 +549,29 @@ type SetCandidateChangesetConfig struct { SetCandidateConfigBase } -func (s SetCandidateChangesetConfig) Validate(e deployment.Environment, state CCIPOnChainState) (donID uint32, err error) { - err = s.SetCandidateConfigBase.Validate(e, state) +func (s SetCandidateChangesetConfig) Validate(e deployment.Environment, state CCIPOnChainState) (map[uint64]uint32, error) { + err := s.SetCandidateConfigBase.Validate(e, state) if err != nil { - return 0, err + return nil, err } - donID, err = internal.DonIDForChain( - state.Chains[s.HomeChainSelector].CapabilityRegistry, - state.Chains[s.HomeChainSelector].CCIPHome, - s.DONChainSelector, - ) - if err != nil { - return 0, fmt.Errorf("fetch don id for chain: %w", err) - } - if donID == 0 { - return 0, fmt.Errorf("don doesn't exist in CR for chain %d", s.DONChainSelector) + chainToDonIDs := make(map[uint64]uint32) + for chainSelector := range s.OCRConfigPerRemoteChainSelector { + donID, err := internal.DonIDForChain( + state.Chains[s.HomeChainSelector].CapabilityRegistry, + state.Chains[s.HomeChainSelector].CCIPHome, + chainSelector, + ) + if err != nil { + return nil, fmt.Errorf("fetch don id for chain: %w", err) + } + if donID == 0 { + return nil, fmt.Errorf("don doesn't exist in CR for chain %d", chainSelector) + } + chainToDonIDs[chainSelector] = donID } - return donID, nil + return chainToDonIDs, nil } // SetCandidateChangeset generates a proposal to call setCandidate on the CCIPHome through the capability registry. @@ -468,7 +585,7 @@ func SetCandidateChangeset( return deployment.ChangesetOutput{}, err } - donID, err := cfg.Validate(e, state) + chainToDonIDs, err := cfg.Validate(e, state) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("%w: %w", deployment.ErrInvalidConfig, err) } @@ -482,37 +599,40 @@ func SetCandidateChangeset( if cfg.MCMS != nil { txOpts = deployment.SimTransactOpts() } + var setCandidateOps []mcms.Operation + for chainSelector, params := range cfg.OCRConfigPerRemoteChainSelector { + newDONArgs, err := internal.BuildOCR3ConfigForCCIPHome( + e.OCRSecrets, + state.Chains[chainSelector].OffRamp, + e.Chains[chainSelector], + nodes.NonBootstraps(), + state.Chains[cfg.HomeChainSelector].RMNHome.Address(), + params.OCRParameters, + params.CommitOffChainConfig, + params.ExecuteOffChainConfig, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } - newDONArgs, err := internal.BuildOCR3ConfigForCCIPHome( - e.OCRSecrets, - state.Chains[cfg.DONChainSelector].OffRamp, - e.Chains[cfg.DONChainSelector], - nodes.NonBootstraps(), - state.Chains[cfg.HomeChainSelector].RMNHome.Address(), - cfg.CCIPOCRParams.OCRParameters, - cfg.CCIPOCRParams.CommitOffChainConfig, - cfg.CCIPOCRParams.ExecuteOffChainConfig, - ) - if err != nil { - return deployment.ChangesetOutput{}, err - } - - config, ok := newDONArgs[cfg.PluginType] - if !ok { - return deployment.ChangesetOutput{}, fmt.Errorf("missing %s plugin in ocr3Configs", cfg.PluginType.String()) - } + config, ok := newDONArgs[cfg.PluginType] + if !ok { + return deployment.ChangesetOutput{}, fmt.Errorf("missing %s plugin in ocr3Configs", cfg.PluginType.String()) + } - setCandidateMCMSOps, err := setCandidateOnExistingDon( - txOpts, - e.Chains[cfg.HomeChainSelector], - state.Chains[cfg.HomeChainSelector].CapabilityRegistry, - nodes.NonBootstraps(), - donID, - config, - cfg.MCMS != nil, - ) - if err != nil { - return deployment.ChangesetOutput{}, err + setCandidateMCMSOps, err := setCandidateOnExistingDon( + txOpts, + e.Chains[cfg.HomeChainSelector], + state.Chains[cfg.HomeChainSelector].CapabilityRegistry, + nodes.NonBootstraps(), + chainToDonIDs[chainSelector], + config, + cfg.MCMS != nil, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + setCandidateOps = append(setCandidateOps, setCandidateMCMSOps...) } if cfg.MCMS == nil { @@ -528,7 +648,7 @@ func SetCandidateChangeset( }, []timelock.BatchChainOperation{{ ChainIdentifier: mcms.ChainIdentifier(cfg.HomeChainSelector), - Batch: setCandidateMCMSOps, + Batch: setCandidateOps, }}, fmt.Sprintf("SetCandidate for %s plugin", cfg.PluginType.String()), cfg.MCMS.MinDelay, @@ -714,9 +834,9 @@ func promoteAllCandidatesForChainOps( type RevokeCandidateChangesetConfig struct { HomeChainSelector uint64 - // DONChainSelector is the chain selector whose candidate config we want to revoke. - DONChainSelector uint64 - PluginType types.PluginType + // RemoteChainSelector is the chain selector whose candidate config we want to revoke. + RemoteChainSelector uint64 + PluginType types.PluginType // MCMS is optional MCMS configuration, if provided the changeset will generate an MCMS proposal. // If nil, the changeset will execute the commands directly using the deployer key @@ -728,7 +848,7 @@ func (r RevokeCandidateChangesetConfig) Validate(e deployment.Environment, state if err := deployment.IsValidChainSelector(r.HomeChainSelector); err != nil { return 0, fmt.Errorf("home chain selector invalid: %w", err) } - if err := deployment.IsValidChainSelector(r.DONChainSelector); err != nil { + if err := deployment.IsValidChainSelector(r.RemoteChainSelector); err != nil { return 0, fmt.Errorf("don chain selector invalid: %w", err) } if len(e.NodeIDs) == 0 { @@ -740,18 +860,25 @@ func (r RevokeCandidateChangesetConfig) Validate(e deployment.Environment, state if state.Chains[r.HomeChainSelector].CapabilityRegistry == nil { return 0, fmt.Errorf("CapabilityRegistry contract does not exist") } + homeChainState, exists := state.Chains[r.HomeChainSelector] + if !exists { + return 0, fmt.Errorf("home chain %d does not exist", r.HomeChainSelector) + } + if err := commoncs.ValidateOwnership(e.GetContext(), r.MCMS != nil, e.Chains[r.HomeChainSelector].DeployerKey.From, homeChainState.Timelock.Address(), homeChainState.CapabilityRegistry); err != nil { + return 0, err + } // check that the don exists for this chain donID, err = internal.DonIDForChain( state.Chains[r.HomeChainSelector].CapabilityRegistry, state.Chains[r.HomeChainSelector].CCIPHome, - r.DONChainSelector, + r.RemoteChainSelector, ) if err != nil { return 0, fmt.Errorf("fetch don id for chain: %w", err) } if donID == 0 { - return 0, fmt.Errorf("don doesn't exist in CR for chain %d", r.DONChainSelector) + return 0, fmt.Errorf("don doesn't exist in CR for chain %d", r.RemoteChainSelector) } // check that candidate digest is not zero - this is enforced onchain. @@ -816,7 +943,7 @@ func RevokeCandidateChangeset(e deployment.Environment, cfg RevokeCandidateChang ChainIdentifier: mcms.ChainIdentifier(cfg.HomeChainSelector), Batch: ops, }}, - fmt.Sprintf("revokeCandidate for don %d", cfg.DONChainSelector), + fmt.Sprintf("revokeCandidate for don %d", cfg.RemoteChainSelector), cfg.MCMS.MinDelay, ) if err != nil { @@ -888,3 +1015,219 @@ func revokeCandidateOps( Value: big.NewInt(0), }}, nil } + +type ChainConfig struct { + Readers [][32]byte + FChain uint8 + EncodableChainConfig chainconfig.ChainConfig +} + +type UpdateChainConfigConfig struct { + HomeChainSelector uint64 + RemoteChainRemoves []uint64 + RemoteChainAdds map[uint64]ChainConfig + MCMS *MCMSConfig +} + +func (c UpdateChainConfigConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + if err != nil { + return err + } + if err := deployment.IsValidChainSelector(c.HomeChainSelector); err != nil { + return fmt.Errorf("home chain selector invalid: %w", err) + } + if len(c.RemoteChainRemoves) == 0 && len(c.RemoteChainAdds) == 0 { + return fmt.Errorf("no chain adds or removes") + } + homeChainState, exists := state.Chains[c.HomeChainSelector] + if !exists { + return fmt.Errorf("home chain %d does not exist", c.HomeChainSelector) + } + if err := commoncs.ValidateOwnership(e.GetContext(), c.MCMS != nil, e.Chains[c.HomeChainSelector].DeployerKey.From, homeChainState.Timelock.Address(), homeChainState.CCIPHome); err != nil { + return err + } + for _, remove := range c.RemoteChainRemoves { + if err := deployment.IsValidChainSelector(remove); err != nil { + return fmt.Errorf("chain remove selector invalid: %w", err) + } + if _, ok := state.SupportedChains()[remove]; !ok { + return fmt.Errorf("chain to remove %d is not supported", remove) + } + } + for add, ccfg := range c.RemoteChainAdds { + if err := deployment.IsValidChainSelector(add); err != nil { + return fmt.Errorf("chain remove selector invalid: %w", err) + } + if _, ok := state.SupportedChains()[add]; !ok { + return fmt.Errorf("chain to add %d is not supported", add) + } + if ccfg.FChain == 0 { + return fmt.Errorf("FChain must be set") + } + if len(ccfg.Readers) == 0 { + return fmt.Errorf("Readers must be set") + } + } + return nil +} + +func UpdateChainConfig(e deployment.Environment, cfg UpdateChainConfigConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("%w: %w", deployment.ErrInvalidConfig, err) + } + state, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + txOpts := e.Chains[cfg.HomeChainSelector].DeployerKey + txOpts.Context = e.GetContext() + if cfg.MCMS != nil { + txOpts = deployment.SimTransactOpts() + } + var adds []ccip_home.CCIPHomeChainConfigArgs + for chain, ccfg := range cfg.RemoteChainAdds { + encodedChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ + GasPriceDeviationPPB: ccfg.EncodableChainConfig.GasPriceDeviationPPB, + DAGasPriceDeviationPPB: ccfg.EncodableChainConfig.DAGasPriceDeviationPPB, + OptimisticConfirmations: ccfg.EncodableChainConfig.OptimisticConfirmations, + }) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("encoding chain config: %w", err) + } + chainConfig := ccip_home.CCIPHomeChainConfig{ + Readers: ccfg.Readers, + FChain: ccfg.FChain, + Config: encodedChainConfig, + } + existingCfg, err := state.Chains[cfg.HomeChainSelector].CCIPHome.GetChainConfig(nil, chain) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("get chain config for selector %d: %w", chain, err) + } + if isChainConfigEqual(existingCfg, chainConfig) { + e.Logger.Infow("Chain config already exists, not applying again", + "addedChain", chain, + "chainConfig", chainConfig, + ) + continue + } + adds = append(adds, ccip_home.CCIPHomeChainConfigArgs{ + ChainSelector: chain, + ChainConfig: chainConfig, + }) + } + + tx, err := state.Chains[cfg.HomeChainSelector].CCIPHome.ApplyChainConfigUpdates(txOpts, cfg.RemoteChainRemoves, adds) + if cfg.MCMS == nil { + _, err = deployment.ConfirmIfNoError(e.Chains[cfg.HomeChainSelector], tx, err) + if err != nil { + return deployment.ChangesetOutput{}, err + } + e.Logger.Infof("Updated chain config on chain %d removes %v, adds %v", cfg.HomeChainSelector, cfg.RemoteChainRemoves, cfg.RemoteChainAdds) + return deployment.ChangesetOutput{}, nil + } + + p, err := proposalutils.BuildProposalFromBatches( + map[uint64]common.Address{ + cfg.HomeChainSelector: state.Chains[cfg.HomeChainSelector].Timelock.Address(), + }, + map[uint64]*gethwrappers.ManyChainMultiSig{ + cfg.HomeChainSelector: state.Chains[cfg.HomeChainSelector].ProposerMcm, + }, + []timelock.BatchChainOperation{{ + ChainIdentifier: mcms.ChainIdentifier(cfg.HomeChainSelector), + Batch: []mcms.Operation{ + { + To: state.Chains[cfg.HomeChainSelector].CCIPHome.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + }}, + "Update chain config", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + e.Logger.Infof("Proposed chain config update on chain %d removes %v, adds %v", cfg.HomeChainSelector, cfg.RemoteChainRemoves, cfg.RemoteChainAdds) + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{ + *p, + }}, nil +} + +func isChainConfigEqual(a, b ccip_home.CCIPHomeChainConfig) bool { + mapReader := make(map[[32]byte]struct{}) + for i := range a.Readers { + mapReader[a.Readers[i]] = struct{}{} + } + for i := range b.Readers { + if _, ok := mapReader[b.Readers[i]]; !ok { + return false + } + } + return bytes.Equal(a.Config, b.Config) && + a.FChain == b.FChain +} + +// ValidateCCIPHomeConfigSetUp checks that the commit and exec active and candidate configs are set up correctly +// TODO: Utilize this +func ValidateCCIPHomeConfigSetUp( + lggr logger.Logger, + capReg *capabilities_registry.CapabilitiesRegistry, + ccipHome *ccip_home.CCIPHome, + chainSel uint64, +) error { + // fetch DONID + donID, err := internal.DonIDForChain(capReg, ccipHome, chainSel) + if err != nil { + return fmt.Errorf("fetch don id for chain: %w", err) + } + if donID == 0 { + return fmt.Errorf("don id for chain (%d) does not exist", chainSel) + } + + // final sanity checks on configs. + commitConfigs, err := ccipHome.GetAllConfigs(&bind.CallOpts{ + //Pending: true, + }, donID, uint8(cctypes.PluginTypeCCIPCommit)) + if err != nil { + return fmt.Errorf("get all commit configs: %w", err) + } + commitActiveDigest, err := ccipHome.GetActiveDigest(nil, donID, uint8(cctypes.PluginTypeCCIPCommit)) + if err != nil { + return fmt.Errorf("get active commit digest: %w", err) + } + lggr.Debugw("Fetched active commit digest", "commitActiveDigest", hex.EncodeToString(commitActiveDigest[:])) + commitCandidateDigest, err := ccipHome.GetCandidateDigest(nil, donID, uint8(cctypes.PluginTypeCCIPCommit)) + if err != nil { + return fmt.Errorf("get commit candidate digest: %w", err) + } + lggr.Debugw("Fetched candidate commit digest", "commitCandidateDigest", hex.EncodeToString(commitCandidateDigest[:])) + if commitConfigs.ActiveConfig.ConfigDigest == [32]byte{} { + return fmt.Errorf( + "active config digest is empty for commit, expected nonempty, donID: %d, cfg: %+v, config digest from GetActiveDigest call: %x, config digest from GetCandidateDigest call: %x", + donID, commitConfigs.ActiveConfig, commitActiveDigest, commitCandidateDigest) + } + if commitConfigs.CandidateConfig.ConfigDigest != [32]byte{} { + return fmt.Errorf( + "candidate config digest is nonempty for commit, expected empty, donID: %d, cfg: %+v, config digest from GetCandidateDigest call: %x, config digest from GetActiveDigest call: %x", + donID, commitConfigs.CandidateConfig, commitCandidateDigest, commitActiveDigest) + } + + execConfigs, err := ccipHome.GetAllConfigs(nil, donID, uint8(cctypes.PluginTypeCCIPExec)) + if err != nil { + return fmt.Errorf("get all exec configs: %w", err) + } + lggr.Debugw("Fetched exec configs", + "ActiveConfig.ConfigDigest", hex.EncodeToString(execConfigs.ActiveConfig.ConfigDigest[:]), + "CandidateConfig.ConfigDigest", hex.EncodeToString(execConfigs.CandidateConfig.ConfigDigest[:]), + ) + if execConfigs.ActiveConfig.ConfigDigest == [32]byte{} { + return fmt.Errorf("active config digest is empty for exec, expected nonempty, cfg: %v", execConfigs.ActiveConfig) + } + if execConfigs.CandidateConfig.ConfigDigest != [32]byte{} { + return fmt.Errorf("candidate config digest is nonempty for exec, expected empty, cfg: %v", execConfigs.CandidateConfig) + } + return nil +} diff --git a/deployment/ccip/changeset/cs_ccip_home_test.go b/deployment/ccip/changeset/cs_ccip_home_test.go index b728e7b0c1d..b487f1acc26 100644 --- a/deployment/ccip/changeset/cs_ccip_home_test.go +++ b/deployment/ccip/changeset/cs_ccip_home_test.go @@ -1,11 +1,15 @@ package changeset import ( + "math/big" "testing" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/assert" "golang.org/x/exp/maps" + "github.com/smartcontractkit/chainlink-ccip/chainconfig" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" @@ -83,9 +87,9 @@ func Test_PromoteCandidate(t *testing.T) { { Changeset: commonchangeset.WrapChangeSet(PromoteAllCandidatesChangeset), Config: PromoteAllCandidatesChangesetConfig{ - HomeChainSelector: tenv.HomeChainSel, - DONChainSelector: dest, - MCMS: mcmsConfig, + HomeChainSelector: tenv.HomeChainSel, + RemoteChainSelectors: []uint64{dest}, + MCMS: mcmsConfig, }, }, }) @@ -176,14 +180,15 @@ func Test_SetCandidate(t *testing.T) { SetCandidateConfigBase: SetCandidateConfigBase{ HomeChainSelector: tenv.HomeChainSel, FeedChainSelector: tenv.FeedChainSel, - DONChainSelector: dest, - PluginType: types.PluginTypeCCIPCommit, - CCIPOCRParams: DefaultOCRParams( - tenv.FeedChainSel, - tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[dest].LinkToken, state.Chains[dest].Weth9), - nil, - ), - MCMS: mcmsConfig, + OCRConfigPerRemoteChainSelector: map[uint64]CCIPOCRParams{ + dest: DefaultOCRParams( + tenv.FeedChainSel, + tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[dest].LinkToken, state.Chains[dest].Weth9), + nil, + ), + }, + PluginType: types.PluginTypeCCIPCommit, + MCMS: mcmsConfig, }, }, }, @@ -193,14 +198,15 @@ func Test_SetCandidate(t *testing.T) { SetCandidateConfigBase: SetCandidateConfigBase{ HomeChainSelector: tenv.HomeChainSel, FeedChainSelector: tenv.FeedChainSel, - DONChainSelector: dest, - PluginType: types.PluginTypeCCIPExec, - CCIPOCRParams: DefaultOCRParams( - tenv.FeedChainSel, - tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[dest].LinkToken, state.Chains[dest].Weth9), - nil, - ), - MCMS: mcmsConfig, + OCRConfigPerRemoteChainSelector: map[uint64]CCIPOCRParams{ + dest: DefaultOCRParams( + tenv.FeedChainSel, + tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[dest].LinkToken, state.Chains[dest].Weth9), + nil, + ), + }, + PluginType: types.PluginTypeCCIPExec, + MCMS: mcmsConfig, }, }, }, @@ -295,14 +301,15 @@ func Test_RevokeCandidate(t *testing.T) { SetCandidateConfigBase: SetCandidateConfigBase{ HomeChainSelector: tenv.HomeChainSel, FeedChainSelector: tenv.FeedChainSel, - DONChainSelector: dest, - PluginType: types.PluginTypeCCIPCommit, - CCIPOCRParams: DefaultOCRParams( - tenv.FeedChainSel, - tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[dest].LinkToken, state.Chains[dest].Weth9), - nil, - ), - MCMS: mcmsConfig, + OCRConfigPerRemoteChainSelector: map[uint64]CCIPOCRParams{ + dest: DefaultOCRParams( + tenv.FeedChainSel, + tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[dest].LinkToken, state.Chains[dest].Weth9), + nil, + ), + }, + PluginType: types.PluginTypeCCIPCommit, + MCMS: mcmsConfig, }, }, }, @@ -312,14 +319,15 @@ func Test_RevokeCandidate(t *testing.T) { SetCandidateConfigBase: SetCandidateConfigBase{ HomeChainSelector: tenv.HomeChainSel, FeedChainSelector: tenv.FeedChainSel, - DONChainSelector: dest, - PluginType: types.PluginTypeCCIPExec, - CCIPOCRParams: DefaultOCRParams( - tenv.FeedChainSel, - tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[dest].LinkToken, state.Chains[dest].Weth9), - nil, - ), - MCMS: mcmsConfig, + OCRConfigPerRemoteChainSelector: map[uint64]CCIPOCRParams{ + dest: DefaultOCRParams( + tenv.FeedChainSel, + tokenConfig.GetTokenInfo(logger.TestLogger(t), state.Chains[dest].LinkToken, state.Chains[dest].Weth9), + nil, + ), + }, + PluginType: types.PluginTypeCCIPExec, + MCMS: mcmsConfig, }, }, }, @@ -352,19 +360,19 @@ func Test_RevokeCandidate(t *testing.T) { { Changeset: commonchangeset.WrapChangeSet(RevokeCandidateChangeset), Config: RevokeCandidateChangesetConfig{ - HomeChainSelector: tenv.HomeChainSel, - DONChainSelector: dest, - PluginType: types.PluginTypeCCIPCommit, - MCMS: mcmsConfig, + HomeChainSelector: tenv.HomeChainSel, + RemoteChainSelector: dest, + PluginType: types.PluginTypeCCIPCommit, + MCMS: mcmsConfig, }, }, { Changeset: commonchangeset.WrapChangeSet(RevokeCandidateChangeset), Config: RevokeCandidateChangesetConfig{ - HomeChainSelector: tenv.HomeChainSel, - DONChainSelector: dest, - PluginType: types.PluginTypeCCIPExec, - MCMS: mcmsConfig, + HomeChainSelector: tenv.HomeChainSel, + RemoteChainSelector: dest, + PluginType: types.PluginTypeCCIPExec, + MCMS: mcmsConfig, }, }, }) @@ -415,3 +423,104 @@ func transferToTimelock( require.NoError(t, err) assertTimelockOwnership(t, tenv, []uint64{source, dest}, state) } + +func Test_UpdateChainConfigs(t *testing.T) { + for _, tc := range []struct { + name string + mcmsEnabled bool + }{ + { + name: "MCMS enabled", + mcmsEnabled: true, + }, + { + name: "MCMS disabled", + mcmsEnabled: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + tenv := NewMemoryEnvironment(t, WithChains(3)) + state, err := LoadOnchainState(tenv.Env) + require.NoError(t, err) + + allChains := maps.Keys(tenv.Env.Chains) + source := allChains[0] + dest := allChains[1] + otherChain := allChains[2] + + if tc.mcmsEnabled { + // Transfer ownership to timelock so that we can promote the zero digest later down the line. + transferToTimelock(t, tenv, state, source, dest) + } + + ccipHome := state.Chains[tenv.HomeChainSel].CCIPHome + otherChainConfig, err := ccipHome.GetChainConfig(nil, otherChain) + require.NoError(t, err) + assert.True(t, otherChainConfig.FChain != 0) + + var mcmsConfig *MCMSConfig + if tc.mcmsEnabled { + mcmsConfig = &MCMSConfig{ + MinDelay: 0, + } + } + _, err = commonchangeset.ApplyChangesets(t, tenv.Env, map[uint64]*proposalutils.TimelockExecutionContracts{ + tenv.HomeChainSel: { + Timelock: state.Chains[tenv.HomeChainSel].Timelock, + CallProxy: state.Chains[tenv.HomeChainSel].CallProxy, + }, + }, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(UpdateChainConfig), + Config: UpdateChainConfigConfig{ + HomeChainSelector: tenv.HomeChainSel, + RemoteChainRemoves: []uint64{otherChain}, + RemoteChainAdds: make(map[uint64]ChainConfig), + MCMS: mcmsConfig, + }, + }, + }) + require.NoError(t, err) + + // other chain should be gone + chainConfigAfter, err := ccipHome.GetChainConfig(nil, otherChain) + require.NoError(t, err) + assert.True(t, chainConfigAfter.FChain == 0) + + // Lets add it back now. + _, err = commonchangeset.ApplyChangesets(t, tenv.Env, map[uint64]*proposalutils.TimelockExecutionContracts{ + tenv.HomeChainSel: { + Timelock: state.Chains[tenv.HomeChainSel].Timelock, + CallProxy: state.Chains[tenv.HomeChainSel].CallProxy, + }, + }, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(UpdateChainConfig), + Config: UpdateChainConfigConfig{ + HomeChainSelector: tenv.HomeChainSel, + RemoteChainRemoves: []uint64{}, + RemoteChainAdds: map[uint64]ChainConfig{ + otherChain: { + EncodableChainConfig: chainconfig.ChainConfig{ + GasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(internal.GasPriceDeviationPPB)}, + DAGasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(internal.DAGasPriceDeviationPPB)}, + OptimisticConfirmations: internal.OptimisticConfirmations, + }, + FChain: otherChainConfig.FChain, + Readers: otherChainConfig.Readers, + }, + }, + MCMS: mcmsConfig, + }, + }, + }) + require.NoError(t, err) + + chainConfigAfter2, err := ccipHome.GetChainConfig(nil, otherChain) + require.NoError(t, err) + assert.Equal(t, chainConfigAfter2.FChain, otherChainConfig.FChain) + assert.Equal(t, chainConfigAfter2.Readers, otherChainConfig.Readers) + assert.Equal(t, chainConfigAfter2.Config, otherChainConfig.Config) + }) + } +} diff --git a/deployment/ccip/changeset/cs_chain_contracts.go b/deployment/ccip/changeset/cs_chain_contracts.go new file mode 100644 index 00000000000..e97772793b0 --- /dev/null +++ b/deployment/ccip/changeset/cs_chain_contracts.go @@ -0,0 +1,757 @@ +package changeset + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "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-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" + commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" + "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/onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" +) + +var ( + _ deployment.ChangeSet[UpdateOnRampDestsConfig] = UpdateOnRampsDests + _ deployment.ChangeSet[UpdateOffRampSourcesConfig] = UpdateOffRampSources + _ deployment.ChangeSet[UpdateRouterRampsConfig] = UpdateRouterRamps + _ deployment.ChangeSet[UpdateFeeQuoterDestsConfig] = UpdateFeeQuoterDests + _ deployment.ChangeSet[SetOCR3OffRampConfig] = SetOCR3OffRamp +) + +type UpdateOnRampDestsConfig struct { + UpdatesByChain map[uint64]map[uint64]OnRampDestinationUpdate + // Disallow mixing MCMS/non-MCMS per chain for simplicity. + // (can still be acheived by calling this function multiple times) + MCMS *MCMSConfig +} + +type OnRampDestinationUpdate struct { + IsEnabled bool // If false, disables the destination by setting router to 0x0. + TestRouter bool // Flag for safety only allow specifying either router or testRouter. + AllowListEnabled bool +} + +func (cfg UpdateOnRampDestsConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + if err != nil { + return err + } + supportedChains := state.SupportedChains() + for chainSel, updates := range cfg.UpdatesByChain { + chainState, ok := state.Chains[chainSel] + if !ok { + return fmt.Errorf("chain %d not found in onchain state", chainSel) + } + if chainState.TestRouter == nil { + return fmt.Errorf("missing test router for chain %d", chainSel) + } + if chainState.Router == nil { + return fmt.Errorf("missing router for chain %d", chainSel) + } + if chainState.OnRamp == nil { + return fmt.Errorf("missing onramp onramp for chain %d", chainSel) + } + if err := commoncs.ValidateOwnership(e.GetContext(), cfg.MCMS != nil, e.Chains[chainSel].DeployerKey.From, chainState.Timelock.Address(), chainState.OnRamp); err != nil { + return err + } + + for destination := range updates { + // Destination cannot be an unknown destination. + if _, ok := supportedChains[destination]; !ok { + return fmt.Errorf("destination chain %d is not a supported %s", destination, chainState.OnRamp.Address()) + } + sc, err := chainState.OnRamp.GetStaticConfig(&bind.CallOpts{Context: e.GetContext()}) + if err != nil { + return fmt.Errorf("failed to get onramp static config %s: %w", chainState.OnRamp.Address(), err) + } + if destination == sc.ChainSelector { + return fmt.Errorf("cannot update onramp destination to the same chain") + } + } + } + return nil +} + +// UpdateOnRampsDests updates the onramp destinations for each onramp +// in the chains specified. Multichain support is important - consider when we add a new chain +// and need to update the onramp destinations for all chains to support the new chain. +func UpdateOnRampsDests(e deployment.Environment, cfg UpdateOnRampDestsConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + s, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + var batches []timelock.BatchChainOperation + timelocks := make(map[uint64]common.Address) + proposers := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for chainSel, updates := range cfg.UpdatesByChain { + txOpts := e.Chains[chainSel].DeployerKey + txOpts.Context = e.GetContext() + if cfg.MCMS != nil { + txOpts = deployment.SimTransactOpts() + } + onRamp := s.Chains[chainSel].OnRamp + var args []onramp.OnRampDestChainConfigArgs + for destination, update := range updates { + router := common.HexToAddress("0x0") + // If not enabled, set router to 0x0. + if update.IsEnabled { + if update.TestRouter { + router = s.Chains[chainSel].TestRouter.Address() + } else { + router = s.Chains[chainSel].Router.Address() + } + } + args = append(args, onramp.OnRampDestChainConfigArgs{ + DestChainSelector: destination, + Router: router, + AllowlistEnabled: update.AllowListEnabled, + }) + } + tx, err := onRamp.ApplyDestChainConfigUpdates(txOpts, args) + if err != nil { + return deployment.ChangesetOutput{}, err + } + if cfg.MCMS == nil { + if _, err := deployment.ConfirmIfNoError(e.Chains[chainSel], tx, err); err != nil { + return deployment.ChangesetOutput{}, err + } + } else { + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chainSel), + Batch: []mcms.Operation{ + { + To: onRamp.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + }) + timelocks[chainSel] = s.Chains[chainSel].Timelock.Address() + proposers[chainSel] = s.Chains[chainSel].ProposerMcm + } + } + if cfg.MCMS == nil { + return deployment.ChangesetOutput{}, nil + } + + p, err := proposalutils.BuildProposalFromBatches( + timelocks, + proposers, + batches, + "Update onramp destinations", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{ + *p, + }}, nil +} + +type UpdateFeeQuoterDestsConfig struct { + UpdatesByChain map[uint64]map[uint64]fee_quoter.FeeQuoterDestChainConfig + // Disallow mixing MCMS/non-MCMS per chain for simplicity. + // (can still be acheived by calling this function multiple times) + MCMS *MCMSConfig +} + +func (cfg UpdateFeeQuoterDestsConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + if err != nil { + return err + } + supportedChains := state.SupportedChains() + for chainSel, updates := range cfg.UpdatesByChain { + chainState, ok := state.Chains[chainSel] + if !ok { + return fmt.Errorf("chain %d not found in onchain state", chainSel) + } + if chainState.TestRouter == nil { + return fmt.Errorf("missing test router for chain %d", chainSel) + } + if chainState.Router == nil { + return fmt.Errorf("missing router for chain %d", chainSel) + } + if chainState.OnRamp == nil { + return fmt.Errorf("missing onramp onramp for chain %d", chainSel) + } + if err := commoncs.ValidateOwnership(e.GetContext(), cfg.MCMS != nil, e.Chains[chainSel].DeployerKey.From, chainState.Timelock.Address(), chainState.FeeQuoter); err != nil { + return err + } + + for destination := range updates { + // Destination cannot be an unknown destination. + if _, ok := supportedChains[destination]; !ok { + return fmt.Errorf("destination chain %d is not a supported %s", destination, chainState.OnRamp.Address()) + } + sc, err := chainState.OnRamp.GetStaticConfig(&bind.CallOpts{Context: e.GetContext()}) + if err != nil { + return fmt.Errorf("failed to get onramp static config %s: %w", chainState.OnRamp.Address(), err) + } + if destination == sc.ChainSelector { + return fmt.Errorf("cannot update onramp destination to the same chain") + } + } + } + return nil +} + +func UpdateFeeQuoterDests(e deployment.Environment, cfg UpdateFeeQuoterDestsConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + s, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + var batches []timelock.BatchChainOperation + timelocks := make(map[uint64]common.Address) + proposers := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for chainSel, updates := range cfg.UpdatesByChain { + txOpts := e.Chains[chainSel].DeployerKey + txOpts.Context = e.GetContext() + if cfg.MCMS != nil { + txOpts = deployment.SimTransactOpts() + } + fq := s.Chains[chainSel].FeeQuoter + var args []fee_quoter.FeeQuoterDestChainConfigArgs + for destination, dc := range updates { + args = append(args, fee_quoter.FeeQuoterDestChainConfigArgs{ + DestChainSelector: destination, + DestChainConfig: dc, + }) + } + tx, err := fq.ApplyDestChainConfigUpdates(txOpts, args) + if err != nil { + return deployment.ChangesetOutput{}, err + } + if cfg.MCMS == nil { + if _, err := deployment.ConfirmIfNoError(e.Chains[chainSel], tx, err); err != nil { + return deployment.ChangesetOutput{}, err + } + } else { + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chainSel), + Batch: []mcms.Operation{ + { + To: fq.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + }) + timelocks[chainSel] = s.Chains[chainSel].Timelock.Address() + proposers[chainSel] = s.Chains[chainSel].ProposerMcm + } + } + if cfg.MCMS == nil { + return deployment.ChangesetOutput{}, nil + } + + p, err := proposalutils.BuildProposalFromBatches( + timelocks, + proposers, + batches, + "Update fq destinations", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{ + *p, + }}, nil +} + +type UpdateOffRampSourcesConfig struct { + UpdatesByChain map[uint64]map[uint64]OffRampSourceUpdate + MCMS *MCMSConfig +} + +type OffRampSourceUpdate struct { + IsEnabled bool // If false, disables the source by setting router to 0x0. + TestRouter bool // Flag for safety only allow specifying either router or testRouter. +} + +func (cfg UpdateOffRampSourcesConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + if err != nil { + return err + } + supportedChains := state.SupportedChains() + for chainSel, updates := range cfg.UpdatesByChain { + chainState, ok := state.Chains[chainSel] + if !ok { + return fmt.Errorf("chain %d not found in onchain state", chainSel) + } + if chainState.TestRouter == nil { + return fmt.Errorf("missing test router for chain %d", chainSel) + } + if chainState.Router == nil { + return fmt.Errorf("missing router for chain %d", chainSel) + } + if chainState.OffRamp == nil { + return fmt.Errorf("missing onramp onramp for chain %d", chainSel) + } + if err := commoncs.ValidateOwnership(e.GetContext(), cfg.MCMS != nil, e.Chains[chainSel].DeployerKey.From, chainState.Timelock.Address(), chainState.OffRamp); err != nil { + return err + } + + for source := range updates { + // Source cannot be an unknown + if _, ok := supportedChains[source]; !ok { + return fmt.Errorf("source chain %d is not a supported chain %s", source, chainState.OffRamp.Address()) + } + + if source == chainSel { + return fmt.Errorf("cannot update offramp source to the same chain %d", source) + } + sourceChain := state.Chains[source] + // Source chain must have the onramp deployed. + // Note this also validates the specified source selector. + if sourceChain.OnRamp == nil { + return fmt.Errorf("missing onramp for source %d", source) + } + } + } + return nil +} + +// UpdateOffRampSources updates the offramp sources for each offramp. +func UpdateOffRampSources(e deployment.Environment, cfg UpdateOffRampSourcesConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + s, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + var batches []timelock.BatchChainOperation + timelocks := make(map[uint64]common.Address) + proposers := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for chainSel, updates := range cfg.UpdatesByChain { + txOpts := e.Chains[chainSel].DeployerKey + txOpts.Context = e.GetContext() + if cfg.MCMS != nil { + txOpts = deployment.SimTransactOpts() + } + offRamp := s.Chains[chainSel].OffRamp + var args []offramp.OffRampSourceChainConfigArgs + for source, update := range updates { + router := common.HexToAddress("0x0") + if update.IsEnabled { + if update.TestRouter { + router = s.Chains[chainSel].TestRouter.Address() + } else { + router = s.Chains[chainSel].Router.Address() + } + } + onRamp := s.Chains[source].OnRamp + args = append(args, offramp.OffRampSourceChainConfigArgs{ + SourceChainSelector: source, + Router: router, + IsEnabled: update.IsEnabled, + OnRamp: common.LeftPadBytes(onRamp.Address().Bytes(), 32), + }) + } + tx, err := offRamp.ApplySourceChainConfigUpdates(txOpts, args) + if err != nil { + return deployment.ChangesetOutput{}, err + } + if cfg.MCMS == nil { + if _, err := deployment.ConfirmIfNoError(e.Chains[chainSel], tx, err); err != nil { + return deployment.ChangesetOutput{}, err + } + } else { + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chainSel), + Batch: []mcms.Operation{ + { + To: offRamp.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + }) + timelocks[chainSel] = s.Chains[chainSel].Timelock.Address() + proposers[chainSel] = s.Chains[chainSel].ProposerMcm + } + } + if cfg.MCMS == nil { + return deployment.ChangesetOutput{}, nil + } + + p, err := proposalutils.BuildProposalFromBatches( + timelocks, + proposers, + batches, + "Update offramp sources", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{ + *p, + }}, nil +} + +type UpdateRouterRampsConfig struct { + // TestRouter means the updates will be applied to the test router + // on all chains. Disallow mixing test router/non-test router per chain for simplicity. + TestRouter bool + UpdatesByChain map[uint64]RouterUpdates + MCMS *MCMSConfig +} + +type RouterUpdates struct { + OffRampUpdates map[uint64]bool + OnRampUpdates map[uint64]bool +} + +func (cfg UpdateRouterRampsConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + if err != nil { + return err + } + supportedChains := state.SupportedChains() + for chainSel, update := range cfg.UpdatesByChain { + chainState, ok := state.Chains[chainSel] + if !ok { + return fmt.Errorf("chain %d not found in onchain state", chainSel) + } + if chainState.TestRouter == nil { + return fmt.Errorf("missing test router for chain %d", chainSel) + } + if chainState.Router == nil { + return fmt.Errorf("missing router for chain %d", chainSel) + } + if chainState.OffRamp == nil { + return fmt.Errorf("missing onramp onramp for chain %d", chainSel) + } + if err := commoncs.ValidateOwnership(e.GetContext(), cfg.MCMS != nil, e.Chains[chainSel].DeployerKey.From, chainState.Timelock.Address(), chainState.Router); err != nil { + return err + } + + for source := range update.OffRampUpdates { + // Source cannot be an unknown + if _, ok := supportedChains[source]; !ok { + return fmt.Errorf("source chain %d is not a supported chain %s", source, chainState.OffRamp.Address()) + } + if source == chainSel { + return fmt.Errorf("cannot update offramp source to the same chain %d", source) + } + sourceChain := state.Chains[source] + // Source chain must have the onramp deployed. + // Note this also validates the specified source selector. + if sourceChain.OnRamp == nil { + return fmt.Errorf("missing onramp for source %d", source) + } + } + for destination := range update.OnRampUpdates { + // Source cannot be an unknown + if _, ok := supportedChains[destination]; !ok { + return fmt.Errorf("dest chain %d is not a supported chain %s", destination, chainState.OffRamp.Address()) + } + if destination == chainSel { + return fmt.Errorf("cannot update onRamp dest to the same chain %d", destination) + } + destChain := state.Chains[destination] + if destChain.OffRamp == nil { + return fmt.Errorf("missing offramp for dest %d", destination) + } + } + + } + return nil +} + +// UpdateRouterRamps updates the on/offramps +// in either the router or test router for a series of chains. Use cases include: +// - Ramp upgrade. After deploying new ramps you can enable them on the test router and +// ensure it works e2e. Then enable the ramps on the real router. +// - New chain support. When adding a new chain, you can enable the new destination +// on all chains to support the new chain through the test router first. Once tested, +// Enable the new destination on the real router. +func UpdateRouterRamps(e deployment.Environment, cfg UpdateRouterRampsConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + s, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + var batches []timelock.BatchChainOperation + timelocks := make(map[uint64]common.Address) + proposers := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for chainSel, update := range cfg.UpdatesByChain { + txOpts := e.Chains[chainSel].DeployerKey + txOpts.Context = e.GetContext() + if cfg.MCMS != nil { + txOpts = deployment.SimTransactOpts() + } + routerC := s.Chains[chainSel].Router + if cfg.TestRouter { + routerC = s.Chains[chainSel].TestRouter + } + // Note if we add distinct offramps per source to the state, + // we'll need to add support here for looking them up. + // For now its simple, all sources use the same offramp. + offRamp := s.Chains[chainSel].OffRamp + var removes, adds []router.RouterOffRamp + for source, enabled := range update.OffRampUpdates { + if enabled { + adds = append(adds, router.RouterOffRamp{ + SourceChainSelector: source, + OffRamp: offRamp.Address(), + }) + } else { + removes = append(removes, router.RouterOffRamp{ + SourceChainSelector: source, + OffRamp: offRamp.Address(), + }) + } + } + // Ditto here, only one onramp expected until 1.7. + onRamp := s.Chains[chainSel].OnRamp + var onRampUpdates []router.RouterOnRamp + for dest, enabled := range update.OnRampUpdates { + if enabled { + onRampUpdates = append(onRampUpdates, router.RouterOnRamp{ + DestChainSelector: dest, + OnRamp: onRamp.Address(), + }) + } else { + onRampUpdates = append(onRampUpdates, router.RouterOnRamp{ + DestChainSelector: dest, + OnRamp: common.HexToAddress("0x0"), + }) + } + } + tx, err := routerC.ApplyRampUpdates(txOpts, onRampUpdates, removes, adds) + if err != nil { + return deployment.ChangesetOutput{}, err + } + if cfg.MCMS == nil { + if _, err := deployment.ConfirmIfNoError(e.Chains[chainSel], tx, err); err != nil { + return deployment.ChangesetOutput{}, err + } + } else { + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chainSel), + Batch: []mcms.Operation{ + { + To: routerC.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + }) + timelocks[chainSel] = s.Chains[chainSel].Timelock.Address() + proposers[chainSel] = s.Chains[chainSel].ProposerMcm + } + } + if cfg.MCMS == nil { + return deployment.ChangesetOutput{}, nil + } + + p, err := proposalutils.BuildProposalFromBatches( + timelocks, + proposers, + batches, + "Update router offramps", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{ + *p, + }}, nil +} + +type SetOCR3OffRampConfig struct { + HomeChainSel uint64 + RemoteChainSels []uint64 + MCMS *MCMSConfig +} + +func (c SetOCR3OffRampConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + if err != nil { + return err + } + if _, ok := state.Chains[c.HomeChainSel]; !ok { + return fmt.Errorf("home chain %d not found in onchain state", c.HomeChainSel) + } + for _, remote := range c.RemoteChainSels { + chainState, ok := state.Chains[remote] + if !ok { + return fmt.Errorf("remote chain %d not found in onchain state", remote) + } + if err := commoncs.ValidateOwnership(e.GetContext(), c.MCMS != nil, e.Chains[remote].DeployerKey.From, chainState.Timelock.Address(), chainState.OffRamp); err != nil { + return err + } + } + return nil +} + +// SetOCR3OffRamp will set the OCR3 offramp for the given chain. +// to the active configuration on CCIPHome. This +// is used to complete the candidate->active promotion cycle, it's +// run after the candidate is confirmed to be working correctly. +// Multichain is especially helpful for NOP rotations where we have +// to touch all the chain to change signers. +func SetOCR3OffRamp(e deployment.Environment, cfg SetOCR3OffRampConfig) (deployment.ChangesetOutput, error) { + if err := cfg.Validate(e); err != nil { + return deployment.ChangesetOutput{}, err + } + state, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + var batches []timelock.BatchChainOperation + timelocks := make(map[uint64]common.Address) + proposers := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for _, remote := range cfg.RemoteChainSels { + donID, err := internal.DonIDForChain( + state.Chains[cfg.HomeChainSel].CapabilityRegistry, + state.Chains[cfg.HomeChainSel].CCIPHome, + remote) + args, err := internal.BuildSetOCR3ConfigArgs(donID, state.Chains[cfg.HomeChainSel].CCIPHome, remote) + if err != nil { + return deployment.ChangesetOutput{}, err + } + set, err := isOCR3ConfigSetOnOffRamp(e.Logger, e.Chains[remote], state.Chains[remote].OffRamp, args) + if err != nil { + return deployment.ChangesetOutput{}, err + } + if set { + e.Logger.Infof("OCR3 config already set on offramp for chain %d", remote) + continue + } + txOpts := e.Chains[remote].DeployerKey + if cfg.MCMS != nil { + txOpts = deployment.SimTransactOpts() + } + offRamp := state.Chains[remote].OffRamp + tx, err := offRamp.SetOCR3Configs(txOpts, args) + if err != nil { + return deployment.ChangesetOutput{}, err + } + if cfg.MCMS == nil { + if _, err := deployment.ConfirmIfNoError(e.Chains[remote], tx, err); err != nil { + return deployment.ChangesetOutput{}, err + } + } else { + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(remote), + Batch: []mcms.Operation{ + { + To: offRamp.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + }) + timelocks[remote] = state.Chains[remote].Timelock.Address() + proposers[remote] = state.Chains[remote].ProposerMcm + } + } + if cfg.MCMS == nil { + return deployment.ChangesetOutput{}, nil + } + p, err := proposalutils.BuildProposalFromBatches( + timelocks, + proposers, + batches, + "Update OCR3 config", + cfg.MCMS.MinDelay, + ) + if err != nil { + return deployment.ChangesetOutput{}, err + } + e.Logger.Infof("Proposing OCR3 config update for", cfg.RemoteChainSels) + return deployment.ChangesetOutput{Proposals: []timelock.MCMSWithTimelockProposal{ + *p, + }}, nil +} + +func isOCR3ConfigSetOnOffRamp( + lggr logger.Logger, + chain deployment.Chain, + offRamp *offramp.OffRamp, + offrampOCR3Configs []offramp.MultiOCR3BaseOCRConfigArgs, +) (bool, error) { + mapOfframpOCR3Configs := make(map[cctypes.PluginType]offramp.MultiOCR3BaseOCRConfigArgs) + for _, config := range offrampOCR3Configs { + mapOfframpOCR3Configs[cctypes.PluginType(config.OcrPluginType)] = config + } + + for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { + ocrConfig, err := offRamp.LatestConfigDetails(&bind.CallOpts{ + Context: context.Background(), + }, uint8(pluginType)) + if err != nil { + return false, fmt.Errorf("error fetching OCR3 config for plugin %s chain %s: %w", pluginType.String(), chain.String(), err) + } + lggr.Debugw("Fetched OCR3 Configs", + "MultiOCR3BaseOCRConfig.F", ocrConfig.ConfigInfo.F, + "MultiOCR3BaseOCRConfig.N", ocrConfig.ConfigInfo.N, + "MultiOCR3BaseOCRConfig.IsSignatureVerificationEnabled", ocrConfig.ConfigInfo.IsSignatureVerificationEnabled, + "Signers", ocrConfig.Signers, + "Transmitters", ocrConfig.Transmitters, + "configDigest", hex.EncodeToString(ocrConfig.ConfigInfo.ConfigDigest[:]), + "chain", chain.String(), + ) + // TODO: assertions to be done as part of full state + // resprentation validation CCIP-3047 + if mapOfframpOCR3Configs[pluginType].ConfigDigest != ocrConfig.ConfigInfo.ConfigDigest { + lggr.Infow("OCR3 config digest mismatch", "pluginType", pluginType.String()) + return false, nil + } + if mapOfframpOCR3Configs[pluginType].F != ocrConfig.ConfigInfo.F { + lggr.Infow("OCR3 config F mismatch", "pluginType", pluginType.String()) + return false, nil + } + if mapOfframpOCR3Configs[pluginType].IsSignatureVerificationEnabled != ocrConfig.ConfigInfo.IsSignatureVerificationEnabled { + lggr.Infow("OCR3 config signature verification mismatch", "pluginType", pluginType.String()) + return false, nil + } + if pluginType == cctypes.PluginTypeCCIPCommit { + // only commit will set signers, exec doesn't need them. + for i, signer := range mapOfframpOCR3Configs[pluginType].Signers { + if !bytes.Equal(signer.Bytes(), ocrConfig.Signers[i].Bytes()) { + lggr.Infow("OCR3 config signer mismatch", "pluginType", pluginType.String()) + return false, nil + } + } + } + for i, transmitter := range mapOfframpOCR3Configs[pluginType].Transmitters { + if !bytes.Equal(transmitter.Bytes(), ocrConfig.Transmitters[i].Bytes()) { + lggr.Infow("OCR3 config transmitter mismatch", "pluginType", pluginType.String()) + return false, nil + } + } + } + return true, nil +} diff --git a/deployment/ccip/changeset/cs_chain_contracts_test.go b/deployment/ccip/changeset/cs_chain_contracts_test.go new file mode 100644 index 00000000000..ad1b1a9f2b5 --- /dev/null +++ b/deployment/ccip/changeset/cs_chain_contracts_test.go @@ -0,0 +1,305 @@ +package changeset + +import ( + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter" +) + +func TestUpdateOnRampsDests(t *testing.T) { + for _, tc := range []struct { + name string + mcmsEnabled bool + }{ + { + name: "MCMS enabled", + mcmsEnabled: true, + }, + { + name: "MCMS disabled", + mcmsEnabled: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := testcontext.Get(t) + // Default env just has 2 chains with all contracts + // deployed but no lanes. + tenv := NewMemoryEnvironment(t) + state, err := LoadOnchainState(tenv.Env) + require.NoError(t, err) + + allChains := maps.Keys(tenv.Env.Chains) + source := allChains[0] + dest := allChains[1] + + if tc.mcmsEnabled { + // Transfer ownership to timelock so that we can promote the zero digest later down the line. + transferToTimelock(t, tenv, state, source, dest) + } + + var mcmsConfig *MCMSConfig + if tc.mcmsEnabled { + mcmsConfig = &MCMSConfig{ + MinDelay: 0, + } + } + _, err = commonchangeset.ApplyChangesets(t, tenv.Env, tenv.TimelockContracts(t), []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(UpdateOnRampsDests), + Config: UpdateOnRampDestsConfig{ + UpdatesByChain: map[uint64]map[uint64]OnRampDestinationUpdate{ + source: { + dest: { + IsEnabled: true, + TestRouter: true, + AllowListEnabled: false, + }, + }, + dest: { + source: { + IsEnabled: true, + TestRouter: false, + AllowListEnabled: true, + }, + }, + }, + MCMS: mcmsConfig, + }, + }, + }) + require.NoError(t, err) + + // Assert the onramp configuration is as we expect. + sourceCfg, err := state.Chains[source].OnRamp.GetDestChainConfig(&bind.CallOpts{Context: ctx}, dest) + require.NoError(t, err) + require.Equal(t, state.Chains[source].TestRouter.Address(), sourceCfg.Router) + require.Equal(t, false, sourceCfg.AllowlistEnabled) + destCfg, err := state.Chains[dest].OnRamp.GetDestChainConfig(&bind.CallOpts{Context: ctx}, source) + require.NoError(t, err) + require.Equal(t, state.Chains[dest].Router.Address(), destCfg.Router) + require.Equal(t, true, destCfg.AllowlistEnabled) + }) + } +} + +func TestUpdateOffRampsSources(t *testing.T) { + for _, tc := range []struct { + name string + mcmsEnabled bool + }{ + { + name: "MCMS enabled", + mcmsEnabled: true, + }, + { + name: "MCMS disabled", + mcmsEnabled: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := testcontext.Get(t) + tenv := NewMemoryEnvironment(t) + state, err := LoadOnchainState(tenv.Env) + require.NoError(t, err) + + allChains := maps.Keys(tenv.Env.Chains) + source := allChains[0] + dest := allChains[1] + + if tc.mcmsEnabled { + // Transfer ownership to timelock so that we can promote the zero digest later down the line. + transferToTimelock(t, tenv, state, source, dest) + } + + var mcmsConfig *MCMSConfig + if tc.mcmsEnabled { + mcmsConfig = &MCMSConfig{ + MinDelay: 0, + } + } + _, err = commonchangeset.ApplyChangesets(t, tenv.Env, tenv.TimelockContracts(t), []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(UpdateOffRampSources), + Config: UpdateOffRampSourcesConfig{ + UpdatesByChain: map[uint64]map[uint64]OffRampSourceUpdate{ + source: { + dest: { + IsEnabled: true, + TestRouter: true, + }, + }, + dest: { + source: { + IsEnabled: true, + TestRouter: false, + }, + }, + }, + MCMS: mcmsConfig, + }, + }, + }) + require.NoError(t, err) + + // Assert the offramp configuration is as we expect. + sourceCfg, err := state.Chains[source].OffRamp.GetSourceChainConfig(&bind.CallOpts{Context: ctx}, dest) + require.NoError(t, err) + require.Equal(t, state.Chains[source].TestRouter.Address(), sourceCfg.Router) + destCfg, err := state.Chains[dest].OffRamp.GetSourceChainConfig(&bind.CallOpts{Context: ctx}, source) + require.NoError(t, err) + require.Equal(t, state.Chains[dest].Router.Address(), destCfg.Router) + }) + } +} + +func TestUpdateFQDests(t *testing.T) { + for _, tc := range []struct { + name string + mcmsEnabled bool + }{ + { + name: "MCMS enabled", + mcmsEnabled: true, + }, + { + name: "MCMS disabled", + mcmsEnabled: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := testcontext.Get(t) + tenv := NewMemoryEnvironment(t) + state, err := LoadOnchainState(tenv.Env) + require.NoError(t, err) + + allChains := maps.Keys(tenv.Env.Chains) + source := allChains[0] + dest := allChains[1] + + if tc.mcmsEnabled { + // Transfer ownership to timelock so that we can promote the zero digest later down the line. + transferToTimelock(t, tenv, state, source, dest) + } + + var mcmsConfig *MCMSConfig + if tc.mcmsEnabled { + mcmsConfig = &MCMSConfig{ + MinDelay: 0, + } + } + + fqCfg1 := DefaultFeeQuoterDestChainConfig() + fqCfg2 := DefaultFeeQuoterDestChainConfig() + fqCfg2.DestGasOverhead = 1000 + _, err = commonchangeset.ApplyChangesets(t, tenv.Env, tenv.TimelockContracts(t), []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(UpdateFeeQuoterDests), + Config: UpdateFeeQuoterDestsConfig{ + UpdatesByChain: map[uint64]map[uint64]fee_quoter.FeeQuoterDestChainConfig{ + source: { + dest: fqCfg1, + }, + dest: { + source: fqCfg2, + }, + }, + MCMS: mcmsConfig, + }, + }, + }) + require.NoError(t, err) + + // Assert the fq configuration is as we expect. + source2destCfg, err := state.Chains[source].FeeQuoter.GetDestChainConfig(&bind.CallOpts{Context: ctx}, dest) + require.NoError(t, err) + AssertEqualFeeConfig(t, fqCfg1, source2destCfg) + dest2sourceCfg, err := state.Chains[dest].FeeQuoter.GetDestChainConfig(&bind.CallOpts{Context: ctx}, source) + require.NoError(t, err) + AssertEqualFeeConfig(t, fqCfg2, dest2sourceCfg) + }) + } +} + +func TestUpdateRouterRamps(t *testing.T) { + for _, tc := range []struct { + name string + mcmsEnabled bool + }{ + { + name: "MCMS enabled", + mcmsEnabled: true, + }, + { + name: "MCMS disabled", + mcmsEnabled: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := testcontext.Get(t) + tenv := NewMemoryEnvironment(t) + state, err := LoadOnchainState(tenv.Env) + require.NoError(t, err) + + allChains := maps.Keys(tenv.Env.Chains) + source := allChains[0] + dest := allChains[1] + + if tc.mcmsEnabled { + // Transfer ownership to timelock so that we can promote the zero digest later down the line. + transferToTimelock(t, tenv, state, source, dest) + } + + var mcmsConfig *MCMSConfig + if tc.mcmsEnabled { + mcmsConfig = &MCMSConfig{ + MinDelay: 0, + } + } + + // Updates test router. + _, err = commonchangeset.ApplyChangesets(t, tenv.Env, tenv.TimelockContracts(t), []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(UpdateRouterRamps), + Config: UpdateRouterRampsConfig{ + TestRouter: true, + UpdatesByChain: map[uint64]RouterUpdates{ + source: { + OffRampUpdates: map[uint64]bool{ + dest: true, + }, + OnRampUpdates: map[uint64]bool{ + dest: true, + }, + }, + dest: { + OffRampUpdates: map[uint64]bool{ + source: true, + }, + OnRampUpdates: map[uint64]bool{ + source: true, + }, + }, + }, + MCMS: mcmsConfig, + }, + }, + }) + require.NoError(t, err) + + // Assert the router configuration is as we expect. + source2destOnRampTest, err := state.Chains[source].TestRouter.GetOnRamp(&bind.CallOpts{Context: ctx}, dest) + require.NoError(t, err) + require.Equal(t, state.Chains[source].OnRamp.Address(), source2destOnRampTest) + source2destOnRampReal, err := state.Chains[source].Router.GetOnRamp(&bind.CallOpts{Context: ctx}, dest) + require.NoError(t, err) + require.Equal(t, common.HexToAddress("0x0"), source2destOnRampReal) + }) + } +} diff --git a/deployment/ccip/changeset/cs_initial_add_chain.go b/deployment/ccip/changeset/cs_initial_add_chain.go deleted file mode 100644 index 4f8b2ac2722..00000000000 --- a/deployment/ccip/changeset/cs_initial_add_chain.go +++ /dev/null @@ -1,532 +0,0 @@ -package changeset - -import ( - "bytes" - "context" - "encoding/hex" - "fmt" - "os" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" - - "github.com/smartcontractkit/chainlink-ccip/chainconfig" - "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink-ccip/pluginconfig" - "github.com/smartcontractkit/chainlink-common/pkg/config" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/merklemulti" - - "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" - "github.com/smartcontractkit/chainlink/deployment/common/types" - cctypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/ccip_home" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" -) - -var _ deployment.ChangeSet[NewChainsConfig] = ConfigureNewChains - -// ConfigureNewChains enables new chains as destination(s) for CCIP -// It performs the following steps per chain: -// - addChainConfig + AddDON (candidate->primary promotion i.e. init) on the home chain -// - SetOCR3Config on the remote chain -// ConfigureNewChains assumes that the home chain is already enabled and all CCIP contracts are already deployed. -func ConfigureNewChains(env deployment.Environment, c NewChainsConfig) (deployment.ChangesetOutput, error) { - if err := c.Validate(); err != nil { - return deployment.ChangesetOutput{}, fmt.Errorf("invalid NewChainsConfig: %w", err) - } - err := configureChain(env, c) - if err != nil { - env.Logger.Errorw("Failed to configure chain", "err", err) - return deployment.ChangesetOutput{}, deployment.MaybeDataErr(err) - } - return deployment.ChangesetOutput{ - Proposals: []timelock.MCMSWithTimelockProposal{}, - AddressBook: nil, - JobSpecs: nil, - }, nil -} - -type CCIPOCRParams struct { - OCRParameters types.OCRParameters - // Note contains pointers to Arb feeds for prices - CommitOffChainConfig pluginconfig.CommitOffchainConfig - // Note ontains USDC config - ExecuteOffChainConfig pluginconfig.ExecuteOffchainConfig -} - -func (c CCIPOCRParams) Validate() error { - if err := c.OCRParameters.Validate(); err != nil { - return fmt.Errorf("invalid OCR parameters: %w", err) - } - if err := c.CommitOffChainConfig.Validate(); err != nil { - return fmt.Errorf("invalid commit off-chain config: %w", err) - } - if err := c.ExecuteOffChainConfig.Validate(); err != nil { - return fmt.Errorf("invalid execute off-chain config: %w", err) - } - return nil -} - -type NewChainsConfig struct { - // Common to all chains - HomeChainSel uint64 - FeedChainSel uint64 - // Per chain config - ChainConfigByChain map[uint64]CCIPOCRParams -} - -func (c NewChainsConfig) Chains() []uint64 { - chains := make([]uint64, 0, len(c.ChainConfigByChain)) - for chain := range c.ChainConfigByChain { - chains = append(chains, chain) - } - return chains -} - -func (c NewChainsConfig) Validate() error { - if err := deployment.IsValidChainSelector(c.HomeChainSel); err != nil { - return fmt.Errorf("invalid home chain selector: %d - %w", c.HomeChainSel, err) - } - if err := deployment.IsValidChainSelector(c.FeedChainSel); err != nil { - return fmt.Errorf("invalid feed chain selector: %d - %w", c.FeedChainSel, err) - } - // Validate chain config - for chain, cfg := range c.ChainConfigByChain { - if err := cfg.Validate(); err != nil { - return fmt.Errorf("invalid OCR params for chain %d: %w", chain, err) - } - if cfg.CommitOffChainConfig.PriceFeedChainSelector != ccipocr3.ChainSelector(c.FeedChainSel) { - return fmt.Errorf("chain %d has invalid feed chain selector", chain) - } - } - return nil -} - -// DefaultOCRParams returns the default OCR parameters for a chain, -// except for a few values which must be parameterized (passed as arguments). -func DefaultOCRParams( - feedChainSel uint64, - tokenInfo map[ccipocr3.UnknownEncodedAddress]pluginconfig.TokenInfo, - tokenDataObservers []pluginconfig.TokenDataObserverConfig, -) CCIPOCRParams { - return CCIPOCRParams{ - OCRParameters: types.OCRParameters{ - DeltaProgress: internal.DeltaProgress, - DeltaResend: internal.DeltaResend, - DeltaInitial: internal.DeltaInitial, - DeltaRound: internal.DeltaRound, - DeltaGrace: internal.DeltaGrace, - DeltaCertifiedCommitRequest: internal.DeltaCertifiedCommitRequest, - DeltaStage: internal.DeltaStage, - Rmax: internal.Rmax, - MaxDurationQuery: internal.MaxDurationQuery, - MaxDurationObservation: internal.MaxDurationObservation, - MaxDurationShouldAcceptAttestedReport: internal.MaxDurationShouldAcceptAttestedReport, - MaxDurationShouldTransmitAcceptedReport: internal.MaxDurationShouldTransmitAcceptedReport, - }, - ExecuteOffChainConfig: pluginconfig.ExecuteOffchainConfig{ - BatchGasLimit: internal.BatchGasLimit, - RelativeBoostPerWaitHour: internal.RelativeBoostPerWaitHour, - InflightCacheExpiry: *config.MustNewDuration(internal.InflightCacheExpiry), - RootSnoozeTime: *config.MustNewDuration(internal.RootSnoozeTime), - MessageVisibilityInterval: *config.MustNewDuration(internal.FirstBlockAge), - BatchingStrategyID: internal.BatchingStrategyID, - TokenDataObservers: tokenDataObservers, - }, - CommitOffChainConfig: pluginconfig.CommitOffchainConfig{ - RemoteGasPriceBatchWriteFrequency: *config.MustNewDuration(internal.RemoteGasPriceBatchWriteFrequency), - TokenPriceBatchWriteFrequency: *config.MustNewDuration(internal.TokenPriceBatchWriteFrequency), - TokenInfo: tokenInfo, - PriceFeedChainSelector: ccipocr3.ChainSelector(feedChainSel), - NewMsgScanBatchSize: merklemulti.MaxNumberTreeLeaves, - MaxReportTransmissionCheckAttempts: 5, - RMNEnabled: os.Getenv("ENABLE_RMN") == "true", // only enabled in manual test - RMNSignaturesTimeout: 30 * time.Minute, - MaxMerkleTreeSize: merklemulti.MaxNumberTreeLeaves, - SignObservationPrefix: "chainlink ccip 1.6 rmn observation", - }, - } -} - -// configureChain assumes the all the Home chain contracts and CCIP contracts are deployed -// It does - -// 1. addChainConfig for each chain in CCIPHome -// 2. Registers the nodes with the capability registry -// 3. SetOCR3Config on the remote chain -func configureChain( - e deployment.Environment, - c NewChainsConfig, -) error { - if e.OCRSecrets.IsEmpty() { - return fmt.Errorf("OCR secrets are empty") - } - nodes, err := deployment.NodeInfo(e.NodeIDs, e.Offchain) - if err != nil || len(nodes) == 0 { - e.Logger.Errorw("Failed to get node info", "err", err) - return err - } - existingState, err := LoadOnchainState(e) - if err != nil { - e.Logger.Errorw("Failed to load existing onchain state", "err") - return err - } - homeChain := e.Chains[c.HomeChainSel] - capReg := existingState.Chains[c.HomeChainSel].CapabilityRegistry - if capReg == nil { - e.Logger.Errorw("Failed to get capability registry", "chain", homeChain.String()) - return fmt.Errorf("capability registry not found") - } - ccipHome := existingState.Chains[c.HomeChainSel].CCIPHome - if ccipHome == nil { - e.Logger.Errorw("Failed to get ccip home", "chain", homeChain.String(), "err", err) - return fmt.Errorf("ccip home not found") - } - rmnHome := existingState.Chains[c.HomeChainSel].RMNHome - if rmnHome == nil { - e.Logger.Errorw("Failed to get rmn home", "chain", homeChain.String(), "err", err) - return fmt.Errorf("rmn home not found") - } - - for chainSel, chainConfig := range c.ChainConfigByChain { - chain, _ := e.Chains[chainSel] - chainState, ok := existingState.Chains[chain.Selector] - if !ok { - return fmt.Errorf("chain state not found for chain %d", chain.Selector) - } - if chainState.OffRamp == nil { - return fmt.Errorf("off ramp not found for chain %d", chain.Selector) - } - _, err = addChainConfig( - e.Logger, - e.Chains[c.HomeChainSel], - ccipHome, - chain.Selector, - nodes.NonBootstraps().PeerIDs()) - if err != nil { - return err - } - // For each chain, we create a DON on the home chain (2 OCR instances) - if err := addDON( - e.Logger, - e.OCRSecrets, - capReg, - ccipHome, - rmnHome.Address(), - chainState.OffRamp, - chain, - e.Chains[c.HomeChainSel], - nodes.NonBootstraps(), - chainConfig, - ); err != nil { - e.Logger.Errorw("Failed to add DON", "err", err) - return err - } - } - - return nil -} - -func setupConfigInfo(chainSelector uint64, readers [][32]byte, fChain uint8, cfg []byte) ccip_home.CCIPHomeChainConfigArgs { - return ccip_home.CCIPHomeChainConfigArgs{ - ChainSelector: chainSelector, - ChainConfig: ccip_home.CCIPHomeChainConfig{ - Readers: readers, - FChain: fChain, - Config: cfg, - }, - } -} - -func isChainConfigEqual(a, b ccip_home.CCIPHomeChainConfig) bool { - mapReader := make(map[[32]byte]struct{}) - for i := range a.Readers { - mapReader[a.Readers[i]] = struct{}{} - } - for i := range b.Readers { - if _, ok := mapReader[b.Readers[i]]; !ok { - return false - } - } - return bytes.Equal(a.Config, b.Config) && - a.FChain == b.FChain -} - -func addChainConfig( - lggr logger.Logger, - h deployment.Chain, - ccipConfig *ccip_home.CCIPHome, - chainSelector uint64, - p2pIDs [][32]byte, -) (ccip_home.CCIPHomeChainConfigArgs, error) { - // First Add CCIPOCRParams that includes all p2pIDs as readers - encodedExtraChainConfig, err := chainconfig.EncodeChainConfig(chainconfig.ChainConfig{ - GasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(1000), - DAGasPriceDeviationPPB: ccipocr3.NewBigIntFromInt64(0), - OptimisticConfirmations: 1, - }) - if err != nil { - return ccip_home.CCIPHomeChainConfigArgs{}, err - } - chainConfig := setupConfigInfo(chainSelector, p2pIDs, uint8(len(p2pIDs)/3), encodedExtraChainConfig) - existingCfg, err := ccipConfig.GetChainConfig(nil, chainSelector) - if err != nil { - return ccip_home.CCIPHomeChainConfigArgs{}, fmt.Errorf("get chain config for selector %d: %w", chainSelector, err) - } - if isChainConfigEqual(existingCfg, chainConfig.ChainConfig) { - lggr.Infow("Chain config already exists, not applying again", - "homeChain", h.String(), - "addedChain", chainSelector, - "chainConfig", chainConfig, - ) - return chainConfig, nil - } - tx, err := ccipConfig.ApplyChainConfigUpdates(h.DeployerKey, nil, []ccip_home.CCIPHomeChainConfigArgs{ - chainConfig, - }) - if _, err := deployment.ConfirmIfNoError(h, tx, err); err != nil { - return ccip_home.CCIPHomeChainConfigArgs{}, err - } - lggr.Infow("Applied chain config updates", "homeChain", h.String(), "addedChain", chainSelector, "chainConfig", chainConfig) - return chainConfig, nil -} - -// createDON creates one DON with 2 plugins (commit and exec) -// It first set a new candidate for the DON with the first plugin type and AddDON on capReg -// Then for subsequent operations it uses UpdateDON to promote the first plugin to the active deployment -// and to set candidate and promote it for the second plugin -func createDON( - lggr logger.Logger, - capReg *capabilities_registry.CapabilitiesRegistry, - ccipHome *ccip_home.CCIPHome, - ocr3Configs map[cctypes.PluginType]ccip_home.CCIPHomeOCR3Config, - home deployment.Chain, - newChainSel uint64, - nodes deployment.Nodes, -) error { - donID, err := internal.DonIDForChain(capReg, ccipHome, newChainSel) - if err != nil { - return fmt.Errorf("fetch don id for chain: %w", err) - } - if donID != 0 { - lggr.Infow("DON already exists not adding it again", "donID", donID, "chain", newChainSel) - return ValidateCCIPHomeConfigSetUp(lggr, capReg, ccipHome, newChainSel) - } - - commitConfig, ok := ocr3Configs[cctypes.PluginTypeCCIPCommit] - if !ok { - return fmt.Errorf("missing commit plugin in ocr3Configs") - } - - execConfig, ok := ocr3Configs[cctypes.PluginTypeCCIPExec] - if !ok { - return fmt.Errorf("missing exec plugin in ocr3Configs") - } - - latestDon, err := internal.LatestCCIPDON(capReg) - if err != nil { - return err - } - - donID = latestDon.Id + 1 - - err = internal.SetupCommitDON(lggr, donID, commitConfig, capReg, home, nodes, ccipHome) - if err != nil { - return fmt.Errorf("setup commit don: %w", err) - } - - // TODO: bug in contract causing this to not work as expected. - err = internal.SetupExecDON(lggr, donID, execConfig, capReg, home, nodes, ccipHome) - if err != nil { - return fmt.Errorf("setup exec don: %w", err) - } - return ValidateCCIPHomeConfigSetUp(lggr, capReg, ccipHome, newChainSel) -} - -func addDON( - lggr logger.Logger, - ocrSecrets deployment.OCRSecrets, - capReg *capabilities_registry.CapabilitiesRegistry, - ccipHome *ccip_home.CCIPHome, - rmnHomeAddress common.Address, - offRamp *offramp.OffRamp, - dest deployment.Chain, - home deployment.Chain, - nodes deployment.Nodes, - ocrParams CCIPOCRParams, -) error { - ocrConfigs, err := internal.BuildOCR3ConfigForCCIPHome( - ocrSecrets, offRamp, dest, nodes, rmnHomeAddress, ocrParams.OCRParameters, ocrParams.CommitOffChainConfig, ocrParams.ExecuteOffChainConfig) - if err != nil { - return err - } - err = createDON(lggr, capReg, ccipHome, ocrConfigs, home, dest.Selector, nodes) - if err != nil { - return err - } - don, err := internal.LatestCCIPDON(capReg) - if err != nil { - return err - } - lggr.Infow("Added DON", "donID", don.Id) - - offrampOCR3Configs, err := internal.BuildSetOCR3ConfigArgs(don.Id, ccipHome, dest.Selector) - if err != nil { - return err - } - lggr.Infow("Setting OCR3 Configs", - "offrampOCR3Configs", offrampOCR3Configs, - "configDigestCommit", hex.EncodeToString(offrampOCR3Configs[cctypes.PluginTypeCCIPCommit].ConfigDigest[:]), - "configDigestExec", hex.EncodeToString(offrampOCR3Configs[cctypes.PluginTypeCCIPExec].ConfigDigest[:]), - "chainSelector", dest.Selector, - ) - - // check if OCR3 config is already set on offramp - ocr3ConfigSet, err := isOCR3ConfigSetOnOffRamp(lggr, dest, offRamp, offrampOCR3Configs) - if err != nil { - return fmt.Errorf("error checking if OCR3 config is set on offramp: %w", err) - } - if ocr3ConfigSet { - lggr.Infow("OCR3 config already set on offramp, not applying again", "chain", dest.String()) - return nil - } - tx, err := offRamp.SetOCR3Configs(dest.DeployerKey, offrampOCR3Configs) - if _, err := deployment.ConfirmIfNoError(dest, tx, err); err != nil { - return err - } - lggr.Infow("Set OCR3 Configs", "chain", dest.String()) - // now check if OCR3 config is set on offramp - ocr3ConfigSet, err = isOCR3ConfigSetOnOffRamp(lggr, dest, offRamp, offrampOCR3Configs) - if err != nil { - return fmt.Errorf("error checking if OCR3 config is set on offramp: %w", err) - } - if !ocr3ConfigSet { - return fmt.Errorf("OCR3 config not set on offramp properly, check logs, chain %s", dest.String()) - } - return nil -} - -func isOCR3ConfigSetOnOffRamp( - lggr logger.Logger, - chain deployment.Chain, - offRamp *offramp.OffRamp, - offrampOCR3Configs []offramp.MultiOCR3BaseOCRConfigArgs, -) (bool, error) { - mapOfframpOCR3Configs := make(map[cctypes.PluginType]offramp.MultiOCR3BaseOCRConfigArgs) - for _, config := range offrampOCR3Configs { - mapOfframpOCR3Configs[cctypes.PluginType(config.OcrPluginType)] = config - } - - for _, pluginType := range []cctypes.PluginType{cctypes.PluginTypeCCIPCommit, cctypes.PluginTypeCCIPExec} { - ocrConfig, err := offRamp.LatestConfigDetails(&bind.CallOpts{ - Context: context.Background(), - }, uint8(pluginType)) - if err != nil { - return false, fmt.Errorf("error fetching OCR3 config for plugin %s chain %s: %w", pluginType.String(), chain.String(), err) - } - lggr.Debugw("Fetched OCR3 Configs", - "MultiOCR3BaseOCRConfig.F", ocrConfig.ConfigInfo.F, - "MultiOCR3BaseOCRConfig.N", ocrConfig.ConfigInfo.N, - "MultiOCR3BaseOCRConfig.IsSignatureVerificationEnabled", ocrConfig.ConfigInfo.IsSignatureVerificationEnabled, - "Signers", ocrConfig.Signers, - "Transmitters", ocrConfig.Transmitters, - "configDigest", hex.EncodeToString(ocrConfig.ConfigInfo.ConfigDigest[:]), - "chain", chain.String(), - ) - // TODO: assertions to be done as part of full state - // resprentation validation CCIP-3047 - if mapOfframpOCR3Configs[pluginType].ConfigDigest != ocrConfig.ConfigInfo.ConfigDigest { - lggr.Infow("OCR3 config digest mismatch", "pluginType", pluginType.String()) - return false, nil - } - if mapOfframpOCR3Configs[pluginType].F != ocrConfig.ConfigInfo.F { - lggr.Infow("OCR3 config F mismatch", "pluginType", pluginType.String()) - return false, nil - } - if mapOfframpOCR3Configs[pluginType].IsSignatureVerificationEnabled != ocrConfig.ConfigInfo.IsSignatureVerificationEnabled { - lggr.Infow("OCR3 config signature verification mismatch", "pluginType", pluginType.String()) - return false, nil - } - if pluginType == cctypes.PluginTypeCCIPCommit { - // only commit will set signers, exec doesn't need them. - for i, signer := range mapOfframpOCR3Configs[pluginType].Signers { - if !bytes.Equal(signer.Bytes(), ocrConfig.Signers[i].Bytes()) { - lggr.Infow("OCR3 config signer mismatch", "pluginType", pluginType.String()) - return false, nil - } - } - } - for i, transmitter := range mapOfframpOCR3Configs[pluginType].Transmitters { - if !bytes.Equal(transmitter.Bytes(), ocrConfig.Transmitters[i].Bytes()) { - lggr.Infow("OCR3 config transmitter mismatch", "pluginType", pluginType.String()) - return false, nil - } - } - } - return true, nil -} - -// ValidateCCIPHomeConfigSetUp checks that the commit and exec active and candidate configs are set up correctly -func ValidateCCIPHomeConfigSetUp( - lggr logger.Logger, - capReg *capabilities_registry.CapabilitiesRegistry, - ccipHome *ccip_home.CCIPHome, - chainSel uint64, -) error { - // fetch DONID - donID, err := internal.DonIDForChain(capReg, ccipHome, chainSel) - if err != nil { - return fmt.Errorf("fetch don id for chain: %w", err) - } - if donID == 0 { - return fmt.Errorf("don id for chain (%d) does not exist", chainSel) - } - - // final sanity checks on configs. - commitConfigs, err := ccipHome.GetAllConfigs(&bind.CallOpts{ - //Pending: true, - }, donID, uint8(cctypes.PluginTypeCCIPCommit)) - if err != nil { - return fmt.Errorf("get all commit configs: %w", err) - } - commitActiveDigest, err := ccipHome.GetActiveDigest(nil, donID, uint8(cctypes.PluginTypeCCIPCommit)) - if err != nil { - return fmt.Errorf("get active commit digest: %w", err) - } - lggr.Debugw("Fetched active commit digest", "commitActiveDigest", hex.EncodeToString(commitActiveDigest[:])) - commitCandidateDigest, err := ccipHome.GetCandidateDigest(nil, donID, uint8(cctypes.PluginTypeCCIPCommit)) - if err != nil { - return fmt.Errorf("get commit candidate digest: %w", err) - } - lggr.Debugw("Fetched candidate commit digest", "commitCandidateDigest", hex.EncodeToString(commitCandidateDigest[:])) - if commitConfigs.ActiveConfig.ConfigDigest == [32]byte{} { - return fmt.Errorf( - "active config digest is empty for commit, expected nonempty, donID: %d, cfg: %+v, config digest from GetActiveDigest call: %x, config digest from GetCandidateDigest call: %x", - donID, commitConfigs.ActiveConfig, commitActiveDigest, commitCandidateDigest) - } - if commitConfigs.CandidateConfig.ConfigDigest != [32]byte{} { - return fmt.Errorf( - "candidate config digest is nonempty for commit, expected empty, donID: %d, cfg: %+v, config digest from GetCandidateDigest call: %x, config digest from GetActiveDigest call: %x", - donID, commitConfigs.CandidateConfig, commitCandidateDigest, commitActiveDigest) - } - - execConfigs, err := ccipHome.GetAllConfigs(nil, donID, uint8(cctypes.PluginTypeCCIPExec)) - if err != nil { - return fmt.Errorf("get all exec configs: %w", err) - } - lggr.Debugw("Fetched exec configs", - "ActiveConfig.ConfigDigest", hex.EncodeToString(execConfigs.ActiveConfig.ConfigDigest[:]), - "CandidateConfig.ConfigDigest", hex.EncodeToString(execConfigs.CandidateConfig.ConfigDigest[:]), - ) - if execConfigs.ActiveConfig.ConfigDigest == [32]byte{} { - return fmt.Errorf("active config digest is empty for exec, expected nonempty, cfg: %v", execConfigs.ActiveConfig) - } - if execConfigs.CandidateConfig.ConfigDigest != [32]byte{} { - return fmt.Errorf("candidate config digest is nonempty for exec, expected empty, cfg: %v", execConfigs.CandidateConfig) - } - return nil -} diff --git a/deployment/ccip/changeset/cs_initial_add_chain_test.go b/deployment/ccip/changeset/cs_initial_add_chain_test.go deleted file mode 100644 index 7e155b82ed1..00000000000 --- a/deployment/ccip/changeset/cs_initial_add_chain_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package changeset - -import ( - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/smartcontractkit/chainlink-ccip/pluginconfig" - "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" - "github.com/stretchr/testify/require" - - commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" - "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" -) - -func TestInitialAddChainAppliedTwice(t *testing.T) { - t.Parallel() - // This already applies the initial add chain changeset. - e := NewMemoryEnvironment(t) - - state, err := LoadOnchainState(e.Env) - require.NoError(t, err) - - // now try to apply it again for the second time - // Build the per chain config. - allChains := e.Env.AllChainSelectors() - tokenConfig := NewTestTokenConfig(state.Chains[e.FeedChainSel].USDFeeds) - chainConfigs := make(map[uint64]CCIPOCRParams) - timelockContractsPerChain := make(map[uint64]*proposalutils.TimelockExecutionContracts) - - for _, chain := range allChains { - timelockContractsPerChain[chain] = &proposalutils.TimelockExecutionContracts{ - Timelock: state.Chains[chain].Timelock, - CallProxy: state.Chains[chain].CallProxy, - } - tokenInfo := tokenConfig.GetTokenInfo(e.Env.Logger, state.Chains[chain].LinkToken, state.Chains[chain].Weth9) - ocrParams := DefaultOCRParams(e.FeedChainSel, tokenInfo, []pluginconfig.TokenDataObserverConfig{}) - chainConfigs[chain] = ocrParams - } - e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, timelockContractsPerChain, []commonchangeset.ChangesetApplication{ - { - Changeset: commonchangeset.WrapChangeSet(ConfigureNewChains), - Config: NewChainsConfig{ - HomeChainSel: e.HomeChainSel, - FeedChainSel: e.FeedChainSel, - ChainConfigByChain: chainConfigs, - }, - }, - }) - require.NoError(t, err) - // send requests - chain1, chain2 := allChains[0], allChains[1] - _, err = AddLanes(e.Env, AddLanesConfig{ - LaneConfigs: []LaneConfig{ - { - SourceSelector: chain1, - DestSelector: chain2, - InitialPricesBySource: DefaultInitialPrices, - FeeQuoterDestChain: DefaultFeeQuoterDestChainConfig(), - TestRouter: true, - }, - }, - }) - require.NoError(t, err) - ReplayLogs(t, e.Env.Offchain, e.ReplayBlocks) - // Need to keep track of the block number for each chain so that event subscription can be done from that block. - startBlocks := make(map[uint64]*uint64) - // Send a message from each chain to every other chain. - expectedSeqNumExec := make(map[SourceDestPair][]uint64) - expectedSeqNum := make(map[SourceDestPair]uint64) - latesthdr, err := e.Env.Chains[chain2].Client.HeaderByNumber(testcontext.Get(t), nil) - require.NoError(t, err) - block := latesthdr.Number.Uint64() - startBlocks[chain2] = &block - msgSentEvent := TestSendRequest(t, e.Env, state, chain1, chain2, true, router.ClientEVM2AnyMessage{ - Receiver: common.LeftPadBytes(state.Chains[chain2].Receiver.Address().Bytes(), 32), - Data: []byte("hello"), - TokenAmounts: nil, - FeeToken: common.HexToAddress("0x0"), - ExtraArgs: nil, - }) - - expectedSeqNum[SourceDestPair{ - SourceChainSelector: chain1, - DestChainSelector: chain2, - }] = msgSentEvent.SequenceNumber - expectedSeqNumExec[SourceDestPair{ - SourceChainSelector: chain1, - DestChainSelector: chain2, - }] = []uint64{msgSentEvent.SequenceNumber} - ConfirmCommitForAllWithExpectedSeqNums(t, e.Env, state, expectedSeqNum, startBlocks) - ConfirmExecWithSeqNrsForAll(t, e.Env, state, expectedSeqNumExec, startBlocks) -} diff --git a/deployment/ccip/changeset/internal/deploy_home_chain.go b/deployment/ccip/changeset/internal/deploy_home_chain.go index e4c97cae326..2c401fd8006 100644 --- a/deployment/ccip/changeset/internal/deploy_home_chain.go +++ b/deployment/ccip/changeset/internal/deploy_home_chain.go @@ -11,8 +11,6 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-ccip/pluginconfig" "github.com/smartcontractkit/chainlink/deployment" @@ -48,6 +46,10 @@ const ( MaxDurationObservation = 5 * time.Second MaxDurationShouldAcceptAttestedReport = 10 * time.Second MaxDurationShouldTransmitAcceptedReport = 10 * time.Second + + GasPriceDeviationPPB = 1000 + DAGasPriceDeviationPPB = 0 + OptimisticConfirmations = 1 ) var ( @@ -188,255 +190,6 @@ func BuildSetOCR3ConfigArgs( return offrampOCR3Configs, nil } -func SetupExecDON( - lggr logger.Logger, - donID uint32, - execConfig ccip_home.CCIPHomeOCR3Config, - capReg *capabilities_registry.CapabilitiesRegistry, - home deployment.Chain, - nodes deployment.Nodes, - ccipHome *ccip_home.CCIPHome, -) error { - encodedSetCandidateCall, err := CCIPHomeABI.Pack( - "setCandidate", - donID, - execConfig.PluginType, - execConfig, - [32]byte{}, - ) - if err != nil { - return fmt.Errorf("pack set candidate call: %w", err) - } - - // set candidate call - tx, err := capReg.UpdateDON( - home.DeployerKey, - donID, - nodes.PeerIDs(), - []capabilities_registry.CapabilitiesRegistryCapabilityConfiguration{ - { - CapabilityId: CCIPCapabilityID, - Config: encodedSetCandidateCall, - }, - }, - false, - nodes.DefaultF(), - ) - if err != nil { - return fmt.Errorf("update don w/ exec config: %w", err) - } - - if _, err := deployment.ConfirmIfNoError(home, tx, err); err != nil { - return fmt.Errorf("confirm update don w/ exec config: %w", err) - } - lggr.Infow("Updated DON with exec config", "chain", home.String(), "donID", donID, "txHash", tx.Hash().Hex(), "setCandidateCall", encodedSetCandidateCall) - - execCandidateDigest, err := ccipHome.GetCandidateDigest(nil, donID, execConfig.PluginType) - if err != nil { - return fmt.Errorf("get exec candidate digest 1st time: %w", err) - } - - if execCandidateDigest == [32]byte{} { - return fmt.Errorf("candidate digest is empty, expected nonempty") - } - lggr.Infow("Got exec candidate digest", "chain", home.String(), "donID", donID, "execCandidateDigest", execCandidateDigest) - // promote candidate call - encodedPromotionCall, err := CCIPHomeABI.Pack( - "promoteCandidateAndRevokeActive", - donID, - execConfig.PluginType, - execCandidateDigest, - [32]byte{}, - ) - if err != nil { - return fmt.Errorf("pack promotion call: %w", err) - } - - tx, err = capReg.UpdateDON( - home.DeployerKey, - donID, - nodes.PeerIDs(), - []capabilities_registry.CapabilitiesRegistryCapabilityConfiguration{ - { - CapabilityId: CCIPCapabilityID, - Config: encodedPromotionCall, - }, - }, - false, - nodes.DefaultF(), - ) - if err != nil { - return fmt.Errorf("update don w/ exec config: %w", err) - } - bn, err := deployment.ConfirmIfNoError(home, tx, err) - if err != nil { - return fmt.Errorf("confirm update don w/ exec config: %w", err) - } - if bn == 0 { - return fmt.Errorf("UpdateDON tx not confirmed") - } - lggr.Infow("Promoted exec candidate", "chain", home.String(), "donID", donID, "txHash", tx.Hash().Hex(), "promotionCall", encodedPromotionCall) - // check if candidate digest is promoted - pEvent, err := ccipHome.FilterConfigPromoted(&bind.FilterOpts{ - Context: context.Background(), - Start: bn, - }, [][32]byte{execCandidateDigest}) - if err != nil { - return fmt.Errorf("filter exec config promoted: %w", err) - } - if !pEvent.Next() { - return fmt.Errorf("exec config not promoted") - } - // check that candidate digest is empty. - execCandidateDigest, err = ccipHome.GetCandidateDigest(nil, donID, execConfig.PluginType) - if err != nil { - return fmt.Errorf("get exec candidate digest 2nd time: %w", err) - } - - if execCandidateDigest != [32]byte{} { - return fmt.Errorf("candidate digest is nonempty after promotion, expected empty") - } - - // check that active digest is non-empty. - execActiveDigest, err := ccipHome.GetActiveDigest(nil, donID, uint8(types.PluginTypeCCIPExec)) - if err != nil { - return fmt.Errorf("get active exec digest: %w", err) - } - - if execActiveDigest == [32]byte{} { - return fmt.Errorf("active exec digest is empty, expected nonempty") - } - - execConfigs, err := ccipHome.GetAllConfigs(nil, donID, uint8(types.PluginTypeCCIPExec)) - if err != nil { - return fmt.Errorf("get all exec configs 2nd time: %w", err) - } - - // log the above info - lggr.Infow("completed exec DON creation and promotion", - "donID", donID, - "execCandidateDigest", execCandidateDigest, - "execActiveDigest", execActiveDigest, - "execCandidateDigestFromGetAllConfigs", execConfigs.CandidateConfig.ConfigDigest, - "execActiveDigestFromGetAllConfigs", execConfigs.ActiveConfig.ConfigDigest, - ) - - return nil -} - -func SetupCommitDON( - lggr logger.Logger, - donID uint32, - commitConfig ccip_home.CCIPHomeOCR3Config, - capReg *capabilities_registry.CapabilitiesRegistry, - home deployment.Chain, - nodes deployment.Nodes, - ccipHome *ccip_home.CCIPHome, -) error { - encodedSetCandidateCall, err := CCIPHomeABI.Pack( - "setCandidate", - donID, - commitConfig.PluginType, - commitConfig, - [32]byte{}, - ) - if err != nil { - return fmt.Errorf("pack set candidate call: %w", err) - } - tx, err := capReg.AddDON(home.DeployerKey, nodes.PeerIDs(), []capabilities_registry.CapabilitiesRegistryCapabilityConfiguration{ - { - CapabilityId: CCIPCapabilityID, - Config: encodedSetCandidateCall, - }, - }, false, false, nodes.DefaultF()) - if err != nil { - return fmt.Errorf("add don w/ commit config: %w", err) - } - - if _, err := deployment.ConfirmIfNoError(home, tx, err); err != nil { - return fmt.Errorf("confirm add don w/ commit config: %w", err) - } - lggr.Debugw("Added DON with commit config", "chain", home.String(), "donID", donID, "txHash", tx.Hash().Hex(), "setCandidateCall", encodedSetCandidateCall) - commitCandidateDigest, err := ccipHome.GetCandidateDigest(nil, donID, commitConfig.PluginType) - if err != nil { - return fmt.Errorf("get commit candidate digest: %w", err) - } - - if commitCandidateDigest == [32]byte{} { - return fmt.Errorf("candidate digest is empty, expected nonempty") - } - lggr.Debugw("Got commit candidate digest", "chain", home.String(), "donID", donID, "commitCandidateDigest", commitCandidateDigest) - - encodedPromotionCall, err := CCIPHomeABI.Pack( - "promoteCandidateAndRevokeActive", - donID, - commitConfig.PluginType, - commitCandidateDigest, - [32]byte{}, - ) - if err != nil { - return fmt.Errorf("pack promotion call: %w", err) - } - - tx, err = capReg.UpdateDON( - home.DeployerKey, - donID, - nodes.PeerIDs(), - []capabilities_registry.CapabilitiesRegistryCapabilityConfiguration{ - { - CapabilityId: CCIPCapabilityID, - Config: encodedPromotionCall, - }, - }, - false, - nodes.DefaultF(), - ) - if err != nil { - return fmt.Errorf("update don w/ commit config: %w", err) - } - - if _, err := deployment.ConfirmIfNoError(home, tx, err); err != nil { - return fmt.Errorf("confirm update don w/ commit config: %w", err) - } - lggr.Debugw("Promoted commit candidate", "chain", home.String(), "donID", donID, "txHash", tx.Hash().Hex(), "promotionCall", encodedPromotionCall) - - // check that candidate digest is empty. - commitCandidateDigest, err = ccipHome.GetCandidateDigest(nil, donID, commitConfig.PluginType) - if err != nil { - return fmt.Errorf("get commit candidate digest 2nd time: %w", err) - } - - if commitCandidateDigest != [32]byte{} { - return fmt.Errorf("candidate digest is nonempty after promotion, expected empty") - } - - // check that active digest is non-empty. - commitActiveDigest, err := ccipHome.GetActiveDigest(nil, donID, uint8(types.PluginTypeCCIPCommit)) - if err != nil { - return fmt.Errorf("get active commit digest: %w", err) - } - - if commitActiveDigest == [32]byte{} { - return fmt.Errorf("active commit digest is empty, expected nonempty") - } - - commitConfigs, err := ccipHome.GetAllConfigs(nil, donID, uint8(types.PluginTypeCCIPCommit)) - if err != nil { - return fmt.Errorf("get all commit configs 2nd time: %w", err) - } - - // log the above information - lggr.Infow("completed commit DON creation and promotion", - "donID", donID, - "commitCandidateDigest", commitCandidateDigest, - "commitActiveDigest", commitActiveDigest, - "commitCandidateDigestFromGetAllConfigs", commitConfigs.CandidateConfig.ConfigDigest, - "commitActiveDigestFromGetAllConfigs", commitConfigs.ActiveConfig.ConfigDigest, - ) - - return nil -} - func BuildOCR3ConfigForCCIPHome( ocrSecrets deployment.OCRSecrets, offRamp *offramp.OffRamp, diff --git a/deployment/ccip/changeset/state.go b/deployment/ccip/changeset/state.go index 2403f3f7cc2..ccd6176a9f7 100644 --- a/deployment/ccip/changeset/state.go +++ b/deployment/ccip/changeset/state.go @@ -289,6 +289,14 @@ func (s CCIPOnChainState) GetAllTimeLocksForChains(chains []uint64) (map[uint64] return timelocks, nil } +func (s CCIPOnChainState) SupportedChains() map[uint64]struct{} { + chains := make(map[uint64]struct{}) + for chain := range s.Chains { + chains[chain] = struct{}{} + } + return chains +} + func (s CCIPOnChainState) View(chains []uint64) (map[string]view.ChainView, error) { m := make(map[string]view.ChainView) for _, chainSelector := range chains { diff --git a/deployment/ccip/changeset/test_assertions.go b/deployment/ccip/changeset/test_assertions.go index a114e52b361..bcfb49250d4 100644 --- a/deployment/ccip/changeset/test_assertions.go +++ b/deployment/ccip/changeset/test_assertions.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" @@ -636,3 +637,19 @@ func executionStateToString(state uint8) string { return "UNKNOWN" } } + +func AssertEqualFeeConfig(t *testing.T, want, have fee_quoter.FeeQuoterDestChainConfig) { + assert.Equal(t, want.DestGasOverhead, have.DestGasOverhead) + assert.Equal(t, want.IsEnabled, have.IsEnabled) + assert.Equal(t, want.ChainFamilySelector, have.ChainFamilySelector) + assert.Equal(t, want.DefaultTokenDestGasOverhead, have.DefaultTokenDestGasOverhead) + assert.Equal(t, want.DefaultTokenFeeUSDCents, have.DefaultTokenFeeUSDCents) + assert.Equal(t, want.DefaultTxGasLimit, have.DefaultTxGasLimit) + assert.Equal(t, want.DestGasPerPayloadByte, have.DestGasPerPayloadByte) + assert.Equal(t, want.DestGasPerDataAvailabilityByte, have.DestGasPerDataAvailabilityByte) + assert.Equal(t, want.DestDataAvailabilityMultiplierBps, have.DestDataAvailabilityMultiplierBps) + assert.Equal(t, want.DestDataAvailabilityOverheadGas, have.DestDataAvailabilityOverheadGas) + assert.Equal(t, want.MaxDataBytes, have.MaxDataBytes) + assert.Equal(t, want.MaxNumberOfTokensPerMsg, have.MaxNumberOfTokensPerMsg) + assert.Equal(t, want.MaxPerMsgGasLimit, have.MaxPerMsgGasLimit) +} diff --git a/deployment/ccip/changeset/test_environment.go b/deployment/ccip/changeset/test_environment.go index cb0760cee1c..8c2ea88b276 100644 --- a/deployment/ccip/changeset/test_environment.go +++ b/deployment/ccip/changeset/test_environment.go @@ -9,14 +9,18 @@ import ( "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/smartcontractkit/chainlink-ccip/chainconfig" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-ccip/pluginconfig" commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" - "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" "github.com/smartcontractkit/chainlink/deployment" commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" @@ -34,8 +38,9 @@ const ( ) type TestConfigs struct { - Type EnvType // set by env var CCIP_V16_TEST_ENV, defaults to Memory - CreateJob bool + Type EnvType // set by env var CCIP_V16_TEST_ENV, defaults to Memory + CreateJob bool + // TODO: This should be CreateContracts so the booleans make sense? CreateJobAndContracts bool Chains int // only used in memory mode, for docker mode, this is determined by the integration-test config toml input NumOfUsersPerChain int // only used in memory mode, for docker mode, this is determined by the integration-test config toml input @@ -177,6 +182,19 @@ type DeployedEnv struct { Users map[uint64][]*bind.TransactOpts } +func (d *DeployedEnv) TimelockContracts(t *testing.T) map[uint64]*proposalutils.TimelockExecutionContracts { + timelocks := make(map[uint64]*proposalutils.TimelockExecutionContracts) + state, err := LoadOnchainState(d.Env) + require.NoError(t, err) + for chain, chainState := range state.Chains { + timelocks[chain] = &proposalutils.TimelockExecutionContracts{ + Timelock: chainState.Timelock, + CallProxy: chainState.CallProxy, + } + } + return timelocks +} + func (d *DeployedEnv) SetupJobs(t *testing.T) { ctx := testcontext.Get(t) out, err := CCIPCapabilityJobspec(d.Env, struct{}{}) @@ -277,6 +295,7 @@ func NewEnvironment(t *testing.T, tc *TestConfigs, tEnv TestEnvironment) Deploye crConfig := DeployTestContracts(t, lggr, ab, dEnv.HomeChainSel, dEnv.FeedChainSel, dEnv.Env.Chains, tc.LinkPrice, tc.WethPrice) tEnv.StartNodes(t, tc, crConfig) dEnv = tEnv.DeployedEnvironment() + // TODO: Should use ApplyChangesets here. envNodes, err := deployment.NodeInfo(dEnv.Env.NodeIDs, dEnv.Env.Offchain) require.NoError(t, err) dEnv.Env.ExistingAddresses = ab @@ -383,8 +402,11 @@ func NewEnvironmentWithJobsAndContracts(t *testing.T, tc *TestConfigs, tEnv Test }}) } // Build the per chain config. - chainConfigs := make(map[uint64]CCIPOCRParams) + ocrConfigs := make(map[uint64]CCIPOCRParams) + chainConfigs := make(map[uint64]ChainConfig) timelockContractsPerChain := make(map[uint64]*proposalutils.TimelockExecutionContracts) + nodeInfo, err := deployment.NodeInfo(e.Env.NodeIDs, e.Env.Offchain) + require.NoError(t, err) for _, chain := range allChains { timelockContractsPerChain[chain] = &proposalutils.TimelockExecutionContracts{ Timelock: state.Chains[chain].Timelock, @@ -395,16 +417,65 @@ func NewEnvironmentWithJobsAndContracts(t *testing.T, tc *TestConfigs, tEnv Test if tc.OCRConfigOverride != nil { ocrParams = tc.OCRConfigOverride(ocrParams) } - chainConfigs[chain] = ocrParams + ocrConfigs[chain] = ocrParams + chainConfigs[chain] = ChainConfig{ + Readers: nodeInfo.NonBootstraps().PeerIDs(), + FChain: uint8(len(nodeInfo.NonBootstraps().PeerIDs()) / 3), + EncodableChainConfig: chainconfig.ChainConfig{ + GasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(internal.GasPriceDeviationPPB)}, + DAGasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(internal.DAGasPriceDeviationPPB)}, + OptimisticConfirmations: internal.OptimisticConfirmations, + }, + } } // Deploy second set of changesets to deploy and configure the CCIP contracts. e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, timelockContractsPerChain, []commonchangeset.ChangesetApplication{ { - Changeset: commonchangeset.WrapChangeSet(ConfigureNewChains), - Config: NewChainsConfig{ - HomeChainSel: e.HomeChainSel, - FeedChainSel: e.FeedChainSel, - ChainConfigByChain: chainConfigs, + // Add the chain configs for the new chains. + Changeset: commonchangeset.WrapChangeSet(UpdateChainConfig), + Config: UpdateChainConfigConfig{ + HomeChainSelector: e.HomeChainSel, + RemoteChainAdds: chainConfigs, + }, + }, + { + // Add the DONs and candidate commit OCR instances for the chain. + Changeset: commonchangeset.WrapChangeSet(AddDonAndSetCandidateChangeset), + Config: AddDonAndSetCandidateChangesetConfig{ + SetCandidateConfigBase{ + HomeChainSelector: e.HomeChainSel, + FeedChainSelector: e.FeedChainSel, + OCRConfigPerRemoteChainSelector: ocrConfigs, + PluginType: types.PluginTypeCCIPCommit, + }, + }, + }, + { + // Add the exec OCR instances for the new chains. + Changeset: commonchangeset.WrapChangeSet(SetCandidateChangeset), + Config: SetCandidateChangesetConfig{ + SetCandidateConfigBase{ + HomeChainSelector: e.HomeChainSel, + FeedChainSelector: e.FeedChainSel, + OCRConfigPerRemoteChainSelector: ocrConfigs, + PluginType: types.PluginTypeCCIPExec, + }, + }, + }, + { + // Promote everything + Changeset: commonchangeset.WrapChangeSet(PromoteAllCandidatesChangeset), + Config: PromoteAllCandidatesChangesetConfig{ + HomeChainSelector: e.HomeChainSel, + RemoteChainSelectors: allChains, + }, + }, + { + // Enable the OCR config on the remote chains. + Changeset: commonchangeset.WrapChangeSet(SetOCR3OffRamp), + Config: SetOCR3OffRampConfig{ + HomeChainSel: e.HomeChainSel, + RemoteChainSels: allChains, }, }, { diff --git a/deployment/common/changeset/deploy_mcms_with_timelock.go b/deployment/common/changeset/deploy_mcms_with_timelock.go index c36e1f1575b..06f9aba6164 100644 --- a/deployment/common/changeset/deploy_mcms_with_timelock.go +++ b/deployment/common/changeset/deploy_mcms_with_timelock.go @@ -1,6 +1,12 @@ package changeset import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/common/changeset/internal" "github.com/smartcontractkit/chainlink/deployment/common/types" @@ -18,3 +24,16 @@ func DeployMCMSWithTimelock(e deployment.Environment, cfgByChain map[uint64]type } return deployment.ChangesetOutput{AddressBook: newAddresses}, nil } + +func ValidateOwnership(ctx context.Context, mcms bool, deployerKey, timelock common.Address, contract Ownable) error { + owner, err := contract.Owner(&bind.CallOpts{Context: ctx}) + if err != nil { + return fmt.Errorf("failed to get owner: %w", err) + } + if mcms && owner != timelock { + return fmt.Errorf("%s not owned by deployer key", contract.Address()) + } else if !mcms && owner != deployerKey { + return fmt.Errorf("%s not owned by deployer key", contract.Address()) + } + return nil +} diff --git a/deployment/common/proposalutils/propose.go b/deployment/common/proposalutils/propose.go index 32a5bcdfda2..baf506cb2f8 100644 --- a/deployment/common/proposalutils/propose.go +++ b/deployment/common/proposalutils/propose.go @@ -40,7 +40,13 @@ func BuildProposalMetadata( } // BuildProposalFromBatches Given batches of operations, we build the metadata and timelock addresses of those opartions -// We then return a proposal that can be executed and signed +// We then return a proposal that can be executed and signed. +// You can specify multiple batches for the same chain, but the only +// usecase to do that would be you have a batch that can't fit in a single +// transaction due to gas or calldata constraints of the chain. +// The batches are specified separately because we eventually intend +// to support user-specified cross chain ordering of batch execution by the tooling itself. +// TODO: Can/should merge timelocks and proposers into a single map for the chain. func BuildProposalFromBatches( timelocksPerChain map[uint64]common.Address, proposerMcmsesPerChain map[uint64]*gethwrappers.ManyChainMultiSig,