From d2ed2e59028929fb4156eb9396ca9a25884d7810 Mon Sep 17 00:00:00 2001 From: faisal-link Date: Mon, 15 Dec 2025 16:23:08 +0400 Subject: [PATCH 1/5] remove unnecessary if statement --- relayer/txm/confirmer.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/relayer/txm/confirmer.go b/relayer/txm/confirmer.go index 972e42a6e..d4a415aed 100644 --- a/relayer/txm/confirmer.go +++ b/relayer/txm/confirmer.go @@ -95,9 +95,6 @@ func handleTransactionError(ctx context.Context, txm *SuiTxm, tx SuiTx, result * txm.lggr.Debugw("Handling transaction error", "transactionID", tx.TransactionID, "error", result.Error) txError := suierrors.ParseSuiErrorMessage(result.Error) - if txError == nil { - txError = suierrors.NewSuiError(suierrors.UnknownErrors, result.Error) - } isRetryable, strategy := txm.retryManager.IsRetryable(&tx, result.Error) if !isRetryable { From f179f2fb40f77dbeb3f57e49a63498eb933ae007 Mon Sep 17 00:00:00 2001 From: faisal-link Date: Mon, 15 Dec 2025 16:24:10 +0400 Subject: [PATCH 2/5] update comment --- relayer/txm/confirmer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relayer/txm/confirmer.go b/relayer/txm/confirmer.go index d4a415aed..65f9900c5 100644 --- a/relayer/txm/confirmer.go +++ b/relayer/txm/confirmer.go @@ -152,7 +152,7 @@ func handleExponentialBackoffRetry(txm *SuiTxm, tx SuiTx) error { // Check if enough time has elapsed since the last update timeElapsed := time.Since(time.Unix(int64(tx.LastUpdatedAt), 0)) if timeElapsed.Seconds() < delaySeconds { - // Not enough time has elapsed for the next retry, mark the transaction as failed + // Not enough time has elapsed for the next retry txm.lggr.Debugw("Not enough time elapsed, no need to retry", "transactionID", tx.TransactionID, "elapsed", timeElapsed, "required", delaySeconds) return nil } From 118e5c2795e2e26c541ff04d3e0f2ace2744d5ab Mon Sep 17 00:00:00 2001 From: faisal-link Date: Mon, 15 Dec 2025 16:27:37 +0400 Subject: [PATCH 3/5] ensure L6 --- relayer/chainreader/indexer/transactions_indexer.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/relayer/chainreader/indexer/transactions_indexer.go b/relayer/chainreader/indexer/transactions_indexer.go index 10b946a0a..1032e83c5 100644 --- a/relayer/chainreader/indexer/transactions_indexer.go +++ b/relayer/chainreader/indexer/transactions_indexer.go @@ -366,8 +366,7 @@ func (tIndexer *TransactionsIndexer) syncTransmitterTransactions(ctx context.Con if moveAbort.Location.FunctionName == nil || *moveAbort.Location.FunctionName == tIndexer.executeFunction { tIndexer.logger.Debugw("Skipping transaction for failed function against init_execute function", "transmitter", transmitter, - "location", moveAbort.Location, - "functionName", *moveAbort.Location.FunctionName, + "moveAbort", *moveAbort, "digest", transactionRecord.Digest, ) From 05672a88e01bd55229f1e9ab1d437c63d9ee0ae0 Mon Sep 17 00:00:00 2001 From: faisal-link Date: Mon, 15 Dec 2025 16:40:56 +0400 Subject: [PATCH 4/5] ensure latest package ID in #fetchGenericDependency --- relayer/chainreader/reader/chainreader.go | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/relayer/chainreader/reader/chainreader.go b/relayer/chainreader/reader/chainreader.go index 20c1f5f44..bc38c7a47 100644 --- a/relayer/chainreader/reader/chainreader.go +++ b/relayer/chainreader/reader/chainreader.go @@ -687,22 +687,14 @@ func (s *suiChainReader) fetchGenericDependency(ctx context.Context, signerAddre s.logger.Debugw("get_token_pool_state_type falling back to OffRamp package", "offrampPackageAddress", offrampPackageAddress) ccipPackageAddress, err = s.client.GetCCIPPackageID(ctx, offrampPackageAddress, signerAddress) - if err != nil { + if err != nil || ccipPackageAddress == "" { return "", fmt.Errorf("failed to get CCIP package ID from offramp package address in fetchGenericDependency: %w", err) } + } - latestCcipPackageAddress, err := s.client.GetLatestPackageId(ctx, ccipPackageAddress, "state_object") - if err != nil { - return "", fmt.Errorf("failed to get latest CCIP package address from offramp package address in fetchGenericDependency: %w", err) - } - - s.logger.Debugw("get_token_pool_state_type using latest CCIP package address", "latestCcipPackageAddress", latestCcipPackageAddress) - - ccipPackageAddress = latestCcipPackageAddress - - if ccipPackageAddress == "" { - return "", fmt.Errorf("get_token_pool_state_type requires that the CCIP / TokenAdminRegistry package has been bound to ChainReader: %w", err) - } + ccipPackageAddress, err = s.client.GetLatestPackageId(ctx, ccipPackageAddress, "state_object") + if err != nil { + return "", fmt.Errorf("failed to get latest CCIP package address from offramp package address in fetchGenericDependency: %w", err) } s.logger.Warnw("get_token_pool_state_type using CCIP package address", "ccipPackageAddress", ccipPackageAddress) From 57828fe0687699986db6924324924259da5a014f Mon Sep 17 00:00:00 2001 From: Faisal Date: Mon, 12 Jan 2026 22:36:15 +0400 Subject: [PATCH 5/5] Extend `transaction_indexer` test (#320) * Fix inconsistent timestamp for failed transactions (#316) * feat(deployment): Curse Uncurse changeset (#310) * feat(deployment): Curse Uncurse changeset * add tests * improve tests --------- Co-authored-by: FelixFan1992 * feat(deployment): Add changesets to configure MCMS and transfer ownership to self (#317) * refactor mcms config into a sequence * refactor to separate accept mcms ownership sequence * changesets to accept ownership and configure mcms * use []any instead of []uint8 for messageId and messageHash in synth events (#318) * use []any instead of []uint8 for messageId and messageHash in synth events * update transaction_index_test * extend transaction_indexer test --------- Co-authored-by: VSG Co-authored-by: Rodrigo Soares <38868277+rodrigombsoares@users.noreply.github.com> Co-authored-by: FelixFan1992 --- deployment/changesets/cs_curse_uncurse.go | 135 ++++++++++++++++ .../cs_mcms_accept_ownership_self.go | 52 ++++++ deployment/changesets/cs_mcms_configure.go | 106 ++++++++++++ deployment/ops/mcms/op_set_config.go | 21 +-- deployment/ops/mcms/seq_accept_ownership.go | 64 ++++++++ deployment/ops/mcms/seq_configure.go | 77 +++++++++ deployment/ops/mcms/seq_deploy.go | 151 +++++++----------- deployment/ops/mcms/seq_deploy_test.go | 10 +- deployment/ops/rmn/op_curse_uncurse.go | 146 +++++++++++++++++ integration-tests/deploy/deploy_test.go | 57 +++++++ .../indexer/transactions_indexer.go | 12 +- .../indexer/transactions_indexer_test.go | 54 +++++-- relayer/codec/type_converters.go | 8 + 13 files changed, 770 insertions(+), 123 deletions(-) create mode 100644 deployment/changesets/cs_curse_uncurse.go create mode 100644 deployment/changesets/cs_mcms_accept_ownership_self.go create mode 100644 deployment/changesets/cs_mcms_configure.go create mode 100644 deployment/ops/mcms/seq_accept_ownership.go create mode 100644 deployment/ops/mcms/seq_configure.go create mode 100644 deployment/ops/rmn/op_curse_uncurse.go diff --git a/deployment/changesets/cs_curse_uncurse.go b/deployment/changesets/cs_curse_uncurse.go new file mode 100644 index 000000000..8929e2923 --- /dev/null +++ b/deployment/changesets/cs_curse_uncurse.go @@ -0,0 +1,135 @@ +package changesets + +import ( + "encoding/binary" + "errors" + "fmt" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + "github.com/smartcontractkit/chainlink-sui/deployment" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + rmn_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops/rmn" +) + +type CurseUncurseOperationType string + +const ( + CurseOperationType CurseUncurseOperationType = "curse" + UncurseOperationType CurseUncurseOperationType = "uncurse" +) + +type CurseUncurseChainsConfig struct { + SuiChainSelector uint64 `yaml:"suiChainSelector"` + OperationType string `yaml:"operationType"` + IsGlobalCurse bool `yaml:"isGlobalCurse"` + DestChainSelectors []uint64 `yaml:"destChainSelectors"` +} + +var _ cldf.ChangeSetV2[CurseUncurseChainsConfig] = CurseUncurseChains{} + +type CurseUncurseChains struct{} + +var globalCurseSubjectBytes = [16]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01} + +func (c CurseUncurseChains) VerifyPreconditions(e cldf.Environment, cfg CurseUncurseChainsConfig) error { + if cfg.OperationType != string(CurseOperationType) && cfg.OperationType != string(UncurseOperationType) { + return fmt.Errorf("invalid operation type %s", cfg.OperationType) + } + if cfg.IsGlobalCurse { + if len(cfg.DestChainSelectors) > 0 { + return errors.New("global curse config must not include destination selectors") + } + return nil + } + if len(cfg.DestChainSelectors) == 0 { + return errors.New("no destination chain selectors provided") + } + return nil +} + +func (c CurseUncurseChains) Apply(e cldf.Environment, cfg CurseUncurseChainsConfig) (cldf.ChangesetOutput, error) { + state, err := deployment.LoadOnchainStatesui(e) + if err != nil { + return cldf.ChangesetOutput{}, err + } + + chainState, ok := state[cfg.SuiChainSelector] + if !ok { + return cldf.ChangesetOutput{}, fmt.Errorf("no Sui chain state for selector %d", cfg.SuiChainSelector) + } + if chainState.CCIPAddress == "" { + return cldf.ChangesetOutput{}, fmt.Errorf("missing CCIP package address for chain %d", cfg.SuiChainSelector) + } + if chainState.CCIPObjectRef == "" { + return cldf.ChangesetOutput{}, fmt.Errorf("missing CCIP object ref for chain %d", cfg.SuiChainSelector) + } + if chainState.CCIPOwnerCapObjectId == "" { + return cldf.ChangesetOutput{}, fmt.Errorf("missing CCIP owner cap object id for chain %d", cfg.SuiChainSelector) + } + + suiChain, ok := e.BlockChains.SuiChains()[cfg.SuiChainSelector] + if !ok { + return cldf.ChangesetOutput{}, fmt.Errorf("no Sui chain client for selector %d", cfg.SuiChainSelector) + } + + subjects, err := buildCurseSubjects(cfg) + if err != nil { + return cldf.ChangesetOutput{}, err + } + + deps := sui_ops.OpTxDeps{ + Client: suiChain.Client, + Signer: suiChain.Signer, + GetCallOpts: func() *bind.CallOpts { + gasBudget := uint64(400_000_000) + return &bind.CallOpts{WaitForExecution: true, GasBudget: &gasBudget} + }, + SuiRPC: suiChain.URL, + } + + input := rmn_ops.CurseUncurseChainInput{ + CCIPPackageId: chainState.CCIPAddress, + StateObjectId: chainState.CCIPObjectRef, + OwnerCapObjectId: chainState.CCIPOwnerCapObjectId, + Subjects: subjects, + } + + var genericReport operations.Report[any, any] + if cfg.OperationType == string(UncurseOperationType) { + report, execErr := operations.ExecuteOperation(e.OperationsBundle, rmn_ops.UncurseChainOp, deps, input) + if execErr != nil { + return cldf.ChangesetOutput{}, execErr + } + genericReport = report.ToGenericReport() + } else { + report, execErr := operations.ExecuteOperation(e.OperationsBundle, rmn_ops.CurseChainOp, deps, input) + if execErr != nil { + return cldf.ChangesetOutput{}, execErr + } + genericReport = report.ToGenericReport() + } + + return cldf.ChangesetOutput{Reports: []operations.Report[any, any]{genericReport}}, nil +} + +func buildCurseSubjects(cfg CurseUncurseChainsConfig) ([][]byte, error) { + if cfg.IsGlobalCurse { + subject := make([]byte, len(globalCurseSubjectBytes)) + copy(subject, globalCurseSubjectBytes[:]) + return [][]byte{subject}, nil + } + subjects := make([][]byte, 0, len(cfg.DestChainSelectors)) + for _, selector := range cfg.DestChainSelectors { + subjects = append(subjects, selectorToSubject(selector)) + } + return subjects, nil +} + +func selectorToSubject(selector uint64) []byte { + subject := make([]byte, 16) + binary.BigEndian.PutUint64(subject[8:], selector) + return subject +} diff --git a/deployment/changesets/cs_mcms_accept_ownership_self.go b/deployment/changesets/cs_mcms_accept_ownership_self.go new file mode 100644 index 000000000..eda36922f --- /dev/null +++ b/deployment/changesets/cs_mcms_accept_ownership_self.go @@ -0,0 +1,52 @@ +package changesets + +import ( + "fmt" + + "github.com/smartcontractkit/mcms" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + mcmsops "github.com/smartcontractkit/chainlink-sui/deployment/ops/mcms" +) + +var _ cldf.ChangeSetV2[mcmsops.AcceptMCMSOwnershipSeqInput] = AcceptMCMSOwnership{} + +type AcceptMCMSOwnership struct{} + +// VerifyPreconditions implements deployment.ChangeSetV2. +func (a AcceptMCMSOwnership) VerifyPreconditions(e cldf.Environment, config mcmsops.AcceptMCMSOwnershipSeqInput) error { + return nil +} + +// Apply implements deployment.ChangeSetV2. +func (a AcceptMCMSOwnership) Apply(e cldf.Environment, config mcmsops.AcceptMCMSOwnershipSeqInput) (cldf.ChangesetOutput, error) { + suiChains := e.BlockChains.SuiChains() + suiChain := suiChains[config.ChainSelector] + deps := sui_ops.OpTxDeps{ + Client: suiChain.Client, + Signer: suiChain.Signer, + GetCallOpts: func() *bind.CallOpts { + b := uint64(400_000_000) + return &bind.CallOpts{ + WaitForExecution: true, + GasBudget: &b, + } + }, + SuiRPC: suiChain.URL, + } + + // Run AcceptMCMSOwnership Sequence + acceptReport, err := cld_ops.ExecuteSequence(e.OperationsBundle, mcmsops.AcceptMCMSOwnershipSequence, deps, config) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to generate accept ownership proposal for Sui chain %d: %w", config.ChainSelector, err) + } + + return cldf.ChangesetOutput{ + Reports: []cld_ops.Report[any, any]{acceptReport.ToGenericReport()}, + MCMSTimelockProposals: []mcms.TimelockProposal{acceptReport.Output}, + }, nil +} diff --git a/deployment/changesets/cs_mcms_configure.go b/deployment/changesets/cs_mcms_configure.go new file mode 100644 index 000000000..6a984ad3f --- /dev/null +++ b/deployment/changesets/cs_mcms_configure.go @@ -0,0 +1,106 @@ +package changesets + +import ( + "fmt" + + "github.com/smartcontractkit/mcms" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + "github.com/smartcontractkit/chainlink-sui/deployment" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + mcmsops "github.com/smartcontractkit/chainlink-sui/deployment/ops/mcms" + "github.com/smartcontractkit/chainlink-sui/deployment/utils" +) + +type ConfigureMCMSConfig struct { + mcmsops.ConfigureMCMSSeqInput + TimelockConfig *utils.TimelockConfig // If nil, configuration will be executed directly +} + +var _ cldf.ChangeSetV2[ConfigureMCMSConfig] = ConfigureMCMS{} + +type ConfigureMCMS struct{} + +// VerifyPreconditions implements deployment.ChangeSetV2. +func (c ConfigureMCMS) VerifyPreconditions(e cldf.Environment, config ConfigureMCMSConfig) error { + return nil +} + +// Apply implements deployment.ChangeSetV2. +func (c ConfigureMCMS) Apply(e cldf.Environment, config ConfigureMCMSConfig) (cldf.ChangesetOutput, error) { + ab := cldf.NewMemoryAddressBook() + seqReports := make([]cld_ops.Report[any, any], 0) + + state, err := deployment.LoadOnchainStatesui(e) + if err != nil { + return cldf.ChangesetOutput{}, err + } + + suiChains := e.BlockChains.SuiChains() + + suiChain := suiChains[config.ChainSelector] + + deps := sui_ops.OpTxDeps{ + Client: suiChain.Client, + Signer: suiChain.Signer, + GetCallOpts: func() *bind.CallOpts { + b := uint64(400_000_000) + return &bind.CallOpts{ + WaitForExecution: true, + GasBudget: &b, + } + }, + SuiRPC: suiChain.URL, + } + + // If timelock proposal is to be generated, disable signer in deps + if config.TimelockConfig != nil { + deps.Signer = nil + } + + // Run ConfigureMCMS Sequence + configReport, err := cld_ops.ExecuteSequence(e.OperationsBundle, mcmsops.ConfigureMCMSSequence, deps, config.ConfigureMCMSSeqInput) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to configure MCMS for Sui chain %d: %w", config.ChainSelector, err) + } + + seqReports = append(seqReports, configReport.ToGenericReport()) + + mcmsProposal := mcms.TimelockProposal{} + if config.TimelockConfig != nil { + defs := []cld_ops.Definition{} + inputs := []any{} + + for _, r := range configReport.Output.Reports { + defs = append(defs, r.Def) + inputs = append(inputs, r.Input) + } + + mcmsConfig := mcmsops.ProposalGenerateInput{ + ChainSelector: config.ChainSelector, + Defs: defs, + Inputs: inputs, + MmcsPackageID: state[config.ChainSelector].MCMSPackageID, + McmsStateObjID: state[config.ChainSelector].MCMSStateObjectID, + TimelockObjID: state[config.ChainSelector].MCMSTimelockObjectID, + AccountObjID: state[config.ChainSelector].MCMSAccountStateObjectID, + RegistryObjID: state[config.ChainSelector].MCMSRegistryObjectID, + TimelockConfig: *config.TimelockConfig, + } + + result, err := cld_ops.ExecuteSequence(e.OperationsBundle, mcmsops.MCMSDynamicProposalGenerateSeq, deps, mcmsConfig) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to generate MCMS proposal: %w", err) + } + mcmsProposal = result.Output + } + + return cldf.ChangesetOutput{ + AddressBook: ab, + Reports: seqReports, + MCMSTimelockProposals: []mcms.TimelockProposal{mcmsProposal}, + }, nil +} diff --git a/deployment/ops/mcms/op_set_config.go b/deployment/ops/mcms/op_set_config.go index d18caebd1..d8676e50a 100644 --- a/deployment/ops/mcms/op_set_config.go +++ b/deployment/ops/mcms/op_set_config.go @@ -8,12 +8,13 @@ import ( cselectors "github.com/smartcontractkit/chain-selectors" cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" - "github.com/smartcontractkit/chainlink-sui/bindings/bind" - modulemcms "github.com/smartcontractkit/chainlink-sui/bindings/generated/mcms/mcms" - sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" "github.com/smartcontractkit/mcms/sdk/evm" suisdk "github.com/smartcontractkit/mcms/sdk/sui" "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + modulemcms "github.com/smartcontractkit/chainlink-sui/bindings/generated/mcms/mcms" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" ) type MCMSSetConfigInput struct { @@ -29,6 +30,13 @@ type MCMSSetConfigInput struct { ClearRoot bool `json:"clearRoot"` } +var SetConfigMCMSOp = cld_ops.NewOperation( + sui_ops.NewSuiOperationName("mcms", "mcms", "set_config"), + semver.MustParse("0.1.0"), + "Set config in the MCMS contract", + setConfigMcmsHandler, +) + var setConfigMcmsHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input MCMSSetConfigInput) (output sui_ops.OpTxResult[cld_ops.EmptyInput], err error) { opts := deps.GetCallOpts() opts.Signer = deps.Signer @@ -92,10 +100,3 @@ var setConfigMcmsHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input M PackageId: input.McmsPackageID, }, err } - -var SetConfigMCMSOp = cld_ops.NewOperation( - sui_ops.NewSuiOperationName("mcms", "mcms", "set_config"), - semver.MustParse("0.1.0"), - "Set config in the MCMS contract", - setConfigMcmsHandler, -) diff --git a/deployment/ops/mcms/seq_accept_ownership.go b/deployment/ops/mcms/seq_accept_ownership.go new file mode 100644 index 000000000..07ad03f03 --- /dev/null +++ b/deployment/ops/mcms/seq_accept_ownership.go @@ -0,0 +1,64 @@ +package mcmsops + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/types" + + cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + "github.com/smartcontractkit/chainlink-sui/deployment/utils" +) + +type AcceptMCMSOwnershipSeqInput struct { + ChainSelector uint64 `json:"chainSelector" yaml:"chainSelector"` + PackageId string `json:"packageId" yaml:"packageId"` + McmsMultisigStateObjectId string `json:"mcmsMultisigStateObjectId" yaml:"mcmsMultisigStateObjectId"` + TimelockObjectId string `json:"timelockObjectId" yaml:"timelockObjectId"` + McmsAccountStateObjectId string `json:"mcmsAccountStateObjectId" yaml:"mcmsAccountStateObjectId"` + McmsRegistryObjectId string `json:"mcmsRegistryObjectId" yaml:"mcmsRegistryObjectId"` + McmsDeployerStateObjectId string `json:"mcmsDeployerStateObjectId" yaml:"mcmsDeployerStateObjectId"` +} + +var AcceptMCMSOwnershipSequence = cld_ops.NewSequence( + "sui-accept-mcms-ownership-seq", + semver.MustParse("0.1.0"), + "Generates the MCMS proposal to accept MCMS ownership via the timelock", + acceptMCMSOwnership, +) + +func acceptMCMSOwnership(env cld_ops.Bundle, deps sui_ops.OpTxDeps, input AcceptMCMSOwnershipSeqInput) (mcms.TimelockProposal, error) { + proposalInput := ProposalGenerateInput{ + Defs: []cld_ops.Definition{ + MCMSAcceptOwnershipOp.Def(), + }, + Inputs: []any{ + MCMSAcceptOwnershipInput{ + McmsPackageID: input.PackageId, + AccountObjectID: input.McmsAccountStateObjectId, + }, + }, + MmcsPackageID: input.PackageId, + McmsStateObjID: input.McmsMultisigStateObjectId, + TimelockObjID: input.TimelockObjectId, + AccountObjID: input.McmsAccountStateObjectId, + RegistryObjID: input.McmsRegistryObjectId, + DeployerStateObjID: input.McmsDeployerStateObjectId, + ChainSelector: input.ChainSelector, + TimelockConfig: utils.TimelockConfig{ + MCMSAction: types.TimelockActionSchedule, + MinDelay: 0, + OverrideRoot: false, + }, + } + + report, err := cld_ops.ExecuteSequence(env, MCMSDynamicProposalGenerateSeq, deps, proposalInput) + if err != nil { + return mcms.TimelockProposal{}, fmt.Errorf("failed to generate accept ownership proposal: %w", err) + } + + return report.Output, nil +} diff --git a/deployment/ops/mcms/seq_configure.go b/deployment/ops/mcms/seq_configure.go new file mode 100644 index 000000000..e97794b72 --- /dev/null +++ b/deployment/ops/mcms/seq_configure.go @@ -0,0 +1,77 @@ +package mcmsops + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + suisdk "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/types" + + cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" +) + +// ConfigureMCMSSeqInput defines the input for configuring MCMS +type ConfigureMCMSSeqInput struct { + ChainSelector uint64 `json:"chainSelector" yaml:"chainSelector"` + PackageId string `json:"packageId" yaml:"packageId"` + McmsAccountOwnerCapObjectId string `json:"mcmsAccountOwnerCapObjectId" yaml:"mcmsAccountOwnerCapObjectId"` + McmsAccountStateObjectId string `json:"mcmsAccountStateObjectId" yaml:"mcmsAccountStateObjectId"` + McmsMultisigStateObjectId string `json:"mcmsMultisigStateObjectId" yaml:"mcmsMultisigStateObjectId"` + + // Optional configs for each timelock role + // If nil, the role will not be configured + 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"` +} + +type ConfigureMCMSSeqOutput struct { + Reports []cld_ops.Report[any, any] +} + +var ConfigureMCMSSequence = cld_ops.NewSequence( + "sui-configure-mcms-seq", + semver.MustParse("0.1.0"), + "Configures the MCMS package with the provided timelock roles configuration", + configureMCMS, +) + +func configureMCMS(env cld_ops.Bundle, deps sui_ops.OpTxDeps, input ConfigureMCMSSeqInput) (ConfigureMCMSSeqOutput, error) { + // Configure each timelock role if config is provided + roleConfigs := []struct { + config *types.Config + role suisdk.TimelockRole + name string + }{ + {input.Bypasser, suisdk.TimelockRoleBypasser, "Bypasser"}, + {input.Canceller, suisdk.TimelockRoleCanceller, "Canceller"}, + {input.Proposer, suisdk.TimelockRoleProposer, "Proposer"}, + } + + opReports := make([]cld_ops.Report[any, any], 0) + for _, roleConfig := range roleConfigs { + if roleConfig.config == nil { + continue + } + + setConfigInput := MCMSSetConfigInput{ + ChainSelector: input.ChainSelector, + McmsPackageID: input.PackageId, + OwnerCap: input.McmsAccountOwnerCapObjectId, + McmsObjectID: input.McmsMultisigStateObjectId, + Role: roleConfig.role, + Config: *roleConfig.config, + } + + report, err := cld_ops.ExecuteOperation(env, SetConfigMCMSOp, deps, setConfigInput) + if err != nil { + return ConfigureMCMSSeqOutput{}, fmt.Errorf("failed to set config for role %s: %w", roleConfig.name, err) + } + opReports = append(opReports, report.ToGenericReport()) + env.Logger.Infow("Set MCMS config", "role", roleConfig.name, "chainSelector", input.ChainSelector) + } + + return ConfigureMCMSSeqOutput{Reports: opReports}, nil +} diff --git a/deployment/ops/mcms/seq_deploy.go b/deployment/ops/mcms/seq_deploy.go index 2aa58b391..eaec56642 100644 --- a/deployment/ops/mcms/seq_deploy.go +++ b/deployment/ops/mcms/seq_deploy.go @@ -6,13 +6,11 @@ import ( "github.com/Masterminds/semver/v3" "github.com/smartcontractkit/mcms" - suisdk "github.com/smartcontractkit/mcms/sdk/sui" "github.com/smartcontractkit/mcms/types" cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" - "github.com/smartcontractkit/chainlink-sui/deployment/utils" ) // DeployMCMSSeqInput defines the input for deploying MCMS with timelock roles configuration @@ -36,93 +34,64 @@ var DeployMCMSSequence = cld_ops.NewSequence( "sui-deploy-mcms-seq", semver.MustParse("0.1.0"), "Deploys the MCMS package, sets the initial configuration, init the ownership transfer to self and generates the proposal to accept the ownership", - func(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{}) - if err != nil { - return DeployMCMSSeqOutput{}, fmt.Errorf("failed to deploy MCMS: %w", err) - } - - // Configure each timelock role if config is provided - roleConfigs := []struct { - config *types.Config - role suisdk.TimelockRole - name string - }{ - {input.Bypasser, suisdk.TimelockRoleBypasser, "Bypasser"}, - {input.Canceller, suisdk.TimelockRoleCanceller, "Canceller"}, - {input.Proposer, suisdk.TimelockRoleProposer, "Proposer"}, - } - - for _, roleConfig := range roleConfigs { - if roleConfig.config != nil { - setConfigInput := MCMSSetConfigInput{ - ChainSelector: input.ChainSelector, - McmsPackageID: deployReport.Output.PackageId, - OwnerCap: deployReport.Output.Objects.McmsAccountOwnerCapObjectId, - McmsObjectID: deployReport.Output.Objects.McmsMultisigStateObjectId, - Role: roleConfig.role, - Config: *roleConfig.config, - } - - _, err = cld_ops.ExecuteOperation(env, SetConfigMCMSOp, deps, setConfigInput) - if err != nil { - return DeployMCMSSeqOutput{}, fmt.Errorf("failed to set config for role %s: %w", roleConfig.name, err) - } - - env.Logger.Infow("Set MCMS config", "role", roleConfig.name, "chainSelector", input.ChainSelector) - } - } - - // Init the ownership transfer to self - transferOwnershipInput := MCMSTransferOwnershipInput{ - McmsPackageID: deployReport.Output.PackageId, - OwnerCap: deployReport.Output.Objects.McmsAccountOwnerCapObjectId, - AccountObjectID: deployReport.Output.Objects.McmsAccountStateObjectId, - } - _, err = cld_ops.ExecuteOperation(env, MCMSTransferOwnershipOp, deps, transferOwnershipInput) - if err != nil { - return DeployMCMSSeqOutput{}, fmt.Errorf("failed to transfer ownership to MCMS: %w", err) - } - - // Generate the proposal to accept the ownership - proposalInput := ProposalGenerateInput{ - Defs: []cld_ops.Definition{ - MCMSAcceptOwnershipOp.Def(), - }, - Inputs: []any{ - MCMSAcceptOwnershipInput{ - McmsPackageID: deployReport.Output.PackageId, - AccountObjectID: deployReport.Output.Objects.McmsAccountStateObjectId, - }, - }, - // MCMS related - MmcsPackageID: deployReport.Output.PackageId, - McmsStateObjID: deployReport.Output.Objects.McmsMultisigStateObjectId, - TimelockObjID: deployReport.Output.Objects.TimelockObjectId, - AccountObjID: deployReport.Output.Objects.McmsAccountStateObjectId, - RegistryObjID: deployReport.Output.Objects.McmsRegistryObjectId, - DeployerStateObjID: deployReport.Output.Objects.McmsDeployerStateObjectId, - ChainSelector: uint64(input.ChainSelector), - // Proposal - TimelockConfig: utils.TimelockConfig{ - MCMSAction: types.TimelockActionSchedule, - MinDelay: 0, - OverrideRoot: false, - }, - } - - acceptOwnershipProposalReport, err := cld_ops.ExecuteSequence(env, MCMSDynamicProposalGenerateSeq, deps, proposalInput) - if err != nil { - return DeployMCMSSeqOutput{}, fmt.Errorf("failed to generate accept ownership proposal: %w", err) - } - - output := DeployMCMSSeqOutput{ - AcceptOwnershipProposal: acceptOwnershipProposalReport.Output, - PackageId: deployReport.Output.PackageId, - Objects: deployReport.Output.Objects, - } - - return output, nil - }, + deployMCMS, ) + +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{}) + if err != nil { + return DeployMCMSSeqOutput{}, fmt.Errorf("failed to deploy MCMS: %w", err) + } + + // Configure each timelock role if config is provided + cfgMCMSInput := ConfigureMCMSSeqInput{ + ChainSelector: input.ChainSelector, + PackageId: deployReport.Output.PackageId, + McmsAccountOwnerCapObjectId: deployReport.Output.Objects.McmsAccountOwnerCapObjectId, + McmsAccountStateObjectId: deployReport.Output.Objects.McmsAccountStateObjectId, + McmsMultisigStateObjectId: deployReport.Output.Objects.McmsMultisigStateObjectId, + Bypasser: input.Bypasser, + Proposer: input.Proposer, + Canceller: input.Canceller, + } + _, err = cld_ops.ExecuteSequence(env, ConfigureMCMSSequence, deps, cfgMCMSInput) + if err != nil { + return DeployMCMSSeqOutput{}, fmt.Errorf("failed to configure MCMS: %w", err) + } + + // Init the ownership transfer to self + transferOwnershipInput := MCMSTransferOwnershipInput{ + McmsPackageID: deployReport.Output.PackageId, + OwnerCap: deployReport.Output.Objects.McmsAccountOwnerCapObjectId, + AccountObjectID: deployReport.Output.Objects.McmsAccountStateObjectId, + } + _, err = cld_ops.ExecuteOperation(env, MCMSTransferOwnershipOp, deps, transferOwnershipInput) + if err != nil { + return DeployMCMSSeqOutput{}, fmt.Errorf("failed to transfer ownership to MCMS: %w", err) + } + + // Generate accept ownership proposal + acceptOwnershipInput := AcceptMCMSOwnershipSeqInput{ + ChainSelector: input.ChainSelector, + PackageId: deployReport.Output.PackageId, + McmsAccountStateObjectId: deployReport.Output.Objects.McmsAccountStateObjectId, + McmsDeployerStateObjectId: deployReport.Output.Objects.McmsDeployerStateObjectId, + McmsMultisigStateObjectId: deployReport.Output.Objects.McmsMultisigStateObjectId, + McmsRegistryObjectId: deployReport.Output.Objects.McmsRegistryObjectId, + TimelockObjectId: deployReport.Output.Objects.TimelockObjectId, + } + + acceptOwnershipProposalReport, err := cld_ops.ExecuteSequence(env, AcceptMCMSOwnershipSequence, deps, acceptOwnershipInput) + if err != nil { + return DeployMCMSSeqOutput{}, fmt.Errorf("failed to generate accept ownership proposal: %w", err) + } + + output := DeployMCMSSeqOutput{ + AcceptOwnershipProposal: acceptOwnershipProposalReport.Output, + PackageId: deployReport.Output.PackageId, + Objects: deployReport.Output.Objects, + } + + return output, nil +} diff --git a/deployment/ops/mcms/seq_deploy_test.go b/deployment/ops/mcms/seq_deploy_test.go index 3e7f12a3d..9d0d7d3e4 100644 --- a/deployment/ops/mcms/seq_deploy_test.go +++ b/deployment/ops/mcms/seq_deploy_test.go @@ -184,5 +184,13 @@ func TestDeployMCMSSeq(t *testing.T) { require.NotEmpty(t, objects.McmsAccountStateObjectId, "MCMS Account State Object ID should not be empty") require.NotEmpty(t, objects.McmsAccountOwnerCapObjectId, "MCMS Account Owner Cap Object ID should not be empty") require.NotEmpty(t, report.Output.PackageId, "Package ID should not be empty") - require.NotEmpty(t, report.Output.AcceptOwnershipProposal, "Accept Ownership Proposal should not be empty") + + // Verify the accept ownership proposal was generated correctly + proposal := report.Output.AcceptOwnershipProposal + require.NotEmpty(t, proposal.Description, "Proposal description should not be empty") + require.Contains(t, proposal.Description, "accept_ownership", "Proposal should reference accept_ownership operation") + require.NotEmpty(t, proposal.Version, "Proposal version should not be empty") + require.NotZero(t, proposal.ValidUntil, "Proposal ValidUntil should be set") + require.NotEmpty(t, proposal.Operations, "Proposal should contain operations") + require.Len(t, proposal.Operations, 1, "Proposal should contain exactly one operation") } diff --git a/deployment/ops/rmn/op_curse_uncurse.go b/deployment/ops/rmn/op_curse_uncurse.go new file mode 100644 index 000000000..79f575902 --- /dev/null +++ b/deployment/ops/rmn/op_curse_uncurse.go @@ -0,0 +1,146 @@ +package rmn + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + module_rmn_remote "github.com/smartcontractkit/chainlink-sui/bindings/generated/ccip/ccip/rmn_remote" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" +) + +type NoObjects struct{} + +type CurseUncurseChainInput struct { + CCIPPackageId string + StateObjectId string + OwnerCapObjectId string + Subjects [][]byte +} + +var CurseChainOp = cld_ops.NewOperation( + sui_ops.NewSuiOperationName("ccip", "rmn_remote", "curse_chain"), + semver.MustParse("0.1.0"), + "Curse a chain selector in the CCIP RMN Remote contract", + curseChainHandler, +) + +func curseChainHandler(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input CurseUncurseChainInput) (output sui_ops.OpTxResult[NoObjects], err error) { + if len(input.Subjects) == 0 { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("at least one subject is required to curse") + } + + contract, err := module_rmn_remote.NewRmnRemote(input.CCIPPackageId, deps.Client) + if err != nil { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to create RMN Remote contract: %w", err) + } + + encodedCall, err := contract.Encoder().CurseMultiple( + bind.Object{Id: input.StateObjectId}, + bind.Object{Id: input.OwnerCapObjectId}, + input.Subjects, + ) + if err != nil { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to encode curse call: %w", err) + } + + call, err := sui_ops.ToTransactionCall(encodedCall, input.StateObjectId) + if err != nil { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to build transaction call for curse: %w", err) + } + + if deps.Signer == nil { + b.Logger.Infow("Skipping execution of curse_chain on RMN Remote as no signer provided") + return sui_ops.OpTxResult[NoObjects]{ + Digest: "", + PackageId: input.CCIPPackageId, + Objects: NoObjects{}, + Call: call, + }, nil + } + + opts := deps.GetCallOpts() + opts.Signer = deps.Signer + tx, err := contract.Bound().ExecuteTransaction( + b.GetContext(), + opts, + encodedCall, + ) + if err != nil { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to execute curse_chain on RMN Remote: %w", err) + } + + b.Logger.Infow("Chains cursed on RMN Remote", "digest", tx.Digest, "count", len(input.Subjects)) + + return sui_ops.OpTxResult[NoObjects]{ + Digest: tx.Digest, + PackageId: input.CCIPPackageId, + Objects: NoObjects{}, + Call: call, + }, nil +} + +var UncurseChainOp = cld_ops.NewOperation( + sui_ops.NewSuiOperationName("ccip", "rmn_remote", "uncurse_chain"), + semver.MustParse("0.1.0"), + "Uncurse a chain selector in the CCIP RMN Remote contract", + uncurseChainHandler, +) + +func uncurseChainHandler(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input CurseUncurseChainInput) (output sui_ops.OpTxResult[NoObjects], err error) { + if len(input.Subjects) == 0 { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("at least one subject is required to uncurse") + } + + contract, err := module_rmn_remote.NewRmnRemote(input.CCIPPackageId, deps.Client) + if err != nil { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to create RMN Remote contract: %w", err) + } + + encodedCall, err := contract.Encoder().UncurseMultiple( + bind.Object{Id: input.StateObjectId}, + bind.Object{Id: input.OwnerCapObjectId}, + input.Subjects, + ) + if err != nil { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to encode uncurse call: %w", err) + } + + call, err := sui_ops.ToTransactionCall(encodedCall, input.StateObjectId) + if err != nil { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to build transaction call for uncurse: %w", err) + } + + if deps.Signer == nil { + b.Logger.Infow("Skipping execution of uncurse_chain on RMN Remote as no signer provided") + return sui_ops.OpTxResult[NoObjects]{ + Digest: "", + PackageId: input.CCIPPackageId, + Objects: NoObjects{}, + Call: call, + }, nil + } + + opts := deps.GetCallOpts() + opts.Signer = deps.Signer + tx, err := contract.Bound().ExecuteTransaction( + b.GetContext(), + opts, + encodedCall, + ) + if err != nil { + return sui_ops.OpTxResult[NoObjects]{}, fmt.Errorf("failed to execute uncurse_chain on RMN Remote: %w", err) + } + + b.Logger.Infow("Chains uncursed on RMN Remote", "digest", tx.Digest, "count", len(input.Subjects)) + + return sui_ops.OpTxResult[NoObjects]{ + Digest: tx.Digest, + PackageId: input.CCIPPackageId, + Objects: NoObjects{}, + Call: call, + }, nil +} diff --git a/integration-tests/deploy/deploy_test.go b/integration-tests/deploy/deploy_test.go index e8604522e..b2f448be9 100644 --- a/integration-tests/deploy/deploy_test.go +++ b/integration-tests/deploy/deploy_test.go @@ -3,6 +3,8 @@ package deploy import ( + "bytes" + "encoding/binary" "fmt" "testing" @@ -11,6 +13,8 @@ import ( cselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + module_rmn_remote "github.com/smartcontractkit/chainlink-sui/bindings/generated/ccip/ccip/rmn_remote" "github.com/smartcontractkit/chainlink-sui/deployment" "github.com/smartcontractkit/chainlink-sui/deployment/changesets" burnminttokenpoolops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip_burn_mint_token_pool" @@ -48,6 +52,28 @@ func (s *DeployTestSuite) TestDeployAndConfigureSuiChain() { // Phase 10: Deploy Managed Token Pool s.DeployManagedTokenPool() + // Phase 11: Test curse/uncurse RMN subjects + // this test is placed here since we have a full deployment to test against + curseCfg := changesets.CurseUncurseChainsConfig{ + SuiChainSelector: SuiChainSelector, + OperationType: string(changesets.CurseOperationType), + IsGlobalCurse: false, + DestChainSelectors: []uint64{EVMChainSelector}, + } + curseOut, err := changesets.CurseUncurseChains{}.Apply(s.env, curseCfg) + s.Require().NoError(err, "failed to curse RMN subjects") + s.Require().Len(curseOut.Reports, 1, "expected single curse report") + + s.assertRMNCurseSubjects(EVMChainSelector, true) + + uncurseCfg := curseCfg + uncurseCfg.OperationType = string(changesets.UncurseOperationType) + uncurseOut, err := changesets.CurseUncurseChains{}.Apply(s.env, uncurseCfg) + s.Require().NoError(err, "failed to uncurse RMN subjects") + s.Require().Len(uncurseOut.Reports, 1, "expected single uncurse report") + + s.assertRMNCurseSubjects(EVMChainSelector, false) + // Load view and check deployments states, err := deployment.LoadOnchainStatesui(s.env) state := states[cselectors.SUI_LOCALNET.Selector] @@ -459,3 +485,34 @@ func (s *DeployTestSuite) DeployManagedTokenPool() { err = s.env.ExistingAddresses.Merge(tokenPoolOut.AddressBook) s.Require().NoError(err, "failed to merge managed token pool addresses") } + +func (s *DeployTestSuite) assertRMNCurseSubjects(selector uint64, expectCursed bool) { + s.T().Helper() + + s.Require().NotEmpty(s.ccipPackageID, "CCIP package ID not set for RMN assertions") + s.Require().NotEmpty(s.ccipObjectRef, "CCIP object ref not set for RMN assertions") + + contract, err := module_rmn_remote.NewRmnRemote(s.ccipPackageID, s.client) + s.Require().NoError(err, "failed to create RMN remote binding") + + callOpts := &bind.CallOpts{Signer: s.signer} + subjects, err := contract.DevInspect().GetCursedSubjects(s.T().Context(), callOpts, bind.Object{Id: s.ccipObjectRef}) + s.Require().NoError(err, "failed to fetch cursed subjects") + + target := make([]byte, 16) + binary.BigEndian.PutUint64(target[8:], selector) + found := false + for _, subj := range subjects { + if bytes.Equal(subj, target) { + found = true + break + } + } + + if expectCursed { + s.Require().True(found, "expected selector to be cursed") + return + } + + s.Require().False(found, "expected selector to be uncursed") +} diff --git a/relayer/chainreader/indexer/transactions_indexer.go b/relayer/chainreader/indexer/transactions_indexer.go index 021f720c6..5240c7c54 100644 --- a/relayer/chainreader/indexer/transactions_indexer.go +++ b/relayer/chainreader/indexer/transactions_indexer.go @@ -490,11 +490,14 @@ func (tIndexer *TransactionsIndexer) syncTransmitterTransactions(ctx context.Con executionStateChanged := map[string]any{ "source_chain_selector": fmt.Sprintf("%d", sourceChainSelector), "sequence_number": fmt.Sprintf("%d", execReport.Message.Header.SequenceNumber), - "message_id": execReport.Message.Header.MessageID, - "message_hash": messageHash[:], - "state": uint8(3), // 3 = FAILURE + // The conversion to []any is needed to avoid the default Go DB SDK behaviour of converting the byte slice to encoded base64 string. + "message_id": codec.BytesToAnySlice(execReport.Message.Header.MessageID), + "message_hash": codec.BytesToAnySlice(messageHash[:]), + "state": uint8(3), // 3 = FAILURE } + tIndexer.logger.Debugw("About to insert synthetic ExecutionStateChanged event", "executionStateChanged", executionStateChanged) + // normalize keys executionStateChanged = common.ConvertMapKeysToCamelCase(executionStateChanged).(map[string]any) @@ -525,7 +528,8 @@ func (tIndexer *TransactionsIndexer) syncTransmitterTransactions(ctx context.Con TxDigest: txDigestHex, BlockHeight: checkpointResponse.SequenceNumber, BlockHash: blockHashBytes, - BlockTimestamp: blockTimestamp, + // Convert to seconds for consistency with events indexer. + BlockTimestamp: blockTimestamp / 1000, Data: executionStateChanged, IsSynthetic: true, } diff --git a/relayer/chainreader/indexer/transactions_indexer_test.go b/relayer/chainreader/indexer/transactions_indexer_test.go index 93cda4ec7..9a6f0f97b 100644 --- a/relayer/chainreader/indexer/transactions_indexer_test.go +++ b/relayer/chainreader/indexer/transactions_indexer_test.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "fmt" "os" + "strings" "testing" "time" @@ -226,7 +227,16 @@ func TestTransactionsIndexer(t *testing.T) { pollingInterval := 4 * time.Second syncTimeout := 3 * time.Second + type OfframpExecutionStateChanged struct { + SourceChainSelector uint64 `json:"sourceChainSelector"` + SequenceNumber uint64 `json:"sequenceNumber"` + MessageId string `json:"messageId"` + MessageHash string `json:"messageHash"` + State int `json:"state"` + } + readerConfig := config.ChainReaderConfig{ + IsLoopPlugin: false, Modules: map[string]*config.ChainReaderModule{ "OffRamp": { Name: "offramp", @@ -240,6 +250,7 @@ func TestTransactionsIndexer(t *testing.T) { Module: "offramp", Event: "ExecutionStateChanged", }, + ExpectedEventType: &OfframpExecutionStateChanged{}, }, "SourceChainConfigSet": { Name: "offramp", @@ -277,7 +288,6 @@ func TestTransactionsIndexer(t *testing.T) { }, }, }, - IsLoopPlugin: false, EventsIndexer: config.EventsIndexerConfig{ PollingInterval: pollingInterval, SyncTimeout: syncTimeout, @@ -354,7 +364,8 @@ func TestTransactionsIndexer(t *testing.T) { // helper: returns true if at least one event with the given key exists for the contract hasEvent := func(contract types.BoundContract, key string) bool { - events, err := cReader.QueryKey(ctx, contract, query.KeyFilter{Key: key}, query.LimitAndSort{}, &database.EventRecord{}) + dataType := map[string]any{} + events, err := cReader.QueryKey(ctx, contract, query.KeyFilter{Key: key}, query.LimitAndSort{}, &dataType) if err != nil { log.Errorw("Error querying events", "contract", contract.Name, "key", key, "error", err) return false @@ -362,7 +373,7 @@ func TestTransactionsIndexer(t *testing.T) { found := len(events) > 0 if found { - log.Debugw("Event found") + log.Debugw("Event found (hasEvent)", "events", events) } else { log.Debugw("Event not found", events) } @@ -380,7 +391,7 @@ func TestTransactionsIndexer(t *testing.T) { found := len(events) > 0 if found { - log.Debugw("Event found") + log.Debugw("Event found (hasEventDBOnlyCheck)", "events", events) } else { log.Debugw("Event not found", events) } @@ -388,32 +399,25 @@ func TestTransactionsIndexer(t *testing.T) { return found } - // 1. Create a few transactions + // Create a few transactions and check they exist via the RPC for range 3 { CreateFailedTransaction(t, relayerClient, packageId, counterObjectId, accountAddress, publicKeyBytes) } - // 2. Query the transactions and ensure that they are findable from the RPC txs_1, err := relayerClient.QueryTransactions(ctx, accountAddress, nil, nil) require.NoError(t, err) require.GreaterOrEqual(t, len(txs_1.Data), 3, "Expected at least 3 transactions") - // 3. Start the indexers and ensure that the events / transactions are indexed - go func() { - _ = cReader.Start(ctx) - _ = txnIndexer.Start(ctx) - }() - - // 4. Create a successful transaction to trigger the transactions indexer + // Insert successful transactions to ensure events queries do not pick them up CreateSuccessfulTransaction(t, relayerClient, packageId, counterObjectId, accountAddress, publicKeyBytes) time.Sleep(15 * time.Second) - // 5. Create the initial OCR event to initiate transaction indexing + // Create the initial OCR event to initiate transaction indexing setConfigResponse, setConfigErr := SetOCRConfig(t, relayerClient, packageId, counterObjectId, accountAddress, publicKeyBytes) require.NoError(t, setConfigErr) testutils.PrettyPrintDebug(log, setConfigResponse, "setConfigResponse") - // 4.a. Wait for the configs to be set + // Wait for the configs to be set and stored in the DB require.Eventually(t, func() bool { okConfig := hasEventDBOnlyCheck(packageId, "ocr3_base", "ConfigSet") okSrcCfg := hasEventDBOnlyCheck(packageId, "offramp", "SourceChainConfigSet") @@ -426,7 +430,7 @@ func TestTransactionsIndexer(t *testing.T) { return okConfig && okSrcCfg }, 90*time.Second, 5*time.Second) - // 5. Create a failed PTB transaction + // Create a failed PTB transaction reportStr := "9b3c1f221aa3f0cc579b9518768ead0a57cc3d9d782049b702fab91dd723c757f287d20217d8e69b9b3c1f221aa3f0ccec1182faa7c27b87a40200000000000000000000000000001407775923481a094e41d51449b0b0f979c126a3b003486579b4dcbf61d5f5f447ae448e3c1503a811d83bdc074a8712ebeb241fd649b372e040420f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" reportBytes, err := hex.DecodeString(reportStr) require.NoError(t, err) @@ -446,10 +450,26 @@ func TestTransactionsIndexer(t *testing.T) { response, _ := relayerClient.FinishPTBAndSend(ctx, txnSigner, ptbTx, client.WaitForLocalExecution) require.Equal(t, "failure", response.Status.Status) - // 5.b. Wait for the execution state changed event to be indexed + // Wait for the execution state changed event to be indexed and inserted into the DB require.Eventually(t, func() bool { return hasEvent(boundContracts[0], "ExecutionStateChanged") }, 90*time.Second, 5*time.Second) + + // Fresh query for events of type ExecutionStateChanged to check values against decoded report + events, err := cReader.QueryKey(ctx, boundContracts[0], query.KeyFilter{Key: "ExecutionStateChanged"}, query.LimitAndSort{}, &OfframpExecutionStateChanged{}) + require.NoError(t, err) + require.NotEmpty(t, events) + + executionStateChanged := events[0].Data.(*OfframpExecutionStateChanged) + + decodedReport, err := codec.DeserializeExecutionReport(reportBytes) + require.NoError(t, err) + require.NotNil(t, decodedReport) + + // The message ID is expected to be encoded as a hex string due to the use of `ExpectedEventType` in the ChainReader config + // for the relevant event. + require.Equal(t, "0x"+hex.EncodeToString(decodedReport.Message.Header.MessageID), executionStateChanged.MessageId) + require.True(t, strings.HasPrefix(executionStateChanged.MessageHash, "0x")) }) } diff --git a/relayer/codec/type_converters.go b/relayer/codec/type_converters.go index 88f3b8924..c330c21d5 100644 --- a/relayer/codec/type_converters.go +++ b/relayer/codec/type_converters.go @@ -765,3 +765,11 @@ func UnifiedTypeConverterHook(from, to reflect.Type, data any) (any, error) { // Use the global converter return getDefaultTypeConverter().Convert(from, to, data) } + +func BytesToAnySlice(b []byte) []any { + result := make([]any, len(b)) + for i, v := range b { + result[i] = v + } + return result +}