From f50b2b62387081648c746837aa94177b99faec1e Mon Sep 17 00:00:00 2001 From: stackman27 Date: Thu, 4 Jun 2026 17:24:51 -0700 Subject: [PATCH] feat(canton): integrate Canton support Squash of canton-main for signed commit history. Original branch preserved as canton-main-backup-20260604. --- chainwrappers/chainaccessor.go | 6 +- chainwrappers/converters.go | 3 + chainwrappers/executors.go | 30 +- chainwrappers/executors_test.go | 49 ++- chainwrappers/inspectors.go | 15 + chainwrappers/mocks/chain_accessor.go | 57 ++++ chainwrappers/timelock_executors.go | 18 +- e2e/config.canton.toml | 10 + e2e/tests/canton/common.go | 270 +++++++++++++++ e2e/tests/canton/configurer.go | 133 ++++++++ e2e/tests/canton/executor.go | 76 +++++ e2e/tests/canton/inspector.go | 146 +++++++++ e2e/tests/canton/set_root_execute.go | 339 +++++++++++++++++++ e2e/tests/canton/shared_setup.go | 81 +++++ e2e/tests/canton/timelock_bypass.go | 143 ++++++++ e2e/tests/canton/timelock_cancel.go | 205 ++++++++++++ e2e/tests/canton/timelock_inspection.go | 163 +++++++++ e2e/tests/canton/timelock_proposal.go | 143 ++++++++ e2e/tests/runner_test.go | 11 + e2e/tests/setup.go | 22 +- factory.go | 9 + go.mod | 110 ++++--- go.sum | 291 ++++++++++++----- sdk/canton/chain.go | 24 ++ sdk/canton/chain_metadata.go | 123 +++++++ sdk/canton/chain_metadata_infer.go | 106 ++++++ sdk/canton/chain_metadata_infer_test.go | 112 +++++++ sdk/canton/chain_metadata_test.go | 115 +++++++ sdk/canton/configurer.go | 145 ++++++++ sdk/canton/constants.go | 22 ++ sdk/canton/encoder.go | 199 +++++++++++ sdk/canton/encoder_test.go | 93 ++++++ sdk/canton/executor.go | 358 ++++++++++++++++++++ sdk/canton/helpers.go | 66 ++++ sdk/canton/helpers_test.go | 52 +++ sdk/canton/inspector.go | 279 ++++++++++++++++ sdk/canton/inspector_test.go | 417 ++++++++++++++++++++++++ sdk/canton/resolver.go | 206 ++++++++++++ sdk/canton/timelock_converter.go | 240 ++++++++++++++ sdk/canton/timelock_crypto.go | 84 +++++ sdk/canton/timelock_crypto_test.go | 27 ++ sdk/canton/timelock_executor.go | 183 +++++++++++ sdk/canton/timelock_inspector.go | 251 ++++++++++++++ sdk/canton/transaction.go | 43 +++ sdk/canton/transaction_test.go | 96 ++++++ taskfiles/test/Taskfile.yml | 7 + types/chain_selector.go | 1 + validation.go | 6 + validation_test.go | 53 +++ 49 files changed, 5483 insertions(+), 155 deletions(-) create mode 100644 e2e/config.canton.toml create mode 100644 e2e/tests/canton/common.go create mode 100644 e2e/tests/canton/configurer.go create mode 100644 e2e/tests/canton/executor.go create mode 100644 e2e/tests/canton/inspector.go create mode 100644 e2e/tests/canton/set_root_execute.go create mode 100644 e2e/tests/canton/shared_setup.go create mode 100644 e2e/tests/canton/timelock_bypass.go create mode 100644 e2e/tests/canton/timelock_cancel.go create mode 100644 e2e/tests/canton/timelock_inspection.go create mode 100644 e2e/tests/canton/timelock_proposal.go create mode 100644 sdk/canton/chain.go create mode 100644 sdk/canton/chain_metadata.go create mode 100644 sdk/canton/chain_metadata_infer.go create mode 100644 sdk/canton/chain_metadata_infer_test.go create mode 100644 sdk/canton/chain_metadata_test.go create mode 100644 sdk/canton/configurer.go create mode 100644 sdk/canton/constants.go create mode 100644 sdk/canton/encoder.go create mode 100644 sdk/canton/encoder_test.go create mode 100644 sdk/canton/executor.go create mode 100644 sdk/canton/helpers.go create mode 100644 sdk/canton/helpers_test.go create mode 100644 sdk/canton/inspector.go create mode 100644 sdk/canton/inspector_test.go create mode 100644 sdk/canton/resolver.go create mode 100644 sdk/canton/timelock_converter.go create mode 100644 sdk/canton/timelock_crypto.go create mode 100644 sdk/canton/timelock_crypto_test.go create mode 100644 sdk/canton/timelock_executor.go create mode 100644 sdk/canton/timelock_inspector.go create mode 100644 sdk/canton/transaction.go create mode 100644 sdk/canton/transaction_test.go diff --git a/chainwrappers/chainaccessor.go b/chainwrappers/chainaccessor.go index ca1fed01c..e8b18fa60 100644 --- a/chainwrappers/chainaccessor.go +++ b/chainwrappers/chainaccessor.go @@ -8,8 +8,9 @@ import ( "github.com/xssnick/tonutils-go/ton" tonwallet "github.com/xssnick/tonutils-go/ton/wallet" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" evmsdk "github.com/smartcontractkit/mcms/sdk/evm" - suisuisdk "github.com/smartcontractkit/mcms/sdk/sui" + suisdk "github.com/smartcontractkit/mcms/sdk/sui" ) type ChainAccessor interface { @@ -21,7 +22,8 @@ type ChainAccessor interface { AptosClient(selector uint64) (aptoslib.AptosRpcClient, bool) AptosSigner(selector uint64) (aptoslib.TransactionSigner, bool) SuiClient(selector uint64) (sui.ISuiAPI, bool) - SuiSigner(selector uint64) (suisuisdk.SuiSigner, bool) + SuiSigner(selector uint64) (suisdk.SuiSigner, bool) TonClient(selector uint64) (ton.APIClientWrapped, bool) TonSigner(selector uint64) (*tonwallet.Wallet, bool) + CantonChain(selector uint64) (cantonsdk.Chain, bool) } diff --git a/chainwrappers/converters.go b/chainwrappers/converters.go index 0ccaa18de..c00619a6b 100644 --- a/chainwrappers/converters.go +++ b/chainwrappers/converters.go @@ -8,6 +8,7 @@ import ( "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/sdk/canton" "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" @@ -51,6 +52,8 @@ func BuildConverter(selector types.ChainSelector, metadata types.ChainMetadata) converter, _ = sui.NewTimelockConverter() case chainsel.FamilyTon: converter = ton.NewTimelockConverter(ton.DefaultSendAmount) + case chainsel.FamilyCanton: + converter = canton.NewTimelockConverter() default: return nil, fmt.Errorf("unsupported chain family %s", fam) } diff --git a/chainwrappers/executors.go b/chainwrappers/executors.go index 1b1b1261c..6ed810c33 100644 --- a/chainwrappers/executors.go +++ b/chainwrappers/executors.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" + "github.com/samber/lo" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/aptos" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" @@ -23,13 +25,11 @@ func BuildExecutors( action types.TimelockAction, ) (map[types.ChainSelector]sdk.Executor, error) { executors := map[types.ChainSelector]sdk.Executor{} - for chainSelector, metadata := range chainMetadata { encoder, ok := encoders[chainSelector] if !ok { return nil, fmt.Errorf("missing encoder for chain selector %d", chainSelector) } - executor, err := BuildExecutor(chains, chainSelector, encoder, action, metadata) if err != nil { return nil, err @@ -167,6 +167,32 @@ func BuildExecutor( Amount: ton.DefaultSendAmount, }) + 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 }) + cantonEncoder, ok := encoder.(*cantonsdk.Encoder) + if !ok { + return nil, fmt.Errorf("invalid encoder type for selector %d: %T", chainSelector, encoder) + } + role, err := cantonsdk.CantonRoleFromAction(action) + if err != nil { + return nil, fmt.Errorf("error getting canton role from proposal: %w", err) + } + inspector := cantonsdk.NewInspector(participant.LedgerServices.State, mcmsParties, role) + + return cantonsdk.NewExecutor( + cantonEncoder, + inspector, + participant.LedgerServices.Command, + participant.PartyID, + mcmsParties, + role, + ) + default: return nil, fmt.Errorf("unsupported chain family %s", family) } diff --git a/chainwrappers/executors_test.go b/chainwrappers/executors_test.go index 626930cf2..6ae302f28 100644 --- a/chainwrappers/executors_test.go +++ b/chainwrappers/executors_test.go @@ -7,6 +7,7 @@ import ( sol "github.com/gagliardetto/solana-go" solrpc "github.com/gagliardetto/solana-go/rpc" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -16,6 +17,7 @@ import ( mcmssdk "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/aptos" aptosmocks "github.com/smartcontractkit/mcms/sdk/aptos/mocks/aptos" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" @@ -27,11 +29,12 @@ import ( ) var ( - evmSelector = mcmstypes.ChainSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector) - solSelector = mcmstypes.ChainSelector(chainsel.SOLANA_DEVNET.Selector) - aptosSelector = mcmstypes.ChainSelector(chainsel.APTOS_TESTNET.Selector) - suiSelector = mcmstypes.ChainSelector(chainsel.SUI_TESTNET.Selector) - tonSelector = mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector) + evmSelector = mcmstypes.ChainSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector) + solSelector = mcmstypes.ChainSelector(chainsel.SOLANA_DEVNET.Selector) + aptosSelector = mcmstypes.ChainSelector(chainsel.APTOS_TESTNET.Selector) + suiSelector = mcmstypes.ChainSelector(chainsel.SUI_TESTNET.Selector) + tonSelector = mcmstypes.ChainSelector(chainsel.TON_TESTNET.Selector) + cantonSelector = mcmstypes.ChainSelector(chainsel.CANTON_TESTNET.Selector) ) func TestBuildExecutors(t *testing.T) { @@ -65,6 +68,12 @@ func TestBuildExecutors(t *testing.T) { tonExecOpts := ton.ExecutorOpts{Encoder: tonEncoder, Client: tonClient, Wallet: tonSigner, Amount: ton.DefaultSendAmount} tonExecutor, err := ton.NewExecutor(tonExecOpts) require.NoError(t, err) + cantonEncoder := cantonsdk.NewEncoder(cantonSelector, 0, false) + cantonChain := cantonsdk.Chain{Participants: []cantonsdk.Participant{{PartyID: "party::test"}}} + cantonInspector := cantonsdk.NewInspector(cantonChain.Participants[0].LedgerServices.State, []string{"party::test"}, cantonsdk.TimelockRoleProposer) + cantonExecutor, err := cantonsdk.NewExecutor(cantonEncoder, cantonInspector, + cantonChain.Participants[0].LedgerServices.Command, "party::test", []string{"party::test"}, cantonsdk.TimelockRoleProposer) + require.NoError(t, err) tests := []struct { name string @@ -77,11 +86,12 @@ func TestBuildExecutors(t *testing.T) { { name: "success", encoders: map[mcmstypes.ChainSelector]mcmssdk.Encoder{ - evmSelector: evmEncoder, - solSelector: solEncoder, - aptosSelector: aptosEncoder, - suiSelector: suiEncoder, - tonSelector: tonEncoder, + evmSelector: evmEncoder, + solSelector: solEncoder, + aptosSelector: aptosEncoder, + suiSelector: suiEncoder, + tonSelector: tonEncoder, + cantonSelector: cantonEncoder, }, chainMetadata: map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata{ mcmstypes.ChainSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector): { @@ -113,6 +123,10 @@ func TestBuildExecutors(t *testing.T) { MCMAddress: "0xton", StartingOpCount: 0, }, + mcmstypes.ChainSelector(chainsel.CANTON_TESTNET.Selector): { + MCMAddress: "0xcanton", + StartingOpCount: 0, + }, }, setup: func(accessor *mocks.ChainAccessor) { accessor.EXPECT().EVMClient(mock.Anything).Return(nil, true) @@ -125,13 +139,15 @@ func TestBuildExecutors(t *testing.T) { accessor.EXPECT().SuiSigner(mock.Anything).Return(nil, true) accessor.EXPECT().TonClient(mock.Anything).Return(tonClient, true) accessor.EXPECT().TonSigner(mock.Anything).Return(tonSigner, true) + accessor.EXPECT().CantonChain(mock.Anything).Return(cantonChain, true) }, want: map[mcmstypes.ChainSelector]mcmssdk.Executor{ - evmSelector: evmExecutor, - solSelector: solExecutor, - aptosSelector: aptosExecutor, - suiSelector: suiExecutor, - tonSelector: tonExecutor, + evmSelector: evmExecutor, + solSelector: solExecutor, + aptosSelector: aptosExecutor, + suiSelector: suiExecutor, + tonSelector: tonExecutor, + cantonSelector: cantonExecutor, }, }, { @@ -166,7 +182,8 @@ func TestBuildExecutors(t *testing.T) { got, err := BuildExecutors(chainAccessor, tt.chainMetadata, tt.encoders, mcmstypes.TimelockActionSchedule) if tt.wantErr == "" { require.NoError(t, err) - require.Empty(t, cmp.Diff(tt.want, got)) + require.Empty(t, cmp.Diff(tt.want, got, + cmpopts.IgnoreUnexported(cantonsdk.Inspector{}, cantonsdk.Executor{}))) } else { require.ErrorContains(t, err, tt.wantErr) } diff --git a/chainwrappers/inspectors.go b/chainwrappers/inspectors.go index 2df8b657c..75d8c04af 100644 --- a/chainwrappers/inspectors.go +++ b/chainwrappers/inspectors.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" + "github.com/samber/lo" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/aptos" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" @@ -50,6 +52,19 @@ func BuildInspector( rawSelector := uint64(selector) switch family { + 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 }) + role, err := cantonsdk.CantonRoleFromAction(action) + if err != nil { + return nil, fmt.Errorf("error getting canton role from proposal: %w", err) + } + + return cantonsdk.NewInspector(participant.LedgerServices.State, mcmsParties, role), nil case chainsel.FamilyEVM: client, ok := chains.EVMClient(rawSelector) if !ok { diff --git a/chainwrappers/mocks/chain_accessor.go b/chainwrappers/mocks/chain_accessor.go index a0bbdbb67..a36b0c3fd 100644 --- a/chainwrappers/mocks/chain_accessor.go +++ b/chainwrappers/mocks/chain_accessor.go @@ -6,6 +6,7 @@ import ( aptos "github.com/aptos-labs/aptos-go-sdk" bind "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" evm "github.com/smartcontractkit/mcms/sdk/evm" mock "github.com/stretchr/testify/mock" @@ -663,6 +664,62 @@ func (_c *ChainAccessor_TonSigner_Call) RunAndReturn(run func(uint64) (*wallet.W return _c } +// CantonChain provides a mock function with given fields: selector +func (_m *ChainAccessor) CantonChain(selector uint64) (cantonsdk.Chain, bool) { + ret := _m.Called(selector) + + if len(ret) == 0 { + panic("no return value specified for CantonChain") + } + + var r0 cantonsdk.Chain + var r1 bool + if rf, ok := ret.Get(0).(func(uint64) (cantonsdk.Chain, bool)); ok { + return rf(selector) + } + if rf, ok := ret.Get(0).(func(uint64) cantonsdk.Chain); ok { + r0 = rf(selector) + } else { + r0 = ret.Get(0).(cantonsdk.Chain) + } + + if rf, ok := ret.Get(1).(func(uint64) bool); ok { + r1 = rf(selector) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// ChainAccessor_CantonChain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CantonChain' +type ChainAccessor_CantonChain_Call struct { + *mock.Call +} + +// CantonChain is a helper method to define mock.On call +// - selector uint64 +func (_e *ChainAccessor_Expecter) CantonChain(selector interface{}) *ChainAccessor_CantonChain_Call { + return &ChainAccessor_CantonChain_Call{Call: _e.mock.On("CantonChain", selector)} +} + +func (_c *ChainAccessor_CantonChain_Call) Run(run func(selector uint64)) *ChainAccessor_CantonChain_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint64)) + }) + return _c +} + +func (_c *ChainAccessor_CantonChain_Call) Return(_a0 cantonsdk.Chain, _a1 bool) *ChainAccessor_CantonChain_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ChainAccessor_CantonChain_Call) RunAndReturn(run func(uint64) (cantonsdk.Chain, bool)) *ChainAccessor_CantonChain_Call { + _c.Call.Return(run) + return _c +} + // NewChainAccessor creates a new instance of ChainAccessor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewChainAccessor(t interface { diff --git a/chainwrappers/timelock_executors.go b/chainwrappers/timelock_executors.go index 86515211a..391f5fb58 100644 --- a/chainwrappers/timelock_executors.go +++ b/chainwrappers/timelock_executors.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" + "github.com/samber/lo" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/aptos" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" @@ -22,7 +24,6 @@ func BuildTimelockExecutors( action types.TimelockAction, ) (map[types.ChainSelector]sdk.TimelockExecutor, error) { executors := map[types.ChainSelector]sdk.TimelockExecutor{} - for chainSelector, metadata := range chainMetadata { executor, err := BuildTimelockExecutor(chains, chainSelector, action, metadata) if err != nil { @@ -135,6 +136,21 @@ func BuildTimelockExecutor( Amount: ton.DefaultSendAmount, }) + 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 + default: return nil, fmt.Errorf("unsupported chain family %s", family) } diff --git a/e2e/config.canton.toml b/e2e/config.canton.toml new file mode 100644 index 000000000..bae2f4da3 --- /dev/null +++ b/e2e/config.canton.toml @@ -0,0 +1,10 @@ +[settings] +private_keys = [ + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +] + +[canton_config] +type = "canton" +number_of_canton_validators = 1 diff --git a/e2e/tests/canton/common.go b/e2e/tests/canton/common.go new file mode 100644 index 000000000..726f0995d --- /dev/null +++ b/e2e/tests/canton/common.go @@ -0,0 +1,270 @@ +//go:build e2e + +package canton + +import ( + "context" + "crypto/ecdsa" + "fmt" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/google/uuid" + mcmsapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/api" + mcmscore "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/core" + "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/mcmstest" + "github.com/smartcontractkit/chainlink-canton/contracts" + "github.com/smartcontractkit/chainlink-canton/testhelpers" + "github.com/smartcontractkit/go-daml/pkg/service/ledger" + "github.com/smartcontractkit/go-daml/pkg/types" + "github.com/stretchr/testify/suite" + + "github.com/smartcontractkit/mcms" + e2e "github.com/smartcontractkit/mcms/e2e/tests" + "github.com/smartcontractkit/mcms/sdk" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +type TestSuite struct { + suite.Suite + e2e.TestSetup + + env testhelpers.TestEnvironment + + participant cantonsdk.Participant + submittingParty string + + chainSelector mcmstypes.ChainSelector + chainId int64 + mcmsInstanceAddress string // InstanceAddress hex (stable across SetRoot/ExecuteOp); use this everywhere instead of contract ID + mcmsId string // MCMS contract instanceId (base); DAML expects metadata.multisigId = makeMcmsId(instanceId, role) e.g. "mcms-test-001-proposer" + proposerMcmsId string // makeMcmsId(mcmsId, Proposer) for chain metadata and Op.multisigId +} + +func (s *TestSuite) SetupSuite() { + shared := GetSharedEnvironment(s.T()) + s.env = shared.Env + cldfParticipant := shared.Env.Chain.Participants[0] + s.participant = cantonsdk.Participant{ + PartyID: cldfParticipant.PartyID, + LedgerServices: cantonsdk.LedgerServices{ + State: cldfParticipant.LedgerServices.State, + Command: cldfParticipant.LedgerServices.Command, + }, + } + s.chainSelector = shared.ChainSelector + s.submittingParty = shared.SubmittingParty +} + +const NumGroups = 32 + +func (s *TestSuite) DeployMCMSContract() { + mcmsOwner := s.participant.PartyID + chainId := int64(1) + mcmsId := "mcms-" + uuid.New().String()[:8] + + mcmsInstanceAddr := s.createMCMS(s.T().Context(), s.participant, mcmsOwner, chainId, mcmsId) + s.mcmsInstanceAddress = mcmsInstanceAddr + s.mcmsId = mcmsId + // multisigId format: instanceId@partyId-role (see MCMS.Main.daml makeMcmsId) + s.proposerMcmsId = fmt.Sprintf("%s@%s-proposer", mcmsId, mcmsOwner) + s.chainId = chainId +} + +func (s *TestSuite) DeployMCMSWithConfig(config *mcmstypes.Config) { + s.DeployMCMSContract() + + // Set the config for all roles (proposer, canceller, bypasser) so tests can use any role + roles := []cantonsdk.TimelockRole{ + cantonsdk.TimelockRoleProposer, + cantonsdk.TimelockRoleCanceller, + cantonsdk.TimelockRoleBypasser, + } + for _, role := range roles { + configurer, err := cantonsdk.NewConfigurer(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, []string{s.participant.PartyID}, role) + s.Require().NoError(err) + + _, err = configurer.SetConfig(s.T().Context(), s.mcmsInstanceAddress, config, true) + s.Require().NoError(err) + } +} + +func (s *mcmsExecutorSetup) DeployCounterContract() { + s.counterInstanceID = "counter-" + uuid.New().String()[:8] + // DAML Counter.instanceId must NOT contain "@" - the full instanceAddress is calculated as instanceId@partyId + + // Create Counter contract + counterContract := mcmstest.Counter{ + Owner: types.PARTY(s.participant.PartyID), + InstanceId: types.TEXT(s.counterInstanceID), + Value: types.INT64(0), + } + + // Parse template ID + exerciseCmd := counterContract.CreateCommand() + packageID, moduleName, entityName, err := cantonsdk.ParseTemplateIDFromString(exerciseCmd.TemplateID) + s.Require().NoError(err, "failed to parse template ID") + + // Convert create arguments to apiv2 format + createArguments := ledger.ConvertToRecord(exerciseCmd.Arguments) + + commandID := uuid.Must(uuid.NewUUID()).String() + submitResp, err := s.participant.LedgerServices.Command.SubmitAndWaitForTransaction(s.T().Context(), &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + WorkflowId: "counter-deploy", + CommandId: commandID, + ActAs: []string{s.participant.PartyID}, + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Create{ + Create: &apiv2.CreateCommand{ + TemplateId: &apiv2.Identifier{ + PackageId: packageID, + ModuleName: moduleName, + EntityName: entityName, + }, + CreateArguments: createArguments, + }, + }, + }}, + }, + }) + s.Require().NoError(err) + + // Extract contract ID + transaction := submitResp.GetTransaction() + for _, event := range transaction.GetEvents() { + if createdEv := event.GetCreated(); createdEv != nil { + templateID := cantonsdk.FormatTemplateID(createdEv.GetTemplateId()) + if templateID != "" { + s.counterCID = createdEv.GetContractId() + break + } + } + } + s.Require().NotEmpty(s.counterCID) +} + +// createMCMS creates an MCMS contract and returns its InstanceAddress hex (stable reference for Canton). +func (s *TestSuite) createMCMS(ctx context.Context, participant cantonsdk.Participant, owner string, chainId int64, mcmsId string) string { + // Create empty config + emptyConfig := mcmsapi.MultisigConfig{ + Signers: []mcmsapi.SignerInfo{}, + GroupQuorums: []types.INT64{types.INT64(1)}, + GroupParents: []types.INT64{types.INT64(1)}, + } + + // Create empty role state using zero values and nil for maps + emptyRoleState := mcmsapi.RoleState{ + Config: emptyConfig, + SeenHashes: nil, + ExpiringRoot: mcmsapi.ExpiringRoot{}, + RootMetadata: mcmsapi.RootMetadata{}, + } + + minDelayValue := &apiv2.Value{Sum: &apiv2.Value_Record{Record: &apiv2.Record{ + Fields: []*apiv2.RecordField{ + {Label: "microseconds", Value: &apiv2.Value{Sum: &apiv2.Value_Int64{Int64: 0}}}, + }, + }}} + + // Create MCMS contract with new structure + mcmsContract := mcmscore.MCMS{ + Owner: types.PARTY(participant.PartyID), + InstanceId: types.TEXT(mcmsId), + ChainId: types.INT64(chainId), + Proposer: emptyRoleState, + Canceller: emptyRoleState, + Bypasser: emptyRoleState, + BlockedFunctions: nil, + TimelockTimestamps: nil, + } + + // Parse template ID + exerciseCmd := mcmsContract.CreateCommand() + packageID, moduleName, entityName, err := cantonsdk.ParseTemplateIDFromString(exerciseCmd.TemplateID) + s.Require().NoError(err, "failed to parse template ID") + + // Convert create arguments to apiv2 format + createArguments := ledger.ConvertToRecord(exerciseCmd.Arguments) + + // Remove minDelay from arguments + filteredFields := make([]*apiv2.RecordField, 0, len(createArguments.Fields)) + for _, field := range createArguments.Fields { + if field.Label != "minDelay" { + filteredFields = append(filteredFields, field) + } + } + createArguments.Fields = filteredFields + + // Submit via CommandService + commandID := uuid.Must(uuid.NewUUID()).String() + submitResp, err := participant.LedgerServices.Command.SubmitAndWaitForTransaction(ctx, &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + WorkflowId: "mcms-deploy", + CommandId: commandID, + ActAs: []string{participant.PartyID}, + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Create{ + Create: &apiv2.CreateCommand{ + TemplateId: &apiv2.Identifier{ + PackageId: packageID, + ModuleName: moduleName, + EntityName: entityName, + }, + CreateArguments: &apiv2.Record{Fields: append( + createArguments.Fields, + &apiv2.RecordField{Label: "minDelay", Value: minDelayValue}, + )}, + }, + }, + }}, + }, + }) + s.Require().NoError(err, "failed to submit MCMS deploy transaction") + + // Retrieve the contract ID and template ID from the CreateEvent + mcmsContractID := "" + mcmsTemplateID := "" + transaction := submitResp.GetTransaction() + for _, event := range transaction.GetEvents() { + if createdEv := event.GetCreated(); createdEv != nil { + templateID := cantonsdk.FormatTemplateID(createdEv.GetTemplateId()) + normalizedTemplateID := cantonsdk.NormalizeTemplateKey(templateID) + if normalizedTemplateID == cantonsdk.MCMSTemplateKey { + mcmsContractID = createdEv.GetContractId() + mcmsTemplateID = templateID + + break + } + } + } + + s.Require().NotEmpty(mcmsContractID, "failed to find MCMS contract in transaction events") + s.Require().NotEmpty(mcmsTemplateID, "failed to find MCMS template ID in transaction events") + + // Return InstanceAddress hex so callers use a stable reference (contract ID changes after SetRoot/ExecuteOp) + instanceAddress := contracts.InstanceID(mcmsId).RawInstanceAddress(types.PARTY(owner)).InstanceAddress() + + return instanceAddress.Hex() +} + +func (s *TestSuite) SignProposal(proposal *mcms.Proposal, inspector sdk.Inspector, keys []*ecdsa.PrivateKey, quorum int) (*mcms.Signable, []mcmstypes.Signature, error) { + inspectorsMap := map[mcmstypes.ChainSelector]sdk.Inspector{ + s.chainSelector: inspector, + } + signable, err := mcms.NewSignable(proposal, inspectorsMap) + if err != nil { + return nil, nil, err + } + + signatures := make([]mcmstypes.Signature, 0, quorum) + for i := 0; i < len(keys) && i < quorum; i++ { + sig, err := signable.SignAndAppend(mcms.NewPrivateKeySigner(keys[i])) + if err != nil { + return nil, nil, err + } + signatures = append(signatures, sig) + } + + return signable, signatures, nil +} diff --git a/e2e/tests/canton/configurer.go b/e2e/tests/canton/configurer.go new file mode 100644 index 000000000..164fa9bca --- /dev/null +++ b/e2e/tests/canton/configurer.go @@ -0,0 +1,133 @@ +//go:build e2e + +package canton + +import ( + "slices" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" + + "github.com/smartcontractkit/mcms/types" +) + +type MCMSConfigurerTestSuite struct { + TestSuite +} + +// SetupSuite runs before the test suite +func (s *MCMSConfigurerTestSuite) SetupSuite() { + s.TestSuite.SetupSuite() + s.DeployMCMSContract() +} + +func (s *MCMSConfigurerTestSuite) TestSetConfig() { + // Signers in each group need to be sorted alphabetically + signers := [30]common.Address{} + for i := range signers { + key, _ := crypto.GenerateKey() + signers[i] = crypto.PubkeyToAddress(key.PublicKey) + } + slices.SortFunc(signers[:], func(a, b common.Address) int { + return a.Cmp(b) + }) + + proposerConfig := &types.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[0], + signers[1], + signers[2], + }, + GroupSigners: []types.Config{ + { + Quorum: 4, + Signers: []common.Address{ + signers[3], + signers[4], + signers[5], + signers[6], + signers[7], + }, + GroupSigners: []types.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[8], + signers[9], + }, + GroupSigners: []types.Config{}, + }, + }, + }, + { + Quorum: 3, + Signers: []common.Address{ + signers[10], + signers[11], + signers[12], + signers[13], + }, + GroupSigners: []types.Config{}, + }, + }, + } + + // Set config (use InstanceAddress); resolve once to get current contract ID for event assertions + { + ctx := s.T().Context() + oldContractID, err := cantonsdk.ResolveMCMSContractID(ctx, s.participant.LedgerServices.State, []string{s.participant.PartyID}, s.mcmsInstanceAddress) + s.Require().NoError(err, "resolve MCMS contract ID before SetConfig") + + configurer, err := cantonsdk.NewConfigurer(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + s.Require().NoError(err, "creating configurer for Canton mcms contract") + tx, err := configurer.SetConfig(ctx, s.mcmsInstanceAddress, proposerConfig, true) + s.Require().NoError(err, "setting config on Canton mcms contract") + + // Verify transaction result + rawData, ok := tx.RawData.(map[string]any) + s.Require().True(ok) + rawTx, ok := rawData["RawTx"] + s.Require().True(ok) + + submitResp, ok := rawTx.(*apiv2.SubmitAndWaitForTransactionResponse) + s.Require().True(ok) + + // Get transaction and events + transaction := submitResp.GetTransaction() + s.Require().NotNil(transaction, "transaction should not be nil") + + events := transaction.GetEvents() + s.Require().Len(events, 2, "transaction should have exactly 2 events (archived + created)") + + // Verify event[0] is Archived (old contract) + s.Require().NotNil(events[0].GetArchived(), "first event should be Archived event") + s.Require().Nil(events[0].GetCreated(), "first event should not be Created event") + s.Require().Equal(oldContractID, events[0].GetArchived().GetContractId(), "archived contract should be the old MCMS contract") + + // Verify event[1] is Created (new contract) + s.Require().NotNil(events[1].GetCreated(), "second event should be Created event") + s.Require().Nil(events[1].GetArchived(), "second event should not be Archived event") + + // Verify Template ID matches + rawData, ok = tx.RawData.(map[string]any) + s.Require().True(ok) + newMCMSTemplateID, ok := rawData["NewMCMSTemplateID"].(string) + s.Require().True(ok) + s.Require().Contains(newMCMSTemplateID, "MCMS.Main:MCMS", "template ID should match MCMS template") + + createdTemplateID := cantonsdk.NormalizeTemplateKey(newMCMSTemplateID) + eventTemplateID := cantonsdk.NormalizeTemplateKey(cantonsdk.FormatTemplateID(events[1].GetCreated().GetTemplateId())) + s.Require().Equal(createdTemplateID, eventTemplateID, "created event template ID should match returned template ID") + + // Verify new contract ID is different from old + newMCMSContractID, ok := rawData["NewMCMSContractID"].(string) + s.Require().True(ok) + s.Require().NotEmpty(newMCMSContractID, "new contract ID should not be empty") + s.Require().NotEqual(oldContractID, newMCMSContractID, "new contract ID should be different from old contract ID") + s.Require().Equal(newMCMSContractID, events[1].GetCreated().GetContractId(), "created event contract ID should match returned contract ID") + } +} diff --git a/e2e/tests/canton/executor.go b/e2e/tests/canton/executor.go new file mode 100644 index 000000000..9606cb979 --- /dev/null +++ b/e2e/tests/canton/executor.go @@ -0,0 +1,76 @@ +//go:build e2e + +package canton + +import ( + "crypto/ecdsa" + "slices" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + mcmscore "github.com/smartcontractkit/mcms" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +// mcmsExecutorSetup holds shared setup (MCMS + config + counter + signers) for suites that need it. +// It has no Test* methods, so embedding it only adds SetupSuite and fields; test methods come from the embedding suite. +type mcmsExecutorSetup struct { + TestSuite + + // Test signers + signers []*ecdsa.PrivateKey + signerAddrs []common.Address + sortedSigners []*ecdsa.PrivateKey + sortedWallets []*mcmscore.PrivateKeySigner + + // Counter contract for testing ExecuteOp + counterInstanceID string + counterCID string +} + +// SetupSuite runs before the test suite. +func (s *mcmsExecutorSetup) SetupSuite() { + s.TestSuite.SetupSuite() + + // Create 3 signers for 2-of-3 multisig + s.signers = make([]*ecdsa.PrivateKey, 3) + for i := range 3 { + key, err := crypto.GenerateKey() + s.Require().NoError(err) + s.signers[i] = key + } + + // Sort signers by address + signersCopy := make([]*ecdsa.PrivateKey, len(s.signers)) + copy(signersCopy, s.signers) + slices.SortFunc(signersCopy, func(a, b *ecdsa.PrivateKey) int { + addrA := crypto.PubkeyToAddress(a.PublicKey) + addrB := crypto.PubkeyToAddress(b.PublicKey) + + return addrA.Cmp(addrB) + }) + s.sortedSigners = signersCopy + s.sortedWallets = make([]*mcmscore.PrivateKeySigner, len(s.sortedSigners)) + + // Derive sorted addresses from sorted signers to ensure they correspond + s.signerAddrs = make([]common.Address, len(s.sortedSigners)) + for i, signer := range s.sortedSigners { + s.sortedWallets[i] = mcmscore.NewPrivateKeySigner(signer) + s.signerAddrs[i] = crypto.PubkeyToAddress(signer.PublicKey) + } + + // Deploy MCMS with config + config := s.create2of3Config() + s.DeployMCMSWithConfig(config) + + // Deploy Counter contract for ExecuteOp tests + s.DeployCounterContract() +} + +func (s *mcmsExecutorSetup) create2of3Config() *mcmstypes.Config { + return &mcmstypes.Config{ + Quorum: 2, + Signers: s.signerAddrs, + } +} diff --git a/e2e/tests/canton/inspector.go b/e2e/tests/canton/inspector.go new file mode 100644 index 000000000..a79fdaab8 --- /dev/null +++ b/e2e/tests/canton/inspector.go @@ -0,0 +1,146 @@ +//go:build e2e + +package canton + +import ( + "slices" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +type MCMSInspectorTestSuite struct { + TestSuite + inspector *cantonsdk.Inspector +} + +// SetupSuite runs before the test suite +func (s *MCMSInspectorTestSuite) SetupSuite() { + s.TestSuite.SetupSuite() + s.DeployMCMSContract() + + // Create inspector instance using participant's State service client + s.inspector = cantonsdk.NewInspector(s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) +} + +func (s *MCMSInspectorTestSuite) TestGetConfig() { + ctx := s.T().Context() + + // Signers in each group need to be sorted alphabetically + signers := [30]common.Address{} + for i := range signers { + key, _ := crypto.GenerateKey() + signers[i] = crypto.PubkeyToAddress(key.PublicKey) + } + slices.SortFunc(signers[:], func(a, b common.Address) int { + return a.Cmp(b) + }) + + expectedConfig := &mcmstypes.Config{ + Quorum: 2, + Signers: []common.Address{ + signers[0], + signers[1], + signers[2], + }, + GroupSigners: []mcmstypes.Config{ + { + Quorum: 4, + Signers: []common.Address{ + signers[3], + signers[4], + signers[5], + signers[6], + signers[7], + }, + GroupSigners: []mcmstypes.Config{ + { + Quorum: 1, + Signers: []common.Address{ + signers[8], + signers[9], + }, + GroupSigners: []mcmstypes.Config{}, + }, + }, + }, + { + Quorum: 3, + Signers: []common.Address{ + signers[10], + signers[11], + signers[12], + signers[13], + }, + GroupSigners: []mcmstypes.Config{}, + }, + }, + } + + // Set config using configurer (InstanceAddress is stable across SetConfig) + configurer, err := cantonsdk.NewConfigurer(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + s.Require().NoError(err, "creating configurer") + + _, err = configurer.SetConfig(ctx, s.mcmsInstanceAddress, expectedConfig, true) + s.Require().NoError(err, "setting config") + + // Inspector resolves InstanceAddress when querying + actualConfig, err := s.inspector.GetConfig(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "getting config from inspector") + s.Require().NotNil(actualConfig, "config should not be nil") + + // Verify the config matches what we set + s.verifyConfigMatch(expectedConfig, actualConfig) +} + +func (s *MCMSInspectorTestSuite) TestGetOpCount() { + ctx := s.T().Context() + + opCount, err := s.inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "getting op count") + + // Initially should be 0 + s.Require().Equal(uint64(0), opCount, "initial op count should be 0") +} + +func (s *MCMSInspectorTestSuite) TestGetRoot() { + ctx := s.T().Context() + + root, validUntil, err := s.inspector.GetRoot(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "getting root") + + // Initially, no SetRoot has been called: root should be empty + s.Require().Equal(common.Hash{}, root, "initial root should be empty") + _ = validUntil // initial value is from MCMS emptyExpiringRoot (epoch); exact value depends on ledger/bindings +} + +func (s *MCMSInspectorTestSuite) TestGetRootMetadata() { + ctx := s.T().Context() + + metadata, err := s.inspector.GetRootMetadata(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "getting root metadata") + + // Verify metadata structure + s.Require().Equal(uint64(0), metadata.StartingOpCount, "initial starting op count should be 0") + s.Require().NotEmpty(metadata.MCMAddress, "MCM address should not be empty") +} + +// Helper to verify config matches +func (s *MCMSInspectorTestSuite) verifyConfigMatch(expected, actual *mcmstypes.Config) { + s.Require().Equal(expected.Quorum, actual.Quorum, "quorum should match") + s.Require().Len(actual.Signers, len(expected.Signers), "number of signers should match") + + // Verify signers + for i, expectedSigner := range expected.Signers { + s.Require().Equal(expectedSigner, actual.Signers[i], "signer %d should match", i) + } + + // Verify group signers recursively + s.Require().Len(actual.GroupSigners, len(expected.GroupSigners), "number of group signers should match") + for i, expectedGroup := range expected.GroupSigners { + s.verifyConfigMatch(&expectedGroup, &actual.GroupSigners[i]) + } +} diff --git a/e2e/tests/canton/set_root_execute.go b/e2e/tests/canton/set_root_execute.go new file mode 100644 index 000000000..648a33a04 --- /dev/null +++ b/e2e/tests/canton/set_root_execute.go @@ -0,0 +1,339 @@ +//go:build e2e + +package canton + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "time" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" + "github.com/smartcontractkit/mcms/types" +) + +// SetRootExecuteTestSuite tests the core MCMS proposal path via the Proposer role: +// build timelock proposal -> convert (ScheduleBatch) -> sign -> SetRoot -> Execute (schedule) +// -> wait for delay -> TimelockExecutable.Execute -> verify op count. +type SetRootExecuteTestSuite struct { + mcmsExecutorSetup +} + +// TestSetRootAndExecute builds a Proposer schedule proposal that increments the counter, +// sets the root, executes (schedules the batch), waits for delay, then executes via timelock. +func (s *SetRootExecuteTestSuite) TestSetRootAndExecute() { + ctx := s.T().Context() + + inspector := cantonsdk.NewInspector(s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + currentOpCount, err := inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "get current op count") + + metadata, err := cantonsdk.NewChainMetadata( + currentOpCount, + s.chainId, + s.proposerMcmsId, + s.mcmsInstanceAddress, + s.mcmsId, + ) + s.Require().NoError(err) + + validUntil := uint32(time.Now().Add(24 * time.Hour).Unix()) + delay := types.NewDuration(1 * time.Second) + + opAdditionalFields := cantonsdk.AdditionalFields{ + TargetInstanceAddress: fmt.Sprintf("%s@%s", s.counterInstanceID, s.participant.PartyID), + FunctionName: "Increment", + TargetCid: s.counterCID, + ContractIds: []string{s.counterCID}, + } + opAdditionalFieldsBytes, err := json.Marshal(opAdditionalFields) + s.Require().NoError(err) + + bop := types.BatchOperation{ + ChainSelector: s.chainSelector, + Transactions: []types.Transaction{{ + To: s.counterCID, + Data: []byte{}, + AdditionalFields: opAdditionalFieldsBytes, + }}, + } + + timelockProposal, err := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetValidUntil(validUntil). + SetDescription("Canton set-root-execute - schedule counter increment"). + AddTimelockAddress(s.chainSelector, s.mcmsInstanceAddress). + AddChainMetadata(s.chainSelector, metadata). + SetAction(types.TimelockActionSchedule). + SetDelay(delay). + AddOperation(bop). + Build() + s.Require().NoError(err) + + converter := cantonsdk.NewTimelockConverter() + convertersMap := map[types.ChainSelector]sdk.TimelockConverter{ + s.chainSelector: converter, + } + proposal, _, err := timelockProposal.Convert(ctx, convertersMap) + s.Require().NoError(err) + + inspectorsMap := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: inspector, + } + signable, err := mcms.NewSignable(&proposal, inspectorsMap) + s.Require().NoError(err) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[0])) + s.Require().NoError(err) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[1])) + s.Require().NoError(err) + quorumMet, err := signable.ValidateSignatures(ctx) + s.Require().NoError(err) + s.Require().True(quorumMet, "quorum not met") + + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + encoder := encoders[s.chainSelector].(*cantonsdk.Encoder) + executor, err := cantonsdk.NewExecutor(encoder, inspector, s.participant.LedgerServices.Command, s.submittingParty, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + s.Require().NoError(err) + executors := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: executor, + } + executable, err := mcms.NewExecutable(&proposal, executors) + s.Require().NoError(err) + + txSetRoot, err := executable.SetRoot(ctx, s.chainSelector) + s.Require().NoError(err) + s.Require().NotEmpty(txSetRoot.Hash) + + for i := range proposal.Operations { + _, execErr := executable.Execute(ctx, i) + s.Require().NoError(execErr, "execute scheduled operation %d", i) + } + + timelockExecutor := cantonsdk.NewTimelockExecutor(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, s.submittingParty, []string{s.participant.PartyID}) + timelockExecutors := map[types.ChainSelector]sdk.TimelockExecutor{ + s.chainSelector: timelockExecutor, + } + timelockExecutable, err := mcms.NewTimelockExecutable(ctx, timelockProposal, timelockExecutors) + s.Require().NoError(err) + + time.Sleep(timelockProposal.Delay.Duration + time.Second) + s.Require().NoError(timelockExecutable.IsReady(ctx), "timelock operation should become ready") + + for i := range timelockProposal.Operations { + _, terr := timelockExecutable.Execute(ctx, i) + s.Require().NoError(terr, "timelock execute operation %d", i) + } + + postOpCount, err := inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().Equal(currentOpCount+1, postOpCount, "op count should increment after execute") +} + +// TestSetRootAndExecuteMultipleOps builds two sequential schedule proposals to verify +// nonce/opCount handling works correctly across multiple proposal executions. +// Uses self-dispatch (UpdateMinDelay) to avoid external contract CID staleness across iterations. +func (s *SetRootExecuteTestSuite) TestSetRootAndExecuteMultipleOps() { + ctx := s.T().Context() + + inspector := cantonsdk.NewInspector(s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + startOpCount, err := inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "get starting op count") + + mcmsTargetInstanceAddr := fmt.Sprintf("%s@%s", s.mcmsId, s.participant.PartyID) + + for i := range uint64(2) { + currentOpCount := startOpCount + i + + metadata, metaErr := cantonsdk.NewChainMetadata( + currentOpCount, + s.chainId, + s.proposerMcmsId, + s.mcmsInstanceAddress, + s.mcmsId, + ) + s.Require().NoError(metaErr) + + validUntil := uint32(time.Now().Add(24 * time.Hour).Unix()) + delay := types.NewDuration(1 * time.Second) + + delayData, decodeErr := hex.DecodeString(encodeMinDelay(1)) + s.Require().NoError(decodeErr) + + opAdditionalFields := cantonsdk.AdditionalFields{ + TargetInstanceAddress: mcmsTargetInstanceAddr, + FunctionName: "UpdateMinDelay", + TargetCid: "", + ContractIds: []string{}, + } + opAdditionalFieldsBytes, marshalErr := json.Marshal(opAdditionalFields) + s.Require().NoError(marshalErr) + + bop := types.BatchOperation{ + ChainSelector: s.chainSelector, + Transactions: []types.Transaction{{ + To: mcmsTargetInstanceAddr, + Data: delayData, + AdditionalFields: opAdditionalFieldsBytes, + }}, + } + + timelockProposal, buildErr := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetValidUntil(validUntil). + SetDescription(fmt.Sprintf("Canton multi-op proposal %d - UpdateMinDelay", i+1)). + AddTimelockAddress(s.chainSelector, s.mcmsInstanceAddress). + AddChainMetadata(s.chainSelector, metadata). + SetAction(types.TimelockActionSchedule). + SetDelay(delay). + SetOverridePreviousRoot(i > 0). + AddOperation(bop). + Build() + s.Require().NoError(buildErr) + + converter := cantonsdk.NewTimelockConverter() + convertersMap := map[types.ChainSelector]sdk.TimelockConverter{ + s.chainSelector: converter, + } + proposal, _, convertErr := timelockProposal.Convert(ctx, convertersMap) + s.Require().NoError(convertErr) + + inspectorsMap := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: inspector, + } + signable, signableErr := mcms.NewSignable(&proposal, inspectorsMap) + s.Require().NoError(signableErr) + _, signErr := signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[0])) + s.Require().NoError(signErr) + _, signErr = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[1])) + s.Require().NoError(signErr) + + encoders, encErr := proposal.GetEncoders() + s.Require().NoError(encErr) + encoder := encoders[s.chainSelector].(*cantonsdk.Encoder) + executor, execErr := cantonsdk.NewExecutor(encoder, inspector, s.participant.LedgerServices.Command, s.submittingParty, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + s.Require().NoError(execErr) + executors := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: executor, + } + executable, exeErr := mcms.NewExecutable(&proposal, executors) + s.Require().NoError(exeErr) + + _, err = executable.SetRoot(ctx, s.chainSelector) + s.Require().NoError(err) + + for j := range proposal.Operations { + _, execErr := executable.Execute(ctx, j) + s.Require().NoError(execErr, "execute scheduled operation %d of proposal %d", j, i+1) + } + + timelockExecutor := cantonsdk.NewTimelockExecutor(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, s.submittingParty, []string{s.participant.PartyID}) + timelockExecutors := map[types.ChainSelector]sdk.TimelockExecutor{ + s.chainSelector: timelockExecutor, + } + timelockExecutable, tlExeErr := mcms.NewTimelockExecutable(ctx, timelockProposal, timelockExecutors) + s.Require().NoError(tlExeErr) + + time.Sleep(timelockProposal.Delay.Duration + time.Second) + s.Require().NoError(timelockExecutable.IsReady(ctx), "timelock operation %d should become ready", i+1) + + for j := range timelockProposal.Operations { + _, terr := timelockExecutable.Execute(ctx, j) + s.Require().NoError(terr, "timelock execute operation %d of proposal %d", j, i+1) + } + + postOpCount, postCountErr := inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(postCountErr) + s.Require().Equal(currentOpCount+1, postOpCount, "op count should be %d after proposal %d", currentOpCount+1, i+1) + } + + finalOpCount, err := inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().Equal(startOpCount+2, finalOpCount, "op count should increase by 2 after two proposals") +} + +// TestSetRootInvalidSignature verifies that SetRoot fails when given an invalid signature +// (signed by a key not in the MCMS config). +func (s *SetRootExecuteTestSuite) TestSetRootInvalidSignature() { + ctx := s.T().Context() + + inspector := cantonsdk.NewInspector(s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + currentOpCount, err := inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + + metadata, err := cantonsdk.NewChainMetadata( + currentOpCount, + s.chainId, + s.proposerMcmsId, + s.mcmsInstanceAddress, + s.mcmsId, + ) + s.Require().NoError(err) + + opAdditionalFields := cantonsdk.AdditionalFields{ + TargetInstanceAddress: fmt.Sprintf("%s@%s", s.counterInstanceID, s.participant.PartyID), + FunctionName: "Increment", + TargetCid: s.counterCID, + ContractIds: []string{s.counterCID}, + } + opAdditionalFieldsBytes, err := json.Marshal(opAdditionalFields) + s.Require().NoError(err) + + proposal := mcms.Proposal{ + BaseProposal: mcms.BaseProposal{ + Version: "v1", + Kind: types.KindProposal, + Description: "Canton invalid signature test", + ValidUntil: uint32(time.Now().Add(24 * time.Hour).Unix()), + Signatures: []types.Signature{}, + OverridePreviousRoot: true, + ChainMetadata: map[types.ChainSelector]types.ChainMetadata{ + s.chainSelector: metadata, + }, + }, + Operations: []types.Operation{ + { + ChainSelector: s.chainSelector, + Transaction: types.Transaction{ + To: s.counterCID, + Data: []byte{}, + AdditionalFields: opAdditionalFieldsBytes, + }, + }, + }, + } + + inspectorsMap := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: inspector, + } + signable, err := mcms.NewSignable(&proposal, inspectorsMap) + s.Require().NoError(err) + + // ValidateSignatures returns (false, QuorumNotReachedError) when quorum is not met + quorumMet, _ := signable.ValidateSignatures(ctx) + s.Require().False(quorumMet, "quorum should not be met with zero signatures") + + // Sign with only 1 of 2 required signers (insufficient quorum) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[0])) + s.Require().NoError(err) + + quorumMet, _ = signable.ValidateSignatures(ctx) + s.Require().False(quorumMet, "quorum should not be met with only 1 of 2 required signatures") + + // Attempt SetRoot with insufficient signatures -- should fail + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + encoder := encoders[s.chainSelector].(*cantonsdk.Encoder) + executor, err := cantonsdk.NewExecutor(encoder, inspector, s.participant.LedgerServices.Command, s.submittingParty, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + s.Require().NoError(err) + executors := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: executor, + } + executable, err := mcms.NewExecutable(&proposal, executors) + s.Require().NoError(err) + + _, err = executable.SetRoot(ctx, s.chainSelector) + s.Require().Error(err, "SetRoot should fail with insufficient signatures") +} diff --git a/e2e/tests/canton/shared_setup.go b/e2e/tests/canton/shared_setup.go new file mode 100644 index 000000000..7f31a2531 --- /dev/null +++ b/e2e/tests/canton/shared_setup.go @@ -0,0 +1,81 @@ +//go:build e2e + +package canton + +import ( + "fmt" + "sync" + "testing" + + "github.com/google/uuid" + "github.com/smartcontractkit/chainlink-canton/contracts" + "github.com/smartcontractkit/chainlink-canton/testhelpers" + "github.com/stretchr/testify/require" + + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +var ( + sharedEnv *SharedCantonEnvironment + sharedEnvOnce sync.Once + errSharedEnv error +) + +type SharedCantonEnvironment struct { + Env testhelpers.TestEnvironment + PackageIDs []string + ChainSelector mcmstypes.ChainSelector + SubmittingParty string +} + +func GetSharedEnvironment(t *testing.T) *SharedCantonEnvironment { + t.Helper() + + sharedEnvOnce.Do(func() { + t.Log("Initializing shared Canton test environment...") + + env := testhelpers.NewTestEnvironment(t, testhelpers.WithNumberOfParticipants(1)) + participant := env.Chain.Participants[0] + + t.Logf("Allocating a submitting party...") + // Create a separate submitting party and grant the participant's user actAs rights for that party + submittingParty := testhelpers.AllocateParty(t, participant, fmt.Sprintf("submittingParty-%s", uuid.NewString()[:8])) + testhelpers.GrantCanActAs(t, participant, submittingParty) + t.Logf("Allocated submitting party %s", submittingParty) + + t.Log("Uploading MCMS and MCMSTest DARs (once for all suites)...") + mcmsDar, err := contracts.GetDar(contracts.MCMS, contracts.CurrentVersion) + if err != nil { + errSharedEnv = err + return + } + + mcmsTestDar, err := contracts.GetDar(contracts.MCMSTest, contracts.CurrentVersion) + if err != nil { + errSharedEnv = err + return + } + + packageIDs, err := testhelpers.UploadDARstoMultipleParticipants( + t.Context(), + [][]byte{mcmsDar, mcmsTestDar}, + participant, + ) + if err != nil { + errSharedEnv = err + return + } + + sharedEnv = &SharedCantonEnvironment{ + Env: env, + PackageIDs: packageIDs, + ChainSelector: mcmstypes.ChainSelector(env.Chain.ChainSelector()), + SubmittingParty: submittingParty, + } + }) + + require.NoError(t, errSharedEnv, "failed to initialize shared environment") + require.NotNil(t, sharedEnv, "shared environment is nil") + + return sharedEnv +} diff --git a/e2e/tests/canton/timelock_bypass.go b/e2e/tests/canton/timelock_bypass.go new file mode 100644 index 000000000..956a0b2ab --- /dev/null +++ b/e2e/tests/canton/timelock_bypass.go @@ -0,0 +1,143 @@ +//go:build e2e + +package canton + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "time" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" + "github.com/smartcontractkit/mcms/types" +) + +// TimelockBypassTestSuite tests the bypass timelock flow: +// execute immediately without scheduling, bypassing the timelock delay. +// Uses self-dispatch (UpdateMinDelay on MCMS itself) to avoid external contract ID issues. +type TimelockBypassTestSuite struct { + mcmsExecutorSetup +} + +// encodeMinDelay encodes a delay in seconds as a 16-char hex string for UpdateMinDelay transaction data. +// The Canton MCMS contract decodes this via decodeInt64At and converts to RelTime via `seconds`. +func encodeMinDelay(seconds int64) string { + return fmt.Sprintf("%016x", seconds) +} + +// TestTimelockBypass executes a batch immediately via bypasser role, skipping timelock delay. +// Uses self-dispatch (UpdateMinDelay) since external contract execution requires additional SDK support. +func (s *TimelockBypassTestSuite) TestTimelockBypass() { + ctx := s.T().Context() + + // Use bypasser role + bypasserInspector := cantonsdk.NewInspector(s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleBypasser) + currentOpCount, err := bypasserInspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "get current op count") + + // Bypasser metadata + bypasserMcmsId := fmt.Sprintf("%s@%s-bypasser", s.mcmsId, s.participant.PartyID) + metadata, err := cantonsdk.NewChainMetadata( + currentOpCount, + s.chainId, + bypasserMcmsId, + s.mcmsInstanceAddress, + s.mcmsId, + ) + s.Require().NoError(err) + + validUntil := uint32(time.Now().Add(24 * time.Hour).Unix()) + + // Batch operation: UpdateMinDelay on MCMS itself (self-dispatch, no external contracts needed) + // Use mcmsId@partyId format for self-dispatch target + mcmsTargetInstanceAddr := fmt.Sprintf("%s@%s", s.mcmsId, s.participant.PartyID) + delayData, err := hex.DecodeString(encodeMinDelay(5)) + s.Require().NoError(err) + + opAdditionalFields := cantonsdk.AdditionalFields{ + TargetInstanceAddress: mcmsTargetInstanceAddr, + FunctionName: "UpdateMinDelay", + TargetCid: "", // Self-dispatch, no external target CID + ContractIds: []string{}, // No external contracts needed + } + opAdditionalFieldsBytes, err := json.Marshal(opAdditionalFields) + s.Require().NoError(err) + + bop := types.BatchOperation{ + ChainSelector: s.chainSelector, + Transactions: []types.Transaction{{ + To: mcmsTargetInstanceAddr, + Data: delayData, + AdditionalFields: opAdditionalFieldsBytes, + }}, + } + + // Build bypass proposal - no delay needed since bypasser executes immediately + bypassProposal, err := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetValidUntil(validUntil). + SetDescription("Canton timelock - bypass UpdateMinDelay"). + AddTimelockAddress(s.chainSelector, s.mcmsInstanceAddress). + AddChainMetadata(s.chainSelector, metadata). + SetAction(types.TimelockActionBypass). + AddOperation(bop). + Build() + s.Require().NoError(err) + + // Convert to MCMS proposal (generates BypasserExecuteBatch choice) + converter := cantonsdk.NewTimelockConverter() + convertersMap := map[types.ChainSelector]sdk.TimelockConverter{ + s.chainSelector: converter, + } + proposal, _, err := bypassProposal.Convert(ctx, convertersMap) + s.Require().NoError(err) + + // Sign proposal + inspectorsMap := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: bypasserInspector, + } + signable, err := mcms.NewSignable(&proposal, inspectorsMap) + s.Require().NoError(err) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[0])) + s.Require().NoError(err) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[1])) + s.Require().NoError(err) + quorumMet, err := signable.ValidateSignatures(ctx) + s.Require().NoError(err) + s.Require().True(quorumMet, "quorum not met") + + // Set root and execute immediately (no timelock wait needed) + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + encoder := encoders[s.chainSelector].(*cantonsdk.Encoder) + executor, err := cantonsdk.NewExecutor(encoder, bypasserInspector, s.participant.LedgerServices.Command, s.submittingParty, []string{s.participant.PartyID}, cantonsdk.TimelockRoleBypasser) + s.Require().NoError(err) + executors := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: executor, + } + executable, err := mcms.NewExecutable(&proposal, executors) + s.Require().NoError(err) + + txSetRoot, err := executable.SetRoot(ctx, s.chainSelector) + s.Require().NoError(err) + s.Require().NotEmpty(txSetRoot.Hash) + + // Execute bypass operation - this executes the batch immediately + for i := range proposal.Operations { + _, execErr := executable.Execute(ctx, i) + s.Require().NoError(execErr, "execute bypass operation %d", i) + } + + // Verify: op count increased (bypasser executes immediately, no TimelockExecutable needed) + postOpCount, err := bypasserInspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().Equal(currentOpCount+1, postOpCount, "op count should increment after bypass execute") + + // Verify: minDelay was updated to 5 seconds (5_000_000 microseconds) + timelockInspector := cantonsdk.NewTimelockInspector(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, s.submittingParty, []string{s.participant.PartyID}) + minDelay, err := timelockInspector.GetMinDelay(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().Equal(uint64(5), minDelay, "minDelay should be 5 seconds after UpdateMinDelay bypass") +} diff --git a/e2e/tests/canton/timelock_cancel.go b/e2e/tests/canton/timelock_cancel.go new file mode 100644 index 000000000..8f2ce17e7 --- /dev/null +++ b/e2e/tests/canton/timelock_cancel.go @@ -0,0 +1,205 @@ +//go:build e2e + +package canton + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" + "github.com/smartcontractkit/mcms/types" +) + +// TimelockCancelTestSuite tests the cancel timelock flow: +// schedule a batch -> cancel it -> verify execution fails. +type TimelockCancelTestSuite struct { + mcmsExecutorSetup +} + +// TestTimelockCancel schedules a batch, cancels it, then verifies execution fails. +func (s *TimelockCancelTestSuite) TestTimelockCancel() { + ctx := s.T().Context() + + // --- Phase 1: Schedule a batch (same as TestTimelockProposal) --- + + proposerInspector := cantonsdk.NewInspector(s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + currentOpCount, err := proposerInspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "get current op count") + + // Proposer metadata for schedule + proposerMetadata, err := cantonsdk.NewChainMetadata( + currentOpCount, + s.chainId, + s.proposerMcmsId, + s.mcmsInstanceAddress, + s.mcmsId, + ) + s.Require().NoError(err) + + validUntil := uint32(time.Now().Add(24 * time.Hour).Unix()) + delay := types.NewDuration(10 * time.Second) // Longer delay so we can cancel before it's ready + + // Batch operation: increment counter + opAdditionalFields := cantonsdk.AdditionalFields{ + TargetInstanceAddress: fmt.Sprintf("%s@%s", s.counterInstanceID, s.participant.PartyID), + FunctionName: "Increment", + TargetCid: s.counterCID, + ContractIds: []string{s.counterCID}, + } + opAdditionalFieldsBytes, err := json.Marshal(opAdditionalFields) + s.Require().NoError(err) + + bop := types.BatchOperation{ + ChainSelector: s.chainSelector, + Transactions: []types.Transaction{{ + To: s.counterCID, + Data: []byte{}, + AdditionalFields: opAdditionalFieldsBytes, + }}, + } + + // Build schedule proposal + scheduleProposal, err := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetValidUntil(validUntil). + SetDescription("Canton timelock - schedule for cancel test"). + AddTimelockAddress(s.chainSelector, s.mcmsInstanceAddress). + AddChainMetadata(s.chainSelector, proposerMetadata). + SetAction(types.TimelockActionSchedule). + SetDelay(delay). + AddOperation(bop). + Build() + s.Require().NoError(err) + + // Convert, sign, and execute schedule + converter := cantonsdk.NewTimelockConverter() + convertersMap := map[types.ChainSelector]sdk.TimelockConverter{ + s.chainSelector: converter, + } + proposal, _, err := scheduleProposal.Convert(ctx, convertersMap) + s.Require().NoError(err) + + inspectorsMap := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: proposerInspector, + } + signable, err := mcms.NewSignable(&proposal, inspectorsMap) + s.Require().NoError(err) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[0])) + s.Require().NoError(err) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[1])) + s.Require().NoError(err) + + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + encoder := encoders[s.chainSelector].(*cantonsdk.Encoder) + proposerExecutor, err := cantonsdk.NewExecutor(encoder, proposerInspector, s.participant.LedgerServices.Command, s.submittingParty, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + s.Require().NoError(err) + executors := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: proposerExecutor, + } + executable, err := mcms.NewExecutable(&proposal, executors) + s.Require().NoError(err) + + _, err = executable.SetRoot(ctx, s.chainSelector) + s.Require().NoError(err) + + for i := range proposal.Operations { + _, execErr := executable.Execute(ctx, i) + s.Require().NoError(execErr, "execute schedule operation %d", i) + } + + // Note: Operation is now scheduled on-chain. We skip the IsOperationPending check + // because the operationID returned by Convert() may differ from what the contract uses. + + // --- Phase 2: Cancel the batch --- + + // Get canceller op count + cancellerInspector := cantonsdk.NewInspector(s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleCanceller) + cancellerOpCount, err := cancellerInspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + + // Canceller metadata + cancellerMcmsId := fmt.Sprintf("%s@%s-canceller", s.mcmsId, s.participant.PartyID) + cancellerMetadata, err := cantonsdk.NewChainMetadata( + cancellerOpCount, + s.chainId, + cancellerMcmsId, + s.mcmsInstanceAddress, + s.mcmsId, + ) + s.Require().NoError(err) + + // Build cancel proposal - reuse the same batch operation (the converter extracts operationId) + // Use the same salt as the schedule proposal to derive the same operation ID + salt := common.Hash(scheduleProposal.Salt()) + cancelProposal, err := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetValidUntil(validUntil). + SetDescription("Canton timelock - cancel scheduled batch"). + AddTimelockAddress(s.chainSelector, s.mcmsInstanceAddress). + AddChainMetadata(s.chainSelector, cancellerMetadata). + SetAction(types.TimelockActionCancel). + SetDelay(delay). + SetSalt(&salt). + AddOperation(bop). + Build() + s.Require().NoError(err) + + // Convert cancel proposal + cancelMcmsProposal, _, err := cancelProposal.Convert(ctx, convertersMap) + s.Require().NoError(err) + + // Sign with canceller role + cancelInspectorsMap := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: cancellerInspector, + } + cancelSignable, err := mcms.NewSignable(&cancelMcmsProposal, cancelInspectorsMap) + s.Require().NoError(err) + _, err = cancelSignable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[0])) + s.Require().NoError(err) + _, err = cancelSignable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[1])) + s.Require().NoError(err) + + // Execute cancel + cancelEncoders, err := cancelMcmsProposal.GetEncoders() + s.Require().NoError(err) + cancelEncoder := cancelEncoders[s.chainSelector].(*cantonsdk.Encoder) + cancellerExecutor, err := cantonsdk.NewExecutor(cancelEncoder, cancellerInspector, s.participant.LedgerServices.Command, s.submittingParty, []string{s.participant.PartyID}, cantonsdk.TimelockRoleCanceller) + s.Require().NoError(err) + cancelExecutors := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: cancellerExecutor, + } + cancelExecutable, err := mcms.NewExecutable(&cancelMcmsProposal, cancelExecutors) + s.Require().NoError(err) + + _, err = cancelExecutable.SetRoot(ctx, s.chainSelector) + s.Require().NoError(err) + + for i := range cancelMcmsProposal.Operations { + _, execErr := cancelExecutable.Execute(ctx, i) + s.Require().NoError(execErr, "execute cancel operation %d", i) + } + + // --- Phase 3: Verify operation is cancelled by attempting to execute --- + + // Wait for the delay to pass so we can attempt execution + time.Sleep(scheduleProposal.Delay.Duration + time.Second) + + // Attempt to execute via timelock should fail because operation was cancelled + timelockExecutor := cantonsdk.NewTimelockExecutor(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, s.submittingParty, []string{s.participant.PartyID}) + timelockExecutors := map[types.ChainSelector]sdk.TimelockExecutor{ + s.chainSelector: timelockExecutor, + } + timelockExecutable, err := mcms.NewTimelockExecutable(ctx, scheduleProposal, timelockExecutors) + s.Require().NoError(err) + + // Execute should fail with "operation not found" or similar + _, err = timelockExecutable.Execute(ctx, 0) + s.Require().Error(err, "timelock execute should fail after cancel") + s.Require().Contains(err.Error(), "not found", "error should indicate operation not found") +} diff --git a/e2e/tests/canton/timelock_inspection.go b/e2e/tests/canton/timelock_inspection.go new file mode 100644 index 000000000..48ea12f48 --- /dev/null +++ b/e2e/tests/canton/timelock_inspection.go @@ -0,0 +1,163 @@ +//go:build e2e + +package canton + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common" + + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +// TimelockInspectionTestSuite defines the test suite for Canton timelock inspection. +type TimelockInspectionTestSuite struct { + TestSuite + inspector *cantonsdk.TimelockInspector +} + +// SetupSuite runs before the test suite. +func (s *TimelockInspectionTestSuite) SetupSuite() { + s.TestSuite.SetupSuite() + s.DeployMCMSContract() + s.inspector = cantonsdk.NewTimelockInspector(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, s.submittingParty, []string{s.participant.PartyID}) +} + +// TestGetProposers tests that GetProposers returns proposer signers from the MCMS contract. +func (s *TimelockInspectionTestSuite) TestGetProposers() { + ctx := s.T().Context() + proposers, err := s.inspector.GetProposers(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().NotNil(proposers) + // Fresh MCMS has no signers configured; list may be empty +} + +// TestGetExecutors tests that GetExecutors returns unsupported on Canton. +func (s *TimelockInspectionTestSuite) TestGetExecutors() { + ctx := s.T().Context() + executors, err := s.inspector.GetExecutors(ctx, s.mcmsInstanceAddress) + s.Require().Error(err, "GetExecutors should return an error on Canton") + s.Require().Contains(err.Error(), "unsupported on Canton") + s.Require().Nil(executors) +} + +// TestGetBypassers tests that GetBypassers returns bypasser signers from the MCMS contract. +func (s *TimelockInspectionTestSuite) TestGetBypassers() { + ctx := s.T().Context() + bypassers, err := s.inspector.GetBypassers(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().NotNil(bypassers) + // Fresh MCMS has no signers configured; list may be empty +} + +// TestGetCancellers tests that GetCancellers returns canceller signers from the MCMS contract. +func (s *TimelockInspectionTestSuite) TestGetCancellers() { + ctx := s.T().Context() + cancellers, err := s.inspector.GetCancellers(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().NotNil(cancellers) + // Fresh MCMS has no signers configured; list may be empty +} + +// TestIsOperation tests that IsOperation queries the ledger (returns false for unknown op ID). +func (s *TimelockInspectionTestSuite) TestIsOperation() { + ctx := s.T().Context() + var opID [32]byte + copy(opID[:], "test-operation-id") + isOp, err := s.inspector.IsOperation(ctx, s.mcmsInstanceAddress, opID) + s.Require().NoError(err) + s.Require().False(isOp) +} + +// TestIsOperationPending tests that IsOperationPending queries the ledger. +func (s *TimelockInspectionTestSuite) TestIsOperationPending() { + ctx := s.T().Context() + var opID [32]byte + copy(opID[:], "test-pending-id") + isPending, err := s.inspector.IsOperationPending(ctx, s.mcmsInstanceAddress, opID) + s.Require().NoError(err) + s.Require().False(isPending) +} + +// TestIsOperationReady tests that IsOperationReady queries the ledger. +func (s *TimelockInspectionTestSuite) TestIsOperationReady() { + ctx := s.T().Context() + var opID [32]byte + copy(opID[:], "test-ready-id") + isReady, err := s.inspector.IsOperationReady(ctx, s.mcmsInstanceAddress, opID) + s.Require().NoError(err) + s.Require().False(isReady) +} + +// TestIsOperationDone tests that IsOperationDone queries the ledger. +func (s *TimelockInspectionTestSuite) TestIsOperationDone() { + ctx := s.T().Context() + var opID [32]byte + copy(opID[:], "test-done-id") + isDone, err := s.inspector.IsOperationDone(ctx, s.mcmsInstanceAddress, opID) + s.Require().NoError(err) + s.Require().False(isDone) +} + +// TestGetMinDelay tests that GetMinDelay returns the MCMS min delay from the ledger. +func (s *TimelockInspectionTestSuite) TestGetMinDelay() { + ctx := s.T().Context() + delay, err := s.inspector.GetMinDelay(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().NotZero(delay) +} + +// TestTimelockConverter tests that ConvertBatchToChainOperations returns one ScheduleBatch operation and a non-zero op ID. +func (s *TimelockInspectionTestSuite) TestTimelockConverter() { + ctx := s.T().Context() + metadata, err := cantonsdk.NewChainMetadata(0, s.chainId, s.proposerMcmsId, s.mcmsInstanceAddress, s.mcmsId) + s.Require().NoError(err) + + af := cantonsdk.AdditionalFields{ + TargetInstanceAddress: "instance@party", + FunctionName: "noop", + TargetCid: s.mcmsInstanceAddress, + ContractIds: []string{s.mcmsInstanceAddress}, + } + afBytes, err := json.Marshal(af) + s.Require().NoError(err) + + bop := mcmstypes.BatchOperation{ + ChainSelector: s.chainSelector, + Transactions: []mcmstypes.Transaction{{ + To: s.mcmsInstanceAddress, + Data: []byte{}, + AdditionalFields: afBytes, + }}, + } + converter := cantonsdk.NewTimelockConverter() + ops, opID, err := converter.ConvertBatchToChainOperations( + ctx, + metadata, + bop, + s.mcmsInstanceAddress, + s.mcmsInstanceAddress, + mcmstypes.NewDuration(0), + mcmstypes.TimelockActionSchedule, + common.Hash{}, + common.Hash{}, + ) + s.Require().NoError(err) + s.Require().Len(ops, 1) + s.Require().NotEqual(common.Hash{}, opID) +} + +// TestTimelockExecutorExecuteEmptyBatch tests that Execute returns an error for empty batch. +func (s *TimelockInspectionTestSuite) TestTimelockExecutorExecuteEmptyBatch() { + ctx := s.T().Context() + executor := cantonsdk.NewTimelockExecutor(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, s.submittingParty, []string{s.participant.PartyID}) + bop := mcmstypes.BatchOperation{ + ChainSelector: s.chainSelector, + Transactions: []mcmstypes.Transaction{}, + } + res, err := executor.Execute(ctx, bop, s.mcmsInstanceAddress, common.Hash{}, common.Hash{}) + s.Require().Error(err, "Execute should return an error for empty batch") + s.Require().Contains(err.Error(), "no transactions") + s.Require().Equal(mcmstypes.TransactionResult{}, res) +} diff --git a/e2e/tests/canton/timelock_proposal.go b/e2e/tests/canton/timelock_proposal.go new file mode 100644 index 000000000..ed8d06275 --- /dev/null +++ b/e2e/tests/canton/timelock_proposal.go @@ -0,0 +1,143 @@ +//go:build e2e + +package canton + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + cantonsdk "github.com/smartcontractkit/mcms/sdk/canton" + "github.com/smartcontractkit/mcms/types" +) + +// TimelockProposalTestSuite defines the test suite for Canton timelock proposal flow: +// build proposal -> Convert -> sign -> SetRoot -> Execute (schedule) -> TimelockExecutable.Execute. +// Embeds mcmsExecutorSetup (not MCMSExecutorTestSuite) so only TestTimelockProposal runs when the suite runs. +type TimelockProposalTestSuite struct { + mcmsExecutorSetup +} + +// TestTimelockProposal runs the full timelock flow: build a Schedule proposal (increment counter), +// convert to MCMS proposal, sign, set root, execute (schedule batch), then execute via timelock. +func (s *TimelockProposalTestSuite) TestTimelockProposal() { + ctx := s.T().Context() + + inspector := cantonsdk.NewInspector(s.participant.LedgerServices.State, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + currentOpCount, err := inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err, "get current op count") + + // Canton chain metadata: multisigId = makeMcmsId(instanceId, Proposer); baseInstanceId for converter TargetInstanceId + metadata, err := cantonsdk.NewChainMetadata( + currentOpCount, + s.chainId, + s.proposerMcmsId, + s.mcmsInstanceAddress, + s.mcmsId, + ) + s.Require().NoError(err) + + validUntil := uint32(time.Now().Add(24 * time.Hour).Unix()) + delay := types.NewDuration(2 * time.Second) + + // Batch operation: increment counter (same shape as in TestSetRootAndExecuteCounterOp) + opAdditionalFields := cantonsdk.AdditionalFields{ + TargetInstanceAddress: fmt.Sprintf("%s@%s", s.counterInstanceID, s.participant.PartyID), + FunctionName: "Increment", + TargetCid: s.counterCID, + ContractIds: []string{s.counterCID}, + } + opAdditionalFieldsBytes, err := json.Marshal(opAdditionalFields) + s.Require().NoError(err) + + bop := types.BatchOperation{ + ChainSelector: s.chainSelector, + Transactions: []types.Transaction{{ + To: s.counterCID, + Data: []byte{}, + AdditionalFields: opAdditionalFieldsBytes, + }}, + } + + // Build timelock proposal (Schedule action); timelock address is InstanceAddress hex + timelockProposal, err := mcms.NewTimelockProposalBuilder(). + SetVersion("v1"). + SetValidUntil(validUntil). + SetDescription("Canton timelock - schedule counter increment"). + AddTimelockAddress(s.chainSelector, s.mcmsInstanceAddress). + AddChainMetadata(s.chainSelector, metadata). + SetAction(types.TimelockActionSchedule). + SetDelay(delay). + AddOperation(bop). + Build() + s.Require().NoError(err) + + // Convert timelock proposal to MCMS proposal (requires Canton TimelockConverter implementation) + converter := cantonsdk.NewTimelockConverter() + convertersMap := map[types.ChainSelector]sdk.TimelockConverter{ + s.chainSelector: converter, + } + proposal, _, err := timelockProposal.Convert(ctx, convertersMap) + s.Require().NoError(err, "Convert: Canton TimelockConverter must be implemented (Phase C)") + + // Sign proposal + inspectorsMap := map[types.ChainSelector]sdk.Inspector{ + s.chainSelector: inspector, + } + signable, err := mcms.NewSignable(&proposal, inspectorsMap) + s.Require().NoError(err) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[0])) + s.Require().NoError(err) + _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(s.sortedSigners[1])) + s.Require().NoError(err) + quorumMet, err := signable.ValidateSignatures(ctx) + s.Require().NoError(err) + s.Require().True(quorumMet, "quorum not met") + + // Set root + encoders, err := proposal.GetEncoders() + s.Require().NoError(err) + encoder := encoders[s.chainSelector].(*cantonsdk.Encoder) + executor, err := cantonsdk.NewExecutor(encoder, inspector, s.participant.LedgerServices.Command, s.submittingParty, []string{s.participant.PartyID}, cantonsdk.TimelockRoleProposer) + s.Require().NoError(err) + executors := map[types.ChainSelector]sdk.Executor{ + s.chainSelector: executor, + } + executable, err := mcms.NewExecutable(&proposal, executors) + s.Require().NoError(err) + txSetRoot, err := executable.SetRoot(ctx, s.chainSelector) + s.Require().NoError(err) + s.Require().NotEmpty(txSetRoot.Hash) + // No proposal mutation: proposal keeps InstanceAddress hex; executor resolves at submit time + + // Execute proposal operations (schedules the batch on-chain) + for i := range proposal.Operations { + _, execErr := executable.Execute(ctx, i) + s.Require().NoError(execErr, "execute scheduled operation %d", i) + } + + // Timelock execution: wait for ready then execute batch + timelockExecutor := cantonsdk.NewTimelockExecutor(s.participant.LedgerServices.Command, s.participant.LedgerServices.State, s.submittingParty, []string{s.participant.PartyID}) + timelockExecutors := map[types.ChainSelector]sdk.TimelockExecutor{ + s.chainSelector: timelockExecutor, + } + timelockExecutable, err := mcms.NewTimelockExecutable(ctx, timelockProposal, timelockExecutors) + s.Require().NoError(err) + + // Wait until operation is ready (delay has passed) + time.Sleep(timelockProposal.Delay.Duration + time.Second) + s.Require().NoError(timelockExecutable.IsReady(ctx), "timelock operation should become ready") + + // Execute the scheduled batch via timelock + for i := range timelockProposal.Operations { + _, terr := timelockExecutable.Execute(ctx, i) + s.Require().NoError(terr, "timelock execute operation %d", i) + } + + // Verify: op count increased (inspector resolves InstanceAddress when querying) + postOpCount, err := inspector.GetOpCount(ctx, s.mcmsInstanceAddress) + s.Require().NoError(err) + s.Require().Equal(currentOpCount+1, postOpCount, "op count should increment after timelock execute") +} diff --git a/e2e/tests/runner_test.go b/e2e/tests/runner_test.go index a04ff50e7..0f778b40c 100644 --- a/e2e/tests/runner_test.go +++ b/e2e/tests/runner_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/suite" aptose2e "github.com/smartcontractkit/mcms/e2e/tests/aptos" + cantone2e "github.com/smartcontractkit/mcms/e2e/tests/canton" evme2e "github.com/smartcontractkit/mcms/e2e/tests/evm" solanae2e "github.com/smartcontractkit/mcms/e2e/tests/solana" suie2e "github.com/smartcontractkit/mcms/e2e/tests/sui" @@ -49,3 +50,13 @@ func TestTONSuite(t *testing.T) { suite.Run(t, new(tone2e.ExecutionTestSuite)) suite.Run(t, new(tone2e.TimelockInspectionTestSuite)) } + +func TestCantonSuite(t *testing.T) { + suite.Run(t, new(cantone2e.MCMSConfigurerTestSuite)) + suite.Run(t, new(cantone2e.MCMSInspectorTestSuite)) + suite.Run(t, new(cantone2e.TimelockInspectionTestSuite)) + suite.Run(t, new(cantone2e.TimelockProposalTestSuite)) + suite.Run(t, new(cantone2e.TimelockCancelTestSuite)) + suite.Run(t, new(cantone2e.TimelockBypassTestSuite)) + suite.Run(t, new(cantone2e.SetRootExecuteTestSuite)) +} diff --git a/e2e/tests/setup.go b/e2e/tests/setup.go index 65adf90f1..c183c37ca 100644 --- a/e2e/tests/setup.go +++ b/e2e/tests/setup.go @@ -43,6 +43,7 @@ type Config struct { AptosChain *blockchain.Input `toml:"aptos_config"` SuiChain *blockchain.Input `toml:"sui_config"` TonChain *blockchain.Input `toml:"ton_config"` + CantonChain *blockchain.Input `toml:"canton_config"` Settings struct { PrivateKeys []string `toml:"private_keys"` @@ -64,6 +65,7 @@ type TestSetup struct { SuiNodeURL string TonClient *ton.APIClient TonBlockchain *blockchain.Output + CantonBlockchain *blockchain.Output Config } @@ -228,14 +230,29 @@ func InitializeSharedTestSetup(t *testing.T) *TestSetup { require.NoError(t, err, "Failed to initialize TON blockchain") nodeURL := tonBlockchainOutput.Nodes[0].ExternalHTTPUrl - pool, err := tonchain.CreateLiteserverConnectionPool(ctx, nodeURL) - require.NoError(t, err, "Failed to initialize TON client - failed to create liteserver connection pool") + pool, poolErr := tonchain.CreateLiteserverConnectionPool(ctx, nodeURL) + require.NoError(t, poolErr, "Failed to initialize TON client - failed to create liteserver connection pool") tonClient = ton.NewAPIClient(pool, ton.ProofCheckPolicyFast) // Test liveness, will also fetch ChainID t.Logf("Initialized TON RPC client @ %s", nodeURL) } + var ( + cantonBlockchainOutput *blockchain.Output + ) + if in.CantonChain != nil { + // Use blockchain network setup (fallback) + ports := freeport.GetN(t, 2) + port := ports[0] + faucetPort := ports[1] + in.CantonChain.Port = strconv.Itoa(port) + in.CantonChain.FaucetPort = strconv.Itoa(faucetPort) + + cantonBlockchainOutput, err = blockchain.NewBlockchainNetwork(in.CantonChain) + require.NoError(t, err, "Failed to initialize Canton blockchain") + } + sharedSetup = &TestSetup{ ClientA: ethClientA, ClientB: ethClientB, @@ -249,6 +266,7 @@ func InitializeSharedTestSetup(t *testing.T) *TestSetup { SuiNodeURL: suiNodeURL, TonClient: tonClient, TonBlockchain: tonBlockchainOutput, + CantonBlockchain: cantonBlockchainOutput, Config: *in, } }) diff --git a/factory.go b/factory.go index 5d7a6a900..f50fbfb58 100644 --- a/factory.go +++ b/factory.go @@ -9,6 +9,7 @@ import ( "github.com/smartcontractkit/mcms/chainwrappers" "github.com/smartcontractkit/mcms/sdk" "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/sdk/canton" "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" @@ -61,6 +62,12 @@ func newEncoder( txCount, overridePreviousRoot, ) + case chainsel.FamilyCanton: + encoder = canton.NewEncoder( + csel, + txCount, + overridePreviousRoot, + ) } return encoder, nil @@ -90,6 +97,8 @@ func operationIDFn(_ context.Context, csel types.ChainSelector) (sdk.OperationID return sui.OperationID, nil case chainsel.FamilyTon: return ton.OperationID, nil + case chainsel.FamilyCanton: + return canton.OperationID, nil default: return nil, fmt.Errorf("unsupported chain family %s", family) } diff --git a/go.mod b/go.mod index 460685248..a164a957a 100644 --- a/go.mod +++ b/go.mod @@ -5,33 +5,41 @@ go 1.26.2 //nolint:gomoddirectives // allow replace directive replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 +replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4 + +replace github.com/digital-asset/dazl-client/v8 => github.com/noders-team/dazl-client/v8 v8.7.1-2 + require ( - github.com/Masterminds/semver/v3 v3.4.0 + github.com/Masterminds/semver/v3 v3.5.0 github.com/aptos-labs/aptos-go-sdk v1.13.0 github.com/block-vision/sui-go-sdk v1.2.1 + github.com/digital-asset/dazl-client/v8 v8.9.0 github.com/ethereum/go-ethereum v1.17.3 github.com/gagliardetto/binary v0.8.0 github.com/gagliardetto/solana-go v1.13.0 github.com/go-playground/validator/v10 v10.30.2 github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb github.com/samber/lo v1.53.0 - github.com/smartcontractkit/chain-selectors v1.0.100 + github.com/smartcontractkit/chain-selectors v1.0.101 github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d - github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 + github.com/smartcontractkit/chainlink-canton v0.0.0-20260602133237-99f834640c9d + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 + github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67 - github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.3 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.4 github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745 github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad + github.com/smartcontractkit/go-daml v0.0.0-20260604143752-c6f6567940ba github.com/spf13/cast v1.10.0 github.com/stellar/go-stellar-sdk v0.5.0 github.com/stretchr/testify v1.11.1 github.com/xssnick/tonutils-go v1.14.1 github.com/zksync-sdk/zksync2-go v1.1.1-0.20250620124214-2c742ee399c6 go.uber.org/zap v1.28.0 - golang.org/x/crypto v0.51.0 + golang.org/x/crypto v0.52.0 golang.org/x/tools v0.45.0 gotest.tools/v3 v3.5.2 ) @@ -40,12 +48,13 @@ require ( dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/BurntSushi/toml v1.5.0 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/DataDog/zstd v1.5.6 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/XSAM/otelsql v0.37.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/avast/retry-go/v4 v4.7.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect @@ -56,13 +65,17 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/btcutil v1.1.6 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/btcsuite/btcutil v1.0.2 // indirect + github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect github.com/buger/jsonparser v1.1.2 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/cockroachdb/errors v1.11.3 // indirect github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect @@ -79,20 +92,23 @@ require ( github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/creachadair/jrpc2 v1.2.0 // indirect + github.com/creachadair/mds v0.13.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dchest/siphash v1.2.3 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/emicklei/dot v1.6.2 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a // indirect - github.com/fatih/color v1.18.0 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fbsobreira/gotron-sdk v0.0.0-20250403083053-2943ce8c759b // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -100,6 +116,8 @@ require ( github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.12.0 // indirect github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -111,25 +129,25 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/rpc v1.2.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go v1.2.8 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/graph-gophers/graphql-go v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.8.0 // indirect @@ -152,25 +170,24 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.5 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.11.1 // indirect + github.com/lib/pq v1.12.3 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/api v1.54.2 // indirect github.com/moby/moby/client v0.4.0 // indirect github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/spdystream v0.5.1 // indirect @@ -184,6 +201,7 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oapi-codegen/runtime v1.4.1 // indirect github.com/oklog/run v1.2.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -199,12 +217,13 @@ require ( github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/common v1.20.99 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/rivo/uniseg v0.4.7 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/rs/zerolog v1.34.0 // indirect + github.com/rs/zerolog v1.35.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect @@ -215,9 +234,12 @@ require ( github.com/sirupsen/logrus v1.9.4 // indirect github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect + github.com/smartcontractkit/chainlink-deployments-framework v0.108.0 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 // indirect + github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9 // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e // indirect github.com/spf13/pflag v1.0.10 // indirect @@ -229,11 +251,13 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/testcontainers/testcontainers-go v0.42.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect github.com/valyala/fastjson v1.6.10 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect @@ -241,12 +265,13 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver v1.17.2 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect @@ -255,28 +280,28 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect - go.opentelemetry.io/otel/log v0.19.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel/log v0.20.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.20.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/arch v0.22.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/net v0.54.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/grpc v1.81.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.81.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -284,12 +309,13 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.3 // indirect - k8s.io/apimachinery v0.32.3 // indirect + k8s.io/apimachinery v0.33.2 // indirect k8s.io/client-go v0.32.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect - k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 898f04ca2..1d5adf7b9 100644 --- a/go.sum +++ b/go.sum @@ -11,16 +11,17 @@ github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj4 github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= @@ -30,12 +31,50 @@ github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKS github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/apache/arrow-go/v18 v18.3.1 h1:oYZT8FqONiK74JhlH3WKVv+2NKYoyZ7C2ioD4Dj3ixk= github.com/apache/arrow-go/v18 v18.3.1/go.mod h1:12QBya5JZT6PnBihi5NJTzbACrDGXYkrgjujz3MRQXU= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aptos-labs/aptos-go-sdk v1.13.0 h1:epv7K/tIbAEO2RfogwGacICBig8rrigJj24fDsy6KTg= github.com/aptos-labs/aptos-go-sdk v1.13.0/go.mod h1:FTgKp0RLfEefllCdkCj0jPU14xWk11yA7SFVfCDLUj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= +github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= +github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.1 h1:wb/PYYm3wlcqGzw7Ls4GD3X5+seDDoNdVYIB6I/V87E= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.1/go.mod h1:xvHowJ6J9CuaFE04S8fitWQXytf4sHz3DTPGhw9FtmU= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= +github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -49,6 +88,7 @@ github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHf github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/block-vision/sui-go-sdk v1.2.1 h1:uwvGbzfcrS4SsIaakclYxy0qgEF1XWIUtTYWXB4PoAw= github.com/block-vision/sui-go-sdk v1.2.1/go.mod h1:t8mWASwfyv+EyqHGO9ZrcDiCJWGOFEXqq50TMJ8GQco= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= @@ -69,8 +109,8 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= -github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= @@ -82,6 +122,12 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -99,6 +145,8 @@ github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 h1:ydUjnKn4RoCe github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny999RuVUtNjzTGa9HCHpXjrLGMipJVq5kqVpudBl0= github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= @@ -127,7 +175,6 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= @@ -136,6 +183,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creachadair/jrpc2 v1.2.0 h1:SXr0OgnwM0X18P+HccJP0uT3KGSDk/BCSRlJBvE2bMY= +github.com/creachadair/jrpc2 v1.2.0/go.mod h1:66uKSdr6tR5ZeNvkIjDSbbVUtOv0UhjS/vcd8ECP7Iw= +github.com/creachadair/mds v0.13.4 h1:RgU0MhiVqkzp6/xtNWhK6Pw7tDeaVuGFtA0UA2RBYvY= +github.com/creachadair/mds v0.13.4/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -165,16 +216,16 @@ github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbz github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -189,12 +240,14 @@ github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSa github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a h1:eIFUceK3U/z9UV0D/kAI6cxA27eH7MPqt2ks7fbzj/k= github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a/go.mod h1:nABYy4hsKZpuN0mu0uybdjrIOuGb1eE7b1lci/ezUAo= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -218,10 +271,16 @@ github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8x github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-snaps v0.5.19 h1:hUJlCQOpTt1M+kSisMwioDWZDWpDtdAvUhvWCx1YGW0= github.com/gkampitakis/go-snaps v0.5.19/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= @@ -264,15 +323,14 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -320,6 +378,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -329,14 +389,16 @@ github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCE github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -389,6 +451,8 @@ github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5Xum github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -400,6 +464,7 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb h1:Ag83At00qa4FLkcdMgrwHVSakqky/eZczOlxd4q336E= github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -408,8 +473,8 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -425,8 +490,8 @@ github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= -github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= @@ -435,19 +500,18 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= +github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= @@ -468,8 +532,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= -github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= @@ -489,6 +553,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -497,9 +563,15 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/noders-team/dazl-client/v8 v8.7.1-2 h1:a8PXw76lE6ozb/MVW/Akw56qXrPM6xUnpz/rT2MXzTA= +github.com/noders-team/dazl-client/v8 v8.7.1-2/go.mod h1:q1KevCJ8FpH8je2MnnjN8/QUfhstB4fKpyKyqDtqFh0= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= +github.com/oapi-codegen/runtime v1.4.1 h1:9nwLoI+KrWxzbBcp0jO/R8uXqbik/HUyCvPeU68Y/qo= +github.com/oapi-codegen/runtime v1.4.1/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -510,16 +582,16 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= +github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -562,15 +634,18 @@ github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v1.20.99 h1:vZEybF3CT0t6L0UjsOtHRML7vuIglHocmvJMMH/se4M= +github.com/prometheus/common v1.20.99/go.mod h1:VX44Tebe4qpuTK+MQWg25h4fJGKBqzObSdxuB7y8K/Y= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -578,9 +653,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= @@ -589,6 +663,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= +github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc= +github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= @@ -602,32 +678,50 @@ github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97M github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= -github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9LsA7vTMPv+0n7ClhSFnZFAk= +github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= +github.com/smartcontractkit/chain-selectors v1.0.101 h1:TF4ma9h3QeyIZ8XoEmgI5lrUvZfzHAz8tfR0pV0+GCA= +github.com/smartcontractkit/chain-selectors v1.0.101/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc h1:Um9FBcf0JNSFuGbxgccDG1vM3cNrMGy0SdJ7r6VbX0o= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d h1:xdFpzbApEMz4Rojg2Y2OjFlrh0wu7eB10V2tSZGW5y8= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d/go.mod h1:bgmqE7x9xwmIVr8PqLbC0M5iPm4AV2DBl596lO6S5Sw= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 h1:Z4t2ZY+ZyGWxtcXvPr11y4o3CGqhg3frJB5jXkCSvWA= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= +github.com/smartcontractkit/chainlink-canton v0.0.0-20260602133237-99f834640c9d h1:aBQYdlGQvqftkcUo0GIgtaLDs/84FQzBYpF4tZUfPA0= +github.com/smartcontractkit/chainlink-canton v0.0.0-20260602133237-99f834640c9d/go.mod h1:/oTkN9bVrQ1ROUNtIJJSNdffl3NReClq+qINNkGAwlY= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 h1:jkChf04hhdiMBApbb+lLDxHMY62Md6UeM7v++GSw3K8= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139/go.mod h1:wuhagkM/lU0GbV2YcrROOH0GlsfXJYwm6qmpa4CK70w= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139 h1:tw3K4UkH5XfW5SoyYkvAlbzrccoGSLdz/XkxD6nyGC8= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260129103204-4c8453dd8139/go.mod h1:1WcontO9PeuKdUf5HXfs3nuICtzUvFNnyCmrHkTCF9Y= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 h1:5z3LQ27MJmhiaeqp9S2TzbF5Wm4GGvUKAYOtE9AauR8= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89/go.mod h1:G2AII0QmWzXx8Ag9IKnGN3h/gwwNnhHUOCviJievdvo= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10/go.mod h1:oiDa54M0FwxevWwyAX773lwdWvFYYlYHHQV1LQ5HpWY= +github.com/smartcontractkit/chainlink-deployments-framework v0.108.0 h1:zdAOm37CLclz9RTItQwe8oBijCVG/cYWhML0MQROMzM= +github.com/smartcontractkit/chainlink-deployments-framework v0.108.0/go.mod h1:ubpvoLoRdru8IQHw3TFr7KthbjYpAwmiRmvvNCf2daA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= +github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/op-catalog v0.1.0 h1:hGEJFD2X3oNIPXQbtIPxCJyg5CcKglRCYBmESS+gmeQ= +github.com/smartcontractkit/chainlink-protos/op-catalog v0.1.0/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67 h1:NNvPOgvf5vbOYVLxLST+5E88iPOAnpmzZGPihEx8DFc= github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67/go.mod h1:k1HSbHyPaQWPOj6lXDIAe04EuwbC5ge1nK+cpG2E8hE= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.3 h1:y3dFPfouGziisDJa0JbY6DE7/JjoSCP/5aebzHcj4jA= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.3/go.mod h1:W+X9xaRulD8dD87QOB0njHkBKkmYxB4E0osfpe9808I= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.4 h1:8M+2pA0qx9rXaxmpKouUHj983vQCGzztHkG0XjE5Eew= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.4/go.mod h1:nyOjn4ADJGqRMe3+4ZXSV+J/7nWb1H2Vx8Qk57eLRYA= +github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 h1:RwZXxdIAOyjp6cwc9Quxgr38k8r7ACz+Lxh9o/A6oH0= +github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5/go.mod h1:kHYJnZUqiPF7/xN5273prV+srrLJkS77GbBXHLKQpx0= github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745 h1:eieKLvYuzwBPh/FdbUS1gnIanI86zgWby1L10o90g4o= github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745/go.mod h1:8vXLeG//BxDF86GWRytzGIy6jc70htD1r/KtPfjrsK0= +github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9 h1:7Ut0g+Pdm+gcu2J/Xv8OpQOVf7uLGErMX8yhC4b4tIA= +github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9/go.mod h1:h9hMs6K4hT1+mjYnJD3/SW1o7yC/sKjNi0Qh8hLfiCE= +github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4 h1:J4qtAo0ZmgX5pIr8Y5mdC+J2rj2e/6CTUC263t6mGOM= +github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.4/go.mod h1:4WhGgCA0smBbBud5mK+jnDb2wwndMvoqaWBJ3OV/7Bw= github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad h1:lgHxTHuzJIF3Vj6LSMOnjhqKgRqYW+0MV2SExtCYL1Q= github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= +github.com/smartcontractkit/go-daml v0.0.0-20260604143752-c6f6567940ba h1:peYJwUWOv54aigdk1VFzkmXdZmZK4xixfxv0Af1l6/I= +github.com/smartcontractkit/go-daml v0.0.0-20260604143752-c6f6567940ba/go.mod h1:SqWfl3Bp9NleC9jhzFUaOGzOZeKfldpY4QOW6A6NSNM= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e h1:poXTj5cFVM6XfC4HICIDYkDVc/A6OYB0eeID0wU2JQE= @@ -636,6 +730,7 @@ github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stellar/go-stellar-sdk v0.5.0 h1:xpOO+ZTyvGz54wTm7pwl2Gf1e6lZl0ExrJ/tKb+Roj4= github.com/stellar/go-stellar-sdk v0.5.0/go.mod h1:tLKAQPxa2I5UvGMabBbUXcY3fmgYnfDudrMeK7CDX4w= github.com/stellar/go-xdr v0.0.0-20260312225820-cc2b0611aabf h1:GY1RVbX3Hg7poPXEf6yojjP0hyypvgUgZmCqQU9D0xg= @@ -647,6 +742,7 @@ github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:Vl github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -658,6 +754,7 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= @@ -669,10 +766,13 @@ github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 h1:AOtFXssrDlLm84A2sTTR/AhvJiYbrIuCO59d+Ro9Tb0= +github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0/go.mod h1:k2a09UKhgSp6vNpliIY0QSgm4Hi7GXVTzWvWgUemu/8= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -682,8 +782,12 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -713,20 +817,22 @@ github.com/zksync-sdk/zksync2-go v1.1.1-0.20250620124214-2c742ee399c6 h1:VRdX3Gn github.com/zksync-sdk/zksync2-go v1.1.1-0.20250620124214-2c742ee399c6/go.mod h1:NWNlQS21isOsSsn+hLRAPpiuv+3P+LcdaZNuRt2T5Yo= go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0/go.mod h1:MP4eemTiI9zC8fgg+DYynhYDYf3ba72S376TvP+Ye0Q= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= @@ -743,24 +849,26 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmc go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= -go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= -go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= +go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs= +go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= -go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY= +go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU= +go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg= +go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -770,6 +878,8 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -780,10 +890,10 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -795,8 +905,8 @@ golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= @@ -840,8 +950,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= @@ -889,18 +999,16 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE= golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -958,18 +1066,18 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= -google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1016,21 +1124,24 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= -k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= -k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno= -k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/sdk/canton/chain.go b/sdk/canton/chain.go new file mode 100644 index 000000000..5680a611c --- /dev/null +++ b/sdk/canton/chain.go @@ -0,0 +1,24 @@ +package canton + +import ( + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" +) + +// LedgerServices holds the ledger API clients required by MCMS Canton integrations. +type LedgerServices struct { + State apiv2.StateServiceClient + Command apiv2.CommandServiceClient +} + +// Participant is a Canton ledger participant used by MCMS. +type Participant struct { + PartyID string + LedgerServices LedgerServices +} + +// Chain holds Canton participants for a chain selector. +// Callers that use chainlink-deployments-framework should map cldf canton.Chain to this type +// when implementing chainwrappers.ChainAccessor. +type Chain struct { + Participants []Participant +} diff --git a/sdk/canton/chain_metadata.go b/sdk/canton/chain_metadata.go new file mode 100644 index 000000000..4c62dbffe --- /dev/null +++ b/sdk/canton/chain_metadata.go @@ -0,0 +1,123 @@ +package canton + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/smartcontractkit/mcms/types" +) + +type TimelockRole uint8 + +func (t TimelockRole) String() string { + switch t { + case TimelockRoleBypasser: + return "Bypasser" + case TimelockRoleProposer: + return "Proposer" + case TimelockRoleCanceller: + return "Canceller" + } + + return "unknown" +} + +func (t TimelockRole) Byte() uint8 { + return uint8(t) +} + +const ( + TimelockRoleBypasser TimelockRole = iota + TimelockRoleCanceller + TimelockRoleProposer +) + +func CantonRoleFromAction(action types.TimelockAction) (TimelockRole, error) { + switch action { + case types.TimelockActionBypass: + return TimelockRoleBypasser, nil + case types.TimelockActionSchedule: + return TimelockRoleProposer, nil + case types.TimelockActionCancel: + return TimelockRoleCanceller, nil + default: + return 0, errors.New("unknown timelock action") + } +} + +// AdditionalFieldsMetadata holds Canton fields that must be supplied in chain metadata additionalFields. +// PreOpCount, PostOpCount, and OverridePreviousRoot come from StartingOpCount, proposal tx count / encoder, +// and the proposal's OverridePreviousRoot flag respectively — not from additionalFields. +type AdditionalFieldsMetadata struct { + ChainId int64 `json:"chainId"` + MultisigId string `json:"multisigId"` + InstanceId string `json:"instanceId,omitempty"` // base instanceId; converter uses for TargetInstanceId in ScheduleBatch etc. +} + +func (f AdditionalFieldsMetadata) Validate() error { + if f.ChainId <= 0 { + return errors.New("chainId must be positive") + } + if f.MultisigId == "" { + return errors.New("multisigId is required") + } + + return nil +} + +// ValidateChainMetadata validates Canton chain metadata +func ValidateChainMetadata(metadata types.ChainMetadata) error { + var additionalFields AdditionalFieldsMetadata + if err := json.Unmarshal(metadata.AdditionalFields, &additionalFields); err != nil { + return fmt.Errorf("unable to unmarshal additional fields: %w", err) + } + + if err := additionalFields.Validate(); err != nil { + return fmt.Errorf("additional fields are invalid: %w", err) + } + + return nil +} + +// NewChainMetadata creates new Canton chain metadata. +// multisigId is "@-" (DAML SetRoot/Op); must match the role used at execution time. +// baseInstanceId is the MCMS contract instanceId; if non-empty, converter uses it for TargetInstanceId in self-dispatch ops. +// mcmsInstanceAddress is the MCMS InstanceAddress hex (32-byte Keccak256 of "instanceId@party"); may be prefixed with "0x". +func NewChainMetadata( + startingOpCount uint64, + chainId int64, + multisigId string, + mcmsInstanceAddress string, + baseInstanceId string, +) (types.ChainMetadata, error) { + if mcmsInstanceAddress == "" { + return types.ChainMetadata{}, errors.New("MCMS InstanceAddress is required") + } + hexStr := strings.TrimPrefix(mcmsInstanceAddress, "0x") + if len(hexStr) != instanceAddressHexLen { + return types.ChainMetadata{}, fmt.Errorf("MCMS InstanceAddress hex must be 64 characters (with or without 0x prefix), got %d", len(hexStr)) + } + + additionalFields := AdditionalFieldsMetadata{ + ChainId: chainId, + MultisigId: multisigId, + InstanceId: baseInstanceId, + } + + if err := additionalFields.Validate(); err != nil { + return types.ChainMetadata{}, fmt.Errorf("additional fields are invalid: %w", err) + } + + additionalFieldsBytes, err := json.Marshal(additionalFields) + if err != nil { + return types.ChainMetadata{}, fmt.Errorf("unable to marshal additional fields: %w", err) + } + + return types.ChainMetadata{ + StartingOpCount: startingOpCount, + AdditionalFields: additionalFieldsBytes, + MCMAddress: mcmsInstanceAddress, + }, nil +} diff --git a/sdk/canton/chain_metadata_infer.go b/sdk/canton/chain_metadata_infer.go new file mode 100644 index 000000000..83069d6c7 --- /dev/null +++ b/sdk/canton/chain_metadata_infer.go @@ -0,0 +1,106 @@ +package canton + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/smartcontractkit/mcms/types" +) + +// defaultMCMSInstanceIDCandidates are common Canton MCMS instance IDs tried when inferring +// chain metadata from mcmAddress + party (e.g. proposals generated without additionalFields). +var defaultMCMSInstanceIDCandidates = []string{mcmsInstanceIDCCIP, mcmsInstanceIDCCV, mcmsInstanceIDDefault} + +func resolveAdditionalFieldsMetadata( + metadata types.ChainMetadata, + bop types.BatchOperation, + action types.TimelockAction, +) (AdditionalFieldsMetadata, error) { + if len(metadata.AdditionalFields) > 0 { + var fields AdditionalFieldsMetadata + if err := json.Unmarshal(metadata.AdditionalFields, &fields); err != nil { + return AdditionalFieldsMetadata{}, fmt.Errorf("unmarshal metadata additional fields: %w", err) + } + if err := fields.Validate(); err == nil { + return fields, nil + } + } + + party, err := partyFromBatchOperation(bop) + if err != nil { + return AdditionalFieldsMetadata{}, err + } + + instanceID, err := resolveInstanceID(metadata.MCMAddress, party, defaultMCMSInstanceIDCandidates) + if err != nil { + return AdditionalFieldsMetadata{}, err + } + + role, err := CantonRoleFromAction(action) + if err != nil { + return AdditionalFieldsMetadata{}, fmt.Errorf("canton role from action: %w", err) + } + multisigID := fmt.Sprintf("%s@%s-%s", instanceID, party, strings.ToLower(role.String())) + + fields := AdditionalFieldsMetadata{ + ChainId: defaultCantonChainID, + MultisigId: multisigID, + InstanceId: instanceID, + } + if err := fields.Validate(); err != nil { + return AdditionalFieldsMetadata{}, fmt.Errorf("inferred canton additional fields invalid: %w", err) + } + + return fields, nil +} + +func partyFromBatchOperation(bop types.BatchOperation) (string, error) { + for _, tx := range bop.Transactions { + if len(tx.AdditionalFields) == 0 { + continue + } + var af AdditionalFields + if err := json.Unmarshal(tx.AdditionalFields, &af); err != nil { + continue + } + if party := partyFromRawInstanceAddress(af.TargetInstanceAddress); party != "" { + return party, nil + } + } + if len(bop.Transactions) > 0 && bop.Transactions[0].To != "" { + if party := partyFromRawInstanceAddress(bop.Transactions[0].To); party != "" { + return party, nil + } + } + + return "", fmt.Errorf("unable to infer Canton party from batch operation transactions") +} + +func partyFromRawInstanceAddress(raw string) string { + at := strings.Index(raw, "@") + if at <= 0 || at >= len(raw)-1 { + return "" + } + + return raw[at+1:] +} + +func resolveInstanceID(mcmAddressHex, party string, candidates []string) (string, error) { + target := strings.ToLower(strings.TrimPrefix(mcmAddressHex, "0x")) + for _, candidate := range candidates { + raw := candidate + "@" + party + hash := crypto.Keccak256([]byte(raw)) + if strings.EqualFold(hex.EncodeToString(hash), target) { + return candidate, nil + } + } + + return "", fmt.Errorf( + "unable to infer MCMS instanceId for address %s and party %s (tried %v)", + mcmAddressHex, party, candidates, + ) +} diff --git a/sdk/canton/chain_metadata_infer_test.go b/sdk/canton/chain_metadata_infer_test.go new file mode 100644 index 000000000..79879c19f --- /dev/null +++ b/sdk/canton/chain_metadata_infer_test.go @@ -0,0 +1,112 @@ +package canton + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms/types" +) + +func TestResolveAdditionalFieldsMetadataUsesStoredFields(t *testing.T) { + t.Parallel() + + staleFields := mustJSON(t, AdditionalFieldsMetadata{ + ChainId: 1, + MultisigId: mcmsInstanceIDCCIP + "@party-proposer", + InstanceId: mcmsInstanceIDCCIP, + }) + + metadata := types.ChainMetadata{ + StartingOpCount: 4, + MCMAddress: "0xabc", + AdditionalFields: staleFields, + } + + fields, err := resolveAdditionalFieldsMetadata(metadata, types.BatchOperation{}, types.TimelockActionSchedule) + require.NoError(t, err) + require.Equal(t, int64(1), fields.ChainId) + require.Equal(t, mcmsInstanceIDCCIP, fields.InstanceId) +} + +func TestResolveAdditionalFieldsMetadataInfersMissingFields(t *testing.T) { + t.Parallel() + + bop := types.BatchOperation{ + ChainSelector: 8706591216959472610, + Transactions: []types.Transaction{{ + AdditionalFields: mustJSON(t, AdditionalFields{ + TargetInstanceAddress: "globalconfig-rklfx@participant1-localparty-1::1220acd0401d95915bef2f498a45cc8f3c43119dde50cf370864e9aa4eb03d817cfb", + FunctionName: "ApplyDestChainConfigUpdates", + }), + }}, + } + + metadata := types.ChainMetadata{ + StartingOpCount: 1, + MCMAddress: "0xd4dcbc33d025740c32b65cb60d208a7eb8f99b3d90903ffe52616e14f9096995", + } + + fields, err := resolveAdditionalFieldsMetadata(metadata, bop, types.TimelockActionSchedule) + require.NoError(t, err) + require.Equal(t, int64(1), fields.ChainId) + require.Equal(t, mcmsInstanceIDCCIP, fields.InstanceId) + require.Equal(t, mcmsInstanceIDCCIP+"@participant1-localparty-1::1220acd0401d95915bef2f498a45cc8f3c43119dde50cf370864e9aa4eb03d817cfb-proposer", fields.MultisigId) +} + +func TestResolveAdditionalFieldsMetadataErrors(t *testing.T) { + t.Parallel() + + _, err := resolveAdditionalFieldsMetadata(types.ChainMetadata{ + MCMAddress: "0xabc", + }, types.BatchOperation{}, types.TimelockActionSchedule) + require.ErrorContains(t, err, "unable to infer Canton party") + + _, err = resolveAdditionalFieldsMetadata(types.ChainMetadata{ + MCMAddress: "0xnotamatch", + }, types.BatchOperation{ + Transactions: []types.Transaction{{ + To: "target@participant1-localparty-1::1220acd0401d95915bef2f498a45cc8f3c43119dde50cf370864e9aa4eb03d817cfb", + }}, + }, types.TimelockActionSchedule) + require.ErrorContains(t, err, "unable to infer MCMS instanceId") + + _, err = resolveAdditionalFieldsMetadata(types.ChainMetadata{ + AdditionalFields: []byte(`{invalid`), + }, types.BatchOperation{}, types.TimelockActionSchedule) + require.ErrorContains(t, err, "unmarshal metadata additional fields") +} + +func TestResolveAdditionalFieldsMetadataBypasserRole(t *testing.T) { + t.Parallel() + + bop := types.BatchOperation{ + Transactions: []types.Transaction{{ + To: "counter@" + testParty, + }}, + } + metadata := types.ChainMetadata{ + MCMAddress: "0xd4dcbc33d025740c32b65cb60d208a7eb8f99b3d90903ffe52616e14f9096995", + } + + fields, err := resolveAdditionalFieldsMetadata(metadata, bop, types.TimelockActionBypass) + require.NoError(t, err) + require.Contains(t, fields.MultisigId, "-bypasser") +} + +func TestPartyFromRawInstanceAddress(t *testing.T) { + t.Parallel() + + require.Equal(t, testParty, partyFromRawInstanceAddress("mcms-ccip@"+testParty)) + require.Empty(t, partyFromRawInstanceAddress("noseparator")) + require.Empty(t, partyFromRawInstanceAddress("@onlyparty")) +} + +func mustJSON(t *testing.T, v any) []byte { + t.Helper() + b, err := json.Marshal(v) + require.NoError(t, err) + + return b +} diff --git a/sdk/canton/chain_metadata_test.go b/sdk/canton/chain_metadata_test.go new file mode 100644 index 000000000..2bd010445 --- /dev/null +++ b/sdk/canton/chain_metadata_test.go @@ -0,0 +1,115 @@ +package canton + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms/types" +) + +func TestCantonRoleFromAction(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + action types.TimelockAction + expectedRole TimelockRole + expectError bool + }{ + { + name: "bypass action returns bypasser role", + action: types.TimelockActionBypass, + expectedRole: TimelockRoleBypasser, + }, + { + name: "schedule action returns proposer role", + action: types.TimelockActionSchedule, + expectedRole: TimelockRoleProposer, + }, + { + name: "cancel action returns canceller role", + action: types.TimelockActionCancel, + expectedRole: TimelockRoleCanceller, + }, + { + name: "unknown action returns error", + action: types.TimelockAction("unknown"), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + role, err := CantonRoleFromAction(tt.action) + if tt.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expectedRole, role) + }) + } +} + +func TestAdditionalFieldsMetadataValidate(t *testing.T) { + t.Parallel() + + require.NoError(t, (AdditionalFieldsMetadata{ + ChainId: 1, + MultisigId: "mcms@party-proposer", + }).Validate()) + + require.Error(t, (AdditionalFieldsMetadata{MultisigId: "x"}).Validate()) + require.Error(t, (AdditionalFieldsMetadata{ChainId: -1, MultisigId: "x"}).Validate()) +} + +func TestNewChainMetadata(t *testing.T) { + t.Parallel() + + addr := "0xd4dcbc33d025740c32b65cb60d208a7eb8f99b3d90903ffe52616e14f9096995" + meta, err := NewChainMetadata(5, 1, "mcms@party-proposer", addr, "mcms") + require.NoError(t, err) + require.Equal(t, uint64(5), meta.StartingOpCount) + require.Equal(t, addr, meta.MCMAddress) + + var fields AdditionalFieldsMetadata + require.NoError(t, json.Unmarshal(meta.AdditionalFields, &fields)) + require.Equal(t, int64(1), fields.ChainId) + require.Equal(t, "mcms", fields.InstanceId) +} + +func TestValidateChainMetadata(t *testing.T) { + t.Parallel() + + addr := "0xd4dcbc33d025740c32b65cb60d208a7eb8f99b3d90903ffe52616e14f9096995" + meta, err := NewChainMetadata(0, 1, "mcms@party-proposer", addr, "mcms") + require.NoError(t, err) + require.NoError(t, ValidateChainMetadata(meta)) + + meta.AdditionalFields = []byte(`{invalid`) + require.Error(t, ValidateChainMetadata(meta)) +} + +func TestNewChainMetadataErrors(t *testing.T) { + t.Parallel() + + _, err := NewChainMetadata(0, 1, "id", "", "mcms") + require.ErrorContains(t, err, "InstanceAddress is required") + + _, err = NewChainMetadata(0, 1, "id", "0xshort", "mcms") + require.ErrorContains(t, err, "64 characters") + + _, err = NewChainMetadata(0, 0, "id", validTestInstanceAddress(t), "mcms") + require.ErrorContains(t, err, "chainId must be positive") +} + +func validTestInstanceAddress(t *testing.T) string { + t.Helper() + + return "0xd4dcbc33d025740c32b65cb60d208a7eb8f99b3d90903ffe52616e14f9096995" +} diff --git a/sdk/canton/configurer.go b/sdk/canton/configurer.go new file mode 100644 index 000000000..da868105a --- /dev/null +++ b/sdk/canton/configurer.go @@ -0,0 +1,145 @@ +package canton + +import ( + "context" + "fmt" + "strings" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/google/uuid" + cselectors "github.com/smartcontractkit/chain-selectors" + mcmsapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/api" + mcmscore "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/core" + "github.com/smartcontractkit/go-daml/pkg/service/ledger" + cantontypes "github.com/smartcontractkit/go-daml/pkg/types" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" +) + +var _ sdk.Configurer = &Configurer{} + +type Configurer struct { + client apiv2.CommandServiceClient + stateClient apiv2.StateServiceClient + // The parties that own the MCMS deployment. + mcmsParties []string + role TimelockRole +} + +func NewConfigurer(client apiv2.CommandServiceClient, stateClient apiv2.StateServiceClient, mcmsParties []string, role TimelockRole) (*Configurer, error) { + return &Configurer{ + client: client, + stateClient: stateClient, + mcmsParties: mcmsParties, + role: role, + }, nil +} + +func (c Configurer) SetConfig(ctx context.Context, mcmsAddr string, cfg *types.Config, clearRoot bool) (types.TransactionResult, error) { + // mcmsAddr is InstanceAddress hex for Canton; resolve to current contract ID + mcmsContractID, err := ResolveMCMSContractID(ctx, c.stateClient, c.mcmsParties, mcmsAddr) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to resolve MCMS contract ID: %w", err) + } + + groupQuorum, groupParents, signerAddresses, signerGroups, err := sdk.ExtractSetConfigInputs(cfg) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("unable to extract set config inputs: %w", err) + } + + signers := make([]mcmsapi.SignerInfo, len(signerAddresses)) + for i, addr := range signerAddresses { + addrStr := strings.ToLower(addr.String()) + addrStr = strings.TrimPrefix(addrStr, "0x") + signers[i] = mcmsapi.SignerInfo{ + SignerAddress: cantontypes.TEXT(addrStr), + SignerGroup: cantontypes.INT64(signerGroups[i]), + SignerIndex: cantontypes.INT64(i), + } + } + + groupQuorumsTyped := make([]cantontypes.INT64, len(groupQuorum)) + for i, q := range groupQuorum { + groupQuorumsTyped[i] = cantontypes.INT64(q) + } + + groupParentsTyped := make([]cantontypes.INT64, len(groupParents)) + for i, p := range groupParents { + groupParentsTyped[i] = cantontypes.INT64(p) + } + + input := mcmscore.SetConfig{ + TargetRole: mcmsapi.Role(c.role.String()), + NewSigners: signers, + NewGroupQuorums: groupQuorumsTyped, + NewGroupParents: groupParentsTyped, + ClearRoot: cantontypes.BOOL(clearRoot), + } + // 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.NewString() + 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, + }, + }, + }}, + }, + }) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to set config: %w", err) + } + + // Extract NEW MCMS CID from Created event + newMCMSContractID := "" + newMCMSTemplateID := "" + transaction := submitResp.GetTransaction() + for _, ev := range transaction.GetEvents() { + if createdEv := ev.GetCreated(); createdEv != nil { + templateID := FormatTemplateID(createdEv.GetTemplateId()) + normalized := NormalizeTemplateKey(templateID) + if normalized == MCMSTemplateKey { + newMCMSContractID = createdEv.GetContractId() + newMCMSTemplateID = templateID + + break + } + } + } + + if newMCMSContractID == "" { + return types.TransactionResult{}, fmt.Errorf("set-config tx had no Created MCMS event; refusing to continue with old CID=%s", mcmsContractID) + } + + return types.TransactionResult{ + Hash: transactionResultHash(transaction, commandID), + ChainFamily: cselectors.FamilyCanton, + RawData: rawDataFromMCMSTx(newMCMSContractID, newMCMSTemplateID, submitResp), + }, nil +} diff --git a/sdk/canton/constants.go b/sdk/canton/constants.go new file mode 100644 index 000000000..b4a8d26df --- /dev/null +++ b/sdk/canton/constants.go @@ -0,0 +1,22 @@ +package canton + +const ( + MCMSTemplateKey = "MCMS.Main:MCMS" + + rawDataKeyNewMCMSContractID = "NewMCMSContractID" + rawDataKeyNewMCMSTemplateID = "NewMCMSTemplateID" + rawDataKeyRawTx = "RawTx" + + instanceAddressHexLen = 64 + hexWordLen = 64 + templateIDPartCount = 3 + hexEncodedByteLen = 2 + maxMCMSGroups = 32 + microsecondsPerSecond = 1_000_000 + + defaultCantonChainID int64 = 1 + + mcmsInstanceIDCCIP = "mcms-ccip" + mcmsInstanceIDCCV = "mcms-ccv" + mcmsInstanceIDDefault = "mcms" +) diff --git a/sdk/canton/encoder.go b/sdk/canton/encoder.go new file mode 100644 index 000000000..cfb8cd335 --- /dev/null +++ b/sdk/canton/encoder.go @@ -0,0 +1,199 @@ +package canton + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" +) + +// Domain separators matching Canton's Crypto.daml +var ( + opLeafDomainSeparator = hex.EncodeToString(crypto.Keccak256( + []byte("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP_CANTON"))) + metadataLeafDomainSeparator = hex.EncodeToString(crypto.Keccak256( + []byte("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA_CANTON"))) +) + +// AdditionalFields represents the additional fields in Canton MCMS operations +type AdditionalFields struct { + TargetInstanceAddress string `json:"targetInstanceAddress"` // Format: "instanceId@partyId" + FunctionName string `json:"functionName"` + TargetCid string `json:"targetCid"` + ContractIds []string `json:"contractIds"` + // TargetTemplateID is the Daml template ID of the target contract (e.g. "#pkg:Module:Entity"). + // When TargetCid is empty at execution time, the SDK uses TargetTemplateID + TargetInstanceAddress + // to dynamically resolve the active contract ID from the ledger. + TargetTemplateID string `json:"targetTemplateId,omitempty"` +} + +var _ sdk.Encoder = &Encoder{} + +type Encoder struct { + ChainSelector types.ChainSelector + TxCount uint64 + OverridePreviousRoot bool +} + +func NewEncoder( + chainSelector types.ChainSelector, + txCount uint64, + overridePreviousRoot bool, +) *Encoder { + return &Encoder{ + ChainSelector: chainSelector, + TxCount: txCount, + OverridePreviousRoot: overridePreviousRoot, + } +} + +// ToRootMetadata resolves Canton root metadata for SetRoot and merkle hashing. +// Like EVM ToGethRootMetadata, postOpCount is derived from StartingOpCount + TxCount at sign time. +// multisigId and related fields must be present in chainMetadata.additionalFields (typically from GetRootMetadata). +func (e *Encoder) ToRootMetadata(metadata types.ChainMetadata) (AdditionalFieldsMetadata, error) { + fields, err := parseAdditionalFieldsMetadata(metadata.AdditionalFields) + if err != nil { + return AdditionalFieldsMetadata{}, err + } + + if err := fields.Validate(); err != nil { + return AdditionalFieldsMetadata{}, fmt.Errorf("canton chain metadata additionalFields required or incomplete: %w", err) + } + + return fields, nil +} + +func parseAdditionalFieldsMetadata(raw json.RawMessage) (AdditionalFieldsMetadata, error) { + if len(raw) == 0 { + return AdditionalFieldsMetadata{}, fmt.Errorf("canton chain metadata additionalFields are required") + } + + var fields AdditionalFieldsMetadata + if err := json.Unmarshal(raw, &fields); err != nil { + return AdditionalFieldsMetadata{}, fmt.Errorf("unmarshal metadata additional fields: %w", err) + } + + return fields, nil +} + +// HashOperation hashes an operation to get its Merkle leaf +// Matches Canton's hashOpLeafNative from Crypto.daml with domain separator and length prefixes +func (e *Encoder) HashOperation(opCount uint32, metadata types.ChainMetadata, op types.Operation) (common.Hash, error) { + metadataFields, err := e.ToRootMetadata(metadata) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to resolve metadata additional fields: %w", err) + } + + // Unmarshal Canton-specific operation fields + var opFields AdditionalFields + if unmarshalErr := json.Unmarshal(op.Transaction.AdditionalFields, &opFields); unmarshalErr != nil { + return common.Hash{}, fmt.Errorf("failed to unmarshal operation additional fields: %w", unmarshalErr) + } + if validateErr := opFields.Validate(); validateErr != nil { + return common.Hash{}, fmt.Errorf("invalid operation additional fields: %w", validateErr) + } + + operationData := operationDataHex(op.Transaction.Data) + + // Convert variable-length fields to hex + multisigIdHex := asciiToHex(metadataFields.MultisigId) + targetAddressHex := asciiToHex(opFields.TargetInstanceAddress) + functionNameHex := asciiToHex(opFields.FunctionName) + + // Build the encoded data following Canton's hashOpLeafNative with domain separator and length prefixes + encoded := opLeafDomainSeparator + + padLeft32(intToHex(int(metadataFields.ChainId))) + + padLeft32(intToHex(len(metadataFields.MultisigId))) + // Length prefix for multisigId + multisigIdHex + + padLeft32(intToHex(int(opCount))) + + padLeft32(intToHex(len(opFields.TargetInstanceAddress))) + // Length prefix for targetInstanceAddress + targetAddressHex + + padLeft32(intToHex(len(opFields.FunctionName))) + // Length prefix for functionName + functionNameHex + + padLeft32(intToHex(len(operationData)/hexEncodedByteLen)) + // Length prefix for operationData (byte count) + operationData + + // Decode hex string and hash + data, err := hex.DecodeString(encoded) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to decode hex string: %w", err) + } + + return crypto.Keccak256Hash(data), nil +} + +// HashMetadata hashes metadata to get its Merkle leaf +// Matches Canton's hashMetadataLeafNative from Crypto.daml with domain separator and length prefixes +func (e *Encoder) HashMetadata(metadata types.ChainMetadata) (common.Hash, error) { + metadataFields, err := e.ToRootMetadata(metadata) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to resolve metadata additional fields: %w", err) + } + + // Build override flag + overrideFlag := "00" + if e.OverridePreviousRoot { + overrideFlag = "01" + } + + // Convert multisigId to hex + multisigIdHex := asciiToHex(metadataFields.MultisigId) + + // Build the encoded data with domain separator and length prefix for multisigId + encoded := metadataLeafDomainSeparator + + padLeft32(intToHex(int(metadataFields.ChainId))) + + padLeft32(intToHex(len(metadataFields.MultisigId))) + // Length prefix for multisigId + multisigIdHex + + padLeft32(uint64ToHex(metadata.StartingOpCount)) + + padLeft32(uint64ToHex(metadata.StartingOpCount+e.TxCount)) + + overrideFlag + + // Decode hex string and hash + data, err := hex.DecodeString(encoded) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to decode hex string: %w", err) + } + + return crypto.Keccak256Hash(data), nil +} + +// Helper functions matching Canton Crypto.daml + +// padLeft32 pads hex string to 64 chars (32 bytes). Panics if input exceeds 32 bytes, +// matching Canton's Crypto.daml behavior. +func padLeft32(hexStr string) string { + if len(hexStr) > hexWordLen { + panic(fmt.Sprintf("padLeft32: input exceeds 32 bytes: %d hex chars", len(hexStr))) + } + if len(hexStr) == hexWordLen { + return hexStr + } + + return strings.Repeat("0", hexWordLen-len(hexStr)) + hexStr +} + +// intToHex converts a non-negative int to hex string (without padding). Panics on negative input, +// matching Canton's Crypto.daml behavior. +func intToHex(n int) string { + if n < 0 { + panic("intToHex: negative numbers not supported") + } + + return fmt.Sprintf("%x", n) +} + +func uint64ToHex(n uint64) string { + return strconv.FormatUint(n, 16) +} + +// asciiToHex converts ASCII string to hex +func asciiToHex(s string) string { + return hex.EncodeToString([]byte(s)) +} diff --git a/sdk/canton/encoder_test.go b/sdk/canton/encoder_test.go new file mode 100644 index 000000000..f616b3b4d --- /dev/null +++ b/sdk/canton/encoder_test.go @@ -0,0 +1,93 @@ +package canton + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms/types" +) + +func TestEncoder_ToRootMetadataReturnsStoredFields(t *testing.T) { + t.Parallel() + + additionalFields, err := json.Marshal(AdditionalFieldsMetadata{ + ChainId: 1, + MultisigId: mcmsInstanceIDCCIP + "@party-proposer", + InstanceId: mcmsInstanceIDCCIP, + }) + require.NoError(t, err) + + encoder := NewEncoder(8706591216959472610, 3, true) + fields, err := encoder.ToRootMetadata(types.ChainMetadata{ + StartingOpCount: 10, + MCMAddress: "0xabc", + AdditionalFields: additionalFields, + }) + require.NoError(t, err) + require.Equal(t, int64(1), fields.ChainId) + require.Equal(t, mcmsInstanceIDCCIP+"@party-proposer", fields.MultisigId) + require.Equal(t, mcmsInstanceIDCCIP, fields.InstanceId) +} + +func TestEncoder_HashMetadataUsesStartingOpCountAndTxCount(t *testing.T) { + t.Parallel() + + additionalFields, err := json.Marshal(AdditionalFieldsMetadata{ + ChainId: 1, + MultisigId: "mcms@party-proposer", + }) + require.NoError(t, err) + + metadata := types.ChainMetadata{ + StartingOpCount: 10, + MCMAddress: "0xabc", + AdditionalFields: additionalFields, + } + + withTxCount := NewEncoder(8706591216959472610, 3, false) + withoutTxCount := NewEncoder(8706591216959472610, 0, false) + + hashWithTx, err := withTxCount.HashMetadata(metadata) + require.NoError(t, err) + hashWithoutTx, err := withoutTxCount.HashMetadata(metadata) + require.NoError(t, err) + require.NotEqual(t, hashWithTx, hashWithoutTx) +} + +func TestEncoder_HashOperationRequiresValidOperationFields(t *testing.T) { + t.Parallel() + + additionalFields, err := json.Marshal(AdditionalFieldsMetadata{ + ChainId: 1, + MultisigId: "mcms@party-proposer", + }) + require.NoError(t, err) + + encoder := NewEncoder(8706591216959472610, 1, false) + _, err = encoder.HashOperation(0, types.ChainMetadata{ + StartingOpCount: 1, + AdditionalFields: additionalFields, + }, types.Operation{ + Transaction: types.Transaction{AdditionalFields: []byte(`not-json`)}, + }) + require.ErrorContains(t, err, "unmarshal operation additional fields") +} + +func TestEncoder_IntToHexPanicsOnNegative(t *testing.T) { + t.Parallel() + + require.Panics(t, func() { intToHex(-1) }) +} + +func TestEncoder_ToRootMetadataRequiresAdditionalFields(t *testing.T) { + t.Parallel() + + encoder := NewEncoder(8706591216959472610, 1, false) + _, err := encoder.ToRootMetadata(types.ChainMetadata{ + StartingOpCount: 1, + MCMAddress: "0xabc", + }) + require.Error(t, err) +} diff --git a/sdk/canton/executor.go b/sdk/canton/executor.go new file mode 100644 index 000000000..6ddc8ed6e --- /dev/null +++ b/sdk/canton/executor.go @@ -0,0 +1,358 @@ +package canton + +import ( + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "time" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/google/uuid" + cselectors "github.com/smartcontractkit/chain-selectors" + mcmsapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/api" + mcmscore "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/core" + "github.com/smartcontractkit/go-daml/pkg/service/ledger" + cantontypes "github.com/smartcontractkit/go-daml/pkg/types" + + "github.com/smartcontractkit/mcms/internal/utils/abi" + "github.com/smartcontractkit/mcms/internal/utils/safecast" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" +) + +const SignMsgABI = `[{"type":"bytes32"},{"type":"uint32"}]` + +var _ sdk.Executor = &Executor{} + +type Executor struct { + *Encoder + *Inspector + client apiv2.CommandServiceClient + // The party that will be used to submit transactions. + // Should be different from mcmsParties. + submittingParty string + // The parties that own the MCMS deployment. + mcmsParties []string + role TimelockRole +} + +func NewExecutor(encoder *Encoder, inspector *Inspector, client apiv2.CommandServiceClient, submittingParty string, mcmsParties []string, role TimelockRole) (*Executor, error) { + return &Executor{ + Encoder: encoder, + Inspector: inspector, + client: client, + submittingParty: submittingParty, + mcmsParties: mcmsParties, + role: role, + }, nil +} + +func (e Executor) ExecuteOperation( + ctx context.Context, + metadata types.ChainMetadata, + nonce uint32, + proof []common.Hash, + op types.Operation, +) (types.TransactionResult, error) { + // Extract Canton-specific operation fields from AdditionalFields + var cantonOpFields AdditionalFields + if len(op.Transaction.AdditionalFields) > 0 { + if err := json.Unmarshal(op.Transaction.AdditionalFields, &cantonOpFields); err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to unmarshal operation additional fields: %w", err) + } + } + + // Resolve MCMAddress (InstanceAddress hex) to current contract ID before submitting + mcmsContractID, err := ResolveMCMSContractID(ctx, e.StateServiceClient(), e.mcmsParties, metadata.MCMAddress) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to resolve MCMS contract ID: %w", err) + } + + // Validate required Canton fields + if cantonOpFields.TargetInstanceAddress == "" { + return types.TransactionResult{}, errors.New("targetInstanceAddress is required in operation additional fields") + } + if cantonOpFields.FunctionName == "" { + return types.TransactionResult{}, errors.New("functionName is required in operation additional fields") + } + if cantonOpFields.TargetCid == "" { + if cantonOpFields.TargetTemplateID == "" { + return types.TransactionResult{}, errors.New("targetCid or targetTemplateId+targetInstanceAddress is required in operation additional fields") + } + resolved, resolveErr := ResolveTargetContractID(ctx, e.StateServiceClient(), e.mcmsParties, cantonOpFields.TargetInstanceAddress, cantonOpFields.TargetTemplateID) + if resolveErr != nil { + return types.TransactionResult{}, fmt.Errorf("resolve target contract ID: %w", resolveErr) + } + cantonOpFields.TargetCid = resolved + if len(cantonOpFields.ContractIds) == 0 { + cantonOpFields.ContractIds = []string{resolved} + } + } + + // Resolve chain metadata (chainId, multisigId) for the Canton Op payload. + fields, err := e.ToRootMetadata(metadata) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("resolve canton root metadata: %w", err) + } + + // Build Canton Op struct + cantonOp := mcmsapi.Op{ + ChainId: cantontypes.INT64(fields.ChainId), + MultisigId: cantontypes.TEXT(fields.MultisigId), + Nonce: cantontypes.INT64(nonce), + TargetInstanceAddress: cantontypes.TEXT(cantonOpFields.TargetInstanceAddress), + FunctionName: cantontypes.TEXT(cantonOpFields.FunctionName), + OperationData: cantontypes.TEXT(operationDataHex(op.Transaction.Data)), + } + + // Convert proof to Canton TEXT array + opProof := make([]cantontypes.TEXT, len(proof)) + for i, p := range proof { + opProof[i] = cantontypes.TEXT(hex.EncodeToString(p[:])) + } + + // Resolve InstanceAddress hex values to current contract IDs before submitting. + stateClient := e.StateServiceClient() + targetCids := make(map[cantontypes.TEXT]cantontypes.CONTRACT_ID) + for _, cid := range cantonOpFields.ContractIds { + resolved, resolveErr := ResolveContractIDIfInstanceAddress(ctx, stateClient, e.mcmsParties, cid) + if resolveErr != nil { + return types.TransactionResult{}, fmt.Errorf("resolve contract ID %q: %w", cid, resolveErr) + } + // Use the original instance address as key, resolved contract ID as value + targetCids[cantontypes.TEXT(cid)] = cantontypes.CONTRACT_ID(resolved) + } + + // Build exercise command using generated bindings + mcmsContract := mcmscore.MCMS{} + var choice string + var choiceArgument *apiv2.Value + + input := mcmscore.ExecuteOp{ + TargetRole: mcmsapi.Role(e.role.String()), + Submitter: cantontypes.PARTY(e.submittingParty), + Op: cantonOp, + OpProof: opProof, + TargetCids: targetCids, + } + exerciseCmd := mcmsContract.ExecuteOp(mcmsContractID, input) + choice = exerciseCmd.Choice + choiceArgument = ledger.MapToValue(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) + } + + commandID := uuid.NewString() + submitResp, err := e.client.SubmitAndWaitForTransaction(ctx, &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + WorkflowId: "mcms-execute-op", + CommandId: commandID, + ActAs: []string{e.submittingParty}, + ReadAs: e.mcmsParties, + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{ + Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{ + PackageId: packageID, + ModuleName: moduleName, + EntityName: entityName, + }, + ContractId: mcmsContractID, + Choice: choice, + ChoiceArgument: choiceArgument, + }, + }, + }}, + }, + }) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to execute operation: %w", err) + } + + // Extract NEW MCMS CID from Created event (for RawData only; proposal keeps InstanceAddress) + newMCMSContractID := "" + newMCMSTemplateID := "" + transaction := submitResp.GetTransaction() + for _, ev := range transaction.GetEvents() { + if createdEv := ev.GetCreated(); createdEv != nil { + templateID := FormatTemplateID(createdEv.GetTemplateId()) + normalized := NormalizeTemplateKey(templateID) + if normalized == MCMSTemplateKey { + newMCMSContractID = createdEv.GetContractId() + newMCMSTemplateID = templateID + + break + } + } + } + + if newMCMSContractID == "" { + return types.TransactionResult{}, fmt.Errorf("execute-op tx had no Created MCMS event; refusing to continue with old CID=%s", mcmsContractID) + } + + return types.TransactionResult{ + Hash: transactionResultHash(transaction, commandID), + ChainFamily: cselectors.FamilyCanton, + RawData: rawDataFromMCMSTx(newMCMSContractID, newMCMSTemplateID, submitResp), + }, nil +} + +func (e Executor) SetRoot( + ctx context.Context, + metadata types.ChainMetadata, + proof []common.Hash, + root [32]byte, + validUntil uint32, + sortedSignatures []types.Signature, +) (types.TransactionResult, error) { + // Resolve MCMAddress (InstanceAddress hex) to current contract ID before submitting + mcmsContractID, err := ResolveMCMSContractID(ctx, e.StateServiceClient(), e.mcmsParties, metadata.MCMAddress) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to resolve MCMS contract ID: %w", err) + } + + rootHex := hex.EncodeToString(root[:]) + // Recalculate msg hash to recover signers + inner, err := abi.Encode(SignMsgABI, root, validUntil) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to ABI-encode signing payload: %w", err) + } + innerHash := crypto.Keccak256(inner) + + // Apply EIP-191 prefix + prefix := []byte("\x19Ethereum Signed Message:\n32") + prefixedData := append(prefix, innerHash...) + cantonSignedHash := crypto.Keccak256Hash(prefixedData) + + // Convert signatures to Canton RawSignature array + signatures := make([]mcmsapi.RawSignature, len(sortedSignatures)) + for i, sig := range sortedSignatures { + pubKey, recoverErr := sig.RecoverPublicKey(cantonSignedHash) + if recoverErr != nil { + return types.TransactionResult{}, fmt.Errorf("failed to recover public key for signature %d: %w", i, recoverErr) + } + + // Convert public key to hex string + pubkeyHex := hex.EncodeToString(crypto.FromECDSAPub(pubKey)) + signatures[i] = mcmsapi.RawSignature{ + PublicKey: cantontypes.TEXT(pubkeyHex), + R: cantontypes.TEXT(hex.EncodeToString(sig.R[:])), + S: cantontypes.TEXT(hex.EncodeToString(sig.S[:])), + } + } + + // Resolve root metadata (EVM-style: enrich minimal proposal metadata at SetRoot time). + fields, err := e.ToRootMetadata(metadata) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("resolve canton root metadata: %w", err) + } + + preOpCount, err := safecast.Uint64ToInt64(metadata.StartingOpCount) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("preOpCount out of range: %w", err) + } + postOpCount, convErr := safecast.Uint64ToInt64(metadata.StartingOpCount + e.TxCount) + if convErr != nil { + return types.TransactionResult{}, fmt.Errorf("postOpCount out of range: %w", convErr) + } + + rootMetadata := mcmsapi.RootMetadata{ + ChainId: cantontypes.INT64(fields.ChainId), + MultisigId: cantontypes.TEXT(fields.MultisigId), + PreOpCount: cantontypes.INT64(preOpCount), + PostOpCount: cantontypes.INT64(postOpCount), + OverridePreviousRoot: cantontypes.BOOL(e.OverridePreviousRoot), + } + + // Convert proof to Canton TEXT array + metadataProof := make([]cantontypes.TEXT, len(proof)) + for i, p := range proof { + metadataProof[i] = cantontypes.TEXT(hex.EncodeToString(p[:])) + } + + // validUntil is Unix seconds; Canton/Daml Timestamp expects time in seconds (binding serializes correctly) + validUntilTime := time.Unix(int64(validUntil), 0) + input := mcmscore.SetRoot{ + TargetRole: mcmsapi.Role(e.role.String()), + Submitter: cantontypes.PARTY(e.submittingParty), + NewRoot: cantontypes.TEXT(rootHex), + ValidUntil: cantontypes.TIMESTAMP(validUntilTime), + Metadata: rootMetadata, + MetadataProof: metadataProof, + Signatures: signatures, + } + + // Build exercise command using generated bindings + mcmsContract := mcmscore.MCMS{} + exerciseCmd := mcmsContract.SetRoot(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.NewString() + submitResp, err := e.client.SubmitAndWaitForTransaction(ctx, &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + WorkflowId: "mcms-set-root", + CommandId: commandID, + ActAs: []string{e.submittingParty}, + ReadAs: e.mcmsParties, + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{ + Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{ + PackageId: packageID, + ModuleName: moduleName, + EntityName: entityName, + }, + ContractId: mcmsContractID, + Choice: exerciseCmd.Choice, + ChoiceArgument: choiceArgument, + }, + }, + }}, + }, + }) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to set root: %w", err) + } + + // Extract NEW MCMS CID from Created event + newMCMSContractID := "" + newMCMSTemplateID := "" + transaction := submitResp.GetTransaction() + for _, ev := range transaction.GetEvents() { + if createdEv := ev.GetCreated(); createdEv != nil { + templateID := FormatTemplateID(createdEv.GetTemplateId()) + normalized := NormalizeTemplateKey(templateID) + if normalized == MCMSTemplateKey { + newMCMSContractID = createdEv.GetContractId() + newMCMSTemplateID = templateID + + break + } + } + } + + if newMCMSContractID == "" { + return types.TransactionResult{}, fmt.Errorf("set-root tx had no Created MCMS event; refusing to continue with old CID=%s", mcmsContractID) + } + + return types.TransactionResult{ + Hash: transactionResultHash(transaction, commandID), + ChainFamily: cselectors.FamilyCanton, + RawData: rawDataFromMCMSTx(newMCMSContractID, newMCMSTemplateID, submitResp), + }, nil +} diff --git a/sdk/canton/helpers.go b/sdk/canton/helpers.go new file mode 100644 index 000000000..01e81d3ac --- /dev/null +++ b/sdk/canton/helpers.go @@ -0,0 +1,66 @@ +package canton + +import ( + "encoding/hex" + "fmt" + "strings" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" +) + +func rawDataFromMCMSTx(newMCMSContractID, newMCMSTemplateID string, rawTx any) map[string]any { + return map[string]any{ + rawDataKeyNewMCMSContractID: newMCMSContractID, + rawDataKeyNewMCMSTemplateID: newMCMSTemplateID, + rawDataKeyRawTx: rawTx, + } +} + +// transactionResultHash returns an identifier for a Canton ledger submission. +// Prefer the ledger external transaction hash when present; otherwise use commandID. +func transactionResultHash(transaction *apiv2.Transaction, commandID string) string { + if transaction != nil { + if ext := transaction.GetExternalTransactionHash(); len(ext) > 0 { + return "0x" + hex.EncodeToString(ext) + } + } + + return commandID +} + +func NormalizeTemplateKey(tid string) string { + tid = strings.TrimPrefix(tid, "#") + parts := strings.Split(tid, ":") + if len(parts) < templateIDPartCount { + return tid + } + + return parts[len(parts)-2] + ":" + parts[len(parts)-1] +} + +// ParseTemplateIDFromString parses a template ID string like "#package:Module:Entity" into its components. +func ParseTemplateIDFromString(templateID string) (packageID, moduleName, entityName string, err error) { + if !strings.HasPrefix(templateID, "#") { + return "", "", "", fmt.Errorf("template ID must start with #") + } + parts := strings.Split(templateID, ":") + if len(parts) != templateIDPartCount { + return "", "", "", fmt.Errorf("template ID must have format #package:module:entity, got: %s", templateID) + } + + return parts[0], parts[1], parts[2], nil +} + +// instanceAddressHexEqual reports whether two InstanceAddress hex strings refer to the same address. +func instanceAddressHexEqual(a, b string) bool { + return strings.EqualFold(strings.TrimPrefix(strings.TrimSpace(a), "0x"), strings.TrimPrefix(strings.TrimSpace(b), "0x")) +} + +// FormatTemplateID converts an apiv2.Identifier to a string template ID format. +func FormatTemplateID(id *apiv2.Identifier) string { + if id == nil { + return "" + } + + return id.GetPackageId() + ":" + id.GetModuleName() + ":" + id.GetEntityName() +} diff --git a/sdk/canton/helpers_test.go b/sdk/canton/helpers_test.go new file mode 100644 index 000000000..bb5ea25bb --- /dev/null +++ b/sdk/canton/helpers_test.go @@ -0,0 +1,52 @@ +package canton + +import ( + "testing" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/stretchr/testify/require" +) + +func TestParseTemplateIDFromString(t *testing.T) { + t.Parallel() + + pkg, mod, ent, err := ParseTemplateIDFromString("#pkg:Module:Entity") + require.NoError(t, err) + require.Equal(t, "#pkg", pkg) + require.Equal(t, "Module", mod) + require.Equal(t, "Entity", ent) + + pkg2, mod2, ent2, err := ParseTemplateIDFromString("pkg:Module:Entity") + _ = pkg2 + _ = mod2 + _ = ent2 + require.Error(t, err) +} + +func TestFormatTemplateID(t *testing.T) { + t.Parallel() + + require.Equal(t, "pkg:Module:Entity", FormatTemplateID(&apiv2.Identifier{ + PackageId: "pkg", + ModuleName: "Module", + EntityName: "Entity", + })) + require.Empty(t, FormatTemplateID(nil)) +} + +func TestNormalizeTemplateKey(t *testing.T) { + t.Parallel() + + require.Equal(t, "Module:Entity", NormalizeTemplateKey("#pkg:Module:Entity")) + require.Equal(t, "short", NormalizeTemplateKey("short")) +} + +func TestTransactionResultHash(t *testing.T) { + t.Parallel() + + require.Equal(t, "cmd-1", transactionResultHash(nil, "cmd-1")) + require.Equal(t, "cmd-1", transactionResultHash(&apiv2.Transaction{}, "cmd-1")) + require.Equal(t, "0xdeadbeef", transactionResultHash(&apiv2.Transaction{ + ExternalTransactionHash: []byte{0xde, 0xad, 0xbe, 0xef}, + }, "cmd-1")) +} diff --git a/sdk/canton/inspector.go b/sdk/canton/inspector.go new file mode 100644 index 000000000..02ecdc80c --- /dev/null +++ b/sdk/canton/inspector.go @@ -0,0 +1,279 @@ +package canton + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/ethereum/go-ethereum/common" + mcmsapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/api" + mcmscore "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/core" + "github.com/smartcontractkit/chainlink-canton/contracts" + damltypes "github.com/smartcontractkit/go-daml/pkg/types" + + "github.com/smartcontractkit/mcms/internal/utils/safecast" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" +) + +var _ sdk.Inspector = &Inspector{} + +type Inspector struct { + stateClient apiv2.StateServiceClient + // The parties that own the MCMS deployment. + mcmsParties []string + role TimelockRole +} + +func NewInspector(stateClient apiv2.StateServiceClient, mcmsParties []string, role TimelockRole) *Inspector { + return &Inspector{ + stateClient: stateClient, + mcmsParties: mcmsParties, + role: role, + } +} + +// StateServiceClient returns the state service client for resolution (e.g. InstanceAddress to contract ID). +func (i *Inspector) StateServiceClient() apiv2.StateServiceClient { + return i.stateClient +} + +func (i *Inspector) GetConfig(ctx context.Context, mcmsAddr string) (*types.Config, error) { + mcmsContract, err := GetMCMSContract(ctx, i.stateClient, i.mcmsParties, mcmsAddr) + if err != nil { + return nil, fmt.Errorf("failed to get MCMS contract: %w", err) + } + + switch i.role { + case TimelockRoleProposer: + return toConfig(mcmsContract.Proposer.Config) + case TimelockRoleBypasser: + return toConfig(mcmsContract.Bypasser.Config) + case TimelockRoleCanceller: + return toConfig(mcmsContract.Canceller.Config) + default: + return nil, fmt.Errorf("unknown timelock role: %s", i.role) + } +} + +func (i *Inspector) GetOpCount(ctx context.Context, mcmsAddr string) (uint64, error) { + mcmsContract, err := GetMCMSContract(ctx, i.stateClient, i.mcmsParties, mcmsAddr) + if err != nil { + return 0, fmt.Errorf("failed to get MCMS contract: %w", err) + } + + return expiringRootOpCount(mcmsContract, i.role) +} + +func expiringRootOpCount(mcmsContract *mcmscore.MCMS, role TimelockRole) (uint64, error) { + switch role { + case TimelockRoleProposer: + return safecast.Int64ToUint64(int64(mcmsContract.Proposer.ExpiringRoot.OpCount)) + case TimelockRoleBypasser: + return safecast.Int64ToUint64(int64(mcmsContract.Bypasser.ExpiringRoot.OpCount)) + case TimelockRoleCanceller: + return safecast.Int64ToUint64(int64(mcmsContract.Canceller.ExpiringRoot.OpCount)) + default: + return 0, fmt.Errorf("unknown timelock role: %s", role) + } +} + +func (i *Inspector) GetRoot(ctx context.Context, mcmsAddr string) (common.Hash, uint32, error) { + mcmsContract, err := GetMCMSContract(ctx, i.stateClient, i.mcmsParties, mcmsAddr) + if err != nil { + return common.Hash{}, 0, fmt.Errorf("failed to get MCMS contract: %w", err) + } + + expiringRoot, err := expiringRootForRole(mcmsContract, i.role) + if err != nil { + return common.Hash{}, 0, err + } + + return rootFromExpiringRoot(expiringRoot) +} + +func expiringRootForRole(mcmsContract *mcmscore.MCMS, role TimelockRole) (mcmsapi.ExpiringRoot, error) { + switch role { + case TimelockRoleProposer: + return mcmsContract.Proposer.ExpiringRoot, nil + case TimelockRoleBypasser: + return mcmsContract.Bypasser.ExpiringRoot, nil + case TimelockRoleCanceller: + return mcmsContract.Canceller.ExpiringRoot, nil + default: + return mcmsapi.ExpiringRoot{}, fmt.Errorf("unknown timelock role: %s", role) + } +} + +func rootFromExpiringRoot(expiringRoot mcmsapi.ExpiringRoot) (common.Hash, uint32, error) { + root := common.HexToHash(string(expiringRoot.Root)) + + timeVal := time.Time(expiringRoot.ValidUntil) + validUntil, err := safecast.Int64ToUint32(timeVal.Unix()) + if err != nil { + return common.Hash{}, 0, fmt.Errorf("valid until out of range: %w", err) + } + + return root, validUntil, nil +} + +func (i *Inspector) GetRootMetadata(ctx context.Context, mcmsAddr string) (types.ChainMetadata, error) { + mcmsContract, err := GetMCMSContract(ctx, i.stateClient, i.mcmsParties, mcmsAddr) + if err != nil { + return types.ChainMetadata{}, fmt.Errorf("failed to get MCMS contract: %w", err) + } + + return chainMetadataFromMCMSContract(mcmsContract, i.role, mcmsAddr) +} + +func rootMetadataForRole(mcmsContract *mcmscore.MCMS, role TimelockRole) (mcmsapi.RootMetadata, error) { + switch role { + case TimelockRoleProposer: + return mcmsContract.Proposer.RootMetadata, nil + case TimelockRoleBypasser: + return mcmsContract.Bypasser.RootMetadata, nil + case TimelockRoleCanceller: + return mcmsContract.Canceller.RootMetadata, nil + default: + return mcmsapi.RootMetadata{}, fmt.Errorf("unknown timelock role: %s", role) + } +} + +// chainMetadataFromMCMSContract builds chain metadata from on-chain MCMS state. +// When lookupAddr is non-empty, MCMAddress is set to lookupAddr (after verifying it matches the ledger +// InstanceAddress) so clients can round-trip proposal ChainMetadata.MCMAddress through GetRootMetadata. +func chainMetadataFromMCMSContract(mcmsContract *mcmscore.MCMS, role TimelockRole, lookupAddr string) (types.ChainMetadata, error) { + rootMetadata, err := rootMetadataForRole(mcmsContract, role) + if err != nil { + return types.ChainMetadata{}, err + } + + canonicalAddress := contracts.InstanceID(string(mcmsContract.InstanceId)).RawInstanceAddress(mcmsContract.Owner).InstanceAddress().Hex() + mcmAddress := canonicalAddress + if lookupAddr != "" { + if !instanceAddressHexEqual(lookupAddr, canonicalAddress) { + return types.ChainMetadata{}, fmt.Errorf( + "MCMS instance address mismatch: lookup %q vs ledger %q", + lookupAddr, + canonicalAddress, + ) + } + mcmAddress = lookupAddr + } + startingOpCount, err := expiringRootOpCount(mcmsContract, role) + if err != nil { + return types.ChainMetadata{}, err + } + + // Before the first SetRoot, role RootMetadata may still be zeroed while the MCMS + // template field ChainId (and multisig id) are authoritative. + chainID := int64(rootMetadata.ChainId) + if chainID <= 0 { + chainID = int64(mcmsContract.ChainId) + } + multisigID := string(rootMetadata.MultisigId) + if multisigID == "" { + multisigID = fmt.Sprintf( + "%s@%s-%s", + string(mcmsContract.InstanceId), + string(mcmsContract.Owner), + strings.ToLower(role.String()), + ) + } + + additionalFields := AdditionalFieldsMetadata{ + ChainId: chainID, + MultisigId: multisigID, + InstanceId: string(mcmsContract.InstanceId), + } + if validateErr := additionalFields.Validate(); validateErr != nil { + return types.ChainMetadata{}, fmt.Errorf("invalid root metadata from ledger: %w", validateErr) + } + + additionalFieldsBytes, err := json.Marshal(additionalFields) + if err != nil { + return types.ChainMetadata{}, fmt.Errorf("marshal canton additional fields: %w", err) + } + + return types.ChainMetadata{ + StartingOpCount: startingOpCount, + MCMAddress: mcmAddress, + AdditionalFields: additionalFieldsBytes, + }, nil +} + +// toConfig converts a Canton MultisigConfig to the chain-agnostic types.Config +func toConfig(bindConfig mcmsapi.MultisigConfig) (*types.Config, error) { + // Group signers by group index + signersByGroup := make([][]common.Address, maxMCMSGroups) + + for _, signer := range bindConfig.Signers { + groupIdx := int(signer.SignerGroup) + if groupIdx < 0 || groupIdx >= maxMCMSGroups { + return nil, fmt.Errorf("signer group index %d out of range [0, %d)", groupIdx, maxMCMSGroups) + } + + // Parse signer address + addr := common.HexToAddress(string(signer.SignerAddress)) + signersByGroup[groupIdx] = append(signersByGroup[groupIdx], addr) + } + + if len(bindConfig.GroupQuorums) > maxMCMSGroups { + return nil, fmt.Errorf("group quorums length %d exceeds maximum %d", len(bindConfig.GroupQuorums), maxMCMSGroups) + } + if len(bindConfig.GroupParents) > maxMCMSGroups { + return nil, fmt.Errorf("group parents length %d exceeds maximum %d", len(bindConfig.GroupParents), maxMCMSGroups) + } + groupQuorums := make([]damltypes.INT64, maxMCMSGroups) + copy(groupQuorums, bindConfig.GroupQuorums) + groupParents := make([]damltypes.INT64, maxMCMSGroups) + copy(groupParents, bindConfig.GroupParents) + + // Build the group configs + groups := make([]types.Config, maxMCMSGroups) + for i := range maxMCMSGroups { + signers := signersByGroup[i] + if signers == nil { + signers = []common.Address{} + } + + q, convErr := safecast.IntToUint8(int(groupQuorums[i])) + if convErr != nil { + return nil, fmt.Errorf("group quorum for group %d: %w", i, convErr) + } + + groups[i] = types.Config{ + Signers: signers, + GroupSigners: []types.Config{}, + Quorum: q, + } + } + + // Link the group signers; this assumes a group's parent always has a lower index + // Process in reverse order to build the tree from leaves to root + for i := maxMCMSGroups - 1; i >= 0; i-- { + parent, convErr := safecast.IntToUint8(int(groupParents[i])) + if convErr != nil { + return nil, fmt.Errorf("group parent for group %d: %w", i, convErr) + } + + // Add non-empty child groups to their parent + // Skip the root group (i == 0) and empty groups (quorum == 0) + if i > 0 && groups[i].Quorum > 0 { + if int(parent) >= len(groups) { + return nil, fmt.Errorf("group parent index %d out of range", parent) + } + groups[parent].GroupSigners = append([]types.Config{groups[i]}, groups[parent].GroupSigners...) + } + } + + // Validate the root group config + if err := groups[0].Validate(); err != nil { + return nil, fmt.Errorf("invalid MCMS config: %w", err) + } + + return &groups[0], nil +} diff --git a/sdk/canton/inspector_test.go b/sdk/canton/inspector_test.go new file mode 100644 index 000000000..4efe4e61b --- /dev/null +++ b/sdk/canton/inspector_test.go @@ -0,0 +1,417 @@ +package canton + +import ( + "encoding/json" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + mcmsapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/api" + mcmscore "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/core" + "github.com/smartcontractkit/chainlink-canton/contracts" + damltypes "github.com/smartcontractkit/go-daml/pkg/types" + "github.com/stretchr/testify/require" + + mcmstypes "github.com/smartcontractkit/mcms/types" +) + +const testParty = "participant1-localparty-1::1220acd0401d95915bef2f498a45cc8f3c43119dde50cf370864e9aa4eb03d817cfb" + +func testMCMSContract(instanceID string, roleState func(mcmsapi.RoleState) mcmsapi.RoleState) mcmscore.MCMS { + proposer := mcmsapi.RoleState{ + Config: mcmsapi.MultisigConfig{ + Signers: []mcmsapi.SignerInfo{}, + GroupQuorums: []damltypes.INT64{damltypes.INT64(1)}, + GroupParents: []damltypes.INT64{damltypes.INT64(0)}, + }, + ExpiringRoot: mcmsapi.ExpiringRoot{ + Root: damltypes.TEXT("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"), + OpCount: damltypes.INT64(7), + ValidUntil: damltypes.TIMESTAMP(time.Unix(1_700_000_000, 0)), + }, + RootMetadata: mcmsapi.RootMetadata{ + ChainId: damltypes.INT64(1), + MultisigId: damltypes.TEXT(instanceID + "@" + testParty + "-proposer"), + }, + } + if roleState != nil { + proposer = roleState(proposer) + } + + return mcmscore.MCMS{ + Owner: damltypes.PARTY(testParty), + InstanceId: damltypes.TEXT(instanceID), + ChainId: damltypes.INT64(1), + Proposer: proposer, + Bypasser: mcmsapi.RoleState{ + ExpiringRoot: mcmsapi.ExpiringRoot{OpCount: damltypes.INT64(3)}, + RootMetadata: mcmsapi.RootMetadata{ + ChainId: damltypes.INT64(1), + MultisigId: damltypes.TEXT(instanceID + "@" + testParty + "-bypasser"), + }, + }, + Canceller: mcmsapi.RoleState{ + ExpiringRoot: mcmsapi.ExpiringRoot{OpCount: damltypes.INT64(5)}, + RootMetadata: mcmsapi.RootMetadata{ + ChainId: damltypes.INT64(1), + MultisigId: damltypes.TEXT(instanceID + "@" + testParty + "-canceller"), + }, + }, + } +} + +func TestExpiringRootOpCount(t *testing.T) { + t.Parallel() + + contract := testMCMSContract(mcmsInstanceIDCCIP, nil) + + opCount, err := expiringRootOpCount(&contract, TimelockRoleProposer) + require.NoError(t, err) + require.Equal(t, uint64(7), opCount) + + opCount, err = expiringRootOpCount(&contract, TimelockRoleBypasser) + require.NoError(t, err) + require.Equal(t, uint64(3), opCount) + + _, err = expiringRootOpCount(&contract, TimelockRole(99)) + require.ErrorContains(t, err, "unknown timelock role") +} + +func TestRootFromExpiringRoot(t *testing.T) { + t.Parallel() + + wantRoot := common.HexToHash("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20") + expiringRoot := mcmsapi.ExpiringRoot{ + Root: damltypes.TEXT(wantRoot.Hex()), + ValidUntil: damltypes.TIMESTAMP(time.Unix(1_700_000_000, 0)), + } + + root, validUntil, err := rootFromExpiringRoot(expiringRoot) + require.NoError(t, err) + require.Equal(t, wantRoot, root) + require.Equal(t, uint32(1_700_000_000), validUntil) + + _, _, err = rootFromExpiringRoot(mcmsapi.ExpiringRoot{ + ValidUntil: damltypes.TIMESTAMP(time.Date(2200, 1, 1, 0, 0, 0, 0, time.UTC)), + }) + require.ErrorContains(t, err, "valid until out of range") +} + +func TestExpiringRootForRole(t *testing.T) { + t.Parallel() + + contract := testMCMSContract(mcmsInstanceIDCCIP, nil) + + root, err := expiringRootForRole(&contract, TimelockRoleProposer) + require.NoError(t, err) + require.Equal(t, damltypes.INT64(7), root.OpCount) + + _, err = expiringRootForRole(&contract, TimelockRole(42)) + require.ErrorContains(t, err, "unknown timelock role") +} + +func TestChainMetadataFromMCMSContract(t *testing.T) { + t.Parallel() + + contract := testMCMSContract(mcmsInstanceIDCCIP, nil) + wantAddress := contracts.InstanceID(mcmsInstanceIDCCIP).RawInstanceAddress(damltypes.PARTY(testParty)).InstanceAddress().Hex() + + meta, err := chainMetadataFromMCMSContract(&contract, TimelockRoleProposer, "") + require.NoError(t, err) + require.Equal(t, uint64(7), meta.StartingOpCount) + require.Equal(t, wantAddress, meta.MCMAddress) + + lookupWithPrefix := "0x" + strings.TrimPrefix(wantAddress, "0x") + meta, err = chainMetadataFromMCMSContract(&contract, TimelockRoleProposer, lookupWithPrefix) + require.NoError(t, err) + require.Equal(t, lookupWithPrefix, meta.MCMAddress) + + var fields AdditionalFieldsMetadata + require.NoError(t, json.Unmarshal(meta.AdditionalFields, &fields)) + require.Equal(t, int64(1), fields.ChainId) + require.Equal(t, mcmsInstanceIDCCIP+"@"+testParty+"-proposer", fields.MultisigId) + require.Equal(t, mcmsInstanceIDCCIP, fields.InstanceId) + + meta, err = chainMetadataFromMCMSContract(&contract, TimelockRoleBypasser, "") + require.NoError(t, err) + require.Equal(t, uint64(3), meta.StartingOpCount) + + freshRoot := testMCMSContract(mcmsInstanceIDCCIP, func(rs mcmsapi.RoleState) mcmsapi.RoleState { + rs.RootMetadata = mcmsapi.RootMetadata{} + return rs + }) + meta, err = chainMetadataFromMCMSContract(&freshRoot, TimelockRoleProposer, "") + require.NoError(t, err) + require.NoError(t, json.Unmarshal(meta.AdditionalFields, &fields)) + require.Equal(t, int64(1), fields.ChainId) + require.Equal(t, mcmsInstanceIDCCIP+"@"+testParty+"-proposer", fields.MultisigId) + + noChainID := freshRoot + noChainID.ChainId = damltypes.INT64(0) + _, err = chainMetadataFromMCMSContract(&noChainID, TimelockRoleProposer, "") + require.ErrorContains(t, err, "invalid root metadata from ledger") +} + +func TestToConfig(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + description string + input mcmsapi.MultisigConfig + expected mcmstypes.Config + }{ + { + name: "simple_2of3", + description: "Simple 2-of-3 multisig with all signers in root group (group 0)", + input: mcmsapi.MultisigConfig{ + Signers: []mcmsapi.SignerInfo{ + {SignerAddress: damltypes.TEXT("0x1111111111111111111111111111111111111111"), SignerIndex: damltypes.INT64(0), SignerGroup: damltypes.INT64(0)}, + {SignerAddress: damltypes.TEXT("0x2222222222222222222222222222222222222222"), SignerIndex: damltypes.INT64(1), SignerGroup: damltypes.INT64(0)}, + {SignerAddress: damltypes.TEXT("0x3333333333333333333333333333333333333333"), SignerIndex: damltypes.INT64(2), SignerGroup: damltypes.INT64(0)}, + }, + GroupQuorums: repeatInt64(2, 0), + GroupParents: repeatInt64(0, 0), + }, + expected: mcmstypes.Config{ + Quorum: 2, + Signers: []common.Address{ + common.HexToAddress("1111111111111111111111111111111111111111"), + common.HexToAddress("2222222222222222222222222222222222222222"), + common.HexToAddress("3333333333333333333333333333333333333333"), + }, + GroupSigners: []mcmstypes.Config{}, + }, + }, + { + name: "hierarchical_2level", + description: "2-level hierarchy: root group 0 has 1 direct signer + group 1 as child. Group 1 has 3 signers with quorum 2. Root quorum is 1 (can be satisfied by direct signer OR group 1 reaching quorum).", + input: mcmsapi.MultisigConfig{ + Signers: []mcmsapi.SignerInfo{ + {SignerAddress: damltypes.TEXT("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), SignerIndex: damltypes.INT64(0), SignerGroup: damltypes.INT64(0)}, + {SignerAddress: damltypes.TEXT("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), SignerIndex: damltypes.INT64(1), SignerGroup: damltypes.INT64(1)}, + {SignerAddress: damltypes.TEXT("0xcccccccccccccccccccccccccccccccccccccccc"), SignerIndex: damltypes.INT64(2), SignerGroup: damltypes.INT64(1)}, + {SignerAddress: damltypes.TEXT("0xdddddddddddddddddddddddddddddddddddddddd"), SignerIndex: damltypes.INT64(3), SignerGroup: damltypes.INT64(1)}, + }, + GroupQuorums: repeatInt64(1, 2), + GroupParents: repeatInt64(0, 0), + }, + expected: mcmstypes.Config{ + Quorum: 1, + Signers: []common.Address{ + common.HexToAddress("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + }, + GroupSigners: []mcmstypes.Config{ + { + Quorum: 2, + Signers: []common.Address{ + common.HexToAddress("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + common.HexToAddress("cccccccccccccccccccccccccccccccccccccccc"), + common.HexToAddress("dddddddddddddddddddddddddddddddddddddddd"), + }, + GroupSigners: []mcmstypes.Config{}, + }, + }, + }, + }, + { + name: "complex_3level", + description: "3-level hierarchy: Group 0 (root) quorum 2, Group 1 (parent 0) quorum 2, Group 2 (parent 0) quorum 1, Group 3 (parent 1) quorum 2. Tests deeper nesting with multiple child groups at same level.", + input: mcmsapi.MultisigConfig{ + Signers: []mcmsapi.SignerInfo{ + {SignerAddress: damltypes.TEXT("0x1000000000000000000000000000000000000001"), SignerIndex: damltypes.INT64(0), SignerGroup: damltypes.INT64(0)}, + {SignerAddress: damltypes.TEXT("0x1000000000000000000000000000000000000002"), SignerIndex: damltypes.INT64(1), SignerGroup: damltypes.INT64(1)}, + {SignerAddress: damltypes.TEXT("0x1000000000000000000000000000000000000003"), SignerIndex: damltypes.INT64(2), SignerGroup: damltypes.INT64(1)}, + {SignerAddress: damltypes.TEXT("0x1000000000000000000000000000000000000004"), SignerIndex: damltypes.INT64(3), SignerGroup: damltypes.INT64(2)}, + {SignerAddress: damltypes.TEXT("0x1000000000000000000000000000000000000005"), SignerIndex: damltypes.INT64(4), SignerGroup: damltypes.INT64(2)}, + {SignerAddress: damltypes.TEXT("0x1000000000000000000000000000000000000006"), SignerIndex: damltypes.INT64(5), SignerGroup: damltypes.INT64(3)}, + {SignerAddress: damltypes.TEXT("0x1000000000000000000000000000000000000007"), SignerIndex: damltypes.INT64(6), SignerGroup: damltypes.INT64(3)}, + {SignerAddress: damltypes.TEXT("0x1000000000000000000000000000000000000008"), SignerIndex: damltypes.INT64(7), SignerGroup: damltypes.INT64(3)}, + }, + GroupQuorums: repeatInt64(2, 2, 1, 2), + GroupParents: repeatInt64(0, 0, 0, 1), + }, + expected: mcmstypes.Config{ + Quorum: 2, + Signers: []common.Address{ + common.HexToAddress("1000000000000000000000000000000000000001"), + }, + GroupSigners: []mcmstypes.Config{ + { + Quorum: 2, + Signers: []common.Address{ + common.HexToAddress("1000000000000000000000000000000000000002"), + common.HexToAddress("1000000000000000000000000000000000000003"), + }, + GroupSigners: []mcmstypes.Config{ + { + Quorum: 2, + Signers: []common.Address{ + common.HexToAddress("1000000000000000000000000000000000000006"), + common.HexToAddress("1000000000000000000000000000000000000007"), + common.HexToAddress("1000000000000000000000000000000000000008"), + }, + GroupSigners: []mcmstypes.Config{}, + }, + }, + }, + { + Quorum: 1, + Signers: []common.Address{ + common.HexToAddress("1000000000000000000000000000000000000004"), + common.HexToAddress("1000000000000000000000000000000000000005"), + }, + GroupSigners: []mcmstypes.Config{}, + }, + }, + }, + }, + { + name: "empty_groups_edge_case", + description: "Edge case: groups with quorum 0 (disabled) interspersed with active groups. Group 0 active (quorum 1), Group 1 disabled (quorum 0), Group 2 active (quorum 2, parent 0). The toConfig function should skip disabled groups.", + input: mcmsapi.MultisigConfig{ + Signers: []mcmsapi.SignerInfo{ + {SignerAddress: damltypes.TEXT("0xdead000000000000000000000000000000000001"), SignerIndex: damltypes.INT64(0), SignerGroup: damltypes.INT64(0)}, + {SignerAddress: damltypes.TEXT("0xdead000000000000000000000000000000000002"), SignerIndex: damltypes.INT64(1), SignerGroup: damltypes.INT64(2)}, + {SignerAddress: damltypes.TEXT("0xdead000000000000000000000000000000000003"), SignerIndex: damltypes.INT64(2), SignerGroup: damltypes.INT64(2)}, + }, + GroupQuorums: repeatInt64(1, 0, 2), + GroupParents: repeatInt64(0, 0, 0), + }, + expected: mcmstypes.Config{ + Quorum: 1, + Signers: []common.Address{ + common.HexToAddress("dead000000000000000000000000000000000001"), + }, + GroupSigners: []mcmstypes.Config{ + { + Quorum: 2, + Signers: []common.Address{ + common.HexToAddress("dead000000000000000000000000000000000002"), + common.HexToAddress("dead000000000000000000000000000000000003"), + }, + GroupSigners: []mcmstypes.Config{}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result, err := toConfig(tt.input) + require.NoError(t, err, tt.description) + require.NotNil(t, result) + + require.Equal(t, tt.expected.Quorum, result.Quorum, "quorum mismatch") + require.Len(t, result.Signers, len(tt.expected.Signers), "signers count mismatch") + + for i, expectedSigner := range tt.expected.Signers { + require.Equal(t, expectedSigner, result.Signers[i], "signer mismatch at index %d", i) + } + + compareGroupSigners(t, tt.expected.GroupSigners, result.GroupSigners) + }) + } +} + +func TestToConfigErrors(t *testing.T) { + t.Parallel() + + validQuorums := repeatInt64(1) + validParents := repeatInt64(0) + + tests := []struct { + name string + input mcmsapi.MultisigConfig + wantErr string + }{ + { + name: "signer group out of range", + input: mcmsapi.MultisigConfig{ + Signers: []mcmsapi.SignerInfo{ + {SignerAddress: damltypes.TEXT("0x1111111111111111111111111111111111111111"), SignerGroup: damltypes.INT64(maxMCMSGroups)}, + }, + GroupQuorums: validQuorums, + GroupParents: validParents, + }, + wantErr: "signer group index", + }, + { + name: "too many group quorums", + input: mcmsapi.MultisigConfig{ + GroupQuorums: append(validQuorums, damltypes.INT64(1)), + GroupParents: validParents, + }, + wantErr: "group quorums length", + }, + { + name: "too many group parents", + input: mcmsapi.MultisigConfig{ + GroupQuorums: validQuorums, + GroupParents: append(validParents, damltypes.INT64(0)), + }, + wantErr: "group parents length", + }, + { + name: "parent index out of range", + input: mcmsapi.MultisigConfig{ + Signers: []mcmsapi.SignerInfo{ + {SignerAddress: damltypes.TEXT("0x1111111111111111111111111111111111111111"), SignerGroup: damltypes.INT64(1)}, + }, + GroupQuorums: repeatInt64(0, 1), + GroupParents: repeatInt64(0, int64(maxMCMSGroups)), + }, + wantErr: "group parent index", + }, + { + name: "invalid root config", + input: mcmsapi.MultisigConfig{ + GroupQuorums: repeatInt64(5), + GroupParents: validParents, + }, + wantErr: "invalid MCMS config", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, err := toConfig(tt.input) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} + +func repeatInt64(values ...int64) []damltypes.INT64 { + out := make([]damltypes.INT64, maxMCMSGroups) + for i, v := range values { + if i >= maxMCMSGroups { + break + } + out[i] = damltypes.INT64(v) + } + + return out +} + +func compareGroupSigners(t *testing.T, expected, actual []mcmstypes.Config) { + t.Helper() + + require.Len(t, actual, len(expected), "group signers count mismatch") + + for i := range expected { + require.Equal(t, expected[i].Quorum, actual[i].Quorum, "group %d quorum mismatch", i) + require.Len(t, actual[i].Signers, len(expected[i].Signers), "group %d signers count mismatch", i) + + for j, expectedSigner := range expected[i].Signers { + require.Equal(t, expectedSigner, actual[i].Signers[j], "group %d signer mismatch at index %d", i, j) + } + + compareGroupSigners(t, expected[i].GroupSigners, actual[i].GroupSigners) + } +} diff --git a/sdk/canton/resolver.go b/sdk/canton/resolver.go new file mode 100644 index 000000000..aee24d4e9 --- /dev/null +++ b/sdk/canton/resolver.go @@ -0,0 +1,206 @@ +package canton + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + cantontypes "github.com/smartcontractkit/go-daml/pkg/types" + + "github.com/smartcontractkit/chainlink-canton/bindings" + mcmscore "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/core" + "github.com/smartcontractkit/chainlink-canton/contracts" +) + +// ResolveMCMSContractID resolves an MCMS InstanceAddress (hex string) to the current active contract ID. +// instanceAddressHex is the hex-encoded InstanceAddress (keccak256 of "instanceId@party"); it may be prefixed with "0x". +func ResolveMCMSContractID(ctx context.Context, stateService apiv2.StateServiceClient, mcmsParties []string, instanceAddressHex string) (string, error) { + if instanceAddressHex == "" { + return "", fmt.Errorf("instance address hex is required") + } + addr := contracts.HexToInstanceAddress(instanceAddressHex) + templateID := mcmscore.MCMS{}.GetTemplateID() + + return findActiveContractIDByInstanceAddress(ctx, stateService, mcmsParties, templateID, addr) +} + +// findActiveContractIDByInstanceAddress returns the active contract ID for the given instance address. +func findActiveContractIDByInstanceAddress(ctx context.Context, stateService apiv2.StateServiceClient, parties []string, templateID string, instanceAddress contracts.InstanceAddress) (string, error) { + activeContract, err := findActiveContractByInstanceAddress(ctx, stateService, parties, templateID, instanceAddress) + if err != nil { + return "", err + } + + return activeContract.GetCreatedEvent().GetContractId(), nil +} + +// findActiveContractByInstanceAddress finds an active contract by its instance address. +// It returns an error if there are multiple or zero active contracts matching the instance address. +// TODO: copied from chainlink-canton deployment/utils/operations/contract/exercise.go to avoid importing +// unwanted dependencies. We should move the helper function to the bindings package and use it here. +func findActiveContractByInstanceAddress(ctx context.Context, stateService apiv2.StateServiceClient, parties []string, templateID string, instanceAddress contracts.InstanceAddress) (*apiv2.ActiveContract, error) { + ledgerEndResp, err := stateService.GetLedgerEnd(ctx, &apiv2.GetLedgerEndRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to get ledger end: %w", err) + } + + packageID, moduleName, entityName, err := ParseTemplateIDFromString(templateID) + if err != nil { + return nil, fmt.Errorf("failed to parse template ID: %w", err) + } + + // Build filters for all parties + filtersByParty := make(map[string]*apiv2.Filters) + for _, party := range parties { + filtersByParty[party] = &apiv2.Filters{ + Cumulative: []*apiv2.CumulativeFilter{ + { + IdentifierFilter: &apiv2.CumulativeFilter_TemplateFilter{ + TemplateFilter: &apiv2.TemplateFilter{ + TemplateId: &apiv2.Identifier{ + PackageId: packageID, + ModuleName: moduleName, + EntityName: entityName, + }, + IncludeCreatedEventBlob: true, + }, + }, + }, + }, + } + } + + activeContractsResp, err := stateService.GetActiveContracts(ctx, &apiv2.GetActiveContractsRequest{ + ActiveAtOffset: ledgerEndResp.GetOffset(), + EventFormat: &apiv2.EventFormat{ + FiltersByParty: filtersByParty, + // Verbose enriches gRPC event payloads (e.g. full template IDs); it does not log to stdout/stderr. + Verbose: true, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to get active contracts: %w", err) + } + defer func() { _ = activeContractsResp.CloseSend() }() + + var activeContract *apiv2.ActiveContract + for { + activeContractResp, err := activeContractsResp.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + + return nil, fmt.Errorf("failed to receive active contracts: %w", err) + } + + if c, ok := activeContractResp.GetContractEntry().(*apiv2.GetActiveContractsResponse_ActiveContract); ok { + createArguments := c.ActiveContract.GetCreatedEvent().GetCreateArguments() + if createArguments == nil { + continue + } + + var contractInstanceID string + for _, field := range createArguments.GetFields() { + if field.GetLabel() == "instanceId" { + contractInstanceID = field.GetValue().GetText() + break + } + } + if contractInstanceID == "" { + continue + } + + instanceID := contracts.InstanceID(contractInstanceID) + signatories := c.ActiveContract.GetCreatedEvent().GetSignatories() + if len(signatories) != 1 { + continue + } + gotAddress := instanceID.RawInstanceAddress(cantontypes.PARTY(signatories[0])).InstanceAddress() + + if instanceAddress != gotAddress { + continue + } + + if activeContract != nil { + existingOffset := activeContract.GetCreatedEvent().GetOffset() + newOffset := c.ActiveContract.GetCreatedEvent().GetOffset() + if newOffset <= existingOffset { + continue + } + } + activeContract = c.ActiveContract + } + } + + if activeContract == nil { + return nil, fmt.Errorf("no active contract found for InstanceAddress %s", instanceAddress.String()) + } + + return activeContract, nil +} + +// GetMCMSContract queries the active MCMS contract by InstanceAddress (hex). +// mcmsAddr is the InstanceAddress hex string (may be prefixed with "0x"). +func GetMCMSContract(ctx context.Context, stateService apiv2.StateServiceClient, mcmsParties []string, mcmsAddr string) (*mcmscore.MCMS, error) { + mcmsAddr = strings.TrimPrefix(mcmsAddr, "0x") + if mcmsAddr == "" { + return nil, fmt.Errorf("MCMS instance address is required") + } + addr := contracts.HexToInstanceAddress(mcmsAddr) + templateID := mcmscore.MCMS{}.GetTemplateID() + activeContract, err := findActiveContractByInstanceAddress(ctx, stateService, mcmsParties, templateID, addr) + if err != nil { + return nil, fmt.Errorf("MCMS contract for InstanceAddress %s: %w", mcmsAddr, err) + } + + // Wrap for bindings unmarshal + wrapped := &apiv2.GetActiveContractsResponse_ActiveContract{ActiveContract: activeContract} + + mcmsContract, err := bindings.UnmarshalActiveContract[mcmscore.MCMS](wrapped) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal MCMS contract: %w", err) + } + + return mcmsContract, nil +} + +// ResolveTargetContractID resolves a target contract's active contract ID from a raw instance address +// ("instanceId@partyId" format) and a Daml template ID (e.g. "#pkg:Module:Entity"). +// This enables dynamic resolution at execution time when TargetCid is not pre-populated in AdditionalFields. +func ResolveTargetContractID(ctx context.Context, stateService apiv2.StateServiceClient, parties []string, rawInstanceAddress, templateID string) (string, error) { + if rawInstanceAddress == "" { + return "", fmt.Errorf("raw instance address is required") + } + if templateID == "" { + return "", fmt.Errorf("template ID is required") + } + raw, err := contracts.RawInstanceAddressFromString(rawInstanceAddress) + if err != nil { + return "", fmt.Errorf("parse raw instance address %q: %w", rawInstanceAddress, err) + } + addr := raw.InstanceAddress() + + return findActiveContractIDByInstanceAddress(ctx, stateService, parties, templateID, addr) +} + +// IsInstanceAddressHex returns true if s looks like an InstanceAddress hex string (64 hex chars, optional 0x prefix). +// Canton contract IDs use a different format; when we have 0x-prefixed 64-char hex we treat it as InstanceAddress. +func IsInstanceAddressHex(s string) bool { + s = strings.TrimPrefix(s, "0x") + + return len(s) == instanceAddressHexLen && isValidHex(s) +} + +// ResolveContractIDIfInstanceAddress returns the current MCMS contract ID if cid is InstanceAddress hex; +// otherwise returns cid unchanged. Use when building TargetCids so Canton receives real contract IDs. +func ResolveContractIDIfInstanceAddress(ctx context.Context, stateService apiv2.StateServiceClient, parties []string, cid string) (string, error) { + if !IsInstanceAddressHex(cid) { + return cid, nil + } + + return ResolveMCMSContractID(ctx, stateService, parties, cid) +} diff --git a/sdk/canton/timelock_converter.go b/sdk/canton/timelock_converter.go new file mode 100644 index 000000000..e6f71fc92 --- /dev/null +++ b/sdk/canton/timelock_converter.go @@ -0,0 +1,240 @@ +package canton + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" + mcmsapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/api" + cantontypes "github.com/smartcontractkit/go-daml/pkg/types" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" +) + +var _ sdk.TimelockConverter = (*TimelockConverter)(nil) + +// TimelockConverter converts Canton timelock batch operations to chain operations. +type TimelockConverter struct{} + +// NewTimelockConverter creates a new TimelockConverter. +func NewTimelockConverter() *TimelockConverter { + return &TimelockConverter{} +} + +// ConvertBatchToChainOperations converts a BatchOperation to Canton-specific timelock operations. +func (t *TimelockConverter) ConvertBatchToChainOperations( + _ context.Context, + metadata types.ChainMetadata, + bop types.BatchOperation, + _ string, // timelockAddress - not used for Canton (MCMS has built-in timelock) + mcmAddress string, + delay types.Duration, + action types.TimelockAction, + predecessor common.Hash, + salt common.Hash, +) ([]types.Operation, common.Hash, error) { + metadataFields, err := resolveAdditionalFieldsMetadata(metadata, bop, action) + if err != nil { + return nil, common.Hash{}, err + } + + calls, callsForHash, allContractIds, err := buildCallsFromBatch(bop) + if err != nil { + return nil, common.Hash{}, err + } + + predecessorHex := hex.EncodeToString(predecessor[:]) + saltHex := hex.EncodeToString(salt[:]) + + operationIDStr, err := hashTimelockOpID(callsForHash, predecessorHex, saltHex) + if err != nil { + return nil, common.Hash{}, err + } + operationIDBytes, err := hex.DecodeString(operationIDStr) + if err != nil || len(operationIDBytes) != 32 { + return nil, common.Hash{}, fmt.Errorf("invalid operation ID hash: %w", err) + } + var operationID common.Hash + copy(operationID[:], operationIDBytes) + + var functionName string + var opDataHex string + switch action { + case types.TimelockActionSchedule: + functionName, opDataHex, err = scheduleActionData(calls, predecessorHex, saltHex, delay) + case types.TimelockActionBypass: + functionName, opDataHex, err = bypassActionData(calls) + case types.TimelockActionCancel: + functionName, opDataHex, err = cancelActionData(operationIDStr) + default: + return nil, common.Hash{}, fmt.Errorf("unsupported timelock action: %v", action) + } + if err != nil { + return nil, common.Hash{}, err + } + + // For timelock operations (schedule/cancel/bypass), the target is the MCMS contract itself. + // Canton expects TargetInstanceAddress in "baseId@partyId" format, not hex. + // MultisigId is in format "baseId@partyId-role"; extract "baseId@partyId". + targetInstanceAddress := extractInstanceAddressFromMultisigId(metadataFields.MultisigId) + op, err := buildTimelockOperation(bop, mcmAddress, targetInstanceAddress, functionName, opDataHex, allContractIds) + if err != nil { + return nil, common.Hash{}, err + } + + return []types.Operation{op}, operationID, nil +} + +func OperationID( + batchOp types.BatchOperation, + _ types.TimelockAction, + predecessor common.Hash, + salt common.Hash, +) (common.Hash, error) { + _, callsForHash, _, err := buildCallsFromBatch(batchOp) + if err != nil { + return common.Hash{}, err + } + + predecessorHex := hex.EncodeToString(predecessor[:]) + saltHex := hex.EncodeToString(salt[:]) + operationIDStr, err := hashTimelockOpID(callsForHash, predecessorHex, saltHex) + if err != nil { + return common.Hash{}, err + } + + operationIDBytes, err := hex.DecodeString(operationIDStr) + if err != nil || len(operationIDBytes) != 32 { + return common.Hash{}, fmt.Errorf("invalid operation ID hash: %w", err) + } + + var operationID common.Hash + copy(operationID[:], operationIDBytes) + + return operationID, nil +} + +// buildCallsFromBatch extracts mcmsapi.TimelockCall, timelockCallForHash, and all ContractIds from bop. +// Uses tx.To and tx.Data as fallbacks when AdditionalFields are missing or empty. +func buildCallsFromBatch(bop types.BatchOperation) ([]mcmsapi.TimelockCall, []timelockCallForHash, []string, error) { + calls := make([]mcmsapi.TimelockCall, 0, len(bop.Transactions)) + callsForHash := make([]timelockCallForHash, 0, len(bop.Transactions)) + allContractIds := make([]string, 0, len(bop.Transactions)) + for _, tx := range bop.Transactions { + var af AdditionalFields + if len(tx.AdditionalFields) > 0 { + if err := json.Unmarshal(tx.AdditionalFields, &af); err != nil { + return nil, nil, nil, fmt.Errorf("unmarshal transaction additional fields: %w", err) + } + } + targetInstanceAddress := af.TargetInstanceAddress + if targetInstanceAddress == "" { + targetInstanceAddress = tx.To + } + operationData := operationDataHex(tx.Data) + calls = append(calls, mcmsapi.TimelockCall{ + TargetInstanceAddress: cantontypes.TEXT(targetInstanceAddress), + FunctionName: cantontypes.TEXT(af.FunctionName), + OperationData: cantontypes.TEXT(operationData), + }) + callsForHash = append(callsForHash, timelockCallForHash{ + TargetInstanceAddress: targetInstanceAddress, + FunctionName: af.FunctionName, + OperationData: operationData, + }) + allContractIds = append(allContractIds, af.ContractIds...) + } + + return calls, callsForHash, allContractIds, nil +} + +// wireToHex encodes raw MCMS wire bytes (HexCodec.Marshal / MarshalHex output) as hex ASCII +// for Canton additionalFields, Merkle hashing, and ledger transport. +func wireToHex(wire string) string { + return hex.EncodeToString([]byte(wire)) +} + +// scheduleActionData returns function name and hex-encoded params for ScheduleBatch. +func scheduleActionData(calls []mcmsapi.TimelockCall, predecessorHex, saltHex string, delay types.Duration) (functionName, opDataHex string, err error) { + delaySecs := max(int64(0), int64(delay.Seconds())) + params := mcmsapi.ScheduleBatchParams{ + Calls: calls, + Predecessor: cantontypes.TEXT(predecessorHex), + Salt: cantontypes.TEXT(saltHex), + DelaySecs: cantontypes.INT64(delaySecs), + } + wire, err := params.MarshalHex() + if err != nil { + return "", "", fmt.Errorf("marshal ScheduleBatchParams: %w", err) + } + + return "ScheduleBatch", wireToHex(wire), nil +} + +// bypassActionData returns function name and hex-encoded params for BypasserExecuteBatch. +func bypassActionData(calls []mcmsapi.TimelockCall) (functionName, opDataHex string, err error) { + params := mcmsapi.BypasserExecuteBatchParams{Calls: calls} + wire, err := params.MarshalHex() + if err != nil { + return "", "", fmt.Errorf("marshal BypasserExecuteBatchParams: %w", err) + } + + return "BypasserExecuteBatch", wireToHex(wire), nil +} + +// cancelActionData returns function name and hex-encoded params for CancelBatch. +func cancelActionData(operationIDStr string) (functionName, opDataHex string, err error) { + params := mcmsapi.CancelBatchParams{OpId: cantontypes.TEXT(operationIDStr)} + wire, err := params.MarshalHex() + if err != nil { + return "", "", fmt.Errorf("marshal CancelBatchParams: %w", err) + } + + return "CancelBatch", wireToHex(wire), nil +} + +// extractInstanceAddressFromMultisigId extracts "baseId@partyId" from "baseId@partyId-role". +// MultisigId format: "baseId@partyId-role" (e.g., "mcms-abc123@party::hash-proposer") +// Returns: "baseId@partyId" (e.g., "mcms-abc123@party::hash") +func extractInstanceAddressFromMultisigId(multisigId string) string { + // Find the last dash that separates the role suffix + lastDash := strings.LastIndex(multisigId, "-") + if lastDash == -1 { + return multisigId + } + + return multisigId[:lastDash] +} + +// buildTimelockOperation builds a single types.Operation for the given timelock action. +// allContractIds are target contract IDs from the batch (ExecuteOp TargetCids); mcmAddress populates tx.To. +func buildTimelockOperation(bop types.BatchOperation, mcmAddress, targetInstanceAddress, functionName, opDataHex string, allContractIds []string) (types.Operation, error) { + opData, err := hex.DecodeString(opDataHex) + if err != nil { + return types.Operation{}, fmt.Errorf("decode timelock operation data: %w", err) + } + + opAdditionalFields := AdditionalFields{ + TargetInstanceAddress: targetInstanceAddress, + FunctionName: functionName, + TargetCid: mcmAddress, + ContractIds: allContractIds, + } + opAdditionalFieldsBytes, err := json.Marshal(opAdditionalFields) + if err != nil { + return types.Operation{}, fmt.Errorf("marshal operation additional fields: %w", err) + } + + return types.Operation{ + ChainSelector: bop.ChainSelector, + Transaction: types.Transaction{ + To: mcmAddress, + Data: opData, + AdditionalFields: opAdditionalFieldsBytes, + }, + }, nil +} diff --git a/sdk/canton/timelock_crypto.go b/sdk/canton/timelock_crypto.go new file mode 100644 index 000000000..70d1ff437 --- /dev/null +++ b/sdk/canton/timelock_crypto.go @@ -0,0 +1,84 @@ +package canton + +import ( + "encoding/hex" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/crypto" +) + +// timelockCallForHash is used for computing the operation ID hash. +// Field semantics match mcms.TimelockCall (TargetInstanceAddress, FunctionName, OperationData). +type timelockCallForHash struct { + TargetInstanceAddress string // Format: "instanceId@partyId" + FunctionName string + OperationData string +} + +// hashTimelockOpID computes the operation ID for timelock operations. +// Matches Canton's hashTimelockOpId: keccak256(encodedCalls || predecessor || salt). +// predecessor and salt must be hex-encoded (e.g. 64-char hex for 32-byte hashes). +func hashTimelockOpID(calls []timelockCallForHash, predecessor, salt string) (string, error) { + var sb strings.Builder + + // Length prefix for calls array (32-byte padded) + sb.WriteString(padLeft32(intToHex(len(calls)))) + + // Encode each call with 32-byte padded length prefixes + for _, call := range calls { + // Length prefix for targetInstanceAddress (UTF-8 byte count) + sb.WriteString(padLeft32(intToHex(len(call.TargetInstanceAddress)))) + sb.WriteString(asciiToHex(call.TargetInstanceAddress)) + // Length prefix for functionName (UTF-8 byte count) + sb.WriteString(padLeft32(intToHex(len(call.FunctionName)))) + sb.WriteString(asciiToHex(call.FunctionName)) + // Length prefix for operationData (byte count = hex length / 2) + opData := encodeOperationDataForHash(call.OperationData) + sb.WriteString(padLeft32(intToHex(len(opData) / hexEncodedByteLen))) + sb.WriteString(opData) + } + + // Length prefix for predecessor (byte count = hex length / 2) + sb.WriteString(padLeft32(intToHex(len(predecessor) / hexEncodedByteLen))) + sb.WriteString(predecessor) + + // Length prefix for salt (byte count = hex length / 2) + sb.WriteString(padLeft32(intToHex(len(salt) / hexEncodedByteLen))) + sb.WriteString(salt) + + data, err := hex.DecodeString(sb.String()) + if err != nil { + return "", fmt.Errorf("hash timelock op id: invalid hex encoding: %w", err) + } + + return hex.EncodeToString(crypto.Keccak256(data)), nil +} + +// encodeOperationDataForHash normalizes operationData for hashing: +// - If operationData is valid hex (even length, hex digits only), use as-is (matches Daml BytesHex type). +// - Otherwise, treat as ASCII and hex-encode it (SDK convenience for non-hex inputs). +func encodeOperationDataForHash(operationData string) string { + if isValidHex(operationData) { + return operationData + } + + return asciiToHex(operationData) +} + +func isValidHex(s string) bool { + if len(s)%hexEncodedByteLen != 0 { + return false + } + for _, c := range s { + switch { + case c >= '0' && c <= '9': + case c >= 'a' && c <= 'f': + case c >= 'A' && c <= 'F': + default: + return false + } + } + + return true +} diff --git a/sdk/canton/timelock_crypto_test.go b/sdk/canton/timelock_crypto_test.go new file mode 100644 index 000000000..e3b1d20cf --- /dev/null +++ b/sdk/canton/timelock_crypto_test.go @@ -0,0 +1,27 @@ +package canton + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHashTimelockOpID(t *testing.T) { + t.Parallel() + + hash, err := hashTimelockOpID([]timelockCallForHash{{ + TargetInstanceAddress: "counter@party::abc", + FunctionName: "Increment", + OperationData: "deadbeef", + }}, "00", "00") + require.NoError(t, err) + require.Len(t, hash, 64) +} + +func TestIsInstanceAddressHex(t *testing.T) { + t.Parallel() + + require.True(t, IsInstanceAddressHex("0xd4dcbc33d025740c32b65cb60d208a7eb8f99b3d90903ffe52616e14f9096995")) + require.False(t, IsInstanceAddressHex("0xshort")) + require.False(t, IsInstanceAddressHex("not-hex")) +} diff --git a/sdk/canton/timelock_executor.go b/sdk/canton/timelock_executor.go new file mode 100644 index 000000000..65bbd6b47 --- /dev/null +++ b/sdk/canton/timelock_executor.go @@ -0,0 +1,183 @@ +package canton + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" + cselectors "github.com/smartcontractkit/chain-selectors" + mcmsapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/api" + mcmscore "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/core" + "github.com/smartcontractkit/go-daml/pkg/service/ledger" + cantontypes "github.com/smartcontractkit/go-daml/pkg/types" + + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" +) + +var _ sdk.TimelockExecutor = (*TimelockExecutor)(nil) + +// TimelockExecutor executes scheduled Canton timelock operations (ExecuteScheduledBatch). +type TimelockExecutor struct { + *TimelockInspector + client apiv2.CommandServiceClient + // The party that will be used to submit transactions. + // Should be different from mcmsParties. + submittingParty string + // The parties that own the MCMS deployment. + mcmsParties []string +} + +// NewTimelockExecutor creates a TimelockExecutor that submits ExecuteScheduledBatch via the given clients and party. +// timelockAddress (in Execute) is InstanceAddress hex; it is resolved to contract ID when submitting. +func NewTimelockExecutor(client apiv2.CommandServiceClient, stateClient apiv2.StateServiceClient, submittingParty string, mcmsParties []string) *TimelockExecutor { + return &TimelockExecutor{ + TimelockInspector: NewTimelockInspector(client, stateClient, submittingParty, mcmsParties), + client: client, + submittingParty: submittingParty, + mcmsParties: mcmsParties, + } +} + +// Execute submits ExecuteScheduledBatch for the given batch operation (same opId hash as converter). +// timelockAddress is InstanceAddress hex; it is resolved to the current MCMS contract ID before submit. +func (t *TimelockExecutor) Execute( + ctx context.Context, + bop types.BatchOperation, + timelockAddress string, + predecessor common.Hash, + salt common.Hash, +) (types.TransactionResult, error) { + contractID, err := ResolveMCMSContractID(ctx, t.StateServiceClient(), t.mcmsParties, timelockAddress) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("resolve MCMS contract ID: %w", err) + } + + if len(bop.Transactions) == 0 { + return types.TransactionResult{}, fmt.Errorf("batch operation has no transactions") + } + + calls := make([]mcmsapi.TimelockCall, 0, len(bop.Transactions)) + callsForHash := make([]timelockCallForHash, 0, len(bop.Transactions)) + // Map from instance address -> contract ID for targetCids + stateClient := t.StateServiceClient() + instanceToContractID := make(map[string]string) + for _, tx := range bop.Transactions { + var af AdditionalFields + if unmarshalErr := json.Unmarshal(tx.AdditionalFields, &af); unmarshalErr != nil { + return types.TransactionResult{}, fmt.Errorf("unmarshal transaction additional fields: %w", unmarshalErr) + } + operationData := operationDataHex(tx.Data) + calls = append(calls, mcmsapi.TimelockCall{ + TargetInstanceAddress: cantontypes.TEXT(af.TargetInstanceAddress), + FunctionName: cantontypes.TEXT(af.FunctionName), + OperationData: cantontypes.TEXT(operationData), + }) + callsForHash = append(callsForHash, timelockCallForHash{ + TargetInstanceAddress: af.TargetInstanceAddress, + FunctionName: af.FunctionName, + OperationData: operationData, + }) + if af.TargetInstanceAddress != "" { + if af.TargetCid != "" { + instanceToContractID[af.TargetInstanceAddress] = af.TargetCid + } else if af.TargetTemplateID != "" { + resolved, resolveErr := ResolveTargetContractID(ctx, stateClient, t.mcmsParties, af.TargetInstanceAddress, af.TargetTemplateID) + if resolveErr != nil { + return types.TransactionResult{}, fmt.Errorf("resolve target contract ID for %s: %w", af.TargetInstanceAddress, resolveErr) + } + instanceToContractID[af.TargetInstanceAddress] = resolved + } + } + } + + predecessorHex := hex.EncodeToString(predecessor[:]) + saltHex := hex.EncodeToString(salt[:]) + opIDStr, err := hashTimelockOpID(callsForHash, predecessorHex, saltHex) + if err != nil { + return types.TransactionResult{}, err + } + + // Build targetCidsMap: instanceAddress -> CONTRACT_ID. + targetCidsMap := make(map[cantontypes.TEXT]cantontypes.CONTRACT_ID) + for instanceAddr, cid := range instanceToContractID { + resolved, resolveErr := ResolveContractIDIfInstanceAddress(ctx, stateClient, t.mcmsParties, cid) + if resolveErr != nil { + return types.TransactionResult{}, fmt.Errorf("resolve contract ID %q: %w", cid, resolveErr) + } + // Key is instance address, value is resolved contract ID + targetCidsMap[cantontypes.TEXT(instanceAddr)] = cantontypes.CONTRACT_ID(resolved) + } + + executeArgs := mcmscore.ExecuteScheduledBatch{ + Submitter: cantontypes.PARTY(t.submittingParty), + OpId: cantontypes.TEXT(opIDStr), + Calls: calls, + Predecessor: cantontypes.TEXT(predecessorHex), + Salt: cantontypes.TEXT(saltHex), + TargetCids: targetCidsMap, + } + + mcmsContract := mcmscore.MCMS{} + packageID, moduleName, entityName, err := ParseTemplateIDFromString(mcmsContract.GetTemplateID()) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("failed to parse template ID: %w", err) + } + + commandID := uuid.NewString() + req := &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: commandID, + ActAs: []string{t.submittingParty}, + ReadAs: t.mcmsParties, + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{ + Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{ + PackageId: packageID, + ModuleName: moduleName, + EntityName: entityName, + }, + ContractId: contractID, + Choice: "ExecuteScheduledBatch", + ChoiceArgument: ledger.MapToValue(executeArgs), + }, + }, + }}, + }, + } + + resp, err := t.client.SubmitAndWaitForTransaction(ctx, req) + if err != nil { + return types.TransactionResult{}, fmt.Errorf("submit ExecuteScheduledBatch: %w", err) + } + + // Extract new MCMS contract ID from Created event (callers need it for subsequent resolution) + newMCMSContractID := "" + newMCMSTemplateID := "" + transaction := resp.GetTransaction() + for _, ev := range transaction.GetEvents() { + if createdEv := ev.GetCreated(); createdEv != nil { + templateID := FormatTemplateID(createdEv.GetTemplateId()) + if NormalizeTemplateKey(templateID) == MCMSTemplateKey { + newMCMSContractID = createdEv.GetContractId() + newMCMSTemplateID = templateID + + break + } + } + } + if newMCMSContractID == "" { + return types.TransactionResult{}, fmt.Errorf("ExecuteScheduledBatch tx had no Created MCMS event; refusing to continue with old CID=%s", contractID) + } + + return types.TransactionResult{ + Hash: transactionResultHash(transaction, commandID), + ChainFamily: cselectors.FamilyCanton, + RawData: rawDataFromMCMSTx(newMCMSContractID, newMCMSTemplateID, resp), + }, nil +} diff --git a/sdk/canton/timelock_inspector.go b/sdk/canton/timelock_inspector.go new file mode 100644 index 000000000..1165c4c0c --- /dev/null +++ b/sdk/canton/timelock_inspector.go @@ -0,0 +1,251 @@ +package canton + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + + apiv2 "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2" + "github.com/google/uuid" + + mcmsapi "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/api" + mcmscore "github.com/smartcontractkit/chainlink-canton/bindings/generated/latest/mcms/core" + "github.com/smartcontractkit/go-daml/pkg/service/ledger" + cantontypes "github.com/smartcontractkit/go-daml/pkg/types" + + "github.com/smartcontractkit/mcms/sdk" +) + +var _ sdk.TimelockInspector = (*TimelockInspector)(nil) + +// TimelockInspector inspects Canton timelock state via MCMS read-only choices +// (IsOperation, IsOperationPending, IsOperationReady, IsOperationDone, GetMinDelay). +// Role lists (GetProposers, etc.) return "unsupported on Canton" like Aptos. +// address parameters are InstanceAddress hex (Canton); they are resolved to contract ID when exercising. +type TimelockInspector struct { + client apiv2.CommandServiceClient + stateClient apiv2.StateServiceClient + // The party that will be used to submit transactions. + // Should be different from mcmsParties. + submittingParty string + // The parties that own the MCMS deployment. + mcmsParties []string +} + +// NewTimelockInspector creates a TimelockInspector that queries the ledger via the given clients. +func NewTimelockInspector(client apiv2.CommandServiceClient, stateClient apiv2.StateServiceClient, submittingParty string, mcmsParties []string) *TimelockInspector { + return &TimelockInspector{ + client: client, + stateClient: stateClient, + submittingParty: submittingParty, + mcmsParties: mcmsParties, + } +} + +// StateServiceClient returns the state service client for resolution (InstanceAddress to contract ID). +func (t *TimelockInspector) StateServiceClient() apiv2.StateServiceClient { + return t.stateClient +} + +// GetProposers returns the signer addresses for the Proposer role. +func (t *TimelockInspector) GetProposers(ctx context.Context, address string) ([]string, error) { + mcmsContract, err := GetMCMSContract(ctx, t.stateClient, t.mcmsParties, address) + if err != nil { + return nil, fmt.Errorf("failed to get MCMS contract: %w", err) + } + + return extractSignerAddresses(mcmsContract.Proposer.Config.Signers), nil +} + +// GetExecutors is unsupported on Canton: there is no separate executor role. +func (t *TimelockInspector) GetExecutors(_ context.Context, _ string) ([]string, error) { + return nil, errors.New("unsupported on Canton: no separate executor role") +} + +// GetBypassers returns the signer addresses for the Bypasser role. +func (t *TimelockInspector) GetBypassers(ctx context.Context, address string) ([]string, error) { + mcmsContract, err := GetMCMSContract(ctx, t.stateClient, t.mcmsParties, address) + if err != nil { + return nil, fmt.Errorf("failed to get MCMS contract: %w", err) + } + + return extractSignerAddresses(mcmsContract.Bypasser.Config.Signers), nil +} + +// GetCancellers returns the signer addresses for the Canceller role. +func (t *TimelockInspector) GetCancellers(ctx context.Context, address string) ([]string, error) { + mcmsContract, err := GetMCMSContract(ctx, t.stateClient, t.mcmsParties, address) + if err != nil { + return nil, fmt.Errorf("failed to get MCMS contract: %w", err) + } + + return extractSignerAddresses(mcmsContract.Canceller.Config.Signers), nil +} + +// extractSignerAddresses extracts signer addresses from a slice of SignerInfo. +func extractSignerAddresses(signers []mcmsapi.SignerInfo) []string { + result := make([]string, len(signers)) + for i, s := range signers { + result[i] = string(s.SignerAddress) + } + + return result +} + +func (t *TimelockInspector) IsOperation(ctx context.Context, address string, opID [32]byte) (bool, error) { + return t.exerciseBoolChoice(ctx, address, "IsOperation", opID) +} + +func (t *TimelockInspector) IsOperationPending(ctx context.Context, address string, opID [32]byte) (bool, error) { + return t.exerciseBoolChoice(ctx, address, "IsOperationPending", opID) +} + +func (t *TimelockInspector) IsOperationReady(ctx context.Context, address string, opID [32]byte) (bool, error) { + return t.exerciseBoolChoice(ctx, address, "IsOperationReady", opID) +} + +func (t *TimelockInspector) IsOperationDone(ctx context.Context, address string, opID [32]byte) (bool, error) { + return t.exerciseBoolChoice(ctx, address, "IsOperationDone", opID) +} + +func (t *TimelockInspector) GetMinDelay(ctx context.Context, address string) (uint64, error) { + contractID, err := ResolveMCMSContractID(ctx, t.stateClient, t.mcmsParties, address) + if err != nil { + return 0, fmt.Errorf("resolve MCMS contract ID: %w", err) + } + args := mcmscore.GetMinDelay{Submitter: cantontypes.PARTY(t.submittingParty)} + req, err := t.exerciseRequest(contractID, "GetMinDelay", ledger.MapToValue(args)) + if err != nil { + return 0, fmt.Errorf("failed to create exercise request: %w", err) + } + resp, err := t.client.SubmitAndWaitForTransaction(ctx, req) + if err != nil { + return 0, fmt.Errorf("GetMinDelay: %w", err) + } + events := resp.GetTransaction().GetEvents() + if len(events) == 0 { + return 0, fmt.Errorf("GetMinDelay: no events in transaction") + } + ex := events[0].GetExercised() + if ex == nil { + return 0, fmt.Errorf("GetMinDelay: first event is not exercise") + } + // GetMinDelay returns RelTime = record with "microseconds" field + rec := ex.GetExerciseResult().GetRecord() + if rec == nil || len(rec.GetFields()) == 0 { + return 0, fmt.Errorf("GetMinDelay: result is not a record with fields") + } + // first field is "microseconds" (Int64) + val := rec.GetFields()[0].GetValue() + if val == nil { + return 0, fmt.Errorf("GetMinDelay: missing microseconds value") + } + us := val.GetInt64() + if us < 0 { + return 0, fmt.Errorf("GetMinDelay: invalid microseconds %d", us) + } + + return uint64(us / microsecondsPerSecond), nil +} + +func (t *TimelockInspector) exerciseBoolChoice(ctx context.Context, address string, choice string, opID [32]byte) (bool, error) { + contractID, err := ResolveMCMSContractID(ctx, t.stateClient, t.mcmsParties, address) + if err != nil { + return false, fmt.Errorf("resolve MCMS contract ID: %w", err) + } + opIDStr := hex.EncodeToString(opID[:]) + var choiceArg *apiv2.Value + switch choice { + case "IsOperation": + choiceArg = ledger.MapToValue(mcmscore.IsOperation{Submitter: cantontypes.PARTY(t.submittingParty), OpId: cantontypes.TEXT(opIDStr)}) + case "IsOperationPending": + choiceArg = ledger.MapToValue(mcmscore.IsOperationPending{Submitter: cantontypes.PARTY(t.submittingParty), OpId: cantontypes.TEXT(opIDStr)}) + case "IsOperationReady": + choiceArg = ledger.MapToValue(mcmscore.IsOperationReady{Submitter: cantontypes.PARTY(t.submittingParty), OpId: cantontypes.TEXT(opIDStr)}) + case "IsOperationDone": + choiceArg = ledger.MapToValue(mcmscore.IsOperationDone{Submitter: cantontypes.PARTY(t.submittingParty), OpId: cantontypes.TEXT(opIDStr)}) + default: + return false, fmt.Errorf("unknown choice %s", choice) + } + req, err := t.exerciseRequest(contractID, choice, choiceArg) + if err != nil { + return false, fmt.Errorf("failed to create exercise request: %w", err) + } + resp, err := t.client.SubmitAndWaitForTransaction(ctx, req) + if err != nil { + return false, fmt.Errorf("%s: %w", choice, err) + } + events := resp.GetTransaction().GetEvents() + if len(events) == 0 { + return false, fmt.Errorf("%s: no events", choice) + } + ex := events[0].GetExercised() + if ex == nil { + return false, fmt.Errorf("%s: first event is not exercise", choice) + } + + return valueToBool(ex.GetExerciseResult()) +} + +func (t *TimelockInspector) exerciseRequest(contractID, choice string, choiceArg *apiv2.Value) (*apiv2.SubmitAndWaitForTransactionRequest, error) { + // Parse template ID + mcmsContract := mcmscore.MCMS{} + packageID, moduleName, entityName, err := ParseTemplateIDFromString(mcmsContract.GetTemplateID()) + if err != nil { + return nil, fmt.Errorf("failed to parse template ID: %w", err) + } + + return &apiv2.SubmitAndWaitForTransactionRequest{ + Commands: &apiv2.Commands{ + CommandId: uuid.NewString(), + ActAs: []string{t.submittingParty}, + ReadAs: t.mcmsParties, + Commands: []*apiv2.Command{{ + Command: &apiv2.Command_Exercise{ + Exercise: &apiv2.ExerciseCommand{ + TemplateId: &apiv2.Identifier{ + PackageId: packageID, + ModuleName: moduleName, + EntityName: entityName, + }, + ContractId: contractID, + Choice: choice, + ChoiceArgument: choiceArg, + }, + }, + }}, + }, + TransactionFormat: &apiv2.TransactionFormat{ + EventFormat: &apiv2.EventFormat{ + FiltersByParty: map[string]*apiv2.Filters{ + t.submittingParty: {}, + }, + }, + TransactionShape: apiv2.TransactionShape_TRANSACTION_SHAPE_LEDGER_EFFECTS, + }, + }, nil +} + +func valueToBool(v *apiv2.Value) (bool, error) { + if v == nil { + return false, errors.New("nil value") + } + switch s := v.Sum.(type) { + case *apiv2.Value_Bool: + return s.Bool, nil + case *apiv2.Value_Variant: + // Daml Bool is sometimes encoded as variant True | False + if s.Variant != nil { + c := s.Variant.Constructor + if c == "True" { + return true, nil + } + if c == "False" { + return false, nil + } + } + } + + return false, fmt.Errorf("value is not Bool or Bool variant: %T", v.Sum) +} diff --git a/sdk/canton/transaction.go b/sdk/canton/transaction.go new file mode 100644 index 000000000..2411f8f32 --- /dev/null +++ b/sdk/canton/transaction.go @@ -0,0 +1,43 @@ +package canton + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" +) + +func ValidateAdditionalFields(additionalFields json.RawMessage) error { + if len(additionalFields) == 0 { + return errors.New("canton additional fields are required") + } + + var fields AdditionalFields + if err := json.Unmarshal(additionalFields, &fields); err != nil { + return fmt.Errorf("failed to unmarshal Canton additional fields: %w", err) + } + + return fields.Validate() +} + +func (f AdditionalFields) Validate() error { + if f.TargetInstanceAddress != "" && !strings.Contains(f.TargetInstanceAddress, "@") { + return errors.New("targetInstanceAddress must be in instanceId@partyId format") + } + + if f.TargetInstanceAddress != "" && f.FunctionName == "" { + return errors.New("functionName is required when targetInstanceAddress is set") + } + + if f.FunctionName != "" && f.TargetInstanceAddress == "" { + return errors.New("targetInstanceAddress is required when functionName is set") + } + + return nil +} + +// operationDataHex returns hex-encoded wire bytes from tx.Data for Canton hashing and ledger transport. +func operationDataHex(data []byte) string { + return hex.EncodeToString(data) +} diff --git a/sdk/canton/transaction_test.go b/sdk/canton/transaction_test.go new file mode 100644 index 000000000..674c9c59f --- /dev/null +++ b/sdk/canton/transaction_test.go @@ -0,0 +1,96 @@ +package canton + +import ( + "encoding/hex" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateAdditionalFields(t *testing.T) { + t.Parallel() + + validFields, err := json.Marshal(AdditionalFields{ + TargetInstanceAddress: "counter@party::abc", + FunctionName: "Increment", + }) + require.NoError(t, err) + + invalidAddressFields, err := json.Marshal(AdditionalFields{ + TargetInstanceAddress: "invalid-address", + FunctionName: "Increment", + }) + require.NoError(t, err) + + onlyFunctionNameFields, err := json.Marshal(AdditionalFields{ + FunctionName: "Increment", + }) + require.NoError(t, err) + + tests := []struct { + name string + input json.RawMessage + wantErr string + }{ + { + name: "empty additional fields", + input: nil, + wantErr: "canton additional fields are required", + }, + { + name: "valid additional fields", + input: validFields, + }, + { + name: "invalid target instance address", + input: invalidAddressFields, + wantErr: "targetInstanceAddress must be in instanceId@partyId format", + }, + { + name: "function name without target instance address", + input: onlyFunctionNameFields, + wantErr: "targetInstanceAddress is required when functionName is set", + }, + { + name: "invalid JSON", + input: []byte("invalid JSON"), + wantErr: "failed to unmarshal Canton additional fields", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := ValidateAdditionalFields(tt.input) + if tt.wantErr == "" { + require.NoError(t, err) + return + } + + require.ErrorContains(t, err, tt.wantErr) + }) + } +} + +func TestOperationDataHex(t *testing.T) { + t.Parallel() + + t.Run("encodes bytes to hex", func(t *testing.T) { + t.Parallel() + require.Equal(t, "deadbeef", operationDataHex([]byte{0xde, 0xad, 0xbe, 0xef})) + }) + + t.Run("empty data returns empty string", func(t *testing.T) { + t.Parallel() + require.Empty(t, operationDataHex(nil)) + }) + + t.Run("round trip", func(t *testing.T) { + t.Parallel() + raw, err := hex.DecodeString("0000000000000005") + require.NoError(t, err) + require.Equal(t, "0000000000000005", operationDataHex(raw)) + }) +} diff --git a/taskfiles/test/Taskfile.yml b/taskfiles/test/Taskfile.yml index d7c65d9a3..89f949bb2 100644 --- a/taskfiles/test/Taskfile.yml +++ b/taskfiles/test/Taskfile.yml @@ -64,6 +64,13 @@ tasks: cmds: - CTF_CONFIGS=$CTF_CONFIGS go test -v -tags=e2e -test.run TestTONSuite {{.CLI_ARGS}} ./e2e/tests... + e2e:canton: + desc: "Run Canton e2e tests" + env: + CTF_CONFIGS: '{{ .CTF_CONFIGS | default "../config.canton.toml" }}' + cmds: + - CTF_CONFIGS=$CTF_CONFIGS go test -v -tags=e2e -test.run TestCantonSuite {{.CLI_ARGS}} ./e2e/tests... + coverage: desc: "Run unit test suite with coverage" cmds: diff --git a/types/chain_selector.go b/types/chain_selector.go index 0851e6350..5dd4d0a89 100644 --- a/types/chain_selector.go +++ b/types/chain_selector.go @@ -32,6 +32,7 @@ var supportedFamilies = []string{ chainsel.FamilySui, chainsel.FamilyTon, chainsel.FamilyStellar, + chainsel.FamilyCanton, } // GetChainSelectorFamily returns the family of the chain selector. diff --git a/validation.go b/validation.go index 4901edf75..06dc8f426 100644 --- a/validation.go +++ b/validation.go @@ -9,6 +9,7 @@ import ( "github.com/smartcontractkit/mcms/types" "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/sdk/canton" "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" @@ -36,6 +37,9 @@ func validateAdditionalFields(additionalFields json.RawMessage, csel types.Chain case chainsel.FamilyTon: return ton.ValidateAdditionalFields(additionalFields) + + case chainsel.FamilyCanton: + return canton.ValidateAdditionalFields(additionalFields) } return nil @@ -57,6 +61,8 @@ func validateChainMetadata(metadata types.ChainMetadata, csel types.ChainSelecto return nil case chainsel.FamilySui: return sui.ValidateChainMetadata(metadata) + case chainsel.FamilyCanton: + return canton.ValidateChainMetadata(metadata) case chainsel.FamilyTon: // TODO (ton): do we need special chain metadata for TON? // Yes! We could attach MCMS -> Timelock value here which is now hardcoded default in timelock converter diff --git a/validation_test.go b/validation_test.go index b2e164f9a..8717a4573 100644 --- a/validation_test.go +++ b/validation_test.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/mcms/internal/testutils/chaintest" "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/sdk/canton" "github.com/smartcontractkit/mcms/sdk/evm" "github.com/smartcontractkit/mcms/sdk/solana" "github.com/smartcontractkit/mcms/sdk/sui" @@ -186,6 +187,18 @@ func TestValidateAdditionalFields(t *testing.T) { invalidSuiFieldsJSON, err := json.Marshal(invalidSuiFields) require.NoError(t, err) + validCantonFields, err := json.Marshal(canton.AdditionalFields{ + TargetInstanceAddress: "counter@party::abc", + FunctionName: "Increment", + }) + require.NoError(t, err) + + invalidCantonFields, err := json.Marshal(canton.AdditionalFields{ + TargetInstanceAddress: "invalid-address", + FunctionName: "Increment", + }) + require.NoError(t, err) + tests := []struct { name string operation types.Operation @@ -262,6 +275,36 @@ func TestValidateAdditionalFields(t *testing.T) { }, expectedErr: errors.New("module name length must be between 1 and 64 characters"), }, + { + name: "valid Canton fields", + operation: types.Operation{ + ChainSelector: types.ChainSelector(chainsel.CANTON_TESTNET.Selector), + Transaction: types.Transaction{ + AdditionalFields: validCantonFields, + }, + }, + expectedErr: nil, + }, + { + name: "invalid Canton fields", + operation: types.Operation{ + ChainSelector: types.ChainSelector(chainsel.CANTON_TESTNET.Selector), + Transaction: types.Transaction{ + AdditionalFields: invalidCantonFields, + }, + }, + expectedErr: errors.New("targetInstanceAddress must be in instanceId@partyId format"), + }, + { + name: "empty Canton additional fields", + operation: types.Operation{ + ChainSelector: types.ChainSelector(chainsel.CANTON_TESTNET.Selector), + Transaction: types.Transaction{ + AdditionalFields: nil, + }, + }, + expectedErr: errors.New("canton additional fields are required"), + }, { name: "unknown chain family", operation: types.Operation{ @@ -302,6 +345,16 @@ func TestValidateAdditionalFields(t *testing.T) { }, expectedErr: errors.New("failed to unmarshal Sui additional fields"), }, + { + name: "invalid JSON for Canton fields", + operation: types.Operation{ + ChainSelector: types.ChainSelector(chainsel.CANTON_TESTNET.Selector), + Transaction: types.Transaction{ + AdditionalFields: []byte("invalid JSON"), + }, + }, + expectedErr: errors.New("failed to unmarshal Canton additional fields"), + }, } for _, tt := range tests {