Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions deployment/ccip/changeset/internal/deploy_home_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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,
Expand All @@ -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{},
Expand Down Expand Up @@ -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
}
}
}

Expand All @@ -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.
Expand Down Expand Up @@ -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)
}
}
Expand Down
135 changes: 117 additions & 18 deletions deployment/ccip/changeset/testhelpers/test_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)},
Expand Down Expand Up @@ -1317,13 +1397,18 @@ 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),
v1_6.UpdateChainConfigConfig{
HomeChainSelector: e.HomeChainSel,
RemoteChainAdds: chainConfigs,
MCMS: mcmsConfig,
AllowZeroFChain: tc.SuiMinDON,
},
))
apps = append(apps, commonchangeset.Configure(
Expand All @@ -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(
Expand All @@ -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
}(),
},
},
))
Expand Down
Loading
Loading