From 5e404d267f1a22f5a88f24555e881b08abd09e45 Mon Sep 17 00:00:00 2001 From: Anindita Ghosh <88458927+AnieeG@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:30:04 -0800 Subject: [PATCH] Ccip-4692 make ccip home and jobspec changeset idempotent (#16022) * remove deployCCIPContracts * deprecate existing add lane * make jobspec and deployhome chain idempotent * make jobspec changeset idempotent * adjust tests * fix lint * more fix * try a fix * more * another rounder of fix --- deployment/ccip/changeset/cs_ccip_home.go | 85 ++--- .../ccip/changeset/cs_ccip_home_test.go | 12 +- .../ccip/changeset/cs_chain_contracts.go | 15 +- deployment/ccip/changeset/cs_home_chain.go | 305 ++++++++++++------ .../ccip/changeset/cs_home_chain_test.go | 21 ++ deployment/ccip/changeset/cs_jobspec.go | 96 +++++- deployment/ccip/changeset/cs_jobspec_test.go | 10 + .../ccip/changeset/cs_update_rmn_config.go | 45 +++ deployment/ccip/changeset/globals/config.go | 37 +++ .../changeset/internal/deploy_home_chain.go | 26 +- .../changeset/testhelpers/test_environment.go | 57 ++-- deployment/environment.go | 13 + deployment/environment/memory/job_client.go | 62 +++- deployment/helpers.go | 2 +- .../smoke/ccip/ccip_add_chain_test.go | 35 +- .../ccip/ccip_migration_to_v_1_6_test.go | 2 +- 16 files changed, 596 insertions(+), 227 deletions(-) create mode 100644 deployment/ccip/changeset/globals/config.go diff --git a/deployment/ccip/changeset/cs_ccip_home.go b/deployment/ccip/changeset/cs_ccip_home.go index f64a0cf5564..67e41f08024 100644 --- a/deployment/ccip/changeset/cs_ccip_home.go +++ b/deployment/ccip/changeset/cs_ccip_home.go @@ -23,6 +23,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/merklemulti" "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" @@ -33,34 +34,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry_1_1_0" ) -const ( - // ========= Changeset Defaults ========= - FirstBlockAge = 8 * time.Hour - RemoteGasPriceBatchWriteFrequency = 30 * time.Minute - TokenPriceBatchWriteFrequency = 30 * time.Minute - BatchGasLimit = 6_500_000 - RelativeBoostPerWaitHour = 0.5 - InflightCacheExpiry = 10 * time.Minute - RootSnoozeTime = 30 * time.Minute - BatchingStrategyID = 0 - DeltaProgress = 30 * time.Second - DeltaResend = 10 * time.Second - DeltaInitial = 20 * time.Second - DeltaRound = 2 * time.Second - DeltaGrace = 2 * time.Second - DeltaCertifiedCommitRequest = 10 * time.Second - DeltaStage = 10 * time.Second - Rmax = 3 - MaxDurationQuery = 500 * time.Millisecond - MaxDurationObservation = 5 * time.Second - MaxDurationShouldAcceptAttestedReport = 10 * time.Second - MaxDurationShouldTransmitAcceptedReport = 10 * time.Second - GasPriceDeviationPPB = 1000 - DAGasPriceDeviationPPB = 0 - OptimisticConfirmations = 1 - // ====================================== -) - var ( _ deployment.ChangeSet[AddDonAndSetCandidateChangesetConfig] = AddDonAndSetCandidateChangeset _ deployment.ChangeSet[PromoteCandidateChangesetConfig] = PromoteCandidateChangeset @@ -219,8 +192,8 @@ func WithDefaultCommitOffChainConfig(feedChainSel uint64, tokenInfo map[ccipocr3 return func(params *CCIPOCRParams) { if params.CommitOffChainConfig == nil { params.CommitOffChainConfig = &pluginconfig.CommitOffchainConfig{ - RemoteGasPriceBatchWriteFrequency: *config.MustNewDuration(RemoteGasPriceBatchWriteFrequency), - TokenPriceBatchWriteFrequency: *config.MustNewDuration(TokenPriceBatchWriteFrequency), + RemoteGasPriceBatchWriteFrequency: *config.MustNewDuration(globals.RemoteGasPriceBatchWriteFrequency), + TokenPriceBatchWriteFrequency: *config.MustNewDuration(globals.TokenPriceBatchWriteFrequency), TokenInfo: tokenInfo, PriceFeedChainSelector: ccipocr3.ChainSelector(feedChainSel), NewMsgScanBatchSize: merklemulti.MaxNumberTreeLeaves, @@ -246,12 +219,12 @@ func WithDefaultExecuteOffChainConfig(tokenDataObservers []pluginconfig.TokenDat return func(params *CCIPOCRParams) { if params.ExecuteOffChainConfig == nil { params.ExecuteOffChainConfig = &pluginconfig.ExecuteOffchainConfig{ - BatchGasLimit: BatchGasLimit, - RelativeBoostPerWaitHour: RelativeBoostPerWaitHour, - InflightCacheExpiry: *config.MustNewDuration(InflightCacheExpiry), - RootSnoozeTime: *config.MustNewDuration(RootSnoozeTime), - MessageVisibilityInterval: *config.MustNewDuration(FirstBlockAge), - BatchingStrategyID: BatchingStrategyID, + BatchGasLimit: globals.BatchGasLimit, + RelativeBoostPerWaitHour: globals.RelativeBoostPerWaitHour, + InflightCacheExpiry: *config.MustNewDuration(globals.InflightCacheExpiry), + RootSnoozeTime: *config.MustNewDuration(globals.RootSnoozeTime), + MessageVisibilityInterval: *config.MustNewDuration(globals.FirstBlockAge), + BatchingStrategyID: globals.BatchingStrategyID, TokenDataObservers: tokenDataObservers, } } else if tokenDataObservers != nil { @@ -266,18 +239,18 @@ func DeriveCCIPOCRParams( ) CCIPOCRParams { params := CCIPOCRParams{ OCRParameters: commontypes.OCRParameters{ - DeltaProgress: DeltaProgress, - DeltaResend: DeltaResend, - DeltaInitial: DeltaInitial, - DeltaRound: DeltaRound, - DeltaGrace: DeltaGrace, - DeltaCertifiedCommitRequest: DeltaCertifiedCommitRequest, - DeltaStage: DeltaStage, - Rmax: Rmax, - MaxDurationQuery: MaxDurationQuery, - MaxDurationObservation: MaxDurationObservation, - MaxDurationShouldAcceptAttestedReport: MaxDurationShouldAcceptAttestedReport, - MaxDurationShouldTransmitAcceptedReport: MaxDurationShouldTransmitAcceptedReport, + DeltaProgress: globals.DeltaProgress, + DeltaResend: globals.DeltaResend, + DeltaInitial: globals.DeltaInitial, + DeltaRound: globals.DeltaRound, + DeltaGrace: globals.DeltaGrace, + DeltaCertifiedCommitRequest: globals.DeltaCertifiedCommitRequest, + DeltaStage: globals.DeltaStage, + Rmax: globals.Rmax, + MaxDurationQuery: globals.MaxDurationQuery, + MaxDurationObservation: globals.MaxDurationObservation, + MaxDurationShouldAcceptAttestedReport: globals.MaxDurationShouldAcceptAttestedReport, + MaxDurationShouldTransmitAcceptedReport: globals.MaxDurationShouldTransmitAcceptedReport, }, } for _, opt := range opts { @@ -289,8 +262,9 @@ func DeriveCCIPOCRParams( type PromoteCandidatePluginInfo struct { // 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. - RemoteChainSelectors []uint64 - PluginType types.PluginType + RemoteChainSelectors []uint64 + PluginType types.PluginType + AllowEmptyConfigPromote bool // safe guard to prevent promoting empty config to active } type PromoteCandidateChangesetConfig struct { @@ -356,7 +330,13 @@ func (p PromoteCandidateChangesetConfig) Validate(e deployment.Environment) (map if err != nil { return nil, fmt.Errorf("fetching %s configs from cciphome: %w", plugin.PluginType.String(), err) } + // If promoteCandidate is called with AllowEmptyConfigPromote set to false and + // the CandidateConfig config digest is zero, do not promote the candidate config to active. + if !plugin.AllowEmptyConfigPromote && pluginConfigs.CandidateConfig.ConfigDigest == [32]byte{} { + return nil, fmt.Errorf("%s candidate config digest is empty", plugin.PluginType.String()) + } + // If the active and candidate config digests are both zero, we should not promote the candidate config to active. if pluginConfigs.ActiveConfig.ConfigDigest == [32]byte{} && pluginConfigs.CandidateConfig.ConfigDigest == [32]byte{} { return nil, fmt.Errorf("%s active and candidate config digests are both zero", plugin.PluginType.String()) @@ -420,6 +400,7 @@ func PromoteCandidateChangeset( nodes.NonBootstraps(), donID, plugin.PluginType, + plugin.AllowEmptyConfigPromote, cfg.MCMS != nil, ) if err != nil { @@ -1019,6 +1000,7 @@ func promoteCandidateForChainOps( nodes deployment.Nodes, donID uint32, pluginType cctypes.PluginType, + allowEmpty bool, mcmsEnabled bool, ) (mcms.Operation, error) { if donID == 0 { @@ -1028,6 +1010,9 @@ func promoteCandidateForChainOps( if err != nil { return mcms.Operation{}, err } + if digest == [32]byte{} && !allowEmpty { + return mcms.Operation{}, errors.New("candidate config digest is zero, promoting empty config is not allowed") + } fmt.Println("Promoting candidate for plugin", pluginType.String(), "with digest", digest) updatePluginOp, err := promoteCandidateOp( txOpts, diff --git a/deployment/ccip/changeset/cs_ccip_home_test.go b/deployment/ccip/changeset/cs_ccip_home_test.go index 8b64426808f..feeefae2fff 100644 --- a/deployment/ccip/changeset/cs_ccip_home_test.go +++ b/deployment/ccip/changeset/cs_ccip_home_test.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" @@ -161,8 +162,9 @@ func Test_PromoteCandidate(t *testing.T) { HomeChainSelector: tenv.HomeChainSel, PluginInfo: []changeset.PromoteCandidatePluginInfo{ { - RemoteChainSelectors: []uint64{dest}, - PluginType: types.PluginTypeCCIPCommit, + RemoteChainSelectors: []uint64{dest}, + PluginType: types.PluginTypeCCIPCommit, + AllowEmptyConfigPromote: true, }, }, MCMS: mcmsConfig, @@ -562,9 +564,9 @@ func Test_UpdateChainConfigs(t *testing.T) { RemoteChainAdds: map[uint64]changeset.ChainConfig{ otherChain: { EncodableChainConfig: chainconfig.ChainConfig{ - GasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(changeset.GasPriceDeviationPPB)}, - DAGasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(changeset.DAGasPriceDeviationPPB)}, - OptimisticConfirmations: changeset.OptimisticConfirmations, + GasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(globals.GasPriceDeviationPPB)}, + DAGasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(globals.DAGasPriceDeviationPPB)}, + OptimisticConfirmations: globals.OptimisticConfirmations, }, FChain: otherChainConfig.FChain, Readers: otherChainConfig.Readers, diff --git a/deployment/ccip/changeset/cs_chain_contracts.go b/deployment/ccip/changeset/cs_chain_contracts.go index 2dbcfa74368..201f724eea5 100644 --- a/deployment/ccip/changeset/cs_chain_contracts.go +++ b/deployment/ccip/changeset/cs_chain_contracts.go @@ -18,6 +18,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" @@ -942,9 +943,10 @@ func UpdateRouterRampsChangeset(e deployment.Environment, cfg UpdateRouterRampsC } type SetOCR3OffRampConfig struct { - HomeChainSel uint64 - RemoteChainSels []uint64 - MCMS *MCMSConfig + HomeChainSel uint64 + RemoteChainSels []uint64 + CCIPHomeConfigType globals.ConfigType + MCMS *MCMSConfig } func (c SetOCR3OffRampConfig) Validate(e deployment.Environment) error { @@ -955,6 +957,10 @@ func (c SetOCR3OffRampConfig) Validate(e deployment.Environment) error { if _, ok := state.Chains[c.HomeChainSel]; !ok { return fmt.Errorf("home chain %d not found in onchain state", c.HomeChainSel) } + if c.CCIPHomeConfigType != globals.ConfigTypeActive && + c.CCIPHomeConfigType != globals.ConfigTypeCandidate { + return fmt.Errorf("invalid CCIPHomeConfigType should be either %s or %s", globals.ConfigTypeActive, globals.ConfigTypeCandidate) + } for _, remote := range c.RemoteChainSels { chainState, ok := state.Chains[remote] if !ok { @@ -989,7 +995,8 @@ func SetOCR3OffRampChangeset(e deployment.Environment, cfg SetOCR3OffRampConfig) state.Chains[cfg.HomeChainSel].CapabilityRegistry, state.Chains[cfg.HomeChainSel].CCIPHome, remote) - args, err := internal.BuildSetOCR3ConfigArgs(donID, state.Chains[cfg.HomeChainSel].CCIPHome, remote) + args, err := internal.BuildSetOCR3ConfigArgs( + donID, state.Chains[cfg.HomeChainSel].CCIPHome, remote, cfg.CCIPHomeConfigType) if err != nil { return deployment.ChangesetOutput{}, err } diff --git a/deployment/ccip/changeset/cs_home_chain.go b/deployment/ccip/changeset/cs_home_chain.go index cc234a45e49..9a691a8d8ab 100644 --- a/deployment/ccip/changeset/cs_home_chain.go +++ b/deployment/ccip/changeset/cs_home_chain.go @@ -3,8 +3,8 @@ package changeset import ( "bytes" "context" - "encoding/json" "fmt" + "math" "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -145,131 +145,244 @@ func deployHomeChain( } lggr.Infow("deployed/connected to capreg", "addr", capReg.Address) - ccipHome, err := deployment.DeployContract( - lggr, chain, ab, - func(chain deployment.Chain) deployment.ContractDeploy[*ccip_home.CCIPHome] { - ccAddr, tx, cc, err2 := ccip_home.DeployCCIPHome( - chain.DeployerKey, - chain.Client, - capReg.Address, - ) - return deployment.ContractDeploy[*ccip_home.CCIPHome]{ - Address: ccAddr, Tv: deployment.NewTypeAndVersion(CCIPHome, deployment.Version1_6_0_dev), Tx: tx, Err: err2, Contract: cc, - } - }) - if err != nil { - lggr.Errorw("Failed to deploy CCIPHome", "chain", chain.String(), "err", err) - return nil, err + var ccipHomeAddr common.Address + if state.Chains[chain.Selector].CCIPHome != nil { + lggr.Infow("CCIPHome already deployed", "addr", state.Chains[chain.Selector].CCIPHome.Address().String()) + ccipHomeAddr = state.Chains[chain.Selector].CCIPHome.Address() + } else { + ccipHome, err := deployment.DeployContract( + lggr, chain, ab, + func(chain deployment.Chain) deployment.ContractDeploy[*ccip_home.CCIPHome] { + ccAddr, tx, cc, err2 := ccip_home.DeployCCIPHome( + chain.DeployerKey, + chain.Client, + capReg.Address, + ) + return deployment.ContractDeploy[*ccip_home.CCIPHome]{ + Address: ccAddr, Tv: deployment.NewTypeAndVersion(CCIPHome, deployment.Version1_6_0_dev), Tx: tx, Err: err2, Contract: cc, + } + }) + if err != nil { + lggr.Errorw("Failed to deploy CCIPHome", "chain", chain.String(), "err", err) + return nil, err + } + ccipHomeAddr = ccipHome.Address + } + rmnHome := state.Chains[chain.Selector].RMNHome + if state.Chains[chain.Selector].RMNHome != nil { + lggr.Infow("RMNHome already deployed", "addr", state.Chains[chain.Selector].RMNHome.Address().String()) + } else { + rmnHomeContract, err := deployment.DeployContract( + lggr, chain, ab, + func(chain deployment.Chain) deployment.ContractDeploy[*rmn_home.RMNHome] { + rmnAddr, tx, rmn, err2 := rmn_home.DeployRMNHome( + chain.DeployerKey, + chain.Client, + ) + return deployment.ContractDeploy[*rmn_home.RMNHome]{ + Address: rmnAddr, Tv: deployment.NewTypeAndVersion(RMNHome, deployment.Version1_6_0_dev), Tx: tx, Err: err2, Contract: rmn, + } + }, + ) + if err != nil { + lggr.Errorw("Failed to deploy RMNHome", "chain", chain.String(), "err", err) + return nil, err + } + rmnHome = rmnHomeContract.Contract } - rmnHome, err := deployment.DeployContract( - lggr, chain, ab, - func(chain deployment.Chain) deployment.ContractDeploy[*rmn_home.RMNHome] { - rmnAddr, tx, rmn, err2 := rmn_home.DeployRMNHome( - chain.DeployerKey, - chain.Client, - ) - return deployment.ContractDeploy[*rmn_home.RMNHome]{ - Address: rmnAddr, Tv: deployment.NewTypeAndVersion(RMNHome, deployment.Version1_6_0_dev), Tx: tx, Err: err2, Contract: rmn, - } - }, - ) + // considering the RMNHome is recently deployed, there is no digest to overwrite + configs, err := rmnHome.GetAllConfigs(nil) if err != nil { - lggr.Errorw("Failed to deploy RMNHome", "chain", chain.String(), "err", err) return nil, err } + setCandidate := false + promoteCandidate := false - // considering the RMNHome is recently deployed, there is no digest to overwrite - tx, err := rmnHome.Contract.SetCandidate(chain.DeployerKey, rmnHomeStatic, rmnHomeDynamic, [32]byte{}) - if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { - lggr.Errorw("Failed to set candidate on RMNHome", "err", err) - return nil, err + // check if the candidate is already set and equal to static and dynamic configs + if isRMNDynamicConfigEqual(rmnHomeDynamic, configs.CandidateConfig.DynamicConfig) && + isRMNStaticConfigEqual(rmnHomeStatic, configs.CandidateConfig.StaticConfig) { + lggr.Infow("RMNHome candidate is already set and equal to given static and dynamic configs,skip setting candidate") + } else { + setCandidate = true + } + // check the active config is equal to the static and dynamic configs + if isRMNDynamicConfigEqual(rmnHomeDynamic, configs.ActiveConfig.DynamicConfig) && + isRMNStaticConfigEqual(rmnHomeStatic, configs.ActiveConfig.StaticConfig) { + lggr.Infow("RMNHome active is already set and equal to given static and dynamic configs," + + "skip setting and promoting candidate") + setCandidate = false + promoteCandidate = false + } else { + promoteCandidate = true } - rmnCandidateDigest, err := rmnHome.Contract.GetCandidateDigest(nil) - if err != nil { - lggr.Errorw("Failed to get RMNHome candidate digest", "chain", chain.String(), "err", err) - return nil, err + if setCandidate { + tx, err := rmnHome.SetCandidate( + chain.DeployerKey, rmnHomeStatic, rmnHomeDynamic, configs.CandidateConfig.ConfigDigest) + if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { + lggr.Errorw("Failed to set candidate on RMNHome", "err", err) + return nil, err + } + lggr.Infow("Set candidate on RMNHome", "chain", chain.String()) } + if promoteCandidate { + rmnCandidateDigest, err := rmnHome.GetCandidateDigest(nil) + if err != nil { + lggr.Errorw("Failed to get RMNHome candidate digest", "chain", chain.String(), "err", err) + return nil, err + } - tx, err = rmnHome.Contract.PromoteCandidateAndRevokeActive(chain.DeployerKey, rmnCandidateDigest, [32]byte{}) - if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { - lggr.Errorw("Failed to promote candidate and revoke active on RMNHome", "chain", chain.String(), "err", err) - return nil, err - } + tx, err := rmnHome.PromoteCandidateAndRevokeActive(chain.DeployerKey, rmnCandidateDigest, [32]byte{}) + if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { + lggr.Errorw("Failed to promote candidate and revoke active on RMNHome", "chain", chain.String(), "err", err) + return nil, err + } - rmnActiveDigest, err := rmnHome.Contract.GetActiveDigest(nil) - if err != nil { - lggr.Errorw("Failed to get RMNHome active digest", "chain", chain.String(), "err", err) - return nil, err - } - lggr.Infow("Got rmn home active digest", "digest", rmnActiveDigest) + rmnActiveDigest, err := rmnHome.GetActiveDigest(nil) + if err != nil { + lggr.Errorw("Failed to get RMNHome active digest", "chain", chain.String(), "err", err) + return nil, err + } + lggr.Infow("Got rmn home active digest", "digest", rmnActiveDigest) - if rmnActiveDigest != rmnCandidateDigest { - lggr.Errorw("RMNHome active digest does not match previously candidate digest", - "active", rmnActiveDigest, "candidate", rmnCandidateDigest) - return nil, errors.New("RMNHome active digest does not match candidate digest") + if rmnActiveDigest != rmnCandidateDigest { + lggr.Errorw("RMNHome active digest does not match previously candidate digest", + "active", rmnActiveDigest, "candidate", rmnCandidateDigest) + return nil, errors.New("RMNHome active digest does not match candidate digest") + } + lggr.Infow("Promoted candidate and revoked active on RMNHome", "chain", chain.String()) } - - tx, err = capReg.Contract.AddCapabilities(chain.DeployerKey, []capabilities_registry.CapabilitiesRegistryCapability{ - { - LabelledName: internal.CapabilityLabelledName, - Version: internal.CapabilityVersion, - CapabilityType: 2, // consensus. not used (?) - ResponseType: 0, // report. not used (?) - ConfigurationContract: ccipHome.Address, - }, - }) - if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { - lggr.Errorw("Failed to add capabilities", "chain", chain.String(), "err", err) - return nil, err + // check if ccip capability exists in cap reg + capabilities, err := capReg.Contract.GetCapabilities(nil) + if err != nil { + return nil, fmt.Errorf("failed to get capabilities: %w", err) + } + capabilityToAdd := capabilities_registry.CapabilitiesRegistryCapability{ + LabelledName: internal.CapabilityLabelledName, + Version: internal.CapabilityVersion, + CapabilityType: 2, // consensus. not used (?) + ResponseType: 0, // report. not used (?) + ConfigurationContract: ccipHomeAddr, + } + addCapability := true + for _, cap := range capabilities { + if cap.LabelledName == capabilityToAdd.LabelledName && cap.Version == capabilityToAdd.Version { + lggr.Infow("Capability already exists, skipping adding capability", + "labelledName", cap.LabelledName, "version", cap.Version) + addCapability = false + break + } + } + // Add the capability to the CapabilitiesRegistry contract only if it does not exist + if addCapability { + tx, err := capReg.Contract.AddCapabilities( + chain.DeployerKey, []capabilities_registry.CapabilitiesRegistryCapability{ + capabilityToAdd, + }) + if _, err := deployment.ConfirmIfNoError(chain, tx, err); err != nil { + lggr.Errorw("Failed to add capabilities", "chain", chain.String(), "err", err) + return nil, err + } + lggr.Infow("Added capability to CapabilitiesRegistry", + "labelledName", capabilityToAdd.LabelledName, "version", capabilityToAdd.Version) } - tx, err = capReg.Contract.AddNodeOperators(chain.DeployerKey, nodeOps) - txBlockNum, err := deployment.ConfirmIfNoError(chain, tx, err) + existingNodeOps, err := capReg.Contract.GetNodeOperators(nil) if err != nil { - lggr.Errorw("Failed to add node operators", "chain", chain.String(), "err", err) return nil, err } - addedEvent, err := capReg.Contract.FilterNodeOperatorAdded(&bind.FilterOpts{ - Start: txBlockNum, - Context: context.Background(), - }, nil, nil) - if err != nil { - lggr.Errorw("Failed to filter NodeOperatorAdded event", "chain", chain.String(), "err", err) - return capReg, err + nodeOpsMap := make(map[string]capabilities_registry.CapabilitiesRegistryNodeOperator) + for _, nop := range nodeOps { + nodeOpsMap[nop.Admin.String()] = nop + } + for _, existingNop := range existingNodeOps { + if _, ok := nodeOpsMap[existingNop.Admin.String()]; ok { + lggr.Infow("Node operator already exists", "admin", existingNop.Admin.String()) + delete(nodeOpsMap, existingNop.Admin.String()) + } + } + nodeOpsToAdd := make([]capabilities_registry.CapabilitiesRegistryNodeOperator, 0, len(nodeOpsMap)) + for _, nop := range nodeOpsMap { + nodeOpsToAdd = append(nodeOpsToAdd, nop) } // Need to fetch nodeoperators ids to be able to add nodes for corresponding node operators - p2pIDsByNodeOpId := make(map[uint32][][32]byte) - for addedEvent.Next() { - for nopName, p2pId := range nodeP2PIDsPerNodeOpAdmin { - if addedEvent.Event.Name == nopName { - lggr.Infow("Added node operator", "admin", addedEvent.Event.Admin, "name", addedEvent.Event.Name) - p2pIDsByNodeOpId[addedEvent.Event.NodeOperatorId] = p2pId + p2pIDsByNodeOpID := make(map[uint32][][32]byte) + if len(nodeOpsToAdd) > 0 { + tx, err := capReg.Contract.AddNodeOperators(chain.DeployerKey, nodeOps) + txBlockNum, err := deployment.ConfirmIfNoError(chain, tx, err) + if err != nil { + lggr.Errorw("Failed to add node operators", "chain", chain.String(), "err", err) + return nil, err + } + addedEvent, err := capReg.Contract.FilterNodeOperatorAdded(&bind.FilterOpts{ + Start: txBlockNum, + Context: context.Background(), + }, nil, nil) + if err != nil { + lggr.Errorw("Failed to filter NodeOperatorAdded event", "chain", chain.String(), "err", err) + return capReg, err + } + + for addedEvent.Next() { + for nopName, p2pID := range nodeP2PIDsPerNodeOpAdmin { + if addedEvent.Event.Name == nopName { + lggr.Infow("Added node operator", "admin", addedEvent.Event.Admin, "name", addedEvent.Event.Name) + p2pIDsByNodeOpID[addedEvent.Event.NodeOperatorId] = p2pID + } + } + } + } else { + lggr.Infow("No new node operators to add") + foundNopID := make(map[uint32]bool) + for nopName, p2pID := range nodeP2PIDsPerNodeOpAdmin { + // this is to find the node operator id for the given node operator name + // node operator start from id 1, starting from 1 to len(existingNodeOps) + totalNops := len(existingNodeOps) + if totalNops >= math.MaxUint32 { + return nil, errors.New("too many node operators") + } + for nopID := uint32(1); nopID <= uint32(totalNops); nopID++ { + // if we already found the node operator id, skip + if foundNopID[nopID] { + continue + } + nodeOp, err := capReg.Contract.GetNodeOperator(nil, nopID) + if err != nil { + return capReg, fmt.Errorf("failed to get node operator %d: %w", nopID, err) + } + if nodeOp.Name == nopName { + p2pIDsByNodeOpID[nopID] = p2pID + foundNopID[nopID] = true + break + } } } } - if len(p2pIDsByNodeOpId) != len(nodeP2PIDsPerNodeOpAdmin) { - lggr.Errorw("Failed to add all node operators", "added", maps.Keys(p2pIDsByNodeOpId), "expected", maps.Keys(nodeP2PIDsPerNodeOpAdmin), "chain", chain.String()) + if len(p2pIDsByNodeOpID) != len(nodeP2PIDsPerNodeOpAdmin) { + lggr.Errorw("Failed to add all node operators", "added", maps.Keys(p2pIDsByNodeOpID), "expected", maps.Keys(nodeP2PIDsPerNodeOpAdmin), "chain", chain.String()) return capReg, errors.New("failed to add all node operators") } // Adds initial set of nodes to CR, who all have the CCIP capability - if err := addNodes(lggr, capReg.Contract, chain, p2pIDsByNodeOpId); err != nil { + if err := addNodes(lggr, capReg.Contract, chain, p2pIDsByNodeOpID); err != nil { return capReg, err } return capReg, nil } -func isEqualCapabilitiesRegistryNodeParams(a, b capabilities_registry.CapabilitiesRegistryNodeParams) (bool, error) { - aBytes, err := json.Marshal(a) - if err != nil { - return false, err +func isEqualCapabilitiesRegistryNodeParams(a, b capabilities_registry.CapabilitiesRegistryNodeParams) bool { + if len(a.HashedCapabilityIds) != len(b.HashedCapabilityIds) { + return false } - bBytes, err := json.Marshal(b) - if err != nil { - return false, err + for i := range a.HashedCapabilityIds { + if !bytes.Equal(a.HashedCapabilityIds[i][:], b.HashedCapabilityIds[i][:]) { + return false + } } - return bytes.Equal(aBytes, bBytes), nil + return a.NodeOperatorId == b.NodeOperatorId && + bytes.Equal(a.Signer[:], b.Signer[:]) && + bytes.Equal(a.P2pId[:], b.P2pId[:]) && + bytes.Equal(a.EncryptionPublicKey[:], b.EncryptionPublicKey[:]) } func addNodes( @@ -289,6 +402,7 @@ func addNodes( NodeOperatorId: node.NodeOperatorId, Signer: node.Signer, P2pId: node.P2pId, + EncryptionPublicKey: node.EncryptionPublicKey, HashedCapabilityIds: node.HashedCapabilityIds, } } @@ -306,7 +420,7 @@ func addNodes( HashedCapabilityIds: [][32]byte{internal.CCIPCapabilityID}, } if existing, ok := existingNodeParams[p2pID]; ok { - if isEqual, err := isEqualCapabilitiesRegistryNodeParams(existing, nodeParam); err != nil && isEqual { + if isEqualCapabilitiesRegistryNodeParams(existing, nodeParam) { lggr.Infow("Node already exists", "p2pID", p2pID) continue } @@ -321,7 +435,8 @@ func addNodes( } tx, err := capReg.AddNodes(chain.DeployerKey, nodeParams) if err != nil { - lggr.Errorw("Failed to add nodes", "chain", chain.String(), "err", deployment.MaybeDataErr(err)) + lggr.Errorw("Failed to add nodes", "chain", chain.String(), + "err", deployment.DecodedErrFromABIIfDataErr(err, capabilities_registry.CapabilitiesRegistryABI)) return err } _, err = chain.Confirm(tx) diff --git a/deployment/ccip/changeset/cs_home_chain_test.go b/deployment/ccip/changeset/cs_home_chain_test.go index 0dbdb866d9b..a836b75952d 100644 --- a/deployment/ccip/changeset/cs_home_chain_test.go +++ b/deployment/ccip/changeset/cs_home_chain_test.go @@ -63,6 +63,27 @@ func TestDeployHomeChain(t *testing.T) { require.Len(t, capRegSnap.Nodes, len(p2pIds)) } +func TestDeployHomeChainIdempotent(t *testing.T) { + e, _ := testhelpers.NewMemoryEnvironment(t) + nodes, err := deployment.NodeInfo(e.Env.NodeIDs, e.Env.Offchain) + require.NoError(t, err) + homeChainCfg := changeset.DeployHomeChainConfig{ + HomeChainSel: e.HomeChainSel, + RMNStaticConfig: testhelpers.NewTestRMNStaticConfig(), + RMNDynamicConfig: testhelpers.NewTestRMNDynamicConfig(), + NodeOperators: testhelpers.NewTestNodeOperator(e.Env.Chains[e.HomeChainSel].DeployerKey.From), + NodeP2PIDsPerNodeOpAdmin: map[string][][32]byte{ + "NodeOperator": nodes.NonBootstraps().PeerIDs(), + }, + } + // apply the changeset once again to ensure idempotency + output, err := changeset.DeployHomeChainChangeset(e.Env, homeChainCfg) + require.NoError(t, err) + require.NoError(t, e.Env.ExistingAddresses.Merge(output.AddressBook)) + _, err = changeset.LoadOnchainState(e.Env) + require.NoError(t, err) +} + func TestRemoveDonsValidate(t *testing.T) { e, _ := testhelpers.NewMemoryEnvironment(t) s, err := changeset.LoadOnchainState(e.Env) diff --git a/deployment/ccip/changeset/cs_jobspec.go b/deployment/ccip/changeset/cs_jobspec.go index e29578a516a..cc75ea31960 100644 --- a/deployment/ccip/changeset/cs_jobspec.go +++ b/deployment/ccip/changeset/cs_jobspec.go @@ -1,11 +1,17 @@ package changeset import ( + "bytes" + "fmt" + + "github.com/pelletier/go-toml/v2" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" + "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/internal" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/validate" + corejob "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/relay" ) @@ -18,6 +24,29 @@ func CCIPCapabilityJobspecChangeset(env deployment.Environment, _ any) (deployme if err != nil { return deployment.ChangesetOutput{}, err } + // find existing jobs + existingSpecs := make(map[string][]string) + for _, node := range nodes { + jobs, err := env.Offchain.ListJobs(env.GetContext(), &job.ListJobsRequest{ + Filter: &job.ListJobsRequest_Filter{ + NodeIds: []string{node.NodeID}, + }, + }) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to list jobs for node %s: %w", node.NodeID, err) + } + for _, j := range jobs.Jobs { + for _, propID := range j.ProposalIds { + jbProposal, err := env.Offchain.GetProposal(env.GetContext(), &job.GetProposalRequest{ + Id: propID, + }) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get job proposal %s on node %s: %w", propID, node.NodeID, err) + } + existingSpecs[node.NodeID] = append(existingSpecs[node.NodeID], jbProposal.Proposal.Spec) + } + } + } // Generate a set of brand new job specs for CCIP for a specific environment // (including NOPs) and new addresses. // We want to assign one CCIP capability job to each node. And node with @@ -55,7 +84,19 @@ func CCIPCapabilityJobspecChangeset(env deployment.Environment, _ any) (deployme if err != nil { return deployment.ChangesetOutput{}, err } - nodesToJobSpecs[node.NodeID] = append(nodesToJobSpecs[node.NodeID], spec) + // If the spec already exists, don't propose it again + specExists := false + if existingSpecs[node.NodeID] != nil { + for _, existingSpec := range existingSpecs[node.NodeID] { + specExists, err = areCCIPSpecsEqual(existingSpec, spec) + if err != nil { + return deployment.ChangesetOutput{}, err + } + } + } + if !specExists { + nodesToJobSpecs[node.NodeID] = append(nodesToJobSpecs[node.NodeID], spec) + } } return deployment.ChangesetOutput{ Proposals: []timelock.MCMSWithTimelockProposal{}, @@ -63,3 +104,56 @@ func CCIPCapabilityJobspecChangeset(env deployment.Environment, _ any) (deployme JobSpecs: nodesToJobSpecs, }, nil } + +func areCCIPSpecsEqual(existingSpecStr, newSpecStr string) (bool, error) { + var existingCCIPSpec, newSpec corejob.CCIPSpec + err := toml.Unmarshal([]byte(existingSpecStr), &existingCCIPSpec) + if err != nil { + return false, fmt.Errorf("failed to unmarshal existing job spec: %w", err) + } + err = toml.Unmarshal([]byte(newSpecStr), &newSpec) + if err != nil { + return false, fmt.Errorf("failed to unmarshal new job spec: %w", err) + } + existingOCRKey, err := existingCCIPSpec.OCRKeyBundleIDs.Value() + if err != nil { + return false, fmt.Errorf("failed to get OCRKeyBundleIDs from existing job spec: %w", err) + } + + newOCRKey, err := newSpec.OCRKeyBundleIDs.Value() + if err != nil { + return false, fmt.Errorf("failed to get OCRKeyBundleIDs from new job spec: %w", err) + } + p2pBootstrapperValue, err := existingCCIPSpec.P2PV2Bootstrappers.Value() + if err != nil { + return false, fmt.Errorf("failed to get P2PV2Bootstrappers from existing job spec: %w", err) + } + pluginConfigValue, err := existingCCIPSpec.PluginConfig.Value() + if err != nil { + return false, fmt.Errorf("failed to get PluginConfig from existing job spec: %w", err) + } + relayConfigValue, err := existingCCIPSpec.RelayConfigs.Value() + if err != nil { + return false, fmt.Errorf("failed to get RelayConfigs from existing job spec: %w", err) + } + p2pBootstrapperValueNew, err := newSpec.P2PV2Bootstrappers.Value() + if err != nil { + return false, fmt.Errorf("failed to get P2PV2Bootstrappers from new job spec: %w", err) + } + pluginConfigValueNew, err := newSpec.PluginConfig.Value() + if err != nil { + return false, fmt.Errorf("failed to get PluginConfig from new job spec: %w", err) + } + relayConfigValueNew, err := newSpec.RelayConfigs.Value() + if err != nil { + return false, fmt.Errorf("failed to get RelayConfigs from new job spec: %w", err) + } + + return existingCCIPSpec.CapabilityLabelledName == newSpec.CapabilityLabelledName && + existingCCIPSpec.CapabilityVersion == newSpec.CapabilityVersion && + bytes.Equal(existingOCRKey.([]byte), newOCRKey.([]byte)) && + existingCCIPSpec.P2PKeyID == newSpec.P2PKeyID && + p2pBootstrapperValue == p2pBootstrapperValueNew && + bytes.Equal(pluginConfigValue.([]byte), pluginConfigValueNew.([]byte)) && + bytes.Equal(relayConfigValue.([]byte), relayConfigValueNew.([]byte)), nil +} diff --git a/deployment/ccip/changeset/cs_jobspec_test.go b/deployment/ccip/changeset/cs_jobspec_test.go index b79dfc47387..11fececb244 100644 --- a/deployment/ccip/changeset/cs_jobspec_test.go +++ b/deployment/ccip/changeset/cs_jobspec_test.go @@ -8,6 +8,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" "github.com/smartcontractkit/chainlink/deployment/environment/memory" ccip "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/validate" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -35,3 +36,12 @@ func TestJobSpecChangeset(t *testing.T) { } } } + +func TestJobSpecChangesetIdempotent(t *testing.T) { + e, _ := testhelpers.NewMemoryEnvironment(t) + // we call the changeset again to ensure that it doesn't return any new job specs + // as the job specs are already created in the first call + output, err := changeset.CCIPCapabilityJobspecChangeset(e.Env, nil) + require.NoError(t, err) + require.Empty(t, output.JobSpecs) +} diff --git a/deployment/ccip/changeset/cs_update_rmn_config.go b/deployment/ccip/changeset/cs_update_rmn_config.go index 9d2023d60c1..f5349cd71f0 100644 --- a/deployment/ccip/changeset/cs_update_rmn_config.go +++ b/deployment/ccip/changeset/cs_update_rmn_config.go @@ -1,6 +1,7 @@ package changeset import ( + "bytes" "errors" "fmt" "math/big" @@ -236,6 +237,50 @@ func (c SetRMNHomeCandidateConfig) Validate(state CCIPOnChainState) error { return nil } +func isRMNStaticConfigEqual(a, b rmn_home.RMNHomeStaticConfig) bool { + if len(a.Nodes) != len(b.Nodes) { + return false + } + nodesByPeerID := make(map[p2pkey.PeerID]rmn_home.RMNHomeNode) + for i := range a.Nodes { + nodesByPeerID[a.Nodes[i].PeerId] = a.Nodes[i] + } + for i := range b.Nodes { + node, exists := nodesByPeerID[b.Nodes[i].PeerId] + if !exists { + return false + } + if !bytes.Equal(node.OffchainPublicKey[:], b.Nodes[i].OffchainPublicKey[:]) { + return false + } + } + + return bytes.Equal(a.OffchainConfig, b.OffchainConfig) +} + +func isRMNDynamicConfigEqual(a, b rmn_home.RMNHomeDynamicConfig) bool { + if len(a.SourceChains) != len(b.SourceChains) { + return false + } + sourceChainBySelector := make(map[uint64]rmn_home.RMNHomeSourceChain) + for i := range a.SourceChains { + sourceChainBySelector[a.SourceChains[i].ChainSelector] = a.SourceChains[i] + } + for i := range b.SourceChains { + sourceChain, exists := sourceChainBySelector[b.SourceChains[i].ChainSelector] + if !exists { + return false + } + if sourceChain.FObserve != b.SourceChains[i].FObserve { + return false + } + if sourceChain.ObserverNodesBitmap.Cmp(b.SourceChains[i].ObserverNodesBitmap) != 0 { + return false + } + } + return bytes.Equal(a.OffchainConfig, b.OffchainConfig) +} + type PromoteRMNHomeCandidateConfig struct { HomeChainSelector uint64 DigestToPromote [32]byte diff --git a/deployment/ccip/changeset/globals/config.go b/deployment/ccip/changeset/globals/config.go new file mode 100644 index 00000000000..27fd5d38b77 --- /dev/null +++ b/deployment/ccip/changeset/globals/config.go @@ -0,0 +1,37 @@ +package globals + +import ( + "time" +) + +type ConfigType string + +const ( + ConfigTypeActive ConfigType = "active" + ConfigTypeCandidate ConfigType = "candidate" + // ========= Changeset Defaults ========= + FirstBlockAge = 8 * time.Hour + RemoteGasPriceBatchWriteFrequency = 30 * time.Minute + TokenPriceBatchWriteFrequency = 30 * time.Minute + BatchGasLimit = 6_500_000 + RelativeBoostPerWaitHour = 0.5 + InflightCacheExpiry = 10 * time.Minute + RootSnoozeTime = 30 * time.Minute + BatchingStrategyID = 0 + DeltaProgress = 30 * time.Second + DeltaResend = 10 * time.Second + DeltaInitial = 20 * time.Second + DeltaRound = 2 * time.Second + DeltaGrace = 2 * time.Second + DeltaCertifiedCommitRequest = 10 * time.Second + DeltaStage = 10 * time.Second + Rmax = 3 + MaxDurationQuery = 500 * time.Millisecond + MaxDurationObservation = 5 * time.Second + MaxDurationShouldAcceptAttestedReport = 10 * time.Second + MaxDurationShouldTransmitAcceptedReport = 10 * time.Second + GasPriceDeviationPPB = 1000 + DAGasPriceDeviationPPB = 0 + OptimisticConfirmations = 1 + // ====================================== +) diff --git a/deployment/ccip/changeset/internal/deploy_home_chain.go b/deployment/ccip/changeset/internal/deploy_home_chain.go index 37e8707f2e8..6b2d5112826 100644 --- a/deployment/ccip/changeset/internal/deploy_home_chain.go +++ b/deployment/ccip/changeset/internal/deploy_home_chain.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/chainlink-ccip/pluginconfig" "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" types2 "github.com/smartcontractkit/chainlink/deployment/common/types" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" @@ -135,6 +136,7 @@ func BuildSetOCR3ConfigArgs( donID uint32, ccipHome *ccip_home.CCIPHome, destSelector uint64, + configType globals.ConfigType, ) ([]offramp.MultiOCR3BaseOCRConfigArgs, error) { var offrampOCR3Configs []offramp.MultiOCR3BaseOCRConfigArgs for _, pluginType := range []types.PluginType{types.PluginTypeCCIPCommit, types.PluginTypeCCIPExec} { @@ -148,24 +150,32 @@ func BuildSetOCR3ConfigArgs( fmt.Printf("pluginType: %s, destSelector: %d, donID: %d, activeConfig digest: %x, candidateConfig digest: %x\n", pluginType.String(), destSelector, donID, ocrConfig.ActiveConfig.ConfigDigest, ocrConfig.CandidateConfig.ConfigDigest) - // we expect only an active config and no candidate config. - if ocrConfig.ActiveConfig.ConfigDigest == [32]byte{} || ocrConfig.CandidateConfig.ConfigDigest != [32]byte{} { - return nil, fmt.Errorf("invalid OCR3 config state, expected active config and no candidate config, donID: %d, activeConfig: %v, candidateConfig: %v", - donID, hexutil.Encode(ocrConfig.ActiveConfig.ConfigDigest[:]), hexutil.Encode(ocrConfig.CandidateConfig.ConfigDigest[:])) + configForOCR3 := ocrConfig.ActiveConfig + // we expect only an active config + if configType == globals.ConfigTypeActive { + if ocrConfig.ActiveConfig.ConfigDigest == [32]byte{} { + return nil, fmt.Errorf("invalid OCR3 config state, expected active config, donID: %d, activeConfig: %v, candidateConfig: %v", + donID, hexutil.Encode(ocrConfig.ActiveConfig.ConfigDigest[:]), hexutil.Encode(ocrConfig.CandidateConfig.ConfigDigest[:])) + } + } else if configType == globals.ConfigTypeCandidate { + if ocrConfig.CandidateConfig.ConfigDigest == [32]byte{} { + return nil, fmt.Errorf("invalid OCR3 config state, expected candidate config, donID: %d, activeConfig: %v, candidateConfig: %v", + donID, hexutil.Encode(ocrConfig.ActiveConfig.ConfigDigest[:]), hexutil.Encode(ocrConfig.CandidateConfig.ConfigDigest[:])) + } + configForOCR3 = ocrConfig.CandidateConfig } - activeConfig := ocrConfig.ActiveConfig var signerAddresses []common.Address var transmitterAddresses []common.Address - for _, node := range activeConfig.Config.Nodes { + for _, node := range configForOCR3.Config.Nodes { signerAddresses = append(signerAddresses, common.BytesToAddress(node.SignerKey)) transmitterAddresses = append(transmitterAddresses, common.BytesToAddress(node.TransmitterKey)) } offrampOCR3Configs = append(offrampOCR3Configs, offramp.MultiOCR3BaseOCRConfigArgs{ - ConfigDigest: activeConfig.ConfigDigest, + ConfigDigest: configForOCR3.ConfigDigest, OcrPluginType: uint8(pluginType), - F: activeConfig.Config.FRoleDON, + F: configForOCR3.Config.FRoleDON, IsSignatureVerificationEnabled: pluginType == types.PluginTypeCCIPCommit, Signers: signerAddresses, Transmitters: transmitterAddresses, diff --git a/deployment/ccip/changeset/testhelpers/test_environment.go b/deployment/ccip/changeset/testhelpers/test_environment.go index b8c243c860a..c8af52c9340 100644 --- a/deployment/ccip/changeset/testhelpers/test_environment.go +++ b/deployment/ccip/changeset/testhelpers/test_environment.go @@ -21,6 +21,8 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" + + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/globals" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/types" "github.com/smartcontractkit/chainlink/deployment" @@ -458,7 +460,7 @@ func NewEnvironmentWithJobsAndContracts(t *testing.T, tEnv TestEnvironment) Depl }) require.NoError(t, err) tEnv.UpdateDeployedEnvironment(e) - e = AddCCIPContractsToEnvironment(t, e.Env.AllChainSelectors(), tEnv, true, true, false) + e = AddCCIPContractsToEnvironment(t, e.Env.AllChainSelectors(), tEnv, false) // now we update RMNProxy to point to RMNRemote e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, nil, []commonchangeset.ChangesetApplication{ { @@ -472,7 +474,7 @@ func NewEnvironmentWithJobsAndContracts(t *testing.T, tEnv TestEnvironment) Depl return e } -func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEnvironment, deployJobs, deployHomeChain, mcmsEnabled bool) DeployedEnv { +func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEnvironment, mcmsEnabled bool) DeployedEnv { tc := tEnv.TestConfigs() e := tEnv.DeployedEnvironment() envNodes, err := deployment.NodeInfo(e.Env.NodeIDs, e.Env.Offchain) @@ -481,8 +483,15 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn // Need to deploy prerequisites first so that we can form the USDC config // no proposals to be made, timelock can be passed as nil here var apps []commonchangeset.ChangesetApplication - if deployHomeChain { - apps = append(apps, commonchangeset.ChangesetApplication{ + allContractParams := make(map[uint64]changeset.ChainContractParams) + for _, chain := range allChains { + allContractParams[chain] = changeset.ChainContractParams{ + FeeQuoterParams: changeset.DefaultFeeQuoterParams(), + OffRampParams: changeset.DefaultOffRampParams(), + } + } + apps = append(apps, []commonchangeset.ChangesetApplication{ + { Changeset: commonchangeset.WrapChangeSet(changeset.DeployHomeChainChangeset), Config: changeset.DeployHomeChainConfig{ HomeChainSel: e.HomeChainSel, @@ -493,22 +502,15 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn TestNodeOperator: envNodes.NonBootstraps().PeerIDs(), }, }, - }) - } - allContractParams := make(map[uint64]changeset.ChainContractParams) - for _, chain := range allChains { - allContractParams[chain] = changeset.ChainContractParams{ - FeeQuoterParams: changeset.DefaultFeeQuoterParams(), - OffRampParams: changeset.DefaultOffRampParams(), - } - } - apps = append(apps, commonchangeset.ChangesetApplication{ - Changeset: commonchangeset.WrapChangeSet(changeset.DeployChainContractsChangeset), - Config: changeset.DeployChainContractsConfig{ - HomeChainSelector: e.HomeChainSel, - ContractParamsPerChain: allContractParams, }, - }) + { + Changeset: commonchangeset.WrapChangeSet(changeset.DeployChainContractsChangeset), + Config: changeset.DeployChainContractsConfig{ + HomeChainSelector: e.HomeChainSel, + ContractParamsPerChain: allContractParams, + }, + }, + }...) e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, nil, apps) require.NoError(t, err) @@ -573,9 +575,9 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn Readers: nodeInfo.NonBootstraps().PeerIDs(), FChain: uint8(len(nodeInfo.NonBootstraps().PeerIDs()) / 3), EncodableChainConfig: chainconfig.ChainConfig{ - GasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(changeset.GasPriceDeviationPPB)}, - DAGasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(changeset.DAGasPriceDeviationPPB)}, - OptimisticConfirmations: changeset.OptimisticConfirmations, + GasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(globals.GasPriceDeviationPPB)}, + DAGasPriceDeviationPPB: cciptypes.BigInt{Int: big.NewInt(globals.DAGasPriceDeviationPPB)}, + OptimisticConfirmations: globals.OptimisticConfirmations, }, } } @@ -654,15 +656,14 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn // Enable the OCR config on the remote chains. Changeset: commonchangeset.WrapChangeSet(changeset.SetOCR3OffRampChangeset), Config: changeset.SetOCR3OffRampConfig{ - HomeChainSel: e.HomeChainSel, - RemoteChainSels: allChains, + HomeChainSel: e.HomeChainSel, + RemoteChainSels: allChains, + CCIPHomeConfigType: globals.ConfigTypeActive, }, }, - } - if deployJobs { - apps = append(apps, commonchangeset.ChangesetApplication{ + { Changeset: commonchangeset.WrapChangeSet(changeset.CCIPCapabilityJobspecChangeset), - }) + }, } e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, timelockContractsPerChain, apps) require.NoError(t, err) diff --git a/deployment/environment.go b/deployment/environment.go index a37622dc3ac..3d9c6c5420a 100644 --- a/deployment/environment.go +++ b/deployment/environment.go @@ -203,6 +203,19 @@ func MaybeDataErr(err error) error { return err } +func DecodedErrFromABIIfDataErr(err error, abi string) error { + var d rpc.DataError + ok := errors.As(err, &d) + if ok { + errReason, err := parseErrorFromABI(fmt.Sprintf("%s", d.ErrorData()), abi) + if err != nil { + return fmt.Errorf("%s: %v", d.Error(), d.ErrorData()) + } + return fmt.Errorf("%s due to %s: %v", d.Error(), errReason, d.ErrorData()) + } + return err +} + func UBigInt(i uint64) *big.Int { return new(big.Int).SetUint64(i) } diff --git a/deployment/environment/memory/job_client.go b/deployment/environment/memory/job_client.go index e025ea18fda..a4ccbea6891 100644 --- a/deployment/environment/memory/job_client.go +++ b/deployment/environment/memory/job_client.go @@ -8,7 +8,9 @@ import ( "strings" "github.com/ethereum/go-ethereum/common" + "github.com/pelletier/go-toml/v2" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/timestamppb" chainsel "github.com/smartcontractkit/chain-selectors" @@ -83,7 +85,6 @@ func (j JobClient) GetNode(ctx context.Context, in *nodev1.GetNodeRequest, opts } func (j JobClient) ListNodes(ctx context.Context, in *nodev1.ListNodesRequest, opts ...grpc.CallOption) (*nodev1.ListNodesResponse, error) { - // TODO CCIP-3108 include := func(node *nodev1.Node) bool { if in.Filter == nil { return true @@ -240,13 +241,60 @@ func (j JobClient) GetJob(ctx context.Context, in *jobv1.GetJobRequest, opts ... } func (j JobClient) GetProposal(ctx context.Context, in *jobv1.GetProposalRequest, opts ...grpc.CallOption) (*jobv1.GetProposalResponse, error) { - // TODO CCIP-3108 implement me - panic("implement me") + // we are using proposal id as job id + // refer to ListJobs and ProposeJobs for the assignment of proposal id + for _, node := range j.Nodes { + jobs, _, err := node.App.JobORM().FindJobs(ctx, 0, 1000) + if err != nil { + return nil, err + } + for _, job := range jobs { + if job.ExternalJobID.String() == in.Id { + specBytes, err := toml.Marshal(job.CCIPSpec) + if err != nil { + return nil, fmt.Errorf("failed to marshal job spec: %w", err) + } + return &jobv1.GetProposalResponse{ + Proposal: &jobv1.Proposal{ + Id: job.ExternalJobID.String(), + Status: jobv1.ProposalStatus_PROPOSAL_STATUS_APPROVED, + Spec: string(specBytes), + JobId: job.ExternalJobID.String(), + }, + }, nil + } + } + } + return nil, fmt.Errorf("job not found: %s", in.Id) } func (j JobClient) ListJobs(ctx context.Context, in *jobv1.ListJobsRequest, opts ...grpc.CallOption) (*jobv1.ListJobsResponse, error) { - // TODO CCIP-3108 implement me - panic("implement me") + jobResponse := make([]*jobv1.Job, 0) + for _, req := range in.Filter.NodeIds { + if _, ok := j.Nodes[req]; !ok { + return nil, fmt.Errorf("node not found: %s", req) + } + n := j.Nodes[req] + jobs, _, err := n.App.JobORM().FindJobs(ctx, 0, 1000) + if err != nil { + return nil, err + } + for _, job := range jobs { + jobResponse = append(jobResponse, &jobv1.Job{ + Id: string(job.ID), + Uuid: job.ExternalJobID.String(), + NodeId: req, + // based on the current implementation, there is only one proposal per job + // see ProposeJobs for ProposalId assignment + ProposalIds: []string{job.ExternalJobID.String()}, + CreatedAt: timestamppb.New(job.CreatedAt), + UpdatedAt: timestamppb.New(job.CreatedAt), + }) + } + } + return &jobv1.ListJobsResponse{ + Jobs: jobResponse, + }, nil } func (j JobClient) ListProposals(ctx context.Context, in *jobv1.ListProposalsRequest, opts ...grpc.CallOption) (*jobv1.ListProposalsResponse, error) { @@ -286,7 +334,9 @@ func (j JobClient) ProposeJob(ctx context.Context, in *jobv1.ProposeJobRequest, return nil, err } return &jobv1.ProposeJobResponse{Proposal: &jobv1.Proposal{ - Id: "", + // make the proposal id the same as the job id for further reference + // if you are changing this make sure to change the GetProposal and ListJobs method implementation + Id: jb.ExternalJobID.String(), // Auto approve for now Status: jobv1.ProposalStatus_PROPOSAL_STATUS_APPROVED, DeliveryStatus: jobv1.ProposalDeliveryStatus_PROPOSAL_DELIVERY_STATUS_DELIVERED, diff --git a/deployment/helpers.go b/deployment/helpers.go index d8e15d0200d..166dfa4ea6d 100644 --- a/deployment/helpers.go +++ b/deployment/helpers.go @@ -107,7 +107,7 @@ func parseErrorFromABI(errorString string, contractABI string) (string, error) { if err3 != nil { return "", errors.Wrap(err3, "error unpacking data") } - return fmt.Sprintf("error is \"%v\" args %v\n", errorName, v), nil + return fmt.Sprintf("error -`%v` args %v", errorName, v), nil } } return "", errors.New("error not found in ABI") diff --git a/integration-tests/smoke/ccip/ccip_add_chain_test.go b/integration-tests/smoke/ccip/ccip_add_chain_test.go index aced6e8bef5..0d729b34371 100644 --- a/integration-tests/smoke/ccip/ccip_add_chain_test.go +++ b/integration-tests/smoke/ccip/ccip_add_chain_test.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" + ccipcs "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" @@ -51,19 +52,11 @@ func Test_AddChain(t *testing.T) { ///////////////////////////////////// // START Setup initial chains ///////////////////////////////////// - e = setupChain( - t, - e, - tEnv, - toDeploy, - true, // deployJobs - true, // deployHomeChain - false, // mcmsEnabled - ) + e = setupChain(t, e, tEnv, toDeploy, false) state, err := ccipcs.LoadOnchainState(e.Env) require.NoError(t, err) - + tEnv.UpdateDeployedEnvironment(e) // check RMNRemote is up and RMNProxy is correctly wired. assertRMNRemoteAndProxyState(t, toDeploy, state) @@ -179,18 +172,11 @@ func Test_AddChain(t *testing.T) { // MCMS needs to be enabled because the home chain contracts have been // transferred to MCMS. - e = setupChain( - t, - e, - tEnv, - []uint64{remainingChain}, - false, // deployJobs - false, // deployHomeChain - true, // mcmsEnabled - ) + e = setupChain(t, e, tEnv, []uint64{remainingChain}, true) state, err = ccipcs.LoadOnchainState(e.Env) require.NoError(t, err) + tEnv.UpdateDeployedEnvironment(e) assertRMNRemoteAndProxyState(t, []uint64{remainingChain}, state) @@ -526,15 +512,8 @@ func setupOutboundWiring( // setupChain will deploy the ccip chain contracts to the provided chains. // Based on the flags provided, it will also deploy the jobs and home chain contracts. // mcmsEnabled should be set to true if the home chain contracts have been transferred to MCMS. -func setupChain(t *testing.T, e testhelpers.DeployedEnv, tEnv testhelpers.TestEnvironment, chains []uint64, deployJobs, deployHomeChain, mcmsEnabled bool) testhelpers.DeployedEnv { - e = testhelpers.AddCCIPContractsToEnvironment( - t, - chains, - tEnv, - deployJobs, - deployHomeChain, - mcmsEnabled, - ) +func setupChain(t *testing.T, e testhelpers.DeployedEnv, tEnv testhelpers.TestEnvironment, chains []uint64, mcmsEnabled bool) testhelpers.DeployedEnv { + e = testhelpers.AddCCIPContractsToEnvironment(t, chains, tEnv, mcmsEnabled) // Need to update what the RMNProxy is pointing to, otherwise plugin will not work. var err error diff --git a/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go b/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go index d70a6e8a3fa..96af87d248a 100644 --- a/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go +++ b/integration-tests/smoke/ccip/ccip_migration_to_v_1_6_test.go @@ -151,7 +151,7 @@ func TestMigrateFromV1_5ToV1_6(t *testing.T) { // add 1.6 contracts to the environment and send 1.6 jobs // First we need to deploy Homechain contracts and restart the nodes with updated cap registry // in this test we have already deployed home chain contracts and the nodes are already running with the deployed cap registry. - e = testhelpers.AddCCIPContractsToEnvironment(t, e.Env.AllChainSelectors(), tEnv, true, true, false) + e = testhelpers.AddCCIPContractsToEnvironment(t, e.Env.AllChainSelectors(), tEnv, false) // Set RMNProxy to point to RMNRemote. // nonce manager should point to 1.5 ramps e.Env, err = commonchangeset.ApplyChangesets(t, e.Env, e.TimelockContracts(t), []commonchangeset.ChangesetApplication{