feat(canton): integrate Canton support#613
Conversation
|
- Adapts to latest contract changes - Removes DAML client to use raw pb client
- E2e tests, contract updates
## Desc Resolve targetCids execution time - Added TargetTemplateID field to AdditionalFields - Added ResolveTargetContractID resolver function - Updated `TimelockExecutor.Execute` and `Executor.ExecuteOperation` for auto-resolution
# Conflicts: # go.mod # go.sum # types/chain_selector.go
Resolve go.mod/go.sum conflicts by keeping main dependency updates alongside Canton-specific modules (chainlink-canton, go-daml, dazl-client, chainlink-deployments-framework). Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Integrates Canton (Daml-based) chain support into MCMS by adding a new sdk/canton package implementing the Encoder/Inspector/Executor/Configurer/Timelock interfaces against the chainlink-canton bindings, wiring Canton into the factory, validation, chain selectors, and chain-wrapper builders, and adding an e2e test suite plus task entry.
Changes:
- New
sdk/cantonpackage: Merkle hashing matching the DamlCrypto.damllayout, MCMS/timelock SetConfig/SetRoot/Execute/Schedule/Cancel/Bypass, InstanceAddress→contract-ID resolver, and metadata inference for timelock proposal conversion. - Wire Canton into shared infrastructure:
factory.go,validation.go,types/chain_selector.go,timelock_proposal.go(Canton-onlyEnsureChainMetadatastep),chainwrappers/*(newCantonChainaccessor + mock),go.moddeps + replace directives. - New
e2e/tests/cantonsuite (configurer, inspector, timelock proposal/cancel/bypass, set-root/execute) ande2e:cantontask.
Reviewed changes
Copilot reviewed 38 out of 40 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| validation.go | Adds Canton case to validateChainMetadata (per-tx validator not wired). |
| types/chain_selector.go | Adds FamilyCanton to supported families. |
| timelock_proposal.go | Adds Canton-only EnsureChainMetadata step during Convert. |
| factory.go | Registers Canton encoder and OperationID. |
| go.mod | Adds Canton/Daml deps and replace directives. |
| taskfiles/test/Taskfile.yml | Adds e2e:canton task. |
| sdk/canton/chain_metadata.go | Defines TimelockRole, AdditionalFieldsMetadata, and NewChainMetadata. |
| sdk/canton/chain_metadata_infer.go | Infers metadata using hard-coded instance ID candidates and default chain id 1. |
| sdk/canton/chain_metadata_infer_test.go | Unit test for inference happy path. |
| sdk/canton/encoder.go | Merkle leaf hashing with domain separators and length prefixes. |
| sdk/canton/configurer.go | SetConfig against MCMS contract; sets Hash to a literal "tx.Digest". |
| sdk/canton/inspector.go | Reads config/op count/root/root metadata; converts MultisigConfig to types.Config. |
| sdk/canton/inspector_test.go | E2E-tagged unit tests for toConfig hierarchy. |
| sdk/canton/executor.go | ExecuteOperation/SetRoot; manual map-based metadata parsing; value receivers; dead PadLeft32. |
| sdk/canton/timelock_converter.go | Schedule/Bypass/Cancel encoding and OperationID. |
| sdk/canton/timelock_crypto.go | HashTimelockOpId matching Daml hashing. |
| sdk/canton/timelock_executor.go | ExecuteScheduledBatch submission. |
| sdk/canton/timelock_inspector.go | Read-only choices (IsOperation*, GetMinDelay) and role getters. |
| sdk/canton/resolver.go | Resolves InstanceAddress hex to active contract ID. |
| sdk/canton/helpers.go | Template ID parsing/formatting and constants. |
| chainwrappers/chainaccessor.go | Adds CantonChain(selector) and fixes a Sui alias. |
| chainwrappers/mocks/chain_accessor.go | Generated CantonChain mock. |
| chainwrappers/converters.go | Canton converter case. |
| chainwrappers/inspectors.go | Canton inspector case + cantonRole helper. |
| chainwrappers/executors.go | Canton executor case (requires *cantonsdk.Encoder). |
| chainwrappers/timelock_executors.go | Canton timelock executor case. |
| e2e/tests/setup.go | Adds CantonChain config and CantonBlockchain to shared setup. |
| e2e/tests/runner_test.go | Registers TestCantonSuite. |
| e2e/config.canton.toml | Canton e2e config. |
| e2e/tests/canton/* | New e2e suites; timelock_cancel.go contains a non-compiling salt expression. |
Files not reviewed (1)
- chainwrappers/mocks/chain_accessor.go: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| AddChainMetadata(s.chainSelector, cancellerMetadata). | ||
| SetAction(types.TimelockActionCancel). | ||
| SetDelay(delay). | ||
| SetSalt((*common.Hash)(new(scheduleProposal.Salt()))). |
| } | ||
|
|
||
| return types.TransactionResult{ | ||
| Hash: "tx.Digest", |
| var additionalFields map[string]any | ||
| if unmarshalErr := json.Unmarshal(metadata.AdditionalFields, &additionalFields); unmarshalErr != nil { | ||
| return types.TransactionResult{}, fmt.Errorf("failed to unmarshal additional fields: %w", unmarshalErr) | ||
| } | ||
|
|
||
| // Extract fields with type assertions | ||
| if chainId, ok := additionalFields["chainId"].(float64); ok { | ||
| rootMetadata.ChainId = cantontypes.INT64(int64(chainId)) | ||
| } | ||
| if multisigId, ok := additionalFields["multisigId"].(string); ok { | ||
| rootMetadata.MultisigId = cantontypes.TEXT(multisigId) | ||
| } | ||
| if preOpCount, ok := additionalFields["preOpCount"].(float64); ok { | ||
| rootMetadata.PreOpCount = cantontypes.INT64(int64(preOpCount)) | ||
| } | ||
| if postOpCount, ok := additionalFields["postOpCount"].(float64); ok { | ||
| rootMetadata.PostOpCount = cantontypes.INT64(int64(postOpCount)) | ||
| } | ||
| if overridePreviousRoot, ok := additionalFields["overridePreviousRoot"].(bool); ok { | ||
| rootMetadata.OverridePreviousRoot = cantontypes.BOOL(overridePreviousRoot) | ||
| } |
| case chainsel.FamilyCanton: | ||
| return canton.ValidateChainMetadata(metadata) |
|
|
||
| func PadLeft32(hexStr string) string { | ||
| if len(hexStr) >= hexWordLen { | ||
| return hexStr[:hexWordLen] | ||
| } | ||
|
|
||
| return strings.Repeat("0", hexWordLen-len(hexStr)) + hexStr | ||
| } |
| }, nil | ||
| } | ||
|
|
||
| func (e Executor) ExecuteOperation( |
| // defaultMCMSInstanceIDCandidates are common Canton MCMS instance IDs tried when inferring | ||
| // chain metadata from mcmAddress + party (e.g. proposals generated without additionalFields). | ||
| var defaultMCMSInstanceIDCandidates = []string{"mcms-ccip", "mcms-ccv", "mcms"} |
| return fields, nil | ||
| } | ||
|
|
||
| const defaultCantonChainID int64 = 1 |
gustavogama-cll
left a comment
There was a problem hiding this comment.
Submitting a few thoughts after a shallow initial pass because I think there is one request (the last comment) that might require a bigger refactoring.
There was a problem hiding this comment.
I don't see canton changes to the chainwrappers/executors_test.go in this PR, nor in #628.
| } | ||
| } | ||
|
|
||
| func cantonRole(action types.TimelockAction) cantonsdk.TimelockRole { |
There was a problem hiding this comment.
could you please move this to the sdk/canton package (like aptos does it)
There was a problem hiding this comment.
moved to canton sdk package under canton_helpers.go
| return nil, fmt.Errorf("missing Canton chain participant for selector %d", rawSelector) | ||
| } | ||
| participant := ch.Participants[0] | ||
| mcmsParties := make([]string, len(ch.Participants)) |
| return Proposal{}, nil, fmt.Errorf("missing chain metadata for chainSelector %d", chainSelector) | ||
| } | ||
|
|
||
| family, err := types.GetChainSelectorFamily(chainSelector) //nolint:contextcheck //OPT-400 |
There was a problem hiding this comment.
we shouldn't need any chain-specific logic here. And we shouldn't be adding to the chain metadata at runtime -- these attributes should be specified by the user when building the proposal.
I took a peek at what "EnsureChainMetadata" does and it seems that at least the attributes that are related to mcms can be computed or retrieved without the need to have them in the chain metadata.
Let's try to keep the "timelock_proposal.Convert" method intact -- if you are unsure about to work around it, let me know and we can iterate.
There was a problem hiding this comment.
agreed and updated. Now users are expected to supply complete chain metadata when building the proposal
| AddChainMetadata(s.chainSelector, cancellerMetadata). | ||
| SetAction(types.TimelockActionCancel). | ||
| SetDelay(delay). | ||
| SetSalt((*common.Hash)(new(scheduleProposal.Salt()))). |
| } | ||
|
|
||
| return types.TransactionResult{ | ||
| Hash: "tx.Digest", |
| case chainsel.FamilyCanton: | ||
| return canton.ValidateChainMetadata(metadata) |
| case chainsel.FamilyCanton: | ||
| ch, ok := chains.CantonChain(rawSelector) | ||
| if !ok || len(ch.Participants) == 0 { | ||
| return nil, fmt.Errorf("missing Canton chain participant for selector %d", rawSelector) | ||
| } | ||
| participant := ch.Participants[0] | ||
| mcmsParties := lo.Map(ch.Participants, func(p cantonsdk.Participant, _ int) string { return p.PartyID }) | ||
|
|
||
| return cantonsdk.NewTimelockExecutor( | ||
| participant.LedgerServices.Command, | ||
| participant.LedgerServices.State, | ||
| participant.PartyID, | ||
| mcmsParties, | ||
| ), nil |
| func PadLeft32(hexStr string) string { | ||
| if len(hexStr) >= hexWordLen { | ||
| return hexStr[:hexWordLen] | ||
| } | ||
|
|
||
| return strings.Repeat("0", hexWordLen-len(hexStr)) + hexStr | ||
| } |
| // Build exercise command using generated bindings | ||
| mcmsContract := mcmscore.MCMS{} | ||
| exerciseCmd := mcmsContract.SetConfig(mcmsContractID, input) | ||
|
|
||
| // Parse template ID | ||
| packageID, moduleName, entityName, err := parseTemplateIDFromString(mcmsContract.GetTemplateID()) | ||
| if err != nil { | ||
| return types.TransactionResult{}, fmt.Errorf("failed to parse template ID: %w", err) | ||
| } | ||
|
|
||
| // Convert input to choice argument | ||
| choiceArgument := ledger.MapToValue(input) | ||
|
|
||
| commandID := uuid.Must(uuid.NewUUID()).String() | ||
| submitResp, err := c.client.SubmitAndWaitForTransaction(ctx, &apiv2.SubmitAndWaitForTransactionRequest{ | ||
| Commands: &apiv2.Commands{ | ||
| WorkflowId: "mcms-set-config", | ||
| CommandId: commandID, | ||
| ActAs: []string{c.mcmsParties[0]}, | ||
| ReadAs: c.mcmsParties, | ||
| Commands: []*apiv2.Command{{ | ||
| Command: &apiv2.Command_Exercise{ | ||
| Exercise: &apiv2.ExerciseCommand{ | ||
| TemplateId: &apiv2.Identifier{ | ||
| PackageId: packageID, | ||
| ModuleName: moduleName, | ||
| EntityName: entityName, | ||
| }, | ||
| ContractId: exerciseCmd.ContractID, | ||
| Choice: exerciseCmd.Choice, | ||
| ChoiceArgument: choiceArgument, | ||
| }, | ||
| }, | ||
| }}, | ||
| }, | ||
| }) |
|


No description provided.