From 794a634fa8de072b80eea9a4ca63c1dda7e07186 Mon Sep 17 00:00:00 2001 From: Agustina Aldasoro Date: Fri, 17 Jul 2026 15:23:48 -0300 Subject: [PATCH 1/2] Avoid init --- .../solana_v0_1_1/cs_deploy_chain.go | 310 +++++++----------- .../changeset/solana_v0_1_1/cs_set_ocr3.go | 5 +- .../ccip/changeset/solana_v0_1_1/utils.go | 12 + 3 files changed, 136 insertions(+), 191 deletions(-) diff --git a/deployment/ccip/changeset/solana_v0_1_1/cs_deploy_chain.go b/deployment/ccip/changeset/solana_v0_1_1/cs_deploy_chain.go index f172c795d21..9167dac87aa 100644 --- a/deployment/ccip/changeset/solana_v0_1_1/cs_deploy_chain.go +++ b/deployment/ccip/changeset/solana_v0_1_1/cs_deploy_chain.go @@ -269,6 +269,63 @@ func DeployAndMaybeSaveToAddressBook( return address, nil } +// upgradeProgramIfConfigured generates the upgrade transactions for a program whose new version is +// set in the upgrade config and appends them as a batch. It is a no-op when newVersion is nil. +func upgradeProgramIfConfigured( + e cldf.Environment, + chain cldf_solana.Chain, + ab cldf.AddressBook, + config DeployChainContractsConfig, + batches []mcmsTypes.BatchOperation, + newVersion *semver.Version, + programID solana.PublicKey, + contractType cldf.ContractType, +) ([]mcmsTypes.BatchOperation, error) { + if newVersion == nil { + return batches, nil + } + e.Logger.Infow("Generating instruction for upgrading contract", "chain", chain.String(), "contractType", contractType) + newTxns, err := generateUpgradeTxns(e, chain, ab, config, newVersion, programID, contractType) + if err != nil { + return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) + } + return appendBatchOperation(batches, chain.Selector, newTxns), nil +} + +// resolveProgram resolves a core program's address for this run and reports whether it was freshly +// deployed. It deploys the program when it does not exist yet (justDeployed=true), upgrades it in +// place when a new version is configured, or reuses the existing address otherwise. Only a freshly +// deployed program needs initializing, so callers gate initialization on justDeployed rather than +// re-reading the program's on-chain config account. +func resolveProgram( + e cldf.Environment, + chain cldf_solana.Chain, + ab cldf.AddressBook, + config DeployChainContractsConfig, + batches []mcmsTypes.BatchOperation, + contractType cldf.ContractType, + existingAddress solana.PublicKey, + newVersion *semver.Version, +) (address solana.PublicKey, justDeployed bool, outBatches []mcmsTypes.BatchOperation, err error) { + switch { + case existingAddress.IsZero(): + address, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, contractType, deployment.Version1_0_0, false, "") + if err != nil { + return solana.PublicKey{}, false, batches, fmt.Errorf("failed to deploy %s: %w", contractType, err) + } + return address, true, batches, nil + case newVersion != nil: + batches, err = upgradeProgramIfConfigured(e, chain, ab, config, batches, newVersion, existingAddress, contractType) + if err != nil { + return existingAddress, false, batches, err + } + return existingAddress, false, batches, nil + default: + e.Logger.Infow("Using existing program", "contractType", contractType, "addr", existingAddress.String()) + return existingAddress, false, batches, nil + } +} + func deployChainContractsSolana( e cldf.Environment, chain cldf_solana.Chain, @@ -293,68 +350,31 @@ func deployChainContractsSolana( params := config.ContractParamsPerChain // FEE QUOTER DEPLOY - var feeQuoterAddress solana.PublicKey - //nolint:gocritic // this is a false positive, we need to check if the address is zero - if chainState.FeeQuoter.IsZero() { - feeQuoterAddress, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, shared.FeeQuoter, deployment.Version1_0_0, false, "") - if err != nil { - return batches, fmt.Errorf("failed to deploy program: %w", err) - } - } else if config.UpgradeConfig.NewFeeQuoterVersion != nil { - // fee quoter updated in place - feeQuoterAddress = chainState.FeeQuoter - newTxns, err := generateUpgradeTxns(e, chain, ab, config, config.UpgradeConfig.NewFeeQuoterVersion, chainState.FeeQuoter, shared.FeeQuoter) - if err != nil { - return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) - } - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } - } else { - e.Logger.Infow("Using existing fee quoter", "addr", chainState.FeeQuoter.String()) - feeQuoterAddress = chainState.FeeQuoter + feeQuoterAddress, fqJustDeployed, batches, err := resolveProgram( + e, chain, ab, config, batches, shared.FeeQuoter, chainState.FeeQuoter, config.UpgradeConfig.NewFeeQuoterVersion) + if err != nil { + return batches, err } runSafely(func() { solFeeQuoter.SetProgramID(feeQuoterAddress) }) // ROUTER DEPLOY - var ccipRouterProgram solana.PublicKey - //nolint:gocritic // this is a false positive, we need to check if the address is zero - if chainState.Router.IsZero() { - // deploy router - ccipRouterProgram, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, shared.Router, deployment.Version1_0_0, false, "") - if err != nil { - return batches, fmt.Errorf("failed to deploy program: %w", err) - } - } else if config.UpgradeConfig.NewRouterVersion != nil { - // router updated in place - ccipRouterProgram = chainState.Router - newTxns, err := generateUpgradeTxns(e, chain, ab, config, config.UpgradeConfig.NewRouterVersion, chainState.Router, shared.Router) - if err != nil { - return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) - } - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } - } else { - e.Logger.Infow("Using existing router", "addr", chainState.Router.String()) - ccipRouterProgram = chainState.Router + ccipRouterProgram, routerJustDeployed, batches, err := resolveProgram( + e, chain, ab, config, batches, shared.Router, chainState.Router, config.UpgradeConfig.NewRouterVersion) + if err != nil { + return batches, err } runSafely(func() { solRouter.SetProgramID(ccipRouterProgram) }) // OFFRAMP DEPLOY + // Offramp is the one core program that may be redeployed (rather than upgraded in place) during + // an upgrade run, so it can be freshly deployed even when chainState.OffRamp is non-zero. We + // track that in offRampJustDeployed so the initialize pass below knows whether it needs setup. var offRampAddress solana.PublicKey + offRampJustDeployed := false //nolint:gocritic // this is a false positive, we need to check if the address is zero if chainState.OffRamp.IsZero() { // deploy offramp @@ -362,6 +382,7 @@ func deployChainContractsSolana( if err != nil { return batches, fmt.Errorf("failed to deploy program: %w", err) } + offRampJustDeployed = true } else if config.UpgradeConfig.NewOffRampVersion != nil { tv := cldf.NewTypeAndVersion(shared.OffRamp, *config.UpgradeConfig.NewOffRampVersion) existingAddresses, err := e.ExistingAddresses.AddressesForChain(chain.Selector) @@ -375,6 +396,7 @@ func deployChainContractsSolana( if err != nil { return batches, fmt.Errorf("failed to deploy program: %w", err) } + offRampJustDeployed = true offRampBillingSignerPDA, _, _ := solState.FindOfframpBillingSignerPDA(offRampAddress) fqAllowedPriceUpdaterOfframpPDA, _, _ := solState.FindFqAllowedPriceUpdaterPDA(offRampBillingSignerPDA, feeQuoterAddress) feeQuoterConfigPDA, _, _ := solState.FindFqConfigPDA(feeQuoterAddress) @@ -394,10 +416,7 @@ func deployChainContractsSolana( if err != nil { return batches, fmt.Errorf("failed to create price updater transaction: %w", err) } - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: []mcmsTypes.Transaction{*priceUpdaterTx}, - }) + batches = appendBatchOperation(batches, chain.Selector, []mcmsTypes.Transaction{*priceUpdaterTx}) } else { if err := chain.Confirm([]solana.Instruction{priceUpdaterix}); err != nil { return batches, fmt.Errorf("failed to confirm initializeFeeQuoter: %w", err) @@ -408,13 +427,7 @@ func deployChainContractsSolana( if err != nil { return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) } - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } + batches = appendBatchOperation(batches, chain.Selector, newTxns) } } else { e.Logger.Infow("Using existing offramp", "addr", chainState.OffRamp.String()) @@ -425,64 +438,35 @@ func deployChainContractsSolana( }) // RMN REMOTE DEPLOY - var rmnRemoteAddress solana.PublicKey - switch { - case chainState.RMNRemote.IsZero(): - rmnRemoteAddress, err = DeployAndMaybeSaveToAddressBook(e, chain, ab, shared.RMNRemote, deployment.Version1_0_0, false, "") - if err != nil { - return batches, fmt.Errorf("failed to deploy program: %w", err) - } - case config.UpgradeConfig.NewRMNRemoteVersion != nil: - rmnRemoteAddress = chainState.RMNRemote - newTxns, err := generateUpgradeTxns(e, chain, ab, config, config.UpgradeConfig.NewRMNRemoteVersion, chainState.RMNRemote, shared.RMNRemote) - if err != nil { - return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) - } - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } - default: - e.Logger.Infow("Using existing rmn remote", "addr", chainState.RMNRemote.String()) - rmnRemoteAddress = chainState.RMNRemote + rmnRemoteAddress, rmnJustDeployed, batches, err := resolveProgram( + e, chain, ab, config, batches, shared.RMNRemote, chainState.RMNRemote, config.UpgradeConfig.NewRMNRemoteVersion) + if err != nil { + return batches, err } runSafely(func() { solRmnRemote.SetProgramID(rmnRemoteAddress) }) - // FEE QUOTER INITIALIZE - var fqConfig solFeeQuoter.Config - feeQuoterConfigPDA, _, _ := solState.FindFqConfigPDA(feeQuoterAddress) - err = chain.GetAccountDataBorshInto(e.GetContext(), feeQuoterConfigPDA, &fqConfig) - if err != nil { - if err2 := initializeFeeQuoter(e, chain, ccipRouterProgram, chainState.LinkToken, feeQuoterAddress, offRampAddress, params.FeeQuoterParams); err2 != nil { - return batches, err2 + // INITIALIZE + // Only programs that were freshly deployed in this run need initializing. We track that in + // memory (the *JustDeployed flags from the deploy pass) instead of reading each config account, + // so a program left untouched by this run — e.g. RMN remote during a fee-quoter-only upgrade — + // is never re-initialized, even when its on-chain config was written by a different program + // version whose layout the pinned gobindings can't decode. + if fqJustDeployed { + if err := initializeFeeQuoter(e, chain, ccipRouterProgram, chainState.LinkToken, feeQuoterAddress, offRampAddress, params.FeeQuoterParams); err != nil { + return batches, err } - } else { - e.Logger.Infow("Fee quoter already initialized, skipping initialization", "chain", chain.String()) } - // ROUTER INITIALIZE - var routerConfigAccount solRouter.Config - // addressing errcheck in the next PR - routerConfigPDA, _, _ := solState.FindConfigPDA(ccipRouterProgram) - err = chain.GetAccountDataBorshInto(e.GetContext(), routerConfigPDA, &routerConfigAccount) - if err != nil { - if err2 := initializeRouter(e, chain, ccipRouterProgram, chainState.LinkToken, feeQuoterAddress, rmnRemoteAddress); err2 != nil { - return batches, err2 + if routerJustDeployed { + if err := initializeRouter(e, chain, ccipRouterProgram, chainState.LinkToken, feeQuoterAddress, rmnRemoteAddress); err != nil { + return batches, err } - } else { - e.Logger.Infow("Router already initialized, skipping initialization", "chain", chain.String()) } - // OFFRAMP INITIALIZE - var offRampConfigAccount solOffRamp.Config - offRampConfigPDA, _, _ := solState.FindOfframpConfigPDA(offRampAddress) - err = chain.GetAccountDataBorshInto(e.GetContext(), offRampConfigPDA, &offRampConfigAccount) - if err != nil { - table, err2 := solCommonUtil.SetupLookupTable( + if offRampJustDeployed { + table, err := solCommonUtil.SetupLookupTable( e.GetContext(), chain.Client, *chain.DeployerKey, @@ -496,26 +480,18 @@ func deployChainContractsSolana( solana.TokenProgramID, solana.SPLAssociatedTokenAccountProgramID, }) - if err2 != nil { + if err != nil { return batches, fmt.Errorf("failed to create address lookup table: %w", err) } - if err2 := initializeOffRamp(e, chain, ccipRouterProgram, feeQuoterAddress, rmnRemoteAddress, offRampAddress, table, params.OffRampParams); err2 != nil { - return batches, err2 + if err := initializeOffRamp(e, chain, ccipRouterProgram, feeQuoterAddress, rmnRemoteAddress, offRampAddress, table, params.OffRampParams); err != nil { + return batches, err } - } else { - e.Logger.Infow("Offramp already initialized, skipping initialization", "chain", chain.String()) } - // RMN REMOTE INITIALIZE - var rmnRemoteConfigAccount solRmnRemote.Config - rmnRemoteConfigPDA, _, _ := solState.FindRMNRemoteConfigPDA(rmnRemoteAddress) - err = chain.GetAccountDataBorshInto(e.GetContext(), rmnRemoteConfigPDA, &rmnRemoteConfigAccount) - if err != nil { - if err2 := initializeRMNRemote(e, chain, rmnRemoteAddress, ccipRouterProgram); err2 != nil { - return batches, err2 + if rmnJustDeployed { + if err := initializeRMNRemote(e, chain, rmnRemoteAddress, ccipRouterProgram); err != nil { + return batches, err } - } else { - e.Logger.Infow("RMN remote already initialized, skipping initialization", "chain", chain.String()) } // TOKEN POOLS DEPLOY @@ -556,13 +532,7 @@ func deployChainContractsSolana( if err != nil { return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) } - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } + batches = appendBatchOperation(batches, chain.Selector, newTxns) burnMintTokenPools = append(burnMintTokenPools, burnMintTokenPool) } else { e.Logger.Infow("Using existing burn mint token pool", "addr", chainState.BurnMintTokenPools[metadata].String()) @@ -608,13 +578,7 @@ func deployChainContractsSolana( return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) } lockReleaseTokenPools = append(lockReleaseTokenPools, lockReleaseTokenPool) - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } + batches = appendBatchOperation(batches, chain.Selector, newTxns) } else { e.Logger.Infow("Using existing lock release token pool", "addr", chainState.LockReleaseTokenPools[metadata].String()) lockReleaseTokenPools = append(lockReleaseTokenPools, chainState.LockReleaseTokenPools[metadata]) @@ -635,28 +599,26 @@ func deployChainContractsSolana( if err != nil { return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) } - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } + batches = appendBatchOperation(batches, chain.Selector, newTxns) default: e.Logger.Infow("Using existing CCTP token pool", "addr", chainState.CCTPTokenPool.String()) cctpTokenPool = chainState.CCTPTokenPool } // CCTP token pool initialization - var cctpTokenPoolConfig cctp_token_pool.PoolConfig - configPDA, _ := solTokens.TokenPoolGlobalConfigPDA(cctpTokenPool) - err = chain.GetAccountDataBorshInto(e.GetContext(), configPDA, &cctpTokenPoolConfig) - if err != nil { + // Only initialize the global config when it does not yet exist. Checking account existence + // (not borsh-decode) skips an already-initialized global config instead of re-initializing it. + cctpConfigPDA, _ := solTokens.TokenPoolGlobalConfigPDA(cctpTokenPool) + _, cctpCfgErr := chain.Client.GetAccountInfo(e.GetContext(), cctpConfigPDA) + switch { + case cctpCfgErr == nil: + e.Logger.Infow("CCTP token pool global config already initialized, skipping initialization", "chain", chain.String()) + case errors.Is(cctpCfgErr, solRpc.ErrNotFound): if err2 := initializeCCTPTokenPoolGlobalConfig(e, chain, cctpTokenPool); err2 != nil { return batches, err2 } - } else { - e.Logger.Infow("CCTP token pool global config already initialized, skipping initialization", "chain", chain.String()) + default: + return batches, fmt.Errorf("failed to check CCTP token pool global config account %s: %w", cctpConfigPDA, cctpCfgErr) } // MCMS @@ -676,47 +638,17 @@ func deployChainContractsSolana( if err != nil { return batches, fmt.Errorf("failed to load MCMS with timelock chain state: %w", err) } - if config.UpgradeConfig.NewMCMVersion != nil { - e.Logger.Infow("Generate instruction for upgrading mcms", "chain", chain.String()) - newTxns, err := generateUpgradeTxns(e, chain, ab, config, config.UpgradeConfig.NewMCMVersion, mcmState.McmProgram, types.ManyChainMultisigProgram) - if err != nil { - return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) - } - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } + batches, err = upgradeProgramIfConfigured(e, chain, ab, config, batches, config.UpgradeConfig.NewMCMVersion, mcmState.McmProgram, types.ManyChainMultisigProgram) + if err != nil { + return batches, err } - if config.UpgradeConfig.NewAccessControllerVersion != nil { - e.Logger.Infow("Generating instruction for upgrading access controller", "chain", chain.String()) - newTxns, err := generateUpgradeTxns(e, chain, ab, config, config.UpgradeConfig.NewAccessControllerVersion, mcmState.AccessControllerProgram, types.AccessControllerProgram) - if err != nil { - return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) - } - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } + batches, err = upgradeProgramIfConfigured(e, chain, ab, config, batches, config.UpgradeConfig.NewAccessControllerVersion, mcmState.AccessControllerProgram, types.AccessControllerProgram) + if err != nil { + return batches, err } - if config.UpgradeConfig.NewTimelockVersion != nil { - e.Logger.Infow("Generate instruction for upgrading timelock", "chain", chain.String()) - newTxns, err := generateUpgradeTxns(e, chain, ab, config, config.UpgradeConfig.NewTimelockVersion, mcmState.TimelockProgram, types.RBACTimelockProgram) - if err != nil { - return batches, fmt.Errorf("failed to generate upgrade txns: %w", err) - } - // create proposals for txns - if len(newTxns) > 0 { - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(chain.Selector), - Transactions: newTxns, - }) - } + batches, err = upgradeProgramIfConfigured(e, chain, ab, config, batches, config.UpgradeConfig.NewTimelockVersion, mcmState.TimelockProgram, types.RBACTimelockProgram) + if err != nil { + return batches, err } // BILLING @@ -730,14 +662,18 @@ func deployChainContractsSolana( // LOOKUP TABLE // off ramp + offRampConfigPDA, _, _ := solState.FindOfframpConfigPDA(offRampAddress) offRampReferenceAddressesPDA, _, _ := solState.FindOfframpReferenceAddressesPDA(offRampAddress) offRampBillingSignerPDA, _, _ := solState.FindOfframpBillingSignerPDA(offRampAddress) // fee quoter + feeQuoterConfigPDA, _, _ := solState.FindFqConfigPDA(feeQuoterAddress) linkFqBillingConfigPDA, _, _ := solState.FindFqBillingTokenConfigPDA(chainState.LinkToken, feeQuoterAddress) wsolFqBillingConfigPDA, _, _ := solState.FindFqBillingTokenConfigPDA(chainState.WSOL, feeQuoterAddress) // router + routerConfigPDA, _, _ := solState.FindConfigPDA(ccipRouterProgram) feeBillingSignerPDA, _, _ := solState.FindFeeBillingSignerPDA(ccipRouterProgram) // rmn remote + rmnRemoteConfigPDA, _, _ := solState.FindRMNRemoteConfigPDA(rmnRemoteAddress) rmnRemoteCursePDA, _, _ := solState.FindRMNRemoteCursesPDA(rmnRemoteAddress) lookupTableKeys := []solana.PublicKey{ // offramp diff --git a/deployment/ccip/changeset/solana_v0_1_1/cs_set_ocr3.go b/deployment/ccip/changeset/solana_v0_1_1/cs_set_ocr3.go index 8307963b27f..eba4d80fef9 100644 --- a/deployment/ccip/changeset/solana_v0_1_1/cs_set_ocr3.go +++ b/deployment/ccip/changeset/solana_v0_1_1/cs_set_ocr3.go @@ -166,10 +166,7 @@ func SetOCR3ConfigSolana(e cldf.Environment, cfg v1_6.SetOCR3OffRampConfig) (cld if err != nil { return cldf.ChangesetOutput{}, fmt.Errorf("failed to create transaction: %w", err) } - batches = append(batches, mcmsTypes.BatchOperation{ - ChainSelector: mcmsTypes.ChainSelector(remote), - Transactions: []mcmsTypes.Transaction{*tx}, - }) + batches = appendBatchOperation(batches, remote, []mcmsTypes.Transaction{*tx}) } } } diff --git a/deployment/ccip/changeset/solana_v0_1_1/utils.go b/deployment/ccip/changeset/solana_v0_1_1/utils.go index 1886d2eb149..7434729d271 100644 --- a/deployment/ccip/changeset/solana_v0_1_1/utils.go +++ b/deployment/ccip/changeset/solana_v0_1_1/utils.go @@ -161,6 +161,18 @@ func mcmSeedForAction(mcmState *solstate.MCMSWithTimelockState, action mcmsTypes // BuildProposalsForTxnsWithConfig wraps the given Solana transactions in a single batch and // builds an MCMS timelock proposal, honoring the MCMS action (schedule/bypass/cancel) in // mcmsCfg and selecting the matching signer group. +// appendBatchOperation appends txns as a single MCMS batch operation for the given chain selector. +// It is a no-op when txns is empty, mirroring the len>0 guard that callers used to inline. +func appendBatchOperation(batches []mcmsTypes.BatchOperation, chainSelector uint64, txns []mcmsTypes.Transaction) []mcmsTypes.BatchOperation { + if len(txns) == 0 { + return batches + } + return append(batches, mcmsTypes.BatchOperation{ + ChainSelector: mcmsTypes.ChainSelector(chainSelector), + Transactions: txns, + }) +} + func BuildProposalsForTxnsWithConfig( e cldf.Environment, chainSelector uint64, From 36bf097340985af9678ca2d5fa854db30c39f2c6 Mon Sep 17 00:00:00 2001 From: Agustina Aldasoro Date: Fri, 17 Jul 2026 15:24:03 -0300 Subject: [PATCH 2/2] check when init --- .../solana_v0_1_1/cs_deploy_chain.go | 75 +++++++++++++++---- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/deployment/ccip/changeset/solana_v0_1_1/cs_deploy_chain.go b/deployment/ccip/changeset/solana_v0_1_1/cs_deploy_chain.go index 9167dac87aa..225b9c1c083 100644 --- a/deployment/ccip/changeset/solana_v0_1_1/cs_deploy_chain.go +++ b/deployment/ccip/changeset/solana_v0_1_1/cs_deploy_chain.go @@ -326,6 +326,26 @@ func resolveProgram( } } +// shouldInitialize reports whether a program needs initializing: either it was freshly deployed in +// this run, or its config account does not yet exist (recovering from a prior run that deployed the +// program but failed before initializing it). Existence is checked with GetAccountInfo rather than +// borsh-decoding, so a config account written by a different program version is recognised as +// "already initialized" instead of triggering a re-init that fails with "account already in use". +func shouldInitialize(e cldf.Environment, chain cldf_solana.Chain, justDeployed bool, configPDA solana.PublicKey) (bool, error) { + if justDeployed { + return true, nil + } + _, err := chain.Client.GetAccountInfo(e.GetContext(), configPDA) + switch { + case err == nil: + return false, nil + case errors.Is(err, solRpc.ErrNotFound): + return true, nil + default: + return false, fmt.Errorf("failed to check config account %s: %w", configPDA, err) + } +} + func deployChainContractsSolana( e cldf.Environment, chain cldf_solana.Chain, @@ -448,24 +468,45 @@ func deployChainContractsSolana( }) // INITIALIZE - // Only programs that were freshly deployed in this run need initializing. We track that in - // memory (the *JustDeployed flags from the deploy pass) instead of reading each config account, - // so a program left untouched by this run — e.g. RMN remote during a fee-quoter-only upgrade — - // is never re-initialized, even when its on-chain config was written by a different program - // version whose layout the pinned gobindings can't decode. - if fqJustDeployed { + // A program is initialized when it was freshly deployed in this run, or when its config account + // does not yet exist (recovering from a prior run that deployed the program but failed before + // initializing it). shouldInitialize checks account existence without borsh-decoding, so a + // program left untouched by this run — e.g. RMN remote during a fee-quoter-only upgrade — is + // recognised as already initialized instead of triggering a re-init, even when its on-chain + // config was written by a different program version whose layout the pinned gobindings can't + // decode. The config PDAs derived here are reused when building the lookup table below. + feeQuoterConfigPDA, _, _ := solState.FindFqConfigPDA(feeQuoterAddress) + initFeeQuoter, err := shouldInitialize(e, chain, fqJustDeployed, feeQuoterConfigPDA) + if err != nil { + return batches, err + } + if initFeeQuoter { if err := initializeFeeQuoter(e, chain, ccipRouterProgram, chainState.LinkToken, feeQuoterAddress, offRampAddress, params.FeeQuoterParams); err != nil { return batches, err } + } else { + e.Logger.Infow("Fee quoter already initialized, skipping initialization", "chain", chain.String()) } - if routerJustDeployed { + routerConfigPDA, _, _ := solState.FindConfigPDA(ccipRouterProgram) + initRouter, err := shouldInitialize(e, chain, routerJustDeployed, routerConfigPDA) + if err != nil { + return batches, err + } + if initRouter { if err := initializeRouter(e, chain, ccipRouterProgram, chainState.LinkToken, feeQuoterAddress, rmnRemoteAddress); err != nil { return batches, err } + } else { + e.Logger.Infow("Router already initialized, skipping initialization", "chain", chain.String()) } - if offRampJustDeployed { + offRampConfigPDA, _, _ := solState.FindOfframpConfigPDA(offRampAddress) + initOffRamp, err := shouldInitialize(e, chain, offRampJustDeployed, offRampConfigPDA) + if err != nil { + return batches, err + } + if initOffRamp { table, err := solCommonUtil.SetupLookupTable( e.GetContext(), chain.Client, @@ -486,12 +527,21 @@ func deployChainContractsSolana( if err := initializeOffRamp(e, chain, ccipRouterProgram, feeQuoterAddress, rmnRemoteAddress, offRampAddress, table, params.OffRampParams); err != nil { return batches, err } + } else { + e.Logger.Infow("Offramp already initialized, skipping initialization", "chain", chain.String()) } - if rmnJustDeployed { + rmnRemoteConfigPDA, _, _ := solState.FindRMNRemoteConfigPDA(rmnRemoteAddress) + initRMNRemote, err := shouldInitialize(e, chain, rmnJustDeployed, rmnRemoteConfigPDA) + if err != nil { + return batches, err + } + if initRMNRemote { if err := initializeRMNRemote(e, chain, rmnRemoteAddress, ccipRouterProgram); err != nil { return batches, err } + } else { + e.Logger.Infow("RMN remote already initialized, skipping initialization", "chain", chain.String()) } // TOKEN POOLS DEPLOY @@ -661,19 +711,16 @@ func deployChainContractsSolana( } // LOOKUP TABLE - // off ramp - offRampConfigPDA, _, _ := solState.FindOfframpConfigPDA(offRampAddress) + // off ramp (offRampConfigPDA, feeQuoterConfigPDA, routerConfigPDA and rmnRemoteConfigPDA are + // derived in the INITIALIZE blocks above and reused here) offRampReferenceAddressesPDA, _, _ := solState.FindOfframpReferenceAddressesPDA(offRampAddress) offRampBillingSignerPDA, _, _ := solState.FindOfframpBillingSignerPDA(offRampAddress) // fee quoter - feeQuoterConfigPDA, _, _ := solState.FindFqConfigPDA(feeQuoterAddress) linkFqBillingConfigPDA, _, _ := solState.FindFqBillingTokenConfigPDA(chainState.LinkToken, feeQuoterAddress) wsolFqBillingConfigPDA, _, _ := solState.FindFqBillingTokenConfigPDA(chainState.WSOL, feeQuoterAddress) // router - routerConfigPDA, _, _ := solState.FindConfigPDA(ccipRouterProgram) feeBillingSignerPDA, _, _ := solState.FindFeeBillingSignerPDA(ccipRouterProgram) // rmn remote - rmnRemoteConfigPDA, _, _ := solState.FindRMNRemoteConfigPDA(rmnRemoteAddress) rmnRemoteCursePDA, _, _ := solState.FindRMNRemoteCursesPDA(rmnRemoteAddress) lookupTableKeys := []solana.PublicKey{ // offramp