diff --git a/ccv/chains/evm/deployment/v1_7_0/adapters/init.go b/ccv/chains/evm/deployment/v1_7_0/adapters/init.go index 459991c796..5a3c9a8a50 100644 --- a/ccv/chains/evm/deployment/v1_7_0/adapters/init.go +++ b/ccv/chains/evm/deployment/v1_7_0/adapters/init.go @@ -9,6 +9,7 @@ import ( adapters1_6 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/adapters" "github.com/smartcontractkit/chainlink-ccip/deployment/deploy" + ccvadapters "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" ) func init() { @@ -22,4 +23,6 @@ func init() { laneMigratorReg := deploy.GetLaneMigratorRegistry() laneMigratorReg.RegisterRampUpdater(chainsel.FamilyEVM, semver.MustParse("1.7.0"), &LaneMigrator{}) laneMigratorReg.RegisterRouterUpdater(chainsel.FamilyEVM, semver.MustParse("1.2.0"), &adapters1_2.RouterUpdater{}) + + ccvadapters.GetTokenVerifierConfigRegistry().Register(chainsel.FamilyEVM, &EVMTokenVerifierConfigAdapter{}) } diff --git a/ccv/chains/evm/deployment/v1_7_0/adapters/token_verifier_config_adapter.go b/ccv/chains/evm/deployment/v1_7_0/adapters/token_verifier_config_adapter.go new file mode 100644 index 0000000000..7bc2b59d68 --- /dev/null +++ b/ccv/chains/evm/deployment/v1_7_0/adapters/token_verifier_config_adapter.go @@ -0,0 +1,78 @@ +package adapters + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/cctp_verifier" + "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/lombard_verifier" + onrampoperations "github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm/deployment/v1_7_0/operations/onramp" + "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_6_0/operations/rmn_remote" + dsutil "github.com/smartcontractkit/chainlink-ccip/deployment/utils/datastore" + ccvadapters "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" +) + +type EVMTokenVerifierConfigAdapter struct{} + +var _ ccvadapters.TokenVerifierConfigAdapter = (*EVMTokenVerifierConfigAdapter)(nil) + +func (a *EVMTokenVerifierConfigAdapter) ResolveTokenVerifierAddresses( + ds datastore.DataStore, + chainSelector uint64, + cctpQualifier string, + lombardQualifier string, +) (*ccvadapters.TokenVerifierChainAddresses, error) { + toAddress := func(ref datastore.AddressRef) (string, error) { return ref.Address, nil } + + onRampAddr, err := dsutil.FindAndFormatRef(ds, datastore.AddressRef{ + Type: datastore.ContractType(onrampoperations.ContractType), + }, chainSelector, toAddress) + if err != nil { + return nil, fmt.Errorf("failed to get on ramp address for chain %d: %w", chainSelector, err) + } + + rmnRemoteAddr, err := dsutil.FindAndFormatRef(ds, datastore.AddressRef{ + Type: datastore.ContractType(rmn_remote.ContractType), + }, chainSelector, toAddress) + if err != nil { + return nil, fmt.Errorf("failed to get rmn remote address for chain %d: %w", chainSelector, err) + } + + result := &ccvadapters.TokenVerifierChainAddresses{ + OnRampAddress: onRampAddr, + RMNRemoteAddress: rmnRemoteAddr, + } + + cctpVerifierAddr, cctpVerifierErr := dsutil.FindAndFormatRef(ds, datastore.AddressRef{ + Type: datastore.ContractType(cctp_verifier.ContractType), + Qualifier: cctpQualifier, + }, chainSelector, toAddress) + + cctpResolverAddr, cctpResolverErr := dsutil.FindAndFormatRef(ds, datastore.AddressRef{ + Type: datastore.ContractType(cctp_verifier.ResolverType), + Qualifier: cctpQualifier, + }, chainSelector, toAddress) + + if (cctpVerifierErr == nil) != (cctpResolverErr == nil) { + return nil, fmt.Errorf( + "chain %d: cctp verifier and resolver must both exist or both be absent (verifier error: %v, resolver error: %v)", + chainSelector, cctpVerifierErr, cctpResolverErr, + ) + } + + if cctpVerifierErr == nil { + result.CCTPVerifierAddress = cctpVerifierAddr + result.CCTPVerifierResolverAddress = cctpResolverAddr + } + + lombardResolverAddr, lombardResolverErr := dsutil.FindAndFormatRef(ds, datastore.AddressRef{ + Type: datastore.ContractType(lombard_verifier.ResolverType), + Qualifier: lombardQualifier, + }, chainSelector, toAddress) + + if lombardResolverErr == nil { + result.LombardVerifierResolverAddress = lombardResolverAddr + } + + return result, nil +} diff --git a/deployment/v1_7_0/adapters/token_verifier_config.go b/deployment/v1_7_0/adapters/token_verifier_config.go new file mode 100644 index 0000000000..4dc4969707 --- /dev/null +++ b/deployment/v1_7_0/adapters/token_verifier_config.go @@ -0,0 +1,79 @@ +package adapters + +import ( + "fmt" + "sync" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" +) + +type TokenVerifierChainAddresses struct { + OnRampAddress string + RMNRemoteAddress string + CCTPVerifierAddress string + CCTPVerifierResolverAddress string + LombardVerifierResolverAddress string +} + +type TokenVerifierConfigAdapter interface { + ResolveTokenVerifierAddresses( + ds datastore.DataStore, + chainSelector uint64, + cctpQualifier string, + lombardQualifier string, + ) (*TokenVerifierChainAddresses, error) +} + +type TokenVerifierConfigRegistry struct { + mu sync.Mutex + adapters map[string]TokenVerifierConfigAdapter +} + +var ( + singletonTokenVerifierConfigRegistry *TokenVerifierConfigRegistry + tokenVerifierConfigRegistryOnce sync.Once +) + +func NewTokenVerifierConfigRegistry() *TokenVerifierConfigRegistry { + return &TokenVerifierConfigRegistry{ + adapters: make(map[string]TokenVerifierConfigAdapter), + } +} + +func GetTokenVerifierConfigRegistry() *TokenVerifierConfigRegistry { + tokenVerifierConfigRegistryOnce.Do(func() { + singletonTokenVerifierConfigRegistry = NewTokenVerifierConfigRegistry() + }) + return singletonTokenVerifierConfigRegistry +} + +func (r *TokenVerifierConfigRegistry) Register(family string, a TokenVerifierConfigAdapter) { + if a == nil { + return + } + r.mu.Lock() + defer r.mu.Unlock() + if _, exists := r.adapters[family]; !exists { + r.adapters[family] = a + } +} + +func (r *TokenVerifierConfigRegistry) Get(family string) (TokenVerifierConfigAdapter, bool) { + r.mu.Lock() + defer r.mu.Unlock() + a, ok := r.adapters[family] + return a, ok +} + +func (r *TokenVerifierConfigRegistry) GetByChain(chainSelector uint64) (TokenVerifierConfigAdapter, error) { + family, err := chainsel.GetSelectorFamily(chainSelector) + if err != nil { + return nil, fmt.Errorf("failed to get chain family for selector %d: %w", chainSelector, err) + } + adapter, ok := r.Get(family) + if !ok { + return nil, fmt.Errorf("no token verifier config adapter registered for chain family %q", family) + } + return adapter, nil +} diff --git a/deployment/v1_7_0/changesets/generate_token_verifier_config.go b/deployment/v1_7_0/changesets/generate_token_verifier_config.go new file mode 100644 index 0000000000..86b8fc3c13 --- /dev/null +++ b/deployment/v1_7_0/changesets/generate_token_verifier_config.go @@ -0,0 +1,250 @@ +package changesets + +import ( + "encoding/hex" + "fmt" + "slices" + "strconv" + "time" + + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/offchain" + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/offchain/shared" +) + +const ( + TestnetCCTPAttestationAPI = "https://iris-api-sandbox.circle.com" + TestnetLombardAttestationAPI = "https://gastald-testnet.prod.lombard.finance/api/" + MainnetCCTPAttestationAPI = "https://iris-api.circle.com" + MainnetLombardAttestationAPI = "https://mainnet.prod.lombard.finance/api/" +) + +var ( + // bytes4(keccak256("CCTPVerifier 1.7.0")) = 0x8e1d1a9d + DefaultCCTPVerifierVersion = mustDecodeHex("8e1d1a9d") + // bytes4(keccak256("LombardVerifier 1.7.0")) = 0xf0f3a135 + DefaultLombardVerifierVersion = mustDecodeHex("f0f3a135") +) + +func mustDecodeHex(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(fmt.Sprintf("failed to decode hex %q: %v", s, err)) + } + return b +} + +type LombardConfigInput struct { + Qualifier string + VerifierID string + VerifierVersion []byte + AttestationAPI string + AttestationAPITimeout time.Duration + AttestationAPIInterval time.Duration + AttestationAPIBatchSize int +} + +type CCTPConfigInput struct { + Qualifier string + VerifierID string + VerifierVersion []byte + AttestationAPI string + AttestationAPITimeout time.Duration + AttestationAPIInterval time.Duration + AttestationAPICooldown time.Duration +} + +type GenerateTokenVerifierConfigInput struct { + ServiceIdentifier string + ChainSelectors []uint64 + PyroscopeURL string + Monitoring shared.MonitoringInput + Lombard LombardConfigInput + CCTP CCTPConfigInput +} + +func GenerateTokenVerifierConfig(registry *adapters.TokenVerifierConfigRegistry) deployment.ChangeSetV2[GenerateTokenVerifierConfigInput] { + validate := func(e deployment.Environment, cfg GenerateTokenVerifierConfigInput) error { + if cfg.ServiceIdentifier == "" { + return fmt.Errorf("service identifier is required") + } + envSelectors := e.BlockChains.ListChainSelectors() + for _, s := range cfg.ChainSelectors { + if !slices.Contains(envSelectors, s) { + return fmt.Errorf("selector %d is not available in environment", s) + } + } + return nil + } + + apply := func(e deployment.Environment, cfg GenerateTokenVerifierConfigInput) (deployment.ChangesetOutput, error) { + selectors := cfg.ChainSelectors + if len(selectors) == 0 { + selectors = e.BlockChains.ListChainSelectors() + } + + isProd := shared.IsProductionEnvironment(e.Name) + lombardCfg := applyLombardDefaults(cfg.Lombard, isProd) + cctpCfg := applyCCTPDefaults(cfg.CCTP, isProd) + + onRampAddresses := make(map[string]string) + rmnRemoteAddresses := make(map[string]string) + cctpVerifierAddresses := make(map[string]string) + cctpVerifierResolverAddresses := make(map[string]string) + lombardVerifierResolverAddresses := make(map[string]string) + + for _, sel := range selectors { + adapter, err := registry.GetByChain(sel) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + addrs, err := adapter.ResolveTokenVerifierAddresses( + e.DataStore, sel, cctpCfg.Qualifier, lombardCfg.Qualifier, + ) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to resolve token verifier addresses for chain %d: %w", sel, err) + } + + chainSelectorStr := strconv.FormatUint(sel, 10) + onRampAddresses[chainSelectorStr] = addrs.OnRampAddress + rmnRemoteAddresses[chainSelectorStr] = addrs.RMNRemoteAddress + + if addrs.CCTPVerifierAddress != "" { + cctpVerifierAddresses[chainSelectorStr] = addrs.CCTPVerifierAddress + cctpVerifierResolverAddresses[chainSelectorStr] = addrs.CCTPVerifierResolverAddress + } + if addrs.LombardVerifierResolverAddress != "" { + lombardVerifierResolverAddresses[chainSelectorStr] = addrs.LombardVerifierResolverAddress + } + } + + config := &offchain.TokenVerifierGeneratedConfig{ + PyroscopeURL: cfg.PyroscopeURL, + OnRampAddresses: onRampAddresses, + RMNRemoteAddresses: rmnRemoteAddresses, + TokenVerifiers: []offchain.TokenVerifierEntry{}, + Monitoring: offchain.TokenVerifierMonitoringConfig{ + Enabled: cfg.Monitoring.Enabled, + Type: cfg.Monitoring.Type, + Beholder: offchain.TokenVerifierBeholderConfig{ + InsecureConnection: cfg.Monitoring.Beholder.InsecureConnection, + CACertFile: cfg.Monitoring.Beholder.CACertFile, + OtelExporterGRPCEndpoint: cfg.Monitoring.Beholder.OtelExporterGRPCEndpoint, + OtelExporterHTTPEndpoint: cfg.Monitoring.Beholder.OtelExporterHTTPEndpoint, + LogStreamingEnabled: cfg.Monitoring.Beholder.LogStreamingEnabled, + MetricReaderInterval: cfg.Monitoring.Beholder.MetricReaderInterval, + TraceSampleRatio: cfg.Monitoring.Beholder.TraceSampleRatio, + TraceBatchTimeout: cfg.Monitoring.Beholder.TraceBatchTimeout, + }, + }, + } + + if len(cctpVerifierAddresses) > 0 { + cctpVerifierID := cctpCfg.VerifierID + if cctpVerifierID == "" { + cctpVerifierID = fmt.Sprintf("cctp-%s", cctpCfg.Qualifier) + } + config.TokenVerifiers = append(config.TokenVerifiers, offchain.TokenVerifierEntry{ + VerifierID: cctpVerifierID, + Type: "cctp", + Version: "2.0", + CCTP: &offchain.CCTPVerifierConfig{ + AttestationAPI: cctpCfg.AttestationAPI, + AttestationAPITimeout: cctpCfg.AttestationAPITimeout, + AttestationAPIInterval: cctpCfg.AttestationAPIInterval, + AttestationAPICooldown: cctpCfg.AttestationAPICooldown, + VerifierVersion: cctpCfg.VerifierVersion, + Verifiers: cctpVerifierAddresses, + VerifierResolvers: cctpVerifierResolverAddresses, + }, + }) + } + + if len(lombardVerifierResolverAddresses) > 0 { + lombardVerifierID := lombardCfg.VerifierID + if lombardVerifierID == "" { + lombardVerifierID = fmt.Sprintf("lombard-%s", lombardCfg.Qualifier) + } + config.TokenVerifiers = append(config.TokenVerifiers, offchain.TokenVerifierEntry{ + VerifierID: lombardVerifierID, + Type: "lombard", + Version: "1.0", + Lombard: &offchain.LombardVerifierConfig{ + AttestationAPI: lombardCfg.AttestationAPI, + AttestationAPITimeout: lombardCfg.AttestationAPITimeout, + AttestationAPIInterval: lombardCfg.AttestationAPIInterval, + AttestationAPIBatchSize: lombardCfg.AttestationAPIBatchSize, + VerifierVersion: lombardCfg.VerifierVersion, + VerifierResolvers: lombardVerifierResolverAddresses, + }, + }) + } + + outputDS := datastore.NewMemoryDataStore() + if e.DataStore != nil { + if err := outputDS.Merge(e.DataStore); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to merge existing datastore: %w", err) + } + } + + if err := offchain.SaveTokenVerifierConfig(outputDS, cfg.ServiceIdentifier, config); err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to save token verifier config: %w", err) + } + + return deployment.ChangesetOutput{ + DataStore: outputDS, + }, nil + } + + return deployment.CreateChangeSet(apply, validate) +} + +func applyLombardDefaults(cfg LombardConfigInput, isProd bool) LombardConfigInput { + if cfg.AttestationAPI == "" { + if isProd { + cfg.AttestationAPI = MainnetLombardAttestationAPI + } else { + cfg.AttestationAPI = TestnetLombardAttestationAPI + } + } + if cfg.AttestationAPITimeout == 0 { + cfg.AttestationAPITimeout = 1 * time.Second + } + if cfg.AttestationAPIInterval == 0 { + cfg.AttestationAPIInterval = 100 * time.Millisecond + } + if cfg.AttestationAPIBatchSize == 0 { + cfg.AttestationAPIBatchSize = 20 + } + if len(cfg.VerifierVersion) == 0 { + cfg.VerifierVersion = DefaultLombardVerifierVersion + } + return cfg +} + +func applyCCTPDefaults(cfg CCTPConfigInput, isProd bool) CCTPConfigInput { + if cfg.AttestationAPI == "" { + if isProd { + cfg.AttestationAPI = MainnetCCTPAttestationAPI + } else { + cfg.AttestationAPI = TestnetCCTPAttestationAPI + } + } + if cfg.AttestationAPITimeout == 0 { + cfg.AttestationAPITimeout = 1 * time.Second + } + if cfg.AttestationAPIInterval == 0 { + cfg.AttestationAPIInterval = 100 * time.Millisecond + } + if cfg.AttestationAPICooldown == 0 { + cfg.AttestationAPICooldown = 5 * time.Minute + } + if len(cfg.VerifierVersion) == 0 { + cfg.VerifierVersion = DefaultCCTPVerifierVersion + } + return cfg +} diff --git a/deployment/v1_7_0/changesets/generate_token_verifier_config_test.go b/deployment/v1_7_0/changesets/generate_token_verifier_config_test.go new file mode 100644 index 0000000000..f4e1766768 --- /dev/null +++ b/deployment/v1_7_0/changesets/generate_token_verifier_config_test.go @@ -0,0 +1,511 @@ +package changesets_test + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/adapters" + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/changesets" + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/offchain" + "github.com/smartcontractkit/chainlink-ccip/deployment/v1_7_0/offchain/shared" +) + +var _ adapters.TokenVerifierConfigAdapter = (*mockTokenVerifierConfigAdapter)(nil) + +type mockTokenVerifierConfigAdapter struct { + addressesByChain map[uint64]*adapters.TokenVerifierChainAddresses + err error +} + +func (m *mockTokenVerifierConfigAdapter) ResolveTokenVerifierAddresses( + _ datastore.DataStore, sel uint64, _ string, _ string, +) (*adapters.TokenVerifierChainAddresses, error) { + if m.err != nil { + return nil, m.err + } + addrs, ok := m.addressesByChain[sel] + if !ok { + return nil, fmt.Errorf("no addresses for chain %d", sel) + } + return addrs, nil +} + +func TestGenerateTokenVerifierConfig(t *testing.T) { + sel1 := chainsel.TEST_90000001.Selector + sel2 := chainsel.TEST_90000002.Selector + + tests := []struct { + name string + mock *mockTokenVerifierConfigAdapter + input changesets.GenerateTokenVerifierConfigInput + selectors []uint64 + envName string + wantErr string + validateOnErr bool + expectedVerifiers int + }{ + { + name: "validates service identifier is required", + mock: &mockTokenVerifierConfigAdapter{}, + input: changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "", + }, + selectors: []uint64{sel1}, + wantErr: "service identifier is required", + validateOnErr: true, + }, + { + name: "validates chain selectors exist in environment", + mock: &mockTokenVerifierConfigAdapter{}, + input: changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "test", + ChainSelectors: []uint64{9999}, + }, + selectors: []uint64{sel1}, + wantErr: "selector 9999 is not available in environment", + validateOnErr: true, + }, + { + name: "returns error when adapter fails", + mock: &mockTokenVerifierConfigAdapter{ + err: fmt.Errorf("adapter failure"), + }, + input: changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "test", + ChainSelectors: []uint64{sel1}, + }, + selectors: []uint64{sel1}, + wantErr: "adapter failure", + }, + { + name: "generates config with both CCTP and Lombard verifiers", + mock: &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + CCTPVerifierAddress: "0xCCTP1", + CCTPVerifierResolverAddress: "0xCCTPResolver1", + LombardVerifierResolverAddress: "0xLombardResolver1", + }, + sel2: { + OnRampAddress: "0xOnRamp2", + RMNRemoteAddress: "0xRMN2", + CCTPVerifierAddress: "0xCCTP2", + CCTPVerifierResolverAddress: "0xCCTPResolver2", + LombardVerifierResolverAddress: "0xLombardResolver2", + }, + }, + }, + input: changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "token-verifier", + ChainSelectors: []uint64{sel1, sel2}, + PyroscopeURL: "http://pyroscope:4040", + CCTP: changesets.CCTPConfigInput{ + Qualifier: "default", + VerifierID: "CCTPVerifier", + }, + Lombard: changesets.LombardConfigInput{ + Qualifier: "default", + VerifierID: "LombardVerifier", + }, + Monitoring: shared.MonitoringInput{ + Enabled: true, + Type: "beholder", + }, + }, + selectors: []uint64{sel1, sel2}, + expectedVerifiers: 2, + }, + { + name: "generates config with only CCTP verifier", + mock: &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + CCTPVerifierAddress: "0xCCTP1", + CCTPVerifierResolverAddress: "0xCCTPResolver1", + }, + }, + }, + input: changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "token-verifier", + ChainSelectors: []uint64{sel1}, + CCTP: changesets.CCTPConfigInput{ + Qualifier: "default", + VerifierID: "CCTPVerifier", + }, + }, + selectors: []uint64{sel1}, + expectedVerifiers: 1, + }, + { + name: "generates config with only Lombard verifier", + mock: &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + LombardVerifierResolverAddress: "0xLombardResolver1", + }, + }, + }, + input: changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "token-verifier", + ChainSelectors: []uint64{sel1}, + Lombard: changesets.LombardConfigInput{ + Qualifier: "default", + VerifierID: "LombardVerifier", + }, + }, + selectors: []uint64{sel1}, + expectedVerifiers: 1, + }, + { + name: "generates config with no token verifiers", + mock: &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + }, + }, + }, + input: changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "token-verifier", + ChainSelectors: []uint64{sel1}, + }, + selectors: []uint64{sel1}, + expectedVerifiers: 0, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + registry := adapters.NewTokenVerifierConfigRegistry() + registry.Register(chainsel.FamilyEVM, tc.mock) + + ds := datastore.NewMemoryDataStore() + env := deployment.Environment{ + Name: tc.envName, + DataStore: ds.Seal(), + BlockChains: newTestBlockChains(tc.selectors), + } + + cs := changesets.GenerateTokenVerifierConfig(registry) + + if tc.validateOnErr { + err := cs.VerifyPreconditions(env, tc.input) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + return + } + + output, err := cs.Apply(env, tc.input) + if tc.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + return + } + + require.NoError(t, err) + require.NotNil(t, output.DataStore) + + cfg, err := offchain.GetTokenVerifierConfig(output.DataStore.Seal(), tc.input.ServiceIdentifier) + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.Equal(t, tc.input.PyroscopeURL, cfg.PyroscopeURL) + assert.Len(t, cfg.TokenVerifiers, tc.expectedVerifiers) + + for _, sel := range tc.input.ChainSelectors { + selStr := strconv.FormatUint(sel, 10) + expectedAddrs, ok := tc.mock.addressesByChain[sel] + require.True(t, ok, "test setup: missing addresses for chain %d", sel) + + assert.Equal(t, expectedAddrs.OnRampAddress, cfg.OnRampAddresses[selStr]) + assert.Equal(t, expectedAddrs.RMNRemoteAddress, cfg.RMNRemoteAddresses[selStr]) + } + }) + } + + t.Run("both CCTP and Lombard verifiers have correct structure", func(t *testing.T) { + sel1Str := strconv.FormatUint(sel1, 10) + sel2Str := strconv.FormatUint(sel2, 10) + + mock := &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + CCTPVerifierAddress: "0xCCTP1", + CCTPVerifierResolverAddress: "0xCCTPResolver1", + LombardVerifierResolverAddress: "0xLombardResolver1", + }, + sel2: { + OnRampAddress: "0xOnRamp2", + RMNRemoteAddress: "0xRMN2", + CCTPVerifierAddress: "0xCCTP2", + CCTPVerifierResolverAddress: "0xCCTPResolver2", + LombardVerifierResolverAddress: "0xLombardResolver2", + }, + }, + } + + registry := adapters.NewTokenVerifierConfigRegistry() + registry.Register(chainsel.FamilyEVM, mock) + + ds := datastore.NewMemoryDataStore() + env := deployment.Environment{ + DataStore: ds.Seal(), + BlockChains: newTestBlockChains([]uint64{sel1, sel2}), + } + + cs := changesets.GenerateTokenVerifierConfig(registry) + output, err := cs.Apply(env, changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "token-verifier", + ChainSelectors: []uint64{sel1, sel2}, + PyroscopeURL: "http://pyroscope:4040", + CCTP: changesets.CCTPConfigInput{ + Qualifier: "default", + VerifierID: "CCTPVerifier", + AttestationAPI: "https://cctp.example.com", + VerifierVersion: []byte{0x8e, 0x1d, 0x1a, 0x9d}, + }, + Lombard: changesets.LombardConfigInput{ + Qualifier: "default", + VerifierID: "LombardVerifier", + AttestationAPI: "https://lombard.example.com", + VerifierVersion: []byte{0xf0, 0xf3, 0xa1, 0x35}, + }, + Monitoring: shared.MonitoringInput{ + Enabled: true, + Type: "beholder", + Beholder: shared.BeholderInput{ + InsecureConnection: true, + }, + }, + }) + require.NoError(t, err) + + cfg, err := offchain.GetTokenVerifierConfig(output.DataStore.Seal(), "token-verifier") + require.NoError(t, err) + + assert.Equal(t, "http://pyroscope:4040", cfg.PyroscopeURL) + assert.Equal(t, "0xOnRamp1", cfg.OnRampAddresses[sel1Str]) + assert.Equal(t, "0xOnRamp2", cfg.OnRampAddresses[sel2Str]) + assert.Equal(t, "0xRMN1", cfg.RMNRemoteAddresses[sel1Str]) + assert.Equal(t, "0xRMN2", cfg.RMNRemoteAddresses[sel2Str]) + assert.True(t, cfg.Monitoring.Enabled) + assert.Equal(t, "beholder", cfg.Monitoring.Type) + assert.True(t, cfg.Monitoring.Beholder.InsecureConnection) + + require.Len(t, cfg.TokenVerifiers, 2) + + cctpEntry := cfg.TokenVerifiers[0] + assert.Equal(t, "CCTPVerifier", cctpEntry.VerifierID) + assert.Equal(t, "cctp", cctpEntry.Type) + assert.Equal(t, "2.0", cctpEntry.Version) + require.NotNil(t, cctpEntry.CCTP) + assert.Equal(t, "https://cctp.example.com", cctpEntry.CCTP.AttestationAPI) + assert.Equal(t, []byte{0x8e, 0x1d, 0x1a, 0x9d}, cctpEntry.CCTP.VerifierVersion) + assert.Equal(t, "0xCCTP1", cctpEntry.CCTP.Verifiers[sel1Str]) + assert.Equal(t, "0xCCTP2", cctpEntry.CCTP.Verifiers[sel2Str]) + assert.Equal(t, "0xCCTPResolver1", cctpEntry.CCTP.VerifierResolvers[sel1Str]) + assert.Equal(t, "0xCCTPResolver2", cctpEntry.CCTP.VerifierResolvers[sel2Str]) + assert.Equal(t, 1*time.Second, cctpEntry.CCTP.AttestationAPITimeout) + assert.Equal(t, 100*time.Millisecond, cctpEntry.CCTP.AttestationAPIInterval) + assert.Equal(t, 5*time.Minute, cctpEntry.CCTP.AttestationAPICooldown) + + lombardEntry := cfg.TokenVerifiers[1] + assert.Equal(t, "LombardVerifier", lombardEntry.VerifierID) + assert.Equal(t, "lombard", lombardEntry.Type) + assert.Equal(t, "1.0", lombardEntry.Version) + require.NotNil(t, lombardEntry.Lombard) + assert.Equal(t, "https://lombard.example.com", lombardEntry.Lombard.AttestationAPI) + assert.Equal(t, []byte{0xf0, 0xf3, 0xa1, 0x35}, lombardEntry.Lombard.VerifierVersion) + assert.Equal(t, "0xLombardResolver1", lombardEntry.Lombard.VerifierResolvers[sel1Str]) + assert.Equal(t, "0xLombardResolver2", lombardEntry.Lombard.VerifierResolvers[sel2Str]) + assert.Equal(t, 1*time.Second, lombardEntry.Lombard.AttestationAPITimeout) + assert.Equal(t, 100*time.Millisecond, lombardEntry.Lombard.AttestationAPIInterval) + assert.Equal(t, 20, lombardEntry.Lombard.AttestationAPIBatchSize) + }) + + t.Run("applies testnet defaults when attestation API not specified", func(t *testing.T) { + mock := &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + CCTPVerifierAddress: "0xCCTP1", + CCTPVerifierResolverAddress: "0xCCTPResolver1", + LombardVerifierResolverAddress: "0xLombardResolver1", + }, + }, + } + + registry := adapters.NewTokenVerifierConfigRegistry() + registry.Register(chainsel.FamilyEVM, mock) + + env := deployment.Environment{ + Name: "testnet", + DataStore: datastore.NewMemoryDataStore().Seal(), + BlockChains: newTestBlockChains([]uint64{sel1}), + } + + cs := changesets.GenerateTokenVerifierConfig(registry) + output, err := cs.Apply(env, changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "test", + ChainSelectors: []uint64{sel1}, + CCTP: changesets.CCTPConfigInput{Qualifier: "default", VerifierID: "CCTP"}, + Lombard: changesets.LombardConfigInput{Qualifier: "default", VerifierID: "Lombard"}, + }) + require.NoError(t, err) + + cfg, err := offchain.GetTokenVerifierConfig(output.DataStore.Seal(), "test") + require.NoError(t, err) + + require.Len(t, cfg.TokenVerifiers, 2) + assert.Equal(t, changesets.TestnetCCTPAttestationAPI, cfg.TokenVerifiers[0].CCTP.AttestationAPI) + assert.Equal(t, changesets.TestnetLombardAttestationAPI, cfg.TokenVerifiers[1].Lombard.AttestationAPI) + }) + + t.Run("applies mainnet defaults when environment is mainnet", func(t *testing.T) { + mock := &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + CCTPVerifierAddress: "0xCCTP1", + CCTPVerifierResolverAddress: "0xCCTPResolver1", + LombardVerifierResolverAddress: "0xLombardResolver1", + }, + }, + } + + registry := adapters.NewTokenVerifierConfigRegistry() + registry.Register(chainsel.FamilyEVM, mock) + + env := deployment.Environment{ + Name: "mainnet", + DataStore: datastore.NewMemoryDataStore().Seal(), + BlockChains: newTestBlockChains([]uint64{sel1}), + } + + cs := changesets.GenerateTokenVerifierConfig(registry) + output, err := cs.Apply(env, changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "test", + ChainSelectors: []uint64{sel1}, + CCTP: changesets.CCTPConfigInput{Qualifier: "default", VerifierID: "CCTP"}, + Lombard: changesets.LombardConfigInput{Qualifier: "default", VerifierID: "Lombard"}, + }) + require.NoError(t, err) + + cfg, err := offchain.GetTokenVerifierConfig(output.DataStore.Seal(), "test") + require.NoError(t, err) + + require.Len(t, cfg.TokenVerifiers, 2) + assert.Equal(t, changesets.MainnetCCTPAttestationAPI, cfg.TokenVerifiers[0].CCTP.AttestationAPI) + assert.Equal(t, changesets.MainnetLombardAttestationAPI, cfg.TokenVerifiers[1].Lombard.AttestationAPI) + }) + + t.Run("missing adapter for chain family returns error", func(t *testing.T) { + registry := adapters.NewTokenVerifierConfigRegistry() + + env := deployment.Environment{ + DataStore: datastore.NewMemoryDataStore().Seal(), + BlockChains: newTestBlockChains([]uint64{sel1}), + } + + cs := changesets.GenerateTokenVerifierConfig(registry) + _, err := cs.Apply(env, changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "test", + ChainSelectors: []uint64{sel1}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "no token verifier config adapter registered") + }) + + t.Run("default verifier ID fallback uses qualifier", func(t *testing.T) { + mock := &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + CCTPVerifierAddress: "0xCCTP1", + CCTPVerifierResolverAddress: "0xCCTPResolver1", + LombardVerifierResolverAddress: "0xLombardResolver1", + }, + }, + } + + registry := adapters.NewTokenVerifierConfigRegistry() + registry.Register(chainsel.FamilyEVM, mock) + + env := deployment.Environment{ + DataStore: datastore.NewMemoryDataStore().Seal(), + BlockChains: newTestBlockChains([]uint64{sel1}), + } + + cs := changesets.GenerateTokenVerifierConfig(registry) + output, err := cs.Apply(env, changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "test", + ChainSelectors: []uint64{sel1}, + CCTP: changesets.CCTPConfigInput{Qualifier: "mycctp"}, + Lombard: changesets.LombardConfigInput{Qualifier: "mylombard"}, + }) + require.NoError(t, err) + + cfg, err := offchain.GetTokenVerifierConfig(output.DataStore.Seal(), "test") + require.NoError(t, err) + + require.Len(t, cfg.TokenVerifiers, 2) + assert.Equal(t, "cctp-mycctp", cfg.TokenVerifiers[0].VerifierID) + assert.Equal(t, "lombard-mylombard", cfg.TokenVerifiers[1].VerifierID) + }) + + t.Run("empty chain selectors defaults to all environment chains", func(t *testing.T) { + mock := &mockTokenVerifierConfigAdapter{ + addressesByChain: map[uint64]*adapters.TokenVerifierChainAddresses{ + sel1: { + OnRampAddress: "0xOnRamp1", + RMNRemoteAddress: "0xRMN1", + }, + sel2: { + OnRampAddress: "0xOnRamp2", + RMNRemoteAddress: "0xRMN2", + }, + }, + } + + registry := adapters.NewTokenVerifierConfigRegistry() + registry.Register(chainsel.FamilyEVM, mock) + + env := deployment.Environment{ + DataStore: datastore.NewMemoryDataStore().Seal(), + BlockChains: newTestBlockChains([]uint64{sel1, sel2}), + } + + cs := changesets.GenerateTokenVerifierConfig(registry) + output, err := cs.Apply(env, changesets.GenerateTokenVerifierConfigInput{ + ServiceIdentifier: "test", + }) + require.NoError(t, err) + + cfg, err := offchain.GetTokenVerifierConfig(output.DataStore.Seal(), "test") + require.NoError(t, err) + + assert.Len(t, cfg.OnRampAddresses, 2) + assert.Len(t, cfg.RMNRemoteAddresses, 2) + }) +}