diff --git a/.github/workflows/sui-ccip-test.yml b/.github/workflows/sui-ccip-test.yml index 15f1414bc..f866568f9 100644 --- a/.github/workflows/sui-ccip-test.yml +++ b/.github/workflows/sui-ccip-test.yml @@ -184,13 +184,35 @@ jobs: go get github.com/smartcontractkit/chainlink-sui@$REF go get github.com/smartcontractkit/chainlink-sui/deployment@$REF + # chainlink-sui/deployment pulls a newer chainlink-ccip/deployment than chainlink + # develop pins for chains/evm; keep both sibling modules on the same commit. + CCIP_DEP=$(grep -m1 'github.com/smartcontractkit/chainlink-ccip/deployment v' \ + ../../chainlink-sui/deployment/go.mod | awk '{print $2}') + CCIP_REF="${CCIP_DEP##*-}" + echo "Aligning chainlink-ccip modules to commit ${CCIP_REF} (from ${CCIP_DEP})" + go get "github.com/smartcontractkit/chainlink-ccip/deployment@${CCIP_REF}" + go get "github.com/smartcontractkit/chainlink-ccip/chains/evm@${CCIP_REF}" + + cd ../deployment + go get "github.com/smartcontractkit/chainlink-ccip/deployment@${CCIP_REF}" + go get "github.com/smartcontractkit/chainlink-ccip/chains/evm@${CCIP_REF}" + cd .. make gomodtidy - # 6) Build LOOP plugin - - name: Build LOOP plugin + # 6) Install public LOOP plugins (same as chainlink integration CI), then overlay PR relayer + - name: Install LOOP plugins (public) + working-directory: temp/chainlink + env: + GOPRIVATE: github.com/smartcontractkit/* + run: | + make install-loopinstall + make install-plugins-public + + - name: Build chainlink-sui LOOP plugin from PR working-directory: temp/chainlink-sui - run: go build -o chainlink-sui ./relayer/cmd/chainlink-sui/main.go + run: | + go build -ldflags=-s -o "$(go env GOPATH)/bin/chainlink-sui" ./relayer/cmd/chainlink-sui # 7) Setup Sui CLI - name: Setup Sui CLI @@ -214,7 +236,6 @@ jobs: env: CL_DATABASE_URL: ${{ env.CL_DATABASE_URL }} TEST_ENV_TYPE: in-memory - CL_SUI_CMD: ${{ github.workspace }}/temp/chainlink-sui/chainlink-sui run: | cd temp/chainlink/integration-tests echo "=== Running ${{ matrix.test_name }} ===" diff --git a/bindings/bind/compile.go b/bindings/bind/compile.go index 4baf1c069..cdcffa257 100644 --- a/bindings/bind/compile.go +++ b/bindings/bind/compile.go @@ -40,8 +40,9 @@ const env = "local" type SourceModifier func(packageRoot string) error var ( - testModifierMu sync.Mutex - testModifier SourceModifier + testModifierMu sync.Mutex + testModifier SourceModifier + compilePackageMu sync.Mutex ) // SetTestModifier sets a source modifier for the next compilation (test only) @@ -208,6 +209,11 @@ func CompilePackage(packageName contracts.Package, namedAddresses map[string]str } func compilePackageInternal(packageName contracts.Package, namedAddresses map[string]string, isUpgrade bool, suiRPC string, modifier SourceModifier) (PackageArtifact, error) { + // CompilePackage uses a process-global SUI_CONFIG_DIR; serialize compiles so + // parallel integration tests do not cross-contaminate temp CLI configs. + compilePackageMu.Lock() + defer compilePackageMu.Unlock() + var rpcURL string // 1️. Detect dynamic RPC from Docker if suiRPC == "" { @@ -1162,7 +1168,7 @@ func setupSuiEnv(alias, rpcURL string) error { newCmd.Env = os.Environ() newOut, err := newCmd.CombinedOutput() if err != nil { - fmt.Printf("failed to create sui env '%s': %v\nOutput:\n%s", alias, err, string(newOut)) + return fmt.Errorf("failed to create sui env '%s': %w\nOutput:\n%s", alias, err, string(newOut)) } // Step 4️ — Switch to new env diff --git a/deployment/changesets/cs_mcms_configure.go b/deployment/changesets/cs_mcms_configure.go index 73b7e51d2..2cadfa823 100644 --- a/deployment/changesets/cs_mcms_configure.go +++ b/deployment/changesets/cs_mcms_configure.go @@ -18,7 +18,7 @@ import ( type ConfigureMCMSConfig struct { mcmsops.ConfigureMCMSSeqInput TimelockConfig *utils.TimelockConfig // If nil, configuration will be executed directly - IsFastCurse bool // If true, the fastcurse MCMS instance is configured + IsFastCurse bool `yaml:"isFastCurse,omitempty"` // If true, the fastcurse MCMS instance is configured } var _ cldf.ChangeSetV2[ConfigureMCMSConfig] = ConfigureMCMS{} @@ -64,8 +64,16 @@ func (c ConfigureMCMS) Apply(e cldf.Environment, config ConfigureMCMSConfig) (cl deps.Signer = nil } + seqInput := config.ConfigureMCMSSeqInput + if seqInput.PackageId == "" { + seqInput.PackageId = mcmsState.PackageID + seqInput.McmsAccountOwnerCapObjectId = mcmsState.AccountOwnerCapObjectID + seqInput.McmsAccountStateObjectId = mcmsState.AccountStateObjectID + seqInput.McmsMultisigStateObjectId = mcmsState.StateObjectID + } + // Run ConfigureMCMS Sequence - configReport, err := cld_ops.ExecuteSequence(e.OperationsBundle, mcmsops.ConfigureMCMSSequence, deps, config.ConfigureMCMSSeqInput) + configReport, err := cld_ops.ExecuteSequence(e.OperationsBundle, mcmsops.ConfigureMCMSSequence, deps, seqInput) if err != nil { return cldf.ChangesetOutput{}, fmt.Errorf("failed to configure MCMS for Sui chain %d: %w", config.ChainSelector, err) } diff --git a/deployment/changesets/cs_mcms_deploy.go b/deployment/changesets/cs_mcms_deploy.go index bf5f9bdb3..833e0ec7a 100644 --- a/deployment/changesets/cs_mcms_deploy.go +++ b/deployment/changesets/cs_mcms_deploy.go @@ -16,12 +16,12 @@ import ( var _ cldf.ChangeSetV2[DeployMCMSConfig] = DeployMCMS{} // DeployMCMSConfig wraps DeployMCMSSeqInput and adds the IsFastCurse flag. -// When IsFastCurse is true all address-book entries are stored with the -// "fastcurse" label so that LoadOnchainStatesui can distinguish the two -// MCMS instances deployed on the same chain. +// When IsFastCurse is true the fast_mcms package is published and all address-book +// entries are stored with the "fastcurse" label so LoadOnchainStatesui can distinguish +// the two MCMS instances deployed on the same chain. type DeployMCMSConfig struct { mcmsops.DeployMCMSSeqInput - IsFastCurse bool + IsFastCurse bool `yaml:"isFastCurse,omitempty"` } type DeployMCMS struct{} @@ -48,8 +48,10 @@ func (d DeployMCMS) Apply(e cldf.Environment, config DeployMCMSConfig) (cldf.Cha SuiRPC: suiChain.URL, } - // Run DeployMCMS Sequence - mcmsReport, err := cld_ops.ExecuteSequence(e.OperationsBundle, mcmsops.DeployMCMSSequence, deps, config.DeployMCMSSeqInput) + seqInput := config.DeployMCMSSeqInput + seqInput.FastMCMS = config.IsFastCurse + + mcmsReport, err := cld_ops.ExecuteSequence(e.OperationsBundle, mcmsops.DeployMCMSSequence, deps, seqInput) if err != nil { return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy MCMS for Sui chain %d: %w", config.ChainSelector, err) } @@ -59,10 +61,15 @@ func (d DeployMCMS) Apply(e cldf.Environment, config DeployMCMSConfig) (cldf.Cha return cldf.ChangesetOutput{}, fmt.Errorf("failed to store MCMS in address book for Sui chain %d: %w", config.ChainSelector, err) } + proposals := []mcms.TimelockProposal{} + if !seqInput.SkipOwnershipTransfer { + proposals = append(proposals, mcmsReport.Output.AcceptOwnershipProposal) + } + return cldf.ChangesetOutput{ AddressBook: ab, Reports: seqReports, - MCMSTimelockProposals: []mcms.TimelockProposal{mcmsReport.Output.AcceptOwnershipProposal}, + MCMSTimelockProposals: proposals, }, nil } diff --git a/deployment/changesets/cs_mcms_dual_test.go b/deployment/changesets/cs_mcms_dual_test.go index c68a247e8..6b31b950b 100644 --- a/deployment/changesets/cs_mcms_dual_test.go +++ b/deployment/changesets/cs_mcms_dual_test.go @@ -51,6 +51,57 @@ func TestDeployMCMS_VerifyPreconditions_RejectsDuplicateInstance(t *testing.T) { require.Contains(t, err.Error(), "slow MCMS is already recorded") } +func TestDeployMCMS_VerifyPreconditions_RejectsDuplicateFastInstance(t *testing.T) { + t.Parallel() + + selector := cselectors.SUI_TESTNET.Selector + ab := cldf.NewMemoryAddressBook() + require.NoError(t, deployment.StoreMCMSInAddressBook(ab, selector, mcmsops.DeployMCMSSeqOutput{ + PackageId: "0xfast_pkg", + Objects: mcmsops.DeployMCMSObjects{ + McmsMultisigStateObjectId: "0xfast_state", + McmsRegistryObjectId: "0xfast_registry", + McmsAccountStateObjectId: "0xfast_account", + McmsAccountOwnerCapObjectId: "0xfast_owner_cap", + TimelockObjectId: "0xfast_timelock", + McmsDeployerStateObjectId: "0xfast_deployer", + }, + }, deployment.MCMSInstanceFastCurse)) + + cs := DeployMCMS{} + err := cs.VerifyPreconditions(dualMCMSEnv(t, ab, selector), DeployMCMSConfig{ + DeployMCMSSeqInput: mcmsops.DeployMCMSSeqInput{ChainSelector: selector}, + IsFastCurse: true, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "fastcurse MCMS is already recorded") +} + +func TestDeployMCMS_VerifyPreconditions_AllowsFastWhenOnlySlowExists(t *testing.T) { + t.Parallel() + + selector := cselectors.SUI_TESTNET.Selector + ab := cldf.NewMemoryAddressBook() + require.NoError(t, deployment.StoreMCMSInAddressBook(ab, selector, mcmsops.DeployMCMSSeqOutput{ + PackageId: "0xslow_pkg", + Objects: mcmsops.DeployMCMSObjects{ + McmsMultisigStateObjectId: "0xslow_state", + McmsRegistryObjectId: "0xslow_registry", + McmsAccountStateObjectId: "0xslow_account", + McmsAccountOwnerCapObjectId: "0xslow_owner_cap", + TimelockObjectId: "0xslow_timelock", + McmsDeployerStateObjectId: "0xslow_deployer", + }, + }, deployment.MCMSInstanceSlow)) + + cs := DeployMCMS{} + err := cs.VerifyPreconditions(dualMCMSEnv(t, ab, selector), DeployMCMSConfig{ + DeployMCMSSeqInput: mcmsops.DeployMCMSSeqInput{ChainSelector: selector}, + IsFastCurse: true, + }) + require.NoError(t, err) +} + func TestRegisterCurserCap_VerifyPreconditions_RequiresBothMCMSInstances(t *testing.T) { t.Parallel() diff --git a/deployment/ops/mcms/seq_deploy.go b/deployment/ops/mcms/seq_deploy.go index b46da386f..5eb9a8377 100644 --- a/deployment/ops/mcms/seq_deploy.go +++ b/deployment/ops/mcms/seq_deploy.go @@ -23,6 +23,13 @@ type DeployMCMSSeqInput struct { Bypasser *types.Config `json:"bypasser,omitempty" yaml:"bypasser,omitempty"` Proposer *types.Config `json:"proposer,omitempty" yaml:"proposer,omitempty"` Canceller *types.Config `json:"canceller,omitempty" yaml:"canceller,omitempty"` + + // FastMCMS publishes contracts/mcms/fast_mcms instead of contracts/mcms/mcms. + // Typically set by the DeployMCMS changeset from IsFastCurse rather than YAML input. + FastMCMS bool `json:"fastMCMS,omitempty" yaml:"fastMCMS,omitempty"` + + // SkipOwnershipTransfer omits the MCMS self-ownership init and accept-ownership proposal. + SkipOwnershipTransfer bool `json:"skipOwnershipTransfer,omitempty" yaml:"skipOwnershipTransfer,omitempty"` } type DeployMCMSSeqOutput struct { @@ -39,10 +46,9 @@ var DeployMCMSSequence = cld_ops.NewSequence( ) func deployMCMS(env cld_ops.Bundle, deps sui_ops.OpTxDeps, input DeployMCMSSeqInput) (DeployMCMSSeqOutput, error) { - // Deploy MCMS first - deployReport, err := cld_ops.ExecuteOperation(env, DeployMCMSOp, deps, cld_ops.EmptyInput{}) + deployReport, err := executeMCMSDeploy(env, deps, input.FastMCMS) if err != nil { - return DeployMCMSSeqOutput{}, fmt.Errorf("failed to deploy MCMS: %w", err) + return DeployMCMSSeqOutput{}, err } // Configure each timelock role if config is provided @@ -61,6 +67,13 @@ func deployMCMS(env cld_ops.Bundle, deps sui_ops.OpTxDeps, input DeployMCMSSeqIn return DeployMCMSSeqOutput{}, fmt.Errorf("failed to configure MCMS: %w", err) } + if input.SkipOwnershipTransfer { + return DeployMCMSSeqOutput{ + PackageId: deployReport.Output.PackageId, + Objects: deployReport.Output.Objects, + }, nil + } + // Init the ownership transfer to self transferOwnershipInput := MCMSTransferOwnershipInput{ McmsPackageID: deployReport.Output.PackageId, @@ -99,3 +112,19 @@ func deployMCMS(env cld_ops.Bundle, deps sui_ops.OpTxDeps, input DeployMCMSSeqIn return output, nil } + +func executeMCMSDeploy(env cld_ops.Bundle, deps sui_ops.OpTxDeps, fastMCMS bool) (cld_ops.Report[cld_ops.EmptyInput, sui_ops.OpTxResult[DeployMCMSObjects]], error) { + if fastMCMS { + deployReport, err := cld_ops.ExecuteOperation(env, DeployFastMCMSOp, deps, cld_ops.EmptyInput{}) + if err != nil { + return cld_ops.Report[cld_ops.EmptyInput, sui_ops.OpTxResult[DeployMCMSObjects]]{}, fmt.Errorf("failed to deploy fast MCMS: %w", err) + } + return deployReport, nil + } + + deployReport, err := cld_ops.ExecuteOperation(env, DeployMCMSOp, deps, cld_ops.EmptyInput{}) + if err != nil { + return cld_ops.Report[cld_ops.EmptyInput, sui_ops.OpTxResult[DeployMCMSObjects]]{}, fmt.Errorf("failed to deploy MCMS: %w", err) + } + return deployReport, nil +} diff --git a/deployment/ops/mcms/seq_deploy_test.go b/deployment/ops/mcms/seq_deploy_test.go index 9d0d7d3e4..da4963225 100644 --- a/deployment/ops/mcms/seq_deploy_test.go +++ b/deployment/ops/mcms/seq_deploy_test.go @@ -40,8 +40,6 @@ func generateSortedSigners(count int) []common.Address { } func TestDeployMCMSSeq(t *testing.T) { - t.Parallel() - signer, client := testenv.SetupEnvironment(t) deps := sui_ops.OpTxDeps{ @@ -194,3 +192,82 @@ func TestDeployMCMSSeq(t *testing.T) { require.NotEmpty(t, proposal.Operations, "Proposal should contain operations") require.Len(t, proposal.Operations, 1, "Proposal should contain exactly one operation") } + +func TestDeployFastMCMSSeq_SkipOwnershipTransfer(t *testing.T) { + signer, client := testenv.SetupEnvironment(t) + + deps := sui_ops.OpTxDeps{ + Client: client, + Signer: signer, + GetCallOpts: func() *bind.CallOpts { + b := uint64(300_000_000) + return &bind.CallOpts{ + WaitForExecution: true, + GasBudget: &b, + } + }, + } + + bundle := cld_ops.NewBundle( + context.Background, + logger.Test(t), + cld_ops.NewMemoryReporter(), + ) + + signers := generateSortedSigners(4) + proposerConfig := &types.Config{ + Quorum: 1, + Signers: signers[:2], + } + + report, err := cld_ops.ExecuteSequence(bundle, DeployMCMSSequence, deps, DeployMCMSSeqInput{ + ChainSelector: cselectors.SUI_TESTNET.Selector, + FastMCMS: true, + SkipOwnershipTransfer: true, + Proposer: proposerConfig, + }) + require.NoError(t, err) + + require.NotEmpty(t, report.Output.PackageId) + require.NotEmpty(t, report.Output.Objects.McmsRegistryObjectId) + require.Empty(t, report.Output.AcceptOwnershipProposal.Operations) +} + +func TestDeployFastMCMSSeq_PublishesDistinctPackageFromSlowMCMS(t *testing.T) { + signer, client := testenv.SetupEnvironment(t) + + deps := sui_ops.OpTxDeps{ + Client: client, + Signer: signer, + GetCallOpts: func() *bind.CallOpts { + b := uint64(300_000_000) + return &bind.CallOpts{ + WaitForExecution: true, + GasBudget: &b, + } + }, + } + + bundle := cld_ops.NewBundle( + context.Background, + logger.Test(t), + cld_ops.NewMemoryReporter(), + ) + + slowReport, err := cld_ops.ExecuteSequence(bundle, DeployMCMSSequence, deps, DeployMCMSSeqInput{ + ChainSelector: cselectors.SUI_TESTNET.Selector, + SkipOwnershipTransfer: true, + }) + require.NoError(t, err) + + fastReport, err := cld_ops.ExecuteSequence(bundle, DeployMCMSSequence, deps, DeployMCMSSeqInput{ + ChainSelector: cselectors.SUI_TESTNET.Selector, + FastMCMS: true, + SkipOwnershipTransfer: true, + }) + require.NoError(t, err) + + require.NotEqual(t, slowReport.Output.PackageId, fastReport.Output.PackageId) + require.NotEqual(t, slowReport.Output.Objects.McmsMultisigStateObjectId, fastReport.Output.Objects.McmsMultisigStateObjectId) + require.NotEqual(t, slowReport.Output.Objects.McmsRegistryObjectId, fastReport.Output.Objects.McmsRegistryObjectId) +}