diff --git a/deployment/ccip/changeset/internal/deploy_home_chain.go b/deployment/ccip/changeset/internal/deploy_home_chain.go index 799f3040298..d6fb91dc426 100644 --- a/deployment/ccip/changeset/internal/deploy_home_chain.go +++ b/deployment/ccip/changeset/internal/deploy_home_chain.go @@ -130,7 +130,7 @@ func BuildSetOCR3ConfigArgs( } configForOCR3 = ocrConfig.CandidateConfig } - if err := validateOCR3Config(destSelector, configForOCR3.Config, &chainCfg); err != nil { + if err := validateOCR3Config(destSelector, configForOCR3.Config, &chainCfg, false); err != nil { return nil, err } @@ -205,7 +205,7 @@ func BuildSetOCR3ConfigArgsSui( configForOCR3 = ocrConfig.CandidateConfig } - if err := validateOCR3Config(destSelector, configForOCR3.Config, &chainCfg); err != nil { + if err := validateOCR3Config(destSelector, configForOCR3.Config, &chainCfg, false); err != nil { return nil, err } @@ -230,10 +230,14 @@ func BuildSetOCR3ConfigArgsSui( return offrampOCR3Configs, nil } -func validateOCR3Config(chainSel uint64, configForOCR3 ccip_home.CCIPHomeOCR3Config, chainConfig *ccip_home.CCIPHomeChainConfig) error { +func validateOCR3Config(chainSel uint64, configForOCR3 ccip_home.CCIPHomeOCR3Config, chainConfig *ccip_home.CCIPHomeChainConfig, allowZeroFChain bool) error { if chainConfig != nil { // chainConfigs must be set before OCR3 configs due to the added fChain == F validation - if chainConfig.FChain == 0 || bytes.IsEmpty(chainConfig.Config) || len(chainConfig.Readers) == 0 { + if chainConfig.FChain == 0 { + if !allowZeroFChain || bytes.IsEmpty(chainConfig.Config) || len(chainConfig.Readers) == 0 { + return fmt.Errorf("chain config is not set for chain selector %d", chainSel) + } + } else if bytes.IsEmpty(chainConfig.Config) || len(chainConfig.Readers) == 0 { return fmt.Errorf("chain config is not set for chain selector %d", chainSel) } for _, reader := range chainConfig.Readers { @@ -254,6 +258,9 @@ func validateOCR3Config(chainSel uint64, configForOCR3 ccip_home.CCIPHomeOCR3Con // note that this is done onchain, but we'll do it here for good measure to avoid reverts. // see https://github.com/smartcontractkit/chainlink-ccip/blob/8529b8c89093d0cd117b73645ea64b2d2a8092f4/chains/evm/contracts/capability/CCIPHome.sol#L511-L514. minTransmitterReq := 3*int(chainConfig.FChain) + 1 + if allowZeroFChain && chainConfig.FChain == 0 { + minTransmitterReq = 1 + } var numNonzeroTransmitters int for _, node := range configForOCR3.Nodes { if len(node.TransmitterKey) > 0 { @@ -353,7 +360,7 @@ func BuildSetOCR3ConfigArgsSolana( } configForOCR3 = ocrConfig.CandidateConfig } - if err := validateOCR3Config(destSelector, configForOCR3.Config, &chainCfg); err != nil { + if err := validateOCR3Config(destSelector, configForOCR3.Config, &chainCfg, false); err != nil { return nil, err } @@ -428,7 +435,7 @@ func BuildSetOCR3ConfigArgsAptos( configForOCR3 = ocrConfig.CandidateConfig } - if err := validateOCR3Config(destSelector, configForOCR3.Config, &chainCfg); err != nil { + if err := validateOCR3Config(destSelector, configForOCR3.Config, &chainCfg, false); err != nil { return nil, err } @@ -451,6 +458,12 @@ func BuildSetOCR3ConfigArgsAptos( return offrampOCR3Configs, nil } +// CCIPHomeOCR3BuildOpts configures test-only OCR3 build behavior for Sui min-DON setups. +type CCIPHomeOCR3BuildOpts struct { + EVMIdentityFallbackForSui bool + AllowZeroFChain bool +} + func BuildOCR3ConfigForCCIPHome( ccipHome *ccip_home.CCIPHome, ocrSecrets focr.OCRSecrets, @@ -462,6 +475,7 @@ func BuildOCR3ConfigForCCIPHome( commitOffchainCfg *pluginconfig.CommitOffchainConfig, execOffchainCfg *pluginconfig.ExecuteOffchainConfig, skipChainConfigValidation bool, + buildOpts CCIPHomeOCR3BuildOpts, ) (map[types.PluginType]ccip_home.CCIPHomeOCR3Config, error) { addressCodec := ccipcommon.NewAddressCodec(map[string]ccipcommon.ChainSpecificAddressCodec{ chain_selectors.FamilyEVM: ccipevm.AddressCodec{}, @@ -493,18 +507,34 @@ func BuildOCR3ConfigForCCIPHome( // This is a HACK, because it is entirely possible that the destination chain is a unique family, // and no other supported chain by the node has the same family, e.g. Solana. cfg, exists := node.OCRConfigForChainSelector(destSelector) + suiEVMFallback := false if !exists { - // check if we have an oracle identity for another chain in the same family as destFamily. allOCRConfigs := node.AllOCRConfigs() - for chainDetails, ocrConfig := range allOCRConfigs { - chainFamily, err := chain_selectors.GetSelectorFamily(chainDetails.ChainSelector) - if err != nil { - return nil, err + if buildOpts.EVMIdentityFallbackForSui && destFamily == chain_selectors.FamilySui { + for chainDetails, ocrConfig := range allOCRConfigs { + chainFamily, err := chain_selectors.GetSelectorFamily(chainDetails.ChainSelector) + if err != nil { + return nil, err + } + if chainFamily == chain_selectors.FamilyEVM { + cfg = ocrConfig + suiEVMFallback = true + break + } } - - if chainFamily == destFamily { - cfg = ocrConfig - break + } + if !suiEVMFallback { + // check if we have an oracle identity for another chain in the same family as destFamily. + for chainDetails, ocrConfig := range allOCRConfigs { + chainFamily, err := chain_selectors.GetSelectorFamily(chainDetails.ChainSelector) + if err != nil { + return nil, err + } + + if chainFamily == destFamily { + cfg = ocrConfig + break + } } } @@ -517,7 +547,7 @@ func BuildOCR3ConfigForCCIPHome( } var transmitAccount ocrtypes.Account - if !exists { + if !exists || suiEVMFallback { // empty account means that the node cannot transmit for this chain // we replace this with a canonical address with the oracle ID as the address when doing the ocr config validation below, but it should remain empty // in the CCIPHome OCR config and it should not be included in the destination chain transmitters whitelist. @@ -697,7 +727,7 @@ func BuildOCR3ConfigForCCIPHome( if err != nil { return nil, fmt.Errorf("can't get chain config for %d: %w", destSelector, err) } - if err := validateOCR3Config(destSelector, ocr3Configs[pluginType], &chainConfig); err != nil { + if err := validateOCR3Config(destSelector, ocr3Configs[pluginType], &chainConfig, buildOpts.AllowZeroFChain); err != nil { return nil, fmt.Errorf("failed to validate ocr3 config: %w", err) } } diff --git a/deployment/ccip/changeset/testhelpers/test_environment.go b/deployment/ccip/changeset/testhelpers/test_environment.go index 87e5a1e10c3..2568096315f 100644 --- a/deployment/ccip/changeset/testhelpers/test_environment.go +++ b/deployment/ccip/changeset/testhelpers/test_environment.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "os" "strconv" + "strings" "testing" "time" @@ -157,6 +158,12 @@ type TestConfigs struct { // SuiContainerConfig allows custom configuration for Sui containers. SuiContainerConfig *onchain.SuiContainerConfig + + // SuiMinDON configures a single-node Sui sub-committee (FChain=0) while keeping the + // full EVM role DON. Only the plugin at SuiCapablePluginIndex runs the Sui chain relayer. + SuiMinDON bool + // SuiCapablePluginIndex is the index into non-bootstrap plugin nodes that supports Sui. + SuiCapablePluginIndex int } func (tc *TestConfigs) Validate() error { @@ -175,6 +182,31 @@ func (tc *TestConfigs) Validate() error { return nil } +func filterZeroFChainErrors(chainErrs map[uint64][]error, suiChains []uint64) map[uint64][]error { + suiSet := make(map[uint64]struct{}, len(suiChains)) + for _, sel := range suiChains { + suiSet[sel] = struct{}{} + } + for sel, errs := range chainErrs { + if _, ok := suiSet[sel]; !ok { + continue + } + var filtered []error + for _, err := range errs { + if err != nil && strings.Contains(err.Error(), "FChain is 0") { + continue + } + filtered = append(filtered, err) + } + if len(filtered) == 0 { + delete(chainErrs, sel) + } else { + chainErrs[sel] = filtered + } + } + return chainErrs +} + func (tc *TestConfigs) MustSetEnvTypeOrDefault(t *testing.T) { envType := os.Getenv(ENVTESTTYPE) if envType == "" || envType == string(Memory) { @@ -352,6 +384,17 @@ func WithSuiContainerConfig(cfg onchain.SuiContainerConfig) TestOps { } } +// WithSuiMinDON enables a single-node Sui CCIPHome sub-committee and production-like +// per-node chain config where only one plugin node runs the Sui relayer. +func WithSuiMinDON() TestOps { + return func(testCfg *TestConfigs) { + testCfg.SuiMinDON = true + if testCfg.SuiCapablePluginIndex == 0 && testCfg.Nodes > 0 { + testCfg.SuiCapablePluginIndex = 0 + } + } +} + func WithNumOfUsersPerChain(numUsers uint) TestOps { return func(testCfg *TestConfigs) { testCfg.NumOfUsersPerChain = numUsers @@ -512,17 +555,41 @@ func (m *MemoryEnvironment) StartChains(t *testing.T) { } } +func blockChainsWithoutSui(all cldf_chain.BlockChains) cldf_chain.BlockChains { + chains := make(map[uint64]cldf_chain.BlockChain) + for sel, chain := range all.All() { + family, err := chain_selectors.GetSelectorFamily(sel) + if err != nil || family == chain_selectors.FamilySui { + continue + } + chains[sel] = chain + } + return cldf_chain.NewBlockChains(chains) +} + func (m *MemoryEnvironment) StartNodes(t *testing.T, crConfig deployment.CapabilityRegistryConfig) { require.NotNil(t, m.Chains, "start chains first, chains are empty") require.NotNil(t, m.DeployedEnv, "start chains and initiate deployed env first before starting nodes") tc := m.TestConfig + var blockChainsForNode nodetestutils.BlockChainsForNodeFunc + if tc.SuiMinDON { + withoutSui := blockChainsWithoutSui(m.Env.BlockChains) + suiIdx := tc.SuiCapablePluginIndex + blockChainsForNode = func(pluginIndex int, isBootstrap bool) cldf_chain.BlockChains { + if isBootstrap || pluginIndex != suiIdx { + return withoutSui + } + return m.Env.BlockChains + } + } c := nodetestutils.NewNodesConfig{ - LogLevel: zapcore.InfoLevel, - BlockChains: m.Env.BlockChains, - NumNodes: tc.Nodes, - NumBootstraps: tc.Bootstraps, - RegistryConfig: crConfig, - CustomDBSetup: nil, + LogLevel: zapcore.InfoLevel, + BlockChains: m.Env.BlockChains, + BlockChainsForNode: blockChainsForNode, + NumNodes: tc.Nodes, + NumBootstraps: tc.Bootstraps, + RegistryConfig: crConfig, + CustomDBSetup: nil, } nodes := nodetestutils.NewNodes(t, c, tc.CLNodeConfigOpts...) ctx := testcontext.Get(t) @@ -809,7 +876,11 @@ func NewEnvironmentWithJobsAndContracts(t *testing.T, tEnv TestEnvironment) Depl state, err := stateview.LoadOnchainState(e.Env, stateview.WithLoadLegacyContracts(true)) require.NoError(t, err) - chainErrs := state.ValidatePostDeploymentStateWithoutMCMSOwnership(e.Env, !tEnv.TestConfigs().SkipDONConfiguration) + tc := tEnv.TestConfigs() + chainErrs := state.ValidatePostDeploymentStateWithoutMCMSOwnership(e.Env, !tc.SkipDONConfiguration) + if tc.SuiMinDON { + chainErrs = filterZeroFChainErrors(chainErrs, suiChains) + } require.Empty(t, chainErrs) return e @@ -1265,10 +1336,19 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn } commitOCRConfigs[chain] = v1_6.DeriveOCRParamsForCommit(v1_6.SimulationTest, e.FeedChainSel, tokenInfo, ocrOverride) execOCRConfigs[chain] = v1_6.DeriveOCRParamsForExec(v1_6.SimulationTest, tokenDataProviders, ocrOverride) + + suiReaders := nodeInfo.NonBootstraps().PeerIDs() + suiFChain := uint8(len(suiReaders) / 3) + if tc.SuiMinDON { + require.Less(t, tc.SuiCapablePluginIndex, len(suiReaders), + "SuiCapablePluginIndex out of range for non-bootstrap nodes") + suiReaders = [][32]byte{suiReaders[tc.SuiCapablePluginIndex]} + suiFChain = 0 + t.Logf("SuiMinDON: using single Sui reader at plugin index %d", tc.SuiCapablePluginIndex) + } chainConfigs[chain] = v1_6.ChainConfig{ - Readers: nodeInfo.NonBootstraps().PeerIDs(), - // #nosec G115 - Overflow is not a concern in this test scenario - FChain: uint8(len(nodeInfo.NonBootstraps().PeerIDs()) / 3), + Readers: suiReaders, + FChain: suiFChain, EncodableChainConfig: chainconfig.ChainConfig{ GasPriceDeviationPPB: ccipocr3common.BigInt{Int: big.NewInt(DefaultGasPriceDeviationPPB)}, DAGasPriceDeviationPPB: ccipocr3common.BigInt{Int: big.NewInt(DefaultDAGasPriceDeviationPPB)}, @@ -1317,6 +1397,10 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn } apps = []commonchangeset.ConfiguredChangeSet{} if !tc.SkipDONConfiguration { + suiMinDONPluginFlags := v1_6.SetCandidatePluginInfo{ + AllowZeroFChain: tc.SuiMinDON, + EVMIdentityFallbackForSui: tc.SuiMinDON, + } apps = append(apps, commonchangeset.Configure( // Add the chain configs for the new chains. cldf.CreateLegacyChangeSet(v1_6.UpdateChainConfigChangeset), @@ -1324,6 +1408,7 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn HomeChainSelector: e.HomeChainSel, RemoteChainAdds: chainConfigs, MCMS: mcmsConfig, + AllowZeroFChain: tc.SuiMinDON, }, )) apps = append(apps, commonchangeset.Configure( @@ -1336,10 +1421,17 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn FeedChainSelector: e.FeedChainSel, MCMS: mcmsConfig, }, - PluginInfo: v1_6.SetCandidatePluginInfo{ - OCRConfigPerRemoteChainSelector: commitOCRConfigs, - PluginType: types.PluginTypeCCIPCommit, - }, + PluginInfo: func() v1_6.SetCandidatePluginInfo { + p := v1_6.SetCandidatePluginInfo{ + OCRConfigPerRemoteChainSelector: commitOCRConfigs, + PluginType: types.PluginTypeCCIPCommit, + } + if tc.SuiMinDON { + p.AllowZeroFChain = suiMinDONPluginFlags.AllowZeroFChain + p.EVMIdentityFallbackForSui = suiMinDONPluginFlags.EVMIdentityFallbackForSui + } + return p + }(), }, )) apps = append(apps, commonchangeset.Configure( @@ -1353,10 +1445,17 @@ func AddCCIPContractsToEnvironment(t *testing.T, allChains []uint64, tEnv TestEn MCMS: mcmsConfig, }, PluginInfo: []v1_6.SetCandidatePluginInfo{ - { - OCRConfigPerRemoteChainSelector: execOCRConfigs, - PluginType: types.PluginTypeCCIPExec, - }, + func() v1_6.SetCandidatePluginInfo { + p := v1_6.SetCandidatePluginInfo{ + OCRConfigPerRemoteChainSelector: execOCRConfigs, + PluginType: types.PluginTypeCCIPExec, + } + if tc.SuiMinDON { + p.AllowZeroFChain = suiMinDONPluginFlags.AllowZeroFChain + p.EVMIdentityFallbackForSui = suiMinDONPluginFlags.EVMIdentityFallbackForSui + } + return p + }(), }, }, )) diff --git a/deployment/ccip/changeset/v1_6/cs_ccip_home.go b/deployment/ccip/changeset/v1_6/cs_ccip_home.go index b04f7846e44..b1fd5e18466 100644 --- a/deployment/ccip/changeset/v1_6/cs_ccip_home.go +++ b/deployment/ccip/changeset/v1_6/cs_ccip_home.go @@ -527,6 +527,12 @@ type SetCandidatePluginInfo struct { // WARNING: Never enable this parameter if running this changeset in isolation. // This is only meant to be enabled when running this changeset as part of a larger changeset that groups multiple proposals together. SkipChainConfigValidation bool `json:"skipChainConfigValidation"` + + // AllowZeroFChain permits FChain=0 on a chain config (test-only Sui min-DON setups). + AllowZeroFChain bool `json:"allowZeroFChain"` + + // EVMIdentityFallbackForSui uses EVM OCR identity for nodes without Sui chain config when building Sui OCR3 config. + EVMIdentityFallbackForSui bool `json:"evmIdentityFallbackForSui"` } func (p SetCandidatePluginInfo) String() string { @@ -562,7 +568,7 @@ func (p SetCandidatePluginInfo) Validate(e cldf.Environment, state stateview.CCI return fmt.Errorf("can't get chain config for %d: %w", chainSelector, err) } // FChain should never be zero if a chain config is set in CCIPHome - if chainConfig.FChain == 0 { + if chainConfig.FChain == 0 && !p.AllowZeroFChain { return fmt.Errorf("chain config not set up for new chain %d", chainSelector) } if len(chainConfig.Readers) == 0 { @@ -733,6 +739,10 @@ func AddDonAndSetCandidateChangeset( params.CommitOffChainConfig, params.ExecuteOffChainConfig, cfg.PluginInfo.SkipChainConfigValidation, + internal.CCIPHomeOCR3BuildOpts{ + EVMIdentityFallbackForSui: cfg.PluginInfo.EVMIdentityFallbackForSui, + AllowZeroFChain: cfg.PluginInfo.AllowZeroFChain, + }, ) if err != nil { return cldf.ChangesetOutput{}, err @@ -856,6 +866,10 @@ func SetCandidateChangeset( params.CommitOffChainConfig, params.ExecuteOffChainConfig, plugin.SkipChainConfigValidation, + internal.CCIPHomeOCR3BuildOpts{ + EVMIdentityFallbackForSui: plugin.EVMIdentityFallbackForSui, + AllowZeroFChain: plugin.AllowZeroFChain, + }, ) if err != nil { return cldf.ChangesetOutput{}, err @@ -1105,6 +1119,7 @@ type UpdateChainConfigConfig struct { RemoteChainRemoves []uint64 `json:"remoteChainRemoves"` RemoteChainAdds map[uint64]ChainConfig `json:"remoteChainAdds"` MCMS *cldfproposalutils.TimelockConfig `json:"mcms,omitempty"` + AllowZeroFChain bool `json:"allowZeroFChain"` } func (c UpdateChainConfigConfig) Validate(e cldf.Environment) error { @@ -1140,7 +1155,7 @@ func (c UpdateChainConfigConfig) Validate(e cldf.Environment) error { if _, ok := state.SupportedChains()[add]; !ok { return fmt.Errorf("chain to add %d is not supported", add) } - if ccfg.FChain == 0 { + if ccfg.FChain == 0 && !c.AllowZeroFChain { return fmt.Errorf("fChain must be set for selector %d", add) } if len(ccfg.Readers) == 0 { diff --git a/deployment/ccip/changeset/v1_6/cs_ccip_home_test.go b/deployment/ccip/changeset/v1_6/cs_ccip_home_test.go index 6907deef005..bc2a76ff4d3 100644 --- a/deployment/ccip/changeset/v1_6/cs_ccip_home_test.go +++ b/deployment/ccip/changeset/v1_6/cs_ccip_home_test.go @@ -94,6 +94,7 @@ func TestInvalidOCR3Params(t *testing.T) { params.CommitOffChainConfig, &globals.DefaultExecuteOffChainCfg, false, + internal.CCIPHomeOCR3BuildOpts{}, ) require.Errorf(t, err, "expected error") pattern := `DeltaRound \(\d+\.\d+s\) must be less than DeltaProgress \(\d+s\)` diff --git a/deployment/utils/nodetestutils/node.go b/deployment/utils/nodetestutils/node.go index b4526932ff9..716bcfb3e0b 100644 --- a/deployment/utils/nodetestutils/node.go +++ b/deployment/utils/nodetestutils/node.go @@ -78,6 +78,8 @@ type NewNodesConfig struct { LogLevel zapcore.Level // BlockChains to be configured BlockChains cldfchain.BlockChains + // BlockChainsForNode optionally overrides BlockChains per node. When nil, BlockChains is used for all nodes. + BlockChainsForNode BlockChainsForNodeFunc NumNodes int NumBootstraps int RegistryConfig deployment.CapabilityRegistryConfig @@ -85,6 +87,16 @@ type NewNodesConfig struct { CustomDBSetup []string } +// BlockChainsForNodeFunc selects the chain set for a node. pluginIndex is 0-based among plugin nodes only. +type BlockChainsForNodeFunc func(pluginIndex int, isBootstrap bool) cldfchain.BlockChains + +func nodeBlockChains(cfg NewNodesConfig, pluginIndex int, isBootstrap bool) cldfchain.BlockChains { + if cfg.BlockChainsForNode != nil { + return cfg.BlockChainsForNode(pluginIndex, isBootstrap) + } + return cfg.BlockChains +} + func NewNodes( t *testing.T, cfg NewNodesConfig, @@ -104,7 +116,7 @@ func NewNodes( // run smoothly. c := NewNodeConfig{ Port: ports[i], - BlockChains: cfg.BlockChains, + BlockChains: nodeBlockChains(cfg, 0, true), LogLevel: cfg.LogLevel, Bootstrap: true, RegistryConfig: cfg.RegistryConfig, @@ -118,7 +130,7 @@ func NewNodes( for i := range cfg.NumNodes { c := NewNodeConfig{ Port: ports[cfg.NumBootstraps+i], - BlockChains: cfg.BlockChains, + BlockChains: nodeBlockChains(cfg, i, false), LogLevel: cfg.LogLevel, Bootstrap: false, RegistryConfig: cfg.RegistryConfig, @@ -140,8 +152,19 @@ func NewNodes( fundNodesSol(t, solChain, nodes) } + suiCapableNodes := make([]*Node, 0, len(nodes)) + for i, node := range nodes { + if len(nodeBlockChains(cfg, i, false).SuiChains()) == 0 { + continue + } + suiCapableNodes = append(suiCapableNodes, node) + } + for _, suiChain := range cfg.BlockChains.SuiChains() { - fundNodesSui(t, suiChain, nodes) + if len(suiCapableNodes) == 0 { + continue + } + fundNodesSui(t, suiChain, suiCapableNodes) } return nodesByPeerID diff --git a/integration-tests/smoke/ccip/ccip_sui_messaging_test.go b/integration-tests/smoke/ccip/ccip_sui_messaging_test.go index 29215043b5a..0c3286693ce 100644 --- a/integration-tests/smoke/ccip/ccip_sui_messaging_test.go +++ b/integration-tests/smoke/ccip/ccip_sui_messaging_test.go @@ -58,6 +58,7 @@ func prepareSui2EvmMessagingTest(t *testing.T) sui2EvmMessagingFixtures { t, testhelpers.WithNumOfChains(2), testhelpers.WithSuiChains(1), + testhelpers.WithSuiMinDON(), ) evmChainSelectors := e.Env.BlockChains.ListChainSelectors(chain.WithFamily(chain_selectors.FamilyEVM)) @@ -363,6 +364,7 @@ func prepareEVM2SuiMessagingTest(t *testing.T) evm2SuiMessagingFixtures { t, testhelpers.WithNumOfChains(2), testhelpers.WithSuiChains(1), + testhelpers.WithSuiMinDON(), ) evmChainSelectors := e.Env.BlockChains.ListChainSelectors(chain.WithFamily(chain_selectors.FamilyEVM)) @@ -610,6 +612,7 @@ func Test_CCIP_EVM2Sui_ZeroReceiver(t *testing.T) { t, testhelpers.WithNumOfChains(2), testhelpers.WithSuiChains(1), + testhelpers.WithSuiMinDON(), ) evmChainSelectors := e.Env.BlockChains.ListChainSelectors(chain.WithFamily(chain_selectors.FamilyEVM))