From 30b1286e6b11033b4aa5ce8d4961c34d3a4628ad Mon Sep 17 00:00:00 2001 From: Gjermund Garaba Date: Fri, 14 Aug 2026 14:43:07 +0200 Subject: [PATCH] refactor(link): expose config package Signed-off-by: Gjermund Garaba --- e2e/go.mod | 3 +- e2e/go.sum | 2 + e2e/internal/e2etest/traffic_setup.go | 5 +- ...elayer_attestor_quorum_integration_test.go | 7 +- e2e/internal/harness/ibclink/attestor.go | 82 +- e2e/internal/harness/ibclink/attestor_test.go | 26 +- e2e/internal/harness/ibclink/deploy.go | 18 +- .../harness/ibclink/relayer_config.go | 133 ++-- .../harness/ibclink/relayer_config_test.go | 160 ++-- e2e/remote_signer_test.go | 3 +- link/README.md | 2 +- link/cmd/ibc/attestor.go | 4 +- link/cmd/ibc/attestor_test.go | 2 +- link/cmd/ibc/attestors.go | 2 +- link/cmd/ibc/attestors_test.go | 2 +- link/cmd/ibc/config.go | 22 +- link/cmd/ibc/config_test.go | 6 +- link/cmd/ibc/deploy.go | 17 +- link/cmd/ibc/deploy_test.go | 6 +- link/cmd/ibc/flags.go | 52 ++ link/cmd/ibc/ift.go | 6 +- link/cmd/ibc/keys.go | 11 +- link/cmd/ibc/main.go | 6 +- link/cmd/ibc/migrate.go | 8 +- link/cmd/ibc/output.go | 55 ++ link/cmd/ibc/relayer.go | 5 +- link/config/config.go | 293 +++++++ link/{internal => }/config/config_test.go | 24 +- link/{internal => }/config/ibc.yml | 0 link/config/relayer.go | 100 +++ link/{internal => }/config/relayer_test.go | 6 +- .../{internal => }/config/testdata/sample.yml | 0 link/config/validate.go | 427 ++++++++++ link/config/yaml.go | 59 ++ link/internal/bootstrap/bootstrap.go | 2 +- link/internal/chains/client.go | 2 +- link/internal/config/config.go | 738 ------------------ link/internal/config/flags.go | 91 --- link/internal/config/relayer.go | 225 ------ link/internal/fsutil/path.go | 58 ++ link/internal/livevalidate/connections.go | 2 +- .../internal/livevalidate/connections_test.go | 2 +- link/internal/livevalidate/quorum.go | 2 +- link/internal/livevalidate/quorum_test.go | 2 +- link/internal/livevalidate/validate.go | 2 +- .../relay/dispatch/dispatcher_test.go | 2 +- link/internal/relay/dispatch/pipeline_set.go | 2 +- link/internal/relay/pipeline/opts.go | 2 +- .../relay/proofgen/attestation/resolve.go | 2 +- .../proofgen/attestation/resolve_test.go | 2 +- link/internal/relay/proofgen/proofgen.go | 2 +- link/internal/relay/proofgen/proofgen_test.go | 2 +- link/internal/relay/txbuilder/txbuilder.go | 2 +- link/internal/service/attestor/local.go | 2 +- link/internal/service/attestor/local_test.go | 2 +- link/internal/service/attestor/resolve.go | 2 +- .../internal/service/attestor/resolve_test.go | 2 +- .../internal/service/attestor/service_test.go | 2 +- link/internal/service/relayer/service.go | 2 +- link/internal/service/relayer/service_test.go | 2 +- link/internal/service/signer/local.go | 4 +- link/internal/service/signer/signer.go | 11 +- link/internal/service/signer/signer_test.go | 2 +- link/internal/store/migrations.go | 2 +- link/internal/store/store.go | 2 +- link/internal/store/store_postgres.go | 2 +- link/internal/store/store_sqlite.go | 5 +- link/internal/txsubmitter/txsubmitter.go | 2 +- link/scripts/migratenew.go | 2 +- 69 files changed, 1336 insertions(+), 1406 deletions(-) create mode 100644 link/cmd/ibc/flags.go create mode 100644 link/cmd/ibc/output.go create mode 100644 link/config/config.go rename link/{internal => }/config/config_test.go (95%) rename link/{internal => }/config/ibc.yml (100%) create mode 100644 link/config/relayer.go rename link/{internal => }/config/relayer_test.go (97%) rename link/{internal => }/config/testdata/sample.yml (100%) create mode 100644 link/config/validate.go create mode 100644 link/config/yaml.go delete mode 100644 link/internal/config/config.go delete mode 100644 link/internal/config/flags.go delete mode 100644 link/internal/config/relayer.go create mode 100644 link/internal/fsutil/path.go diff --git a/e2e/go.mod b/e2e/go.mod index 676d25f4a..b33c920c2 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -81,6 +81,7 @@ require ( github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/googleapis v1.4.1 // indirect @@ -337,7 +338,7 @@ require ( golang.org/x/net v0.56.0 // indirect golang.org/x/sys v0.47.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect - gopkg.in/yaml.v3 v3.0.1 + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/cosmos/ibc/link => ../link diff --git a/e2e/go.sum b/e2e/go.sum index 93ce7024c..38c8180a3 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -400,6 +400,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +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 v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= diff --git a/e2e/internal/e2etest/traffic_setup.go b/e2e/internal/e2etest/traffic_setup.go index 050bc12b5..43471e2c9 100644 --- a/e2e/internal/e2etest/traffic_setup.go +++ b/e2e/internal/e2etest/traffic_setup.go @@ -24,6 +24,7 @@ import ( "github.com/cosmos/ibc/e2e/internal/harness/environment/solidityibc/iftsendcallconstructor" "github.com/cosmos/ibc/e2e/internal/harness/environment/solidityibc/testerc20" "github.com/cosmos/ibc/e2e/internal/harness/ibclink" + linkconfig "github.com/cosmos/ibc/link/config" ) const ( @@ -150,7 +151,7 @@ func DeployWithRelayerConfig( if configure != nil { configure(&config) } - if config.SignerType == "" || config.SignerType == ibclink.RelayerSignerLocal { + if config.SignerType == "" || config.SignerType == linkconfig.SignerLocal { require.NoError(t, relayer.storeKey(config.SignerKeyFile), "e2etest: store relayer signer key") } require.NoError(t, ibclink.WriteRelayerConfig(configPath, config), "e2etest: write config") @@ -378,7 +379,7 @@ func buildConfig( t.Fatalf("e2etest: resolve Attestor %q: %v", id, err) } config.Attestors = append(config.Attestors, ibclink.RelayerAttestor{ - Name: string(attestor.ID()), Type: ibclink.RelayerAttestorRemote, GRPC: attestor.Endpoint(), + Name: string(attestor.ID()), Type: linkconfig.AttestorTypeRemote, GRPC: attestor.Endpoint(), }) } diff --git a/e2e/internal/harness/environment/relayer_attestor_quorum_integration_test.go b/e2e/internal/harness/environment/relayer_attestor_quorum_integration_test.go index ce152d1cf..bd78ef30a 100644 --- a/e2e/internal/harness/environment/relayer_attestor_quorum_integration_test.go +++ b/e2e/internal/harness/environment/relayer_attestor_quorum_integration_test.go @@ -14,6 +14,7 @@ import ( "github.com/cosmos/ibc/e2e/internal/harness/environment" "github.com/cosmos/ibc/e2e/internal/harness/ibclink" + linkconfig "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/keyfile" ) @@ -130,12 +131,12 @@ func TestStartFailsWhenConfiguredAttestorsDoNotSatisfyOnChainQuorum(t *testing.T require.NoError(t, err) attestors := []ibclink.RelayerAttestor{ - {Name: string(attestorA1), Type: ibclink.RelayerAttestorRemote, GRPC: resolvedA1.Endpoint()}, - {Name: string(attestorB), Type: ibclink.RelayerAttestorRemote, GRPC: resolvedB.Endpoint()}, + {Name: string(attestorA1), Type: linkconfig.AttestorTypeRemote, GRPC: resolvedA1.Endpoint()}, + {Name: string(attestorB), Type: linkconfig.AttestorTypeRemote, GRPC: resolvedB.Endpoint()}, } if includeSecondAttestorA { attestors = append(attestors, ibclink.RelayerAttestor{ - Name: string(attestorA2), Type: ibclink.RelayerAttestorRemote, GRPC: resolvedA2.Endpoint(), + Name: string(attestorA2), Type: linkconfig.AttestorTypeRemote, GRPC: resolvedA2.Endpoint(), }) } diff --git a/e2e/internal/harness/ibclink/attestor.go b/e2e/internal/harness/ibclink/attestor.go index 2b1167a8c..b99164b18 100644 --- a/e2e/internal/harness/ibclink/attestor.go +++ b/e2e/internal/harness/ibclink/attestor.go @@ -27,9 +27,9 @@ import ( "github.com/ethereum/go-ethereum/crypto" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "gopkg.in/yaml.v3" attestorv2 "github.com/cosmos/ibc/link/api/v2/attestor" + linkconfig "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/keyfile" ) @@ -394,42 +394,42 @@ func prepareAttestorWorkspace( return workspacePaths{}, fmt.Errorf("start IBC Link attestor: create private work dir %q: %w", dir, mkdirErr) } - signer := signerConfig{Alias: signerAlias, Type: RelayerSignerRemote} + signer := linkconfig.SignerConfig{Alias: signerAlias, Type: linkconfig.SignerRemote} if key != nil { keyPath := filepath.Join(dir, "keys", keyFilename) if writeErr := keyfile.Store(keyPath, keyfile.ECDSA, crypto.FromECDSA(key)); writeErr != nil { return workspacePaths{}, fmt.Errorf("start IBC Link attestor: write private key file: %w", writeErr) } - signer.Type = RelayerSignerLocal + signer.Type = linkconfig.SignerLocal signer.File = keyPath } else { signer.GRPC = spec.SignerGRPC signer.RemoteKeyID = spec.SignerRemoteKeyID } - config := fileConfig{ - Server: serverConfig{ListenAddress: listenAddress}, - DB: dbConfig{ - Type: dbTypeSQLite, + config := linkconfig.Config{ + Server: linkconfig.ServerConfig{ListenAddress: listenAddress}, + DB: linkconfig.DBConfig{ + Type: linkconfig.DBTypeSQLite, URL: filepath.Join(dir, "ibc.db"), }, - Chains: []chainConfig{{ + Chains: []linkconfig.ChainConfig{{ ChainID: spec.ChainID, - EVM: evmChainConfig{ + EVM: &linkconfig.EVMChainConfig{ RPC: spec.RPCURL, ICS26Router: spec.ICS26Router, }, }}, - Attestors: []attestorFileConfig{{ + Attestors: linkconfig.Attestors{{ Name: spec.Name, ChainID: spec.ChainID, - Type: RelayerAttestorLocal, + Type: linkconfig.AttestorTypeLocal, Signer: signerAlias, FinalityOffset: HarnessFinalityOffset, }}, - Signers: []signerConfig{signer}, + Signers: linkconfig.Signers{signer}, } - configData, err := yaml.Marshal(config) + configData, err := linkconfig.MarshalYAML(config) if err != nil { return workspacePaths{}, fmt.Errorf("start IBC Link attestor: encode config: %w", err) } @@ -527,57 +527,5 @@ func (w *logWriter) close() { } } -// Shared config-literal defaults for harness-written Link config files. -const ( - loopbackAnyPort = "127.0.0.1:0" - dbTypeSQLite = "sqlite" -) - -type fileConfig struct { - Server serverConfig `yaml:"server"` - DB dbConfig `yaml:"db"` - Chains []chainConfig `yaml:"chains"` - Relayer *relayerFileConfig `yaml:"relayer,omitempty"` - Attestors []attestorFileConfig `yaml:"attestors"` - Signers []signerConfig `yaml:"signers"` -} - -type chainConfig struct { - ChainID string `yaml:"chainId"` - EVM evmChainConfig `yaml:"evm"` - Deployer string `yaml:"deployer,omitempty"` -} - -type evmChainConfig struct { - RPC string `yaml:"rpc"` - ICS26Router string `yaml:"ics26Router"` -} - -type serverConfig struct { - ListenAddress string `yaml:"listenAddr"` -} - -type dbConfig struct { - Type string `yaml:"type"` - URL string `yaml:"url"` -} - -type attestorFileConfig struct { - Name string `yaml:"name"` - ChainID string `yaml:"chainId"` - Type string `yaml:"type"` - Signer string `yaml:"signer,omitempty"` - - // FinalityOffset is kept at 1 ("latest" minus one block) because the dev - // chains behind the harness do not expose the "finalized" block tag. - FinalityOffset uint `yaml:"finalityOffset,omitempty"` - GRPC string `yaml:"grpc,omitempty"` -} - -type signerConfig struct { - Alias string `yaml:"alias"` - Type string `yaml:"type"` - File string `yaml:"file,omitempty"` - GRPC string `yaml:"grpc,omitempty"` - RemoteKeyID string `yaml:"remoteKeyId,omitempty"` -} +// Shared config-literal default for harness-written Link config files. +const loopbackAnyPort = "127.0.0.1:0" diff --git a/e2e/internal/harness/ibclink/attestor_test.go b/e2e/internal/harness/ibclink/attestor_test.go index 035a327b8..eb6cab8d0 100644 --- a/e2e/internal/harness/ibclink/attestor_test.go +++ b/e2e/internal/harness/ibclink/attestor_test.go @@ -24,9 +24,9 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" "google.golang.org/grpc" - "gopkg.in/yaml.v3" attestorv2 "github.com/cosmos/ibc/link/api/v2/attestor" + linkconfig "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/keyfile" ) @@ -113,13 +113,14 @@ func TestStartWritesRemoteSignerWithoutLocalKey(t *testing.T) { common.HexToAddress("0xE57bFE9F44b819898F47BF37E5AF72a0783e1141"), process.SignerAddress()) - configData, err := os.ReadFile(filepath.Join(workDir, configFilename)) + config, err := linkconfig.LoadFile( + filepath.Join(workDir, configFilename), + linkconfig.LoadOptions{SkipValidation: true}, + ) require.NoError(t, err) - var config fileConfig - require.NoError(t, yaml.Unmarshal(configData, &config)) - require.Equal(t, []signerConfig{{ + require.Equal(t, linkconfig.Signers{{ Alias: signerAlias, - Type: RelayerSignerRemote, + Type: linkconfig.SignerRemote, GRPC: remoteSignerEndpoint, RemoteKeyID: "attestor-key", }}, config.Signers) @@ -265,13 +266,12 @@ func runAttestorHelper() error { if err != nil { return err } - configData, err := os.ReadFile(filepath.Join(home, configName)) + config, err := linkconfig.LoadFile( + filepath.Join(home, configName), + linkconfig.LoadOptions{SkipValidation: true}, + ) if err != nil { - return fmt.Errorf("helper read config: %w", err) - } - var config fileConfig - if decodeErr := yaml.Unmarshal(configData, &config); decodeErr != nil { - return fmt.Errorf("helper decode config: %w", decodeErr) + return fmt.Errorf("helper load config: %w", err) } if len(config.Attestors) != 1 || config.Attestors[0].Name != "attestor-a" { return fmt.Errorf("helper received unexpected attestor config: %+v", config.Attestors) @@ -284,7 +284,7 @@ func runAttestorHelper() error { return fmt.Errorf("helper received unexpected signer config: %+v", config.Signers) } signer := config.Signers[0] - if signer.Type == RelayerSignerLocal { + if signer.Type == linkconfig.SignerLocal { if _, readErr := os.ReadFile(signer.File); readErr != nil { return fmt.Errorf("helper read signer key: %w", readErr) } diff --git a/e2e/internal/harness/ibclink/deploy.go b/e2e/internal/harness/ibclink/deploy.go index 56f93eec8..b69597d90 100644 --- a/e2e/internal/harness/ibclink/deploy.go +++ b/e2e/internal/harness/ibclink/deploy.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "gopkg.in/yaml.v3" + linkconfig "github.com/cosmos/ibc/link/config" ) // deployCommandTimeout bounds the sequential deployment transactions a @@ -53,24 +53,24 @@ func WriteDeployConfig(path string, cfg DeployConfig) error { return errors.New("ibclink: deploy config: at least one chain is required") } - file := fileConfig{ - Server: serverConfig{ListenAddress: loopbackAnyPort}, - DB: dbConfig{Type: dbTypeSQLite, URL: cfg.DBPath}, - Signers: []signerConfig{{ + file := linkconfig.Config{ + Server: linkconfig.ServerConfig{ListenAddress: loopbackAnyPort}, + DB: linkconfig.DBConfig{Type: linkconfig.DBTypeSQLite, URL: cfg.DBPath}, + Signers: linkconfig.Signers{{ Alias: cfg.SignerAlias, - Type: RelayerSignerLocal, + Type: linkconfig.SignerLocal, File: cfg.SignerKeyFile, }}, } for _, chain := range cfg.Chains { - file.Chains = append(file.Chains, chainConfig{ + file.Chains = append(file.Chains, linkconfig.ChainConfig{ ChainID: chain.ChainID, - EVM: evmChainConfig{RPC: chain.RPC, ICS26Router: placeholderRouter}, + EVM: &linkconfig.EVMChainConfig{RPC: chain.RPC, ICS26Router: placeholderRouter}, Deployer: cfg.SignerAlias, }) } - data, err := yaml.Marshal(file) + data, err := linkconfig.MarshalYAML(file) if err != nil { return fmt.Errorf("ibclink: encode deploy config: %w", err) } diff --git a/e2e/internal/harness/ibclink/relayer_config.go b/e2e/internal/harness/ibclink/relayer_config.go index 7997f3e27..8dc884580 100644 --- a/e2e/internal/harness/ibclink/relayer_config.go +++ b/e2e/internal/harness/ibclink/relayer_config.go @@ -9,7 +9,7 @@ import ( "path/filepath" "time" - "gopkg.in/yaml.v3" + linkconfig "github.com/cosmos/ibc/link/config" ) // RelayerConfig describes one relayer process configuration for the black-box @@ -17,7 +17,7 @@ import ( type RelayerConfig struct { DBPath string SignerAlias string - // SignerType defaults to RelayerSignerLocal. A remote transaction signer is + // SignerType defaults to config.SignerLocal. A remote transaction signer is // shared by both ends of every route. SignerType string SignerGRPC string @@ -59,7 +59,7 @@ type RelayerConnection struct { // remote entry is reached at a bare gRPC host:port type RelayerAttestor struct { Name string - Type string + Type linkconfig.AttestorType ChainID string // local only GRPC string // remote only KeyFile string // local only @@ -71,7 +71,7 @@ func WriteRelayerConfig(path string, cfg RelayerConfig) error { if err != nil { return fmt.Errorf("ibclink: relayer config: %w", err) } - data, err := yaml.Marshal(file) + data, err := linkconfig.MarshalYAML(file) if err != nil { return fmt.Errorf("ibclink: encode relayer config: %w", err) } @@ -84,37 +84,38 @@ func WriteRelayerConfig(path string, cfg RelayerConfig) error { return nil } -func buildRelayerFileConfig(cfg RelayerConfig) (fileConfig, error) { +func buildRelayerFileConfig(cfg RelayerConfig) (linkconfig.Config, error) { signerType := cfg.SignerType if signerType == "" { - signerType = RelayerSignerLocal + signerType = linkconfig.SignerLocal } switch { case cfg.DBPath == "": - return fileConfig{}, errors.New("db path is required") + return linkconfig.Config{}, errors.New("db path is required") case cfg.SignerAlias == "": - return fileConfig{}, errors.New("signer alias is required") - case signerType == RelayerSignerLocal && cfg.SignerKeyFile == "": - return fileConfig{}, errors.New("signer key file is required") + return linkconfig.Config{}, errors.New("signer alias is required") + case signerType == linkconfig.SignerLocal && cfg.SignerKeyFile == "": + return linkconfig.Config{}, errors.New("signer key file is required") case len(cfg.Chains) == 0: - return fileConfig{}, errors.New("at least one chain is required") + return linkconfig.Config{}, errors.New("at least one chain is required") case len(cfg.Connections) == 0: - return fileConfig{}, errors.New("at least one connection is required") + return linkconfig.Config{}, errors.New("at least one connection is required") } - processSigner := signerConfig{Alias: cfg.SignerAlias, Type: signerType} - if signerType == RelayerSignerRemote { + processSigner := linkconfig.SignerConfig{Alias: cfg.SignerAlias, Type: signerType} + if signerType == linkconfig.SignerRemote { processSigner.GRPC = cfg.SignerGRPC processSigner.RemoteKeyID = cfg.SignerRemoteKeyID } else { processSigner.File = cfg.SignerKeyFile } - file := fileConfig{ - Server: serverConfig{ListenAddress: loopbackAnyPort}, - DB: dbConfig{Type: dbTypeSQLite, URL: cfg.DBPath}, - Signers: []signerConfig{processSigner}, + dispatchPollInterval := 100 * time.Millisecond + file := linkconfig.Config{ + Server: linkconfig.ServerConfig{ListenAddress: loopbackAnyPort}, + DB: linkconfig.DBConfig{Type: linkconfig.DBTypeSQLite, URL: cfg.DBPath}, + Signers: linkconfig.Signers{processSigner}, // The default 5s dispatch poll is mainnet-shaped; harness awaits are sub-second. - Relayer: &relayerFileConfig{DispatchPollInterval: "100ms"}, + Relayer: linkconfig.RelayerConfig{DispatchPollInterval: &dispatchPollInterval}, } for _, chain := range cfg.Chains { @@ -122,9 +123,9 @@ func buildRelayerFileConfig(cfg RelayerConfig) (fileConfig, error) { if batchSize == 0 { batchSize = 1 } - file.Chains = append(file.Chains, chainConfig{ + file.Chains = append(file.Chains, linkconfig.ChainConfig{ ChainID: chain.ChainID, - EVM: evmChainConfig{ + EVM: &linkconfig.EVMChainConfig{ RPC: chain.RPC, ICS26Router: chain.ICS26Router, }, @@ -134,12 +135,16 @@ func buildRelayerFileConfig(cfg RelayerConfig) (fileConfig, error) { // submission delay paces only consecutive transactions on one chain // (retries and multi-route traffic); it must stay non-zero because // zero is coerced back to the mainnet default. - file.Relayer.ChainOverrides = append(file.Relayer.ChainOverrides, chainOverrideFileConfig{ - ChainID: chain.ChainID, - TxSubmissionDelay: "10ms", - PacketBatchSize: batchSize, - PacketBatchTimeout: chain.PacketBatchTimeout, - }) + txSubmissionDelay := 10 * time.Millisecond + override := linkconfig.RelayerChainOverride{ + ChainID: chain.ChainID, + TxSubmissionDelay: &txSubmissionDelay, + PacketBatchSize: &batchSize, + } + if chain.PacketBatchTimeout != 0 { + override.PacketBatchTimeout = &chain.PacketBatchTimeout + } + file.Relayer.ChainOverrides = append(file.Relayer.ChainOverrides, override) } if len(cfg.Attestors) == 0 { @@ -149,25 +154,25 @@ func buildRelayerFileConfig(cfg RelayerConfig) (fileConfig, error) { } else { for _, attestor := range cfg.Attestors { if err := addAttestor(&file, cfg.FinalityOffset, attestor); err != nil { - return fileConfig{}, fmt.Errorf("attestor %q: %w", attestor.Name, err) + return linkconfig.Config{}, fmt.Errorf("attestor %q: %w", attestor.Name, err) } } } for _, connection := range cfg.Connections { - file.Relayer.Connections = append(file.Relayer.Connections, connectionFileConfig{ + file.Relayer.Connections = append(file.Relayer.Connections, linkconfig.ConnectionConfig{ Alias: connection.ClientA + "-" + connection.ClientB, - ClientA: clientEndFileConfig{ + ClientA: linkconfig.ClientEnd{ ChainID: connection.ChainA, Signer: cfg.SignerAlias, ClientID: connection.ClientA, - Type: "attestation", + Type: linkconfig.ClientTypeAttestation, }, - ClientB: clientEndFileConfig{ + ClientB: linkconfig.ClientEnd{ ChainID: connection.ChainB, Signer: cfg.SignerAlias, ClientID: connection.ClientB, - Type: "attestation", + Type: linkconfig.ClientTypeAttestation, }, }) } @@ -178,14 +183,14 @@ func buildRelayerFileConfig(cfg RelayerConfig) (fileConfig, error) { // Local entries always bring their own key file, unlike the implicit // default (addDefaultLocalAttestor), so multiple local attestors don't // share a signing identity. -func addAttestor(file *fileConfig, finalityOffset uint64, attestor RelayerAttestor) error { +func addAttestor(file *linkconfig.Config, finalityOffset uint64, attestor RelayerAttestor) error { switch attestor.Type { - case RelayerAttestorRemote: - file.Attestors = append(file.Attestors, attestorFileConfig{ - Name: attestor.Name, Type: RelayerAttestorRemote, GRPC: attestor.GRPC, + case linkconfig.AttestorTypeRemote: + file.Attestors = append(file.Attestors, linkconfig.AttestorConfig{ + Name: attestor.Name, Type: linkconfig.AttestorTypeRemote, GRPC: attestor.GRPC, }) return nil - case RelayerAttestorLocal: + case linkconfig.AttestorTypeLocal: switch { case attestor.ChainID == "": return errors.New("chainId is required for local attestors") @@ -194,11 +199,11 @@ func addAttestor(file *fileConfig, finalityOffset uint64, attestor RelayerAttest } signerAlias := attestor.Name + "-signer" - file.Signers = append(file.Signers, signerConfig{ - Alias: signerAlias, Type: RelayerSignerLocal, File: attestor.KeyFile, + file.Signers = append(file.Signers, linkconfig.SignerConfig{ + Alias: signerAlias, Type: linkconfig.SignerLocal, File: attestor.KeyFile, }) - file.Attestors = append(file.Attestors, attestorFileConfig{ - Name: attestor.Name, ChainID: attestor.ChainID, Type: RelayerAttestorLocal, + file.Attestors = append(file.Attestors, linkconfig.AttestorConfig{ + Name: attestor.Name, ChainID: attestor.ChainID, Type: linkconfig.AttestorTypeLocal, Signer: signerAlias, FinalityOffset: uint(finalityOffset), }) return nil @@ -209,15 +214,20 @@ func addAttestor(file *fileConfig, finalityOffset uint64, attestor RelayerAttest // addDefaultLocalAttestor declares the default local attestor for a chain, // backed by the relayer process's own signer. -func addDefaultLocalAttestor(file *fileConfig, processSigner signerConfig, finalityOffset uint64, chainID string) { +func addDefaultLocalAttestor( + file *linkconfig.Config, + processSigner linkconfig.SignerConfig, + finalityOffset uint64, + chainID string, +) { name := localAttestorName(chainID) signerAlias := name + "-signer" signer := processSigner signer.Alias = signerAlias file.Signers = append(file.Signers, signer) - file.Attestors = append(file.Attestors, attestorFileConfig{ - Name: name, ChainID: chainID, Type: RelayerAttestorLocal, + file.Attestors = append(file.Attestors, linkconfig.AttestorConfig{ + Name: name, ChainID: chainID, Type: linkconfig.AttestorTypeLocal, Signer: signerAlias, FinalityOffset: uint(finalityOffset), }) } @@ -225,36 +235,3 @@ func addDefaultLocalAttestor(file *fileConfig, processSigner signerConfig, final func localAttestorName(chainID string) string { return "local-attestor-" + chainID } - -const ( - RelayerSignerLocal = "local" - RelayerSignerRemote = "remote" - RelayerAttestorLocal = "local" - RelayerAttestorRemote = "remote" -) - -type relayerFileConfig struct { - DispatchPollInterval string `yaml:"dispatchPollInterval,omitempty"` - ChainOverrides []chainOverrideFileConfig `yaml:"chainOverrides,omitempty"` - Connections []connectionFileConfig `yaml:"connections"` -} - -type chainOverrideFileConfig struct { - ChainID string `yaml:"chainId"` - TxSubmissionDelay string `yaml:"txSubmissionDelay"` - PacketBatchSize int `yaml:"packetBatchSize"` - PacketBatchTimeout time.Duration `yaml:"packetBatchTimeout,omitempty"` -} - -type connectionFileConfig struct { - Alias string `yaml:"alias"` - ClientA clientEndFileConfig `yaml:"clientA"` - ClientB clientEndFileConfig `yaml:"clientB"` -} - -type clientEndFileConfig struct { - ChainID string `yaml:"chainId"` - Signer string `yaml:"signer"` - ClientID string `yaml:"clientId"` - Type string `yaml:"type"` -} diff --git a/e2e/internal/harness/ibclink/relayer_config_test.go b/e2e/internal/harness/ibclink/relayer_config_test.go index b8dbe8fed..3463333c1 100644 --- a/e2e/internal/harness/ibclink/relayer_config_test.go +++ b/e2e/internal/harness/ibclink/relayer_config_test.go @@ -7,101 +7,103 @@ import ( "time" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + + linkconfig "github.com/cosmos/ibc/link/config" ) func TestBuildRelayerConfigPreservesDefaultYAML(t *testing.T) { file, err := buildRelayerFileConfig(testRelayerConfig()) require.NoError(t, err) - data, err := yaml.Marshal(file) + data, err := linkconfig.MarshalYAML(file) require.NoError(t, err) require.Equal(t, `server: - listenAddr: 127.0.0.1:0 + listenAddr: 127.0.0.1:0 db: - type: sqlite - url: /tmp/ibc.db + type: sqlite + url: /tmp/ibc.db chains: - - chainId: "1" - evm: - rpc: http://chain-1 - ics26Router: router-1 - - chainId: "2" - evm: - rpc: http://chain-2 - ics26Router: router-2 +- chainId: "1" + evm: + rpc: http://chain-1 + ics26Router: router-1 +- chainId: "2" + evm: + rpc: http://chain-2 + ics26Router: router-2 relayer: - dispatchPollInterval: 100ms - chainOverrides: - - chainId: "1" - txSubmissionDelay: 10ms - packetBatchSize: 1 - - chainId: "2" - txSubmissionDelay: 10ms - packetBatchSize: 1 - connections: - - alias: client-1-client-2 - clientA: - chainId: "1" - signer: tx - clientId: client-1 - type: attestation - clientB: - chainId: "2" - signer: tx - clientId: client-2 - type: attestation -attestors: - - name: local-attestor-1 + dispatchPollInterval: 100ms + chainOverrides: + - chainId: "1" + txSubmissionDelay: 10ms + packetBatchSize: 1 + - chainId: "2" + txSubmissionDelay: 10ms + packetBatchSize: 1 + connections: + - alias: client-1-client-2 + clientA: chainId: "1" - type: local - signer: local-attestor-1-signer - finalityOffset: 3 - - name: local-attestor-2 + signer: tx + clientId: client-1 + type: attestation + clientB: chainId: "2" - type: local - signer: local-attestor-2-signer - finalityOffset: 3 + signer: tx + clientId: client-2 + type: attestation +attestors: +- chainId: "1" + name: local-attestor-1 + type: local + signer: local-attestor-1-signer + finalityOffset: 3 +- chainId: "2" + name: local-attestor-2 + type: local + signer: local-attestor-2-signer + finalityOffset: 3 signers: - - alias: tx - type: local - file: /tmp/default.key - - alias: local-attestor-1-signer - type: local - file: /tmp/default.key - - alias: local-attestor-2-signer - type: local - file: /tmp/default.key +- alias: tx + type: local + file: /tmp/default.key +- alias: local-attestor-1-signer + type: local + file: /tmp/default.key +- alias: local-attestor-2-signer + type: local + file: /tmp/default.key `, string(data)) } func TestBuildRelayerConfigOverrides(t *testing.T) { cfg := testRelayerConfig() - cfg.SignerType = RelayerSignerRemote + cfg.SignerType = linkconfig.SignerRemote cfg.SignerKeyFile = "" cfg.SignerGRPC = "kms:9090" cfg.SignerRemoteKeyID = "relay-key" cfg.Chains[0].PacketBatchSize = 7 cfg.Chains[0].PacketBatchTimeout = 250 * time.Millisecond cfg.Attestors = []RelayerAttestor{ - {Name: "alice", Type: RelayerAttestorLocal, ChainID: "2", KeyFile: "/tmp/alice.key"}, - {Name: "bob", Type: RelayerAttestorRemote, GRPC: "bob:8080"}, - {Name: "carol", Type: RelayerAttestorRemote, GRPC: "carol:8080"}, + {Name: "alice", Type: linkconfig.AttestorTypeLocal, ChainID: "2", KeyFile: "/tmp/alice.key"}, + {Name: "bob", Type: linkconfig.AttestorTypeRemote, GRPC: "bob:8080"}, + {Name: "carol", Type: linkconfig.AttestorTypeRemote, GRPC: "carol:8080"}, } file, err := buildRelayerFileConfig(cfg) require.NoError(t, err) - require.Equal(t, chainOverrideFileConfig{ - ChainID: "1", TxSubmissionDelay: "10ms", PacketBatchSize: 7, - PacketBatchTimeout: 250 * time.Millisecond, - }, file.Relayer.ChainOverrides[0]) - require.Equal(t, []signerConfig{ - {Alias: "tx", Type: RelayerSignerRemote, GRPC: "kms:9090", RemoteKeyID: "relay-key"}, - {Alias: "alice-signer", Type: RelayerSignerLocal, File: "/tmp/alice.key"}, + override := file.Relayer.ChainOverrides[0] + require.Equal(t, "1", override.ChainID) + require.Equal(t, 10*time.Millisecond, *override.TxSubmissionDelay) + require.Equal(t, 7, *override.PacketBatchSize) + require.Equal(t, 250*time.Millisecond, *override.PacketBatchTimeout) + require.Equal(t, linkconfig.Signers{ + {Alias: "tx", Type: linkconfig.SignerRemote, GRPC: "kms:9090", RemoteKeyID: "relay-key"}, + {Alias: "alice-signer", Type: linkconfig.SignerLocal, File: "/tmp/alice.key"}, }, file.Signers) - require.Equal(t, []attestorFileConfig{ - {Name: "alice", ChainID: "2", Type: RelayerAttestorLocal, Signer: "alice-signer", FinalityOffset: 3}, - {Name: "bob", Type: RelayerAttestorRemote, GRPC: "bob:8080"}, - {Name: "carol", Type: RelayerAttestorRemote, GRPC: "carol:8080"}, + require.Equal(t, linkconfig.Attestors{ + {Name: "alice", ChainID: "2", Type: linkconfig.AttestorTypeLocal, Signer: "alice-signer", FinalityOffset: 3}, + {Name: "bob", Type: linkconfig.AttestorTypeRemote, GRPC: "bob:8080"}, + {Name: "carol", Type: linkconfig.AttestorTypeRemote, GRPC: "carol:8080"}, }, file.Attestors) require.Equal(t, cfg.SignerAlias, file.Relayer.Connections[0].ClientA.Signer) require.Equal(t, cfg.SignerAlias, file.Relayer.Connections[0].ClientB.Signer) @@ -109,21 +111,21 @@ func TestBuildRelayerConfigOverrides(t *testing.T) { func TestRemoteSignerBacksDefaultLocalAttestors(t *testing.T) { cfg := testRelayerConfig() - cfg.SignerType = RelayerSignerRemote + cfg.SignerType = linkconfig.SignerRemote cfg.SignerKeyFile = "" cfg.SignerGRPC = "kms:9090" cfg.SignerRemoteKeyID = "relay-key" file, err := buildRelayerFileConfig(cfg) require.NoError(t, err) - require.Equal(t, []signerConfig{ - {Alias: "tx", Type: RelayerSignerRemote, GRPC: "kms:9090", RemoteKeyID: "relay-key"}, + require.Equal(t, linkconfig.Signers{ + {Alias: "tx", Type: linkconfig.SignerRemote, GRPC: "kms:9090", RemoteKeyID: "relay-key"}, { - Alias: "local-attestor-1-signer", Type: RelayerSignerRemote, + Alias: "local-attestor-1-signer", Type: linkconfig.SignerRemote, GRPC: "kms:9090", RemoteKeyID: "relay-key", }, { - Alias: "local-attestor-2-signer", Type: RelayerSignerRemote, + Alias: "local-attestor-2-signer", Type: linkconfig.SignerRemote, GRPC: "kms:9090", RemoteKeyID: "relay-key", }, }, file.Signers) @@ -131,16 +133,18 @@ func TestRemoteSignerBacksDefaultLocalAttestors(t *testing.T) { func TestExplicitAttestorsSuppressDefaultLocalAttestors(t *testing.T) { cfg := testRelayerConfig() - cfg.Attestors = []RelayerAttestor{{Name: "remote", Type: RelayerAttestorRemote, GRPC: "attestor:8080"}} + cfg.Attestors = []RelayerAttestor{{ + Name: "remote", Type: linkconfig.AttestorTypeRemote, GRPC: "attestor:8080", + }} file, err := buildRelayerFileConfig(cfg) require.NoError(t, err) // Explicit attestors suppress defaults for every chain, not just the ones referenced. - require.Equal(t, []attestorFileConfig{ - {Name: "remote", Type: RelayerAttestorRemote, GRPC: "attestor:8080"}, + require.Equal(t, linkconfig.Attestors{ + {Name: "remote", Type: linkconfig.AttestorTypeRemote, GRPC: "attestor:8080"}, }, file.Attestors) - require.Equal(t, []signerConfig{ - {Alias: "tx", Type: RelayerSignerLocal, File: "/tmp/default.key"}, + require.Equal(t, linkconfig.Signers{ + {Alias: "tx", Type: linkconfig.SignerLocal, File: "/tmp/default.key"}, }, file.Signers) } @@ -152,10 +156,10 @@ func TestBuildRelayerConfigRejectsHarnessInvalidConfig(t *testing.T) { }{ {"signer key", func(c *RelayerConfig) { c.SignerKeyFile = "" }, "signer key file is required"}, {"attestor key required", func(c *RelayerConfig) { - c.Attestors = []RelayerAttestor{{Name: "a", Type: RelayerAttestorLocal, ChainID: "1"}} + c.Attestors = []RelayerAttestor{{Name: "a", Type: linkconfig.AttestorTypeLocal, ChainID: "1"}} }, "key file is required for local attestors"}, {"attestor chainId required", func(c *RelayerConfig) { - c.Attestors = []RelayerAttestor{{Name: "a", Type: RelayerAttestorLocal, KeyFile: "key"}} + c.Attestors = []RelayerAttestor{{Name: "a", Type: linkconfig.AttestorTypeLocal, KeyFile: "key"}} }, "chainId is required for local attestors"}, {"unsupported attestor type", func(c *RelayerConfig) { c.Attestors = []RelayerAttestor{{Name: "a", Type: "hybrid"}} diff --git a/e2e/remote_signer_test.go b/e2e/remote_signer_test.go index c9190240e..d1f274597 100644 --- a/e2e/remote_signer_test.go +++ b/e2e/remote_signer_test.go @@ -23,6 +23,7 @@ import ( "github.com/cosmos/ibc/e2e/internal/harness/environment" "github.com/cosmos/ibc/e2e/internal/harness/ibclink" relayerv2 "github.com/cosmos/ibc/link/api/v2/relayer" + linkconfig "github.com/cosmos/ibc/link/config" ) const ( @@ -161,7 +162,7 @@ func TestIFTTransfer_RemoteSigner(t *testing.T) { sender, relayerSigner, func(config *ibclink.RelayerConfig) { - config.SignerType = ibclink.RelayerSignerRemote + config.SignerType = linkconfig.SignerRemote config.SignerGRPC = remoteSignerEndpoint config.SignerRemoteKeyID = remoteSignerKeyID }, diff --git a/link/README.md b/link/README.md index e4b251acc..df371741f 100644 --- a/link/README.md +++ b/link/README.md @@ -35,7 +35,7 @@ make build ## Configuration See [`docs/configuration.md`](docs/configuration.md) for the full config -reference, or [`internal/config/ibc.yml`](internal/config/ibc.yml) for a +reference, or [`config/ibc.yml`](config/ibc.yml) for a worked example. ## E2E diff --git a/link/cmd/ibc/attestor.go b/link/cmd/ibc/attestor.go index 550990ec7..7ae87c0d0 100644 --- a/link/cmd/ibc/attestor.go +++ b/link/cmd/ibc/attestor.go @@ -13,8 +13,8 @@ import ( "github.com/spf13/cobra" attestorv2 "github.com/cosmos/ibc/link/api/v2/attestor" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/bootstrap" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/pkg/graceful" ) @@ -146,7 +146,7 @@ func attestorCall[Req, Resp any]( return errors.Wrap(err, cmd.Name()) } - return config.PrintJSON(res.Msg) + return printJSON(res.Msg) } // requireLocalAttestor errors unless name is a locally-run attestor in cfg -- diff --git a/link/cmd/ibc/attestor_test.go b/link/cmd/ibc/attestor_test.go index db69cff25..2438311a2 100644 --- a/link/cmd/ibc/attestor_test.go +++ b/link/cmd/ibc/attestor_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" ) func TestRequireLocalAttestor(t *testing.T) { diff --git a/link/cmd/ibc/attestors.go b/link/cmd/ibc/attestors.go index 2a981b624..f00d18a42 100644 --- a/link/cmd/ibc/attestors.go +++ b/link/cmd/ibc/attestors.go @@ -5,7 +5,7 @@ package main import ( "github.com/pkg/errors" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/service/signer" ) diff --git a/link/cmd/ibc/attestors_test.go b/link/cmd/ibc/attestors_test.go index 0d523d459..189bba59d 100644 --- a/link/cmd/ibc/attestors_test.go +++ b/link/cmd/ibc/attestors_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/keyfile" ) diff --git a/link/cmd/ibc/config.go b/link/cmd/ibc/config.go index 25ea9aee8..d22eaf5c0 100644 --- a/link/cmd/ibc/config.go +++ b/link/cmd/ibc/config.go @@ -9,7 +9,8 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" + "github.com/cosmos/ibc/link/internal/fsutil" "github.com/cosmos/ibc/link/internal/livevalidate" ) @@ -83,7 +84,7 @@ func configAddChain(_ *cobra.Command, _ []string) error { return err } - return cfg.StoreToFileWithComments(configPath) + return writeConfigFile(configPath, cfg, config.CollectComments(cfg)) } func configNew(_ *cobra.Command, _ []string) error { @@ -92,17 +93,17 @@ func configNew(_ *cobra.Command, _ []string) error { return err } - cfg := config.DefaultConfig() + cfg := config.Default() if flagConfigNewOut { - return config.PrintYAML(cfg) + return printYAMLWithComments(cfg, nil) } if _, err := os.Stat(configPath); err == nil { return fmt.Errorf("config file %s already exists", configPath) } - if err := cfg.StoreToFile(configPath); err != nil { + if err := writeConfigFile(configPath, cfg, nil); err != nil { return errors.Wrap(err, "unable to write file") } @@ -126,7 +127,7 @@ func configValidate(cmd *cobra.Command, _ []string) error { // todo: it still logs store's log, we need to add config.logging{} params // to truly suppress logging (in followup PRs) if !globalFlags.Quiet { - return config.PrintJSON(map[string]any{useStatus: "valid"}) + return printJSON(map[string]any{useStatus: "valid"}) } return nil @@ -140,7 +141,7 @@ func printConfigHome(_ *cobra.Command, _ []string) { // setupHomeWithConfig changes process directory to `--home` and parses the config func setupHomeWithConfig() (config.Config, error) { - home, err := config.ExpandHome(globalFlags.Home) + home, err := fsutil.ExpandHome(globalFlags.Home) if err != nil { return config.Config{}, errors.Wrap(err, "home") } @@ -151,7 +152,7 @@ func setupHomeWithConfig() (config.Config, error) { } // ensure --home exists - if err = config.EnsureDirectory(configPath); err != nil { + if err = fsutil.EnsureDirectory(configPath); err != nil { return config.Config{}, errors.Wrapf(err, "unable to create home directory %s", home) } @@ -159,7 +160,10 @@ func setupHomeWithConfig() (config.Config, error) { return config.Config{}, errors.Wrapf(err, "unable to change working directory to %s", home) } - cfg, err := config.LoadFromFile(configPath, globalFlags.ValidateConfig(), flagConfigValidateStrict) + cfg, err := config.LoadFile(configPath, config.LoadOptions{ + SkipValidation: !globalFlags.ValidateConfig(), + DisallowUnknownFields: flagConfigValidateStrict, + }) if err != nil { return config.Config{}, err } diff --git a/link/cmd/ibc/config_test.go b/link/cmd/ibc/config_test.go index 3034a3a85..108d2444f 100644 --- a/link/cmd/ibc/config_test.go +++ b/link/cmd/ibc/config_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" ) func TestConfigValidate(t *testing.T) { @@ -17,10 +17,10 @@ func TestConfigValidate(t *testing.T) { home := t.TempDir() t.Chdir(home) - globalFlags = config.DefaultFlagSet() + globalFlags = defaultFlagSet() globalFlags.Home = home globalFlags.Quiet = true - require.NoError(t, config.DefaultConfig().StoreToFile(filepath.Join(home, globalFlags.Config))) + require.NoError(t, writeConfigFile(filepath.Join(home, globalFlags.Config), config.Default(), nil)) require.NoError(t, configValidate(nil, nil)) } diff --git a/link/cmd/ibc/deploy.go b/link/cmd/ibc/deploy.go index 2bac07d54..6ce5751c5 100644 --- a/link/cmd/ibc/deploy.go +++ b/link/cmd/ibc/deploy.go @@ -17,10 +17,11 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/deploy" "github.com/cosmos/ibc/link/internal/deploy/evm" "github.com/cosmos/ibc/link/internal/deploy/manifest" + "github.com/cosmos/ibc/link/internal/fsutil" "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/keyfile" ) @@ -135,11 +136,11 @@ func deployerKeyHex(cfg config.Config, alias string) (string, error) { if sc.Type != config.SignerLocal { return "", errors.Errorf("deployer signer %q must be a local key (deployment tooling needs the raw key)", alias) } - path, err := config.ExpandHome(sc.File) + path, err := fsutil.ExpandHome(sc.File) if err != nil { return "", err } - key, err := signer.LocalKeyFromFile(config.KeyFileFallbacks(path)...) + key, err := signer.LocalKeyFromFile(fsutil.KeyFileFallbacks(path)...) if err != nil { return "", err } @@ -215,13 +216,13 @@ func planThenRun(ctx context.Context, steps []deploy.Step) error { return err } if flagDeployDryRun { - return config.PrintJSON(preview) + return printJSON(preview) } if confirmErr := confirmOrAbort(preview); confirmErr != nil { return confirmErr } results, err := deploy.RunSteps(ctx, log, false, steps) - if printErr := config.PrintJSON(results); printErr != nil { + if printErr := printJSON(results); printErr != nil { return printErr } return err @@ -430,7 +431,7 @@ func deployStatus(cmd *cobra.Command, _ []string) error { failed = true } } - if err := config.PrintJSON(out); err != nil { + if err := printJSON(out); err != nil { return err } if failed { @@ -454,7 +455,7 @@ func deployShow(_ *cobra.Command, args []string) error { chainID, flagDeployManifestDir) } - return config.PrintJSON(m) + return printJSON(m) } // attestorsFromClient projects one client's on-chain attestor addresses into @@ -616,7 +617,7 @@ func deployRenderConfig(_ *cobra.Command, args []string) error { if err != nil { return err } - return config.PrintYAMLWithComments(out, comments) + return printYAMLWithComments(out, comments) } // deployerAddress derives the deployer's EVM address, for use as the default diff --git a/link/cmd/ibc/deploy_test.go b/link/cmd/ibc/deploy_test.go index dd0d8171f..065ffe9c7 100644 --- a/link/cmd/ibc/deploy_test.go +++ b/link/cmd/ibc/deploy_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/deploy/manifest" "github.com/cosmos/ibc/link/internal/service/signer" ) @@ -177,7 +177,7 @@ func TestRenderRelayConfig(t *testing.T) { // goccy silently drops comments whose path doesn't resolve, so assert the // paths agree with the emitted document rather than just with each other. -// CollectComments' own logic is unit-tested directly in internal/config. +// CollectComments' own logic is unit-tested directly in config. func TestRenderConfigEmitsComments(t *testing.T) { a := manifest.New("1", "evm") a.Core.Router = "0xrouterA" @@ -199,7 +199,7 @@ func TestRenderConfigEmitsComments(t *testing.T) { require.NoError(t, err) rendered := captureStdout(t, func() { - require.NoError(t, config.PrintYAMLWithComments(out, comments)) + require.NoError(t, printYAMLWithComments(out, comments)) }) require.Contains(t, rendered, `signer: "" # TODO: signers[] alias that submits relay txs on chainA`) diff --git a/link/cmd/ibc/flags.go b/link/cmd/ibc/flags.go new file mode 100644 index 000000000..ddd0ed97c --- /dev/null +++ b/link/cmd/ibc/flags.go @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/cosmos/ibc/link/internal/fsutil" +) + +type flagSet struct { + Home string + Config string + DB string + Quiet bool + LogJSON bool + + skipConfigValidation bool +} + +func defaultFlagSet() flagSet { + return flagSet{Home: "~/.ibc", Config: "ibc.yml"} +} + +func declarePersistentFlags(cmd *cobra.Command, flags *flagSet) { + pf := cmd.PersistentFlags() + + pf.StringVarP(&flags.Home, "home", "", flags.Home, "IBC home directory") + pf.StringVarP(&flags.Config, "config", "", flags.Config, "Config file relative to home") + pf.StringVarP(&flags.DB, "db", "", flags.DB, "Database URL override") + pf.BoolVarP(&flags.Quiet, "quiet", "q", flags.Quiet, "Quiet mode") + pf.BoolVarP(&flags.LogJSON, "log-json", "", flags.LogJSON, "Enable JSON logging") +} + +func (fs *flagSet) ConfigPath() (string, error) { + home, err := fsutil.ExpandHome(fs.Home) + if err != nil { + return "", err + } + + return filepath.Abs(filepath.Join(home, fs.Config)) +} + +func (fs *flagSet) ValidateConfig() bool { + return !fs.skipConfigValidation +} + +func (fs *flagSet) SkipConfigValidation() { + fs.skipConfigValidation = true +} diff --git a/link/cmd/ibc/ift.go b/link/cmd/ibc/ift.go index 18d1f6147..a5b506aa0 100644 --- a/link/cmd/ibc/ift.go +++ b/link/cmd/ibc/ift.go @@ -18,7 +18,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/service/signer" ) @@ -210,7 +210,7 @@ func queryIFTBalance(cmd *cobra.Command, _ []string) error { return errors.Wrap(err, "symbol") } - return config.PrintJSON(map[string]string{ + return printJSON(map[string]string{ "address": account, "symbol": symbol, "balance": balance.String(), @@ -218,7 +218,7 @@ func queryIFTBalance(cmd *cobra.Command, _ []string) error { } func printTxHash(txHash string) error { - return config.PrintJSON(map[string]string{"txHash": txHash}) + return printJSON(map[string]string{"txHash": txHash}) } func parseIFTAmount(s string) *big.Int { diff --git a/link/cmd/ibc/keys.go b/link/cmd/ibc/keys.go index 2288b2287..f7e626101 100644 --- a/link/cmd/ibc/keys.go +++ b/link/cmd/ibc/keys.go @@ -11,7 +11,8 @@ import ( "github.com/spf13/cobra" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" + "github.com/cosmos/ibc/link/internal/fsutil" "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/keyfile" ) @@ -154,7 +155,7 @@ func keysList(_ *cobra.Command, _ []string) error { return err } - keyPath, err := config.ExpandHome(filepath.Join(globalFlags.Home, "keys")) + keyPath, err := fsutil.ExpandHome(filepath.Join(globalFlags.Home, "keys")) if err != nil { return err } @@ -174,7 +175,7 @@ func keysList(_ *cobra.Command, _ []string) error { }) } - return config.PrintJSON(out) + return printJSON(out) } func keysImport(_ *cobra.Command, args []string) error { @@ -270,7 +271,7 @@ func addSignerToConfig(cfg config.Config, alias string) error { return err } - return cfg.StoreToFileWithComments(configPath) + return writeConfigFile(configPath, cfg, config.CollectComments(cfg)) } func printKey(key signer.LocalKey, showPrivate bool, extra map[string]any) error { @@ -298,7 +299,7 @@ func printKey(key signer.LocalKey, showPrivate bool, extra map[string]any) error kv[k] = v } - return config.PrintJSON(kv) + return printJSON(kv) } func toHex(b []byte) string { diff --git a/link/cmd/ibc/main.go b/link/cmd/ibc/main.go index 80c05851a..f471889bd 100644 --- a/link/cmd/ibc/main.go +++ b/link/cmd/ibc/main.go @@ -12,13 +12,11 @@ import ( "github.com/spf13/cobra" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/deploy" "github.com/cosmos/ibc/link/internal/pkg/logging" ) -// global globalFlags, loaded in config.DeclarePersistentFlags() -var globalFlags = config.DefaultFlagSet() +var globalFlags = defaultFlagSet() // useStatus is the shared "status" subcommand name and status-field key, // factored out to satisfy goconst across cmd/ibc. @@ -49,7 +47,7 @@ func runMain() int { // single init() for binding all commands to rootCmd func init() { // setup global flags - config.DeclarePersistentFlags(rootCmd, &globalFlags) + declarePersistentFlags(rootCmd, &globalFlags) cobra.OnInitialize(func() { slog.SetDefault(logging.Default(globalFlags.LogJSON)) diff --git a/link/cmd/ibc/migrate.go b/link/cmd/ibc/migrate.go index f4e997130..adbe4d9a1 100644 --- a/link/cmd/ibc/migrate.go +++ b/link/cmd/ibc/migrate.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/store" ) @@ -49,7 +49,7 @@ func migrateUp(_ *cobra.Command, _ []string) error { return err } - return config.PrintJSON(map[string]any{ + return printJSON(map[string]any{ "db": cfg.DB.Label(), "applied": applied, }) @@ -67,7 +67,7 @@ func migrateDown(_ *cobra.Command, _ []string) error { return err } - return config.PrintJSON(map[string]any{ + return printJSON(map[string]any{ "db": cfg.DB.Label(), "rolledBack": rolledBack, }) @@ -85,7 +85,7 @@ func migrateStatus(_ *cobra.Command, _ []string) error { return err } - return config.PrintJSON(map[string]any{ + return printJSON(map[string]any{ "db": cfg.DB.Label(), "migrations": statuses, }) diff --git a/link/cmd/ibc/output.go b/link/cmd/ibc/output.go new file mode 100644 index 000000000..99607c4b9 --- /dev/null +++ b/link/cmd/ibc/output.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/json" + "fmt" + "os" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "github.com/cosmos/ibc/link/config" + "github.com/cosmos/ibc/link/internal/fsutil" +) + +func printJSON(v any) error { + bz, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + fmt.Println(string(bz)) + return nil +} + +func printProtoJSON(msg proto.Message) error { + bz, err := protojson.MarshalOptions{Indent: " ", UseProtoNames: true, EmitUnpopulated: true}.Marshal(msg) + if err != nil { + return err + } + fmt.Println(string(bz)) + return nil +} + +func printYAMLWithComments(v any, comments map[string]string) error { + bz, err := config.MarshalYAMLWithComments(v, comments) + if err != nil { + return err + } + fmt.Println(string(bz)) + return nil +} + +func writeConfigFile(path string, cfg config.Config, comments map[string]string) error { + if err := fsutil.EnsureDirectory(path); err != nil { + return err + } + + bz, err := config.MarshalYAMLWithComments(cfg, comments) + if err != nil { + return err + } + + return os.WriteFile(path, bz, 0o644) +} diff --git a/link/cmd/ibc/relayer.go b/link/cmd/ibc/relayer.go index b7dc7b43d..176d56079 100644 --- a/link/cmd/ibc/relayer.go +++ b/link/cmd/ibc/relayer.go @@ -13,7 +13,6 @@ import ( relayerv2 "github.com/cosmos/ibc/link/api/v2/relayer" "github.com/cosmos/ibc/link/internal/bootstrap" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/pkg/graceful" ) @@ -155,8 +154,8 @@ func relayerCall[Req, Resp any]( } if pm, ok := any(res.Msg).(proto.Message); ok { - return config.PrintProtoJSON(pm) + return printProtoJSON(pm) } - return config.PrintJSON(res.Msg) + return printJSON(res.Msg) } diff --git a/link/config/config.go b/link/config/config.go new file mode 100644 index 000000000..ce815dbb3 --- /dev/null +++ b/link/config/config.go @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package config defines, validates, and encodes IBC Link configuration. +package config + +import ( + "fmt" + "path/filepath" + "strings" +) + +// Database type +const ( + DBTypeSQLite = "sqlite" + DBTypePostgres = "postgres" +) + +// Signer type. Local represents a private key file. Remote connects to cosmos/KMS. +const ( + SignerLocal = "local" + SignerRemote = "remote" +) + +const sqliteInMemory = ":memory:" + +// Config represents a config file +// Should only contain `camelCase` keywords +type Config struct { + Server ServerConfig `yaml:"server"` + DB DBConfig `yaml:"db"` + Chains []ChainConfig `yaml:"chains"` + Relayer RelayerConfig `yaml:"relayer"` + Attestors Attestors `yaml:"attestors"` + Signers Signers `yaml:"signers"` +} + +// ServerConfig config for RPC server for both relayer and attestor +type ServerConfig struct { + ListenAddress string `yaml:"listenAddr"` +} + +// DBConfig config for database storage. +type DBConfig struct { + Type string `yaml:"type"` + URL string `yaml:"url"` +} + +// Attestors is the list of attestors, used both by the relayer +// (to resolve who to query) and the attestor binary (to know what it +// serves locally). +type Attestors []AttestorConfig + +// AttestorConfig describes one attestor, either run by this process +// (type: local) or reachable over gRPC (type: remote). +type AttestorConfig struct { + // ChainID is the chain this attestor watches. + ChainID string `yaml:"chainId,omitempty"` + + // Name is the attestor's own self-reported identity. Not required unique. + Name string `yaml:"name"` + + Type AttestorType `yaml:"type"` + + // Signer required for type: local only -- the signer used to sign + // attestations. + Signer string `yaml:"signer"` + + // FinalityOffset local only. Zero attests up to the chain's "finalized" + // tag; n > 0 attests up to "latest" - n instead. + FinalityOffset uint `yaml:"finalityOffset"` + + // GRPC required for type: remote only. Bare host:port. + GRPC string `yaml:"grpc,omitempty"` +} + +// Signers is the list of configured signer backends. +type Signers []SignerConfig + +// SignerConfig represents a single signer configuration in the config. +type SignerConfig struct { + // Alias unique name for a signer + Alias string `yaml:"alias"` + + // Type [local, remote] + Type string `yaml:"type"` + + // File key file path for a local signer + File string `yaml:"file,omitempty"` + + // GRPC address for a remote signer + GRPC string `yaml:"grpc,omitempty"` + + // RemoteKeyID KMS key ID for a remote signer + RemoteKeyID string `yaml:"remoteKeyId,omitempty"` +} + +// ChainType the execution environment of a chain. +type ChainType string + +// Chain types +const ( + ChainTypeEVM ChainType = "evm" +) + +// ChainConfig chain information shared by the attestor and relayer. +type ChainConfig struct { + ChainID string `yaml:"chainId"` + EVM *EVMChainConfig `yaml:"evm,omitempty"` + + // Deployer optional signer alias used by `ibc deploy` for this chain. + Deployer string `yaml:"deployer,omitempty"` +} + +// Type returns the chain type implied by the configured settings. +func (c ChainConfig) Type() ChainType { + if c.EVM != nil { + return ChainTypeEVM + } + + return "" +} + +// EVMChainConfig EVM-specific chain details. +type EVMChainConfig struct { + RPC string `yaml:"rpc"` + ICS26Router string `yaml:"ics26Router"` +} + +// Default returns a config populated with the standard defaults. +func Default() Config { + return Config{ + Server: ServerConfig{ + ListenAddress: "0.0.0.0:3000", + }, + DB: DBConfig{ + Type: DBTypeSQLite, + URL: "ibc.db", + }, + Chains: []ChainConfig{}, + Relayer: RelayerConfig{ + ChainOverrides: []RelayerChainOverride{}, + Connections: []ConnectionConfig{}, + }, + Attestors: Attestors{}, + Signers: Signers{}, + } +} + +// ChainSignerPair one (chain, signer alias) pair a client end submits with. +type ChainSignerPair struct { + ChainID string + SignerAlias string +} + +// RelayerChainSignerPairs resolves the unique (chain, signer) pairs across +// every configured connection's two client ends. +func RelayerChainSignerPairs(c Config) []ChainSignerPair { + seen := make(map[ChainSignerPair]struct{}) + + var pairs []ChainSignerPair + + for _, conn := range c.Relayer.Connections { + for _, end := range []ClientEnd{conn.ClientA, conn.ClientB} { + pair := ChainSignerPair{ChainID: end.ChainID, SignerAlias: end.Signer} + if _, dup := seen[pair]; dup { + continue + } + + seen[pair] = struct{}{} + pairs = append(pairs, pair) + } + } + + return pairs +} + +func (c Config) Chain(chainID string) (ChainConfig, bool) { + for _, chain := range c.Chains { + if chain.ChainID == chainID { + return chain, true + } + } + + return ChainConfig{}, false +} + +// Signer returns the signer with the given alias. +func (c Config) Signer(alias string) (SignerConfig, bool) { + for _, signer := range c.Signers { + if signer.Alias == alias { + return signer, true + } + } + + return SignerConfig{}, false +} + +// AttestorByName returns the configured attestor with the given name. +func (c Config) AttestorByName(name string) (AttestorConfig, bool) { + for _, attestor := range c.Attestors { + if attestor.Name == name { + return attestor, true + } + } + + return AttestorConfig{}, false +} + +// AttestorsForChain returns every configured attestor watching chainID. +func (c Config) AttestorsForChain(chainID string) []AttestorConfig { + var attestors []AttestorConfig + for _, attestor := range c.Attestors { + if attestor.ChainID == chainID { + attestors = append(attestors, attestor) + } + } + + return attestors +} + +// Label returns a human-readable label for the DB config. +func (c DBConfig) Label() string { + if c.Type != DBTypeSQLite { + return c.Type + } + + // sqlite case + path := c.URL + if abs, err := filepath.Abs(path); err == nil { + path = abs + } + + return path +} + +// DBConfigFromURL infers DB type from a CLI database URL override. +func DBConfigFromURL(url string) (DBConfig, error) { + db := DBConfig{ + URL: url, + Type: dbTypeFromURL(url), + } + + return db, db.Validate() +} + +const finalityOffsetTODO = `TODO: set appropriately. 0 defaults to chain finality` + +// CollectComments builds TODO comments for every field in cfg that's left +// for the operator to fill in by hand, keyed by YAML path for +// MarshalYAMLWithComments. +func CollectComments(cfg Config) map[string]string { + comments := map[string]string{} + + for i, chain := range cfg.Chains { + if chain.EVM != nil && chain.EVM.ICS26Router == "" { + path := fmt.Sprintf("$.chains[%d].evm.ics26Router", i) + comments[path] = "TODO: fill in" + } + } + + for i, conn := range cfg.Relayer.Connections { + if conn.ClientA.Signer == "" { + path := fmt.Sprintf("$.relayer.connections[%d].clientA.signer", i) + comments[path] = "TODO: signers[] alias that submits relay txs on chainA" + } + if conn.ClientB.Signer == "" { + path := fmt.Sprintf("$.relayer.connections[%d].clientB.signer", i) + comments[path] = "TODO: signers[] alias that submits relay txs on chainB" + } + } + + for i, attestor := range cfg.Attestors { + if attestor.Type != AttestorTypeLocal { + continue + } + if attestor.Signer == "" { + path := fmt.Sprintf("$.attestors[%d].signer", i) + comments[path] = "TODO: signers[] alias backing this attestor's key" + } + path := fmt.Sprintf("$.attestors[%d].finalityOffset", i) + comments[path] = finalityOffsetTODO + } + + return comments +} + +func dbTypeFromURL(raw string) string { + if strings.HasPrefix(raw, "postgres://") || strings.HasPrefix(raw, "postgresql://") { + return DBTypePostgres + } + + return DBTypeSQLite +} diff --git a/link/internal/config/config_test.go b/link/config/config_test.go similarity index 95% rename from link/internal/config/config_test.go rename to link/config/config_test.go index bb54a1ba3..93e55ceb5 100644 --- a/link/internal/config/config_test.go +++ b/link/config/config_test.go @@ -47,7 +47,7 @@ func TestConfig(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - config := DefaultConfig() + config := Default() if tt.patch != nil { tt.patch(&config) } @@ -63,7 +63,7 @@ func TestConfig(t *testing.T) { } }) - t.Run("LoadFromFile", func(t *testing.T) { + t.Run("LoadFile", func(t *testing.T) { t.Run("valid", func(t *testing.T) { // ARRANGE path := writeTestConfig(t, ` @@ -75,7 +75,7 @@ db: `) // ACT - config, err := LoadFromFile(path, true, true) + config, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.NoError(t, err) @@ -93,7 +93,7 @@ server: `) // ACT - config, err := LoadFromFile(path, true, true) + config, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.NoError(t, err) @@ -108,7 +108,7 @@ server: `) // ACT - _, err := LoadFromFile(path, true, true) + _, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.Error(t, err) @@ -122,7 +122,7 @@ server: `) // ACT - _, err := LoadFromFile(path, true, true) + _, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.ErrorContains(t, err, "validation failed") @@ -134,7 +134,7 @@ server: path := filepath.Join(t.TempDir(), "missing.yml") // ACT - _, err := LoadFromFile(path, true, true) + _, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.Error(t, err) @@ -173,7 +173,7 @@ server: path := writeTestConfig(t, tt.body) // ACT - _, err := LoadFromFile(path, true, true) + _, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.ErrorContains(t, err, "unknown field") @@ -189,7 +189,7 @@ server: `) // ACT - config, err := LoadFromFile(path, true, false) + config, err := LoadFile(path, LoadOptions{}) // ASSERT require.NoError(t, err) @@ -212,7 +212,7 @@ attestors: `) // ACT - config, err := LoadFromFile(path, true, true) + config, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.NoError(t, err) @@ -236,7 +236,7 @@ attestors: `) // ACT - _, err := LoadFromFile(path, true, true) + _, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.ErrorContains(t, err, `references unknown signer: "missing-signer"`) @@ -569,7 +569,7 @@ func TestSignerConfigValidateRequiresExactLocalFilePath(t *testing.T) { } func TestChainDeployerCrossValidation(t *testing.T) { - base := DefaultConfig() + base := Default() base.Chains = []ChainConfig{ { ChainID: "1", diff --git a/link/internal/config/ibc.yml b/link/config/ibc.yml similarity index 100% rename from link/internal/config/ibc.yml rename to link/config/ibc.yml diff --git a/link/config/relayer.go b/link/config/relayer.go new file mode 100644 index 000000000..70929f1b0 --- /dev/null +++ b/link/config/relayer.go @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 + +package config + +import "time" + +// ClientType the light client type. +type ClientType string + +// Client types +const ( + ClientTypeAttestation ClientType = "attestation" +) + +// AttestorType how an attestor is reached. +type AttestorType string + +// Attestor types +const ( + AttestorTypeRemote AttestorType = "remote" + AttestorTypeLocal AttestorType = "local" +) + +// RelayerConfig the relayer block of the config. +type RelayerConfig struct { + DispatchPollInterval *time.Duration `yaml:"dispatchPollInterval,omitempty"` + ChainOverrides []RelayerChainOverride `yaml:"chainOverrides"` + Connections []ConnectionConfig `yaml:"connections"` +} + +// RelayerChainOverride relay settings for one chain. +type RelayerChainOverride struct { + ChainID string `yaml:"chainId"` + EVM *RelayerEVMConfig `yaml:"evm,omitempty"` + TxSubmissionDelay *time.Duration `yaml:"txSubmissionDelay,omitempty"` + PacketBatchSize *int `yaml:"packetBatchSize,omitempty"` + PacketBatchTimeout *time.Duration `yaml:"packetBatchTimeout,omitempty"` +} + +// RelayerEVMConfig EVM relaying settings. +type RelayerEVMConfig struct { + GasFeeCapMultiplier *float64 `yaml:"gasFeeCapMultiplier,omitempty"` + GasTipCapMultiplier *float64 `yaml:"gasTipCapMultiplier,omitempty"` +} + +// ConnectionConfig one bidirectional IBC connection the relayer actively +// relays, in both directions. ClientA's counterparty is simply ClientB (and +// vice versa). +type ConnectionConfig struct { + Alias string `yaml:"alias"` + ClientA ClientEnd `yaml:"clientA"` + ClientB ClientEnd `yaml:"clientB"` +} + +// ClientEnd one side of a connection: a light client on chainId, +// tracking the connection's other end as its counterparty +type ClientEnd struct { + ChainID string `yaml:"chainId"` + Signer string `yaml:"signer"` + ClientID string `yaml:"clientId"` + Type ClientType `yaml:"type"` + + // AutoRelay configures auto-relay for packets flowing FROM this end's + // chain TOWARD the counterparty end. + AutoRelay AutoRelayConfig `yaml:"autoRelay,omitempty"` +} + +// AutoRelayConfig automatic relaying settings. +type AutoRelayConfig struct { + Enabled *bool `yaml:"enabled,omitempty"` + // Lookback the number of blocks the relayer looks back from the latest + // block to check for packets to relay. + Lookback uint64 `yaml:"lookback,omitempty"` +} + +// ChainOverride returns the relay settings override for a chain. +func (c RelayerConfig) ChainOverride(chainID string) (RelayerChainOverride, bool) { + for _, override := range c.ChainOverrides { + if override.ChainID == chainID { + return override, true + } + } + + return RelayerChainOverride{}, false +} + +// ClientEnd returns the client end matching (chainID, clientID) in any +// configured connection, along with its counterparty +func (c RelayerConfig) ClientEnd(chainID, clientID string) (end, counterparty ClientEnd, ok bool) { + for _, conn := range c.Connections { + switch { + case conn.ClientA.ChainID == chainID && conn.ClientA.ClientID == clientID: + return conn.ClientA, conn.ClientB, true + case conn.ClientB.ChainID == chainID && conn.ClientB.ClientID == clientID: + return conn.ClientB, conn.ClientA, true + } + } + + return ClientEnd{}, ClientEnd{}, false +} diff --git a/link/internal/config/relayer_test.go b/link/config/relayer_test.go similarity index 97% rename from link/internal/config/relayer_test.go rename to link/config/relayer_test.go index 34ea644cd..59121d14a 100644 --- a/link/internal/config/relayer_test.go +++ b/link/config/relayer_test.go @@ -17,7 +17,7 @@ func TestRelayerConfig(t *testing.T) { path := filepath.Join("testdata", "sample.yml") // ACT - config, err := LoadFromFile(path, true, true) + config, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) // ASSERT require.NoError(t, err) @@ -60,7 +60,7 @@ func TestRelayerConfig(t *testing.T) { t.Run("Helpers", func(t *testing.T) { // ARRANGE path := filepath.Join("testdata", "sample.yml") - config, err := LoadFromFile(path, true, true) + config, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) require.NoError(t, err) // ACT / ASSERT @@ -260,7 +260,7 @@ func TestRelayerConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // ARRANGE path := filepath.Join("testdata", "sample.yml") - config, err := LoadFromFile(path, true, true) + config, err := LoadFile(path, LoadOptions{DisallowUnknownFields: true}) require.NoError(t, err) tt.patch(&config) diff --git a/link/internal/config/testdata/sample.yml b/link/config/testdata/sample.yml similarity index 100% rename from link/internal/config/testdata/sample.yml rename to link/config/testdata/sample.yml diff --git a/link/config/validate.go b/link/config/validate.go new file mode 100644 index 000000000..6d14abd11 --- /dev/null +++ b/link/config/validate.go @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "os" + "strings" + + "github.com/pkg/errors" + + "github.com/cosmos/ibc/link/internal/fsutil" + "github.com/cosmos/ibc/link/internal/network" +) + +func (c Config) Validate() error { + if err := c.Server.Validate(); err != nil { + return errors.Wrap(err, ".server") + } + + if err := c.DB.Validate(); err != nil { + return errors.Wrap(err, ".db") + } + + chainIDs := make(map[string]struct{}) + for _, chain := range c.Chains { + if err := chain.Validate(); err != nil { + return errors.Wrapf(err, ".chains[%s]", chain.ChainID) + } + + if _, ok := chainIDs[chain.ChainID]; ok { + return errors.Wrapf(errors.Errorf("duplicate chainId: %q", chain.ChainID), ".chains") + } + chainIDs[chain.ChainID] = struct{}{} + } + + if err := c.Relayer.Validate(); err != nil { + return errors.Wrap(err, ".relayer") + } + + if err := c.Attestors.Validate(); err != nil { + return errors.Wrap(err, ".attestors") + } + + if err := c.Signers.Validate(); err != nil { + return errors.Wrap(err, ".signers") + } + + return c.crossValidate() +} + +func (c Config) crossValidate() error { + signerSet := make(map[string]struct{}, len(c.Signers)) + for _, signer := range c.Signers { + signerSet[signer.Alias] = struct{}{} + } + + for i, a := range c.Attestors { + if a.Type != AttestorTypeLocal { + continue + } + if _, exists := signerSet[a.Signer]; !exists { + return errors.Errorf(".attestors[%d].signer references unknown signer: %q", i, a.Signer) + } + } + + for _, chain := range c.Chains { + if chain.Deployer == "" { + continue + } + if _, exists := signerSet[chain.Deployer]; !exists { + return errors.Errorf(".chains[%s].deployer references unknown signer: %q", chain.ChainID, chain.Deployer) + } + } + + if err := c.validateChainReferences(); err != nil { + return err + } + + if err := c.validateConnectionSigners(signerSet); err != nil { + return errors.Wrap(err, ".relayer.connections") + } + + return nil +} + +type namedClientEnd struct { + label string + cfg ClientEnd +} + +func connectionEnds(conn ConnectionConfig) []namedClientEnd { + return []namedClientEnd{{"clientA", conn.ClientA}, {"clientB", conn.ClientB}} +} + +// validateChainReferences ensures chains referenced by the relayer config are +// declared in the top-level chains block. +func (c Config) validateChainReferences() error { + for _, chain := range c.Relayer.ChainOverrides { + if _, ok := c.Chain(chain.ChainID); chain.ChainID != "" && !ok { + return errors.Errorf(".chainOverrides[%s] chainId not declared in top-level chains", chain.ChainID) + } + } + + for _, conn := range c.Relayer.Connections { + for _, end := range connectionEnds(conn) { + if _, ok := c.Chain(end.cfg.ChainID); end.cfg.ChainID != "" && !ok { + return errors.Errorf( + ".connections[%s].%s chainId %q not declared in top-level chains", + conn.Alias, end.label, end.cfg.ChainID, + ) + } + } + } + + return nil +} + +// validateConnectionSigners ensures every client end's signer resolves to a +// configured signer. +func (c Config) validateConnectionSigners(signerSet map[string]struct{}) error { + for _, conn := range c.Relayer.Connections { + for _, end := range connectionEnds(conn) { + if _, exists := signerSet[end.cfg.Signer]; !exists { + return errors.Errorf( + "connection %q %s references unknown signer %q", + conn.Alias, end.label, end.cfg.Signer, + ) + } + } + } + + return nil +} + +func (c ChainConfig) Validate() error { + if c.ChainID == "" { + return errors.New(".chainId required") + } + + if c.Type() == ChainTypeEVM && c.EVM.RPC == "" { + return errors.New(".evm.rpc required") + } + + return nil +} + +func (c ServerConfig) Validate() error { + if err := network.ValidateListenAddr(c.ListenAddress); err != nil { + return errors.Wrapf(err, ".listenAddr %q", c.ListenAddress) + } + + return nil +} + +func (c DBConfig) Validate() error { + switch { + case c.Type != DBTypeSQLite && c.Type != DBTypePostgres: + return errors.Errorf(".type must be one of [%q, %q], got %q", DBTypeSQLite, DBTypePostgres, c.Type) + case c.Type == DBTypeSQLite && c.URL == sqliteInMemory: + return errors.New(".url must not be :memory: for sqlite") + case c.URL == "": + return errors.New(".url must not be empty") + } + + return nil +} + +// Validate validates the attestors list. Allows empty. +func (a Attestors) Validate() error { + localNames := make(map[string]struct{}) + // keyed by chainId+signer: the same signer backing one operator's local + // attestor on two different chains is fine, but reusing it for two + // attestors on the same chain is always a redundant duplicate. + localChainSigners := make(map[string]struct{}) + for i, attestor := range a { + if err := attestor.Validate(); err != nil { + return errors.Wrapf(err, "[%d]", i) + } + + if attestor.Type != AttestorTypeLocal { + continue + } + + if _, exists := localNames[attestor.Name]; exists { + return errors.Errorf("duplicate local attestor name: %q", attestor.Name) + } + localNames[attestor.Name] = struct{}{} + + chainSigner := attestor.ChainID + "/" + attestor.Signer + if _, exists := localChainSigners[chainSigner]; exists { + return errors.Errorf("duplicate local attestor signer %q on chain %q", attestor.Signer, attestor.ChainID) + } + localChainSigners[chainSigner] = struct{}{} + } + + return nil +} + +func (c AttestorConfig) Validate() error { + switch { + case c.Name == "": + return errors.New(".name required") + case c.Type != AttestorTypeLocal && c.Type != AttestorTypeRemote: + return errors.Errorf(".type unknown attestor type: %q", c.Type) + } + + switch c.Type { + case AttestorTypeLocal: + switch { + case c.ChainID == "": + return errors.New(".chainId required for local attestors") + case c.Signer == "": + return errors.New(".signer required for local attestors") + case c.GRPC != "": + return errors.New(".grpc must not be set for local attestors") + } + case AttestorTypeRemote: + switch { + case c.GRPC == "": + return errors.New(".grpc required for remote attestors") + case strings.Contains(c.GRPC, "://"): + return errors.Errorf(".grpc must be a bare host:port, not a URL: %q", c.GRPC) + case c.ChainID != "": + return errors.New(".chainId must not be set for remote attestors") + case c.Signer != "": + return errors.New(".signer must not be set for remote attestors") + case c.FinalityOffset != 0: + return errors.New(".finalityOffset must not be set for remote attestors") + } + } + + return nil +} + +func (c Signers) Validate() error { + set := make(map[string]struct{}) + + for i, signer := range c { + if err := signer.Validate(); err != nil { + return errors.Wrapf(err, ".signers[%d]", i) + } + + if _, exists := set[signer.Alias]; exists { + return errors.Errorf(".signers duplicate alias: %q", signer.Alias) + } + + set[signer.Alias] = struct{}{} + } + + return nil +} + +func (c SignerConfig) Validate() error { + switch { + case c.Alias == "": + return errors.New(".alias required") + case c.Type == "": + return errors.New(".type required") + case c.Type != SignerLocal && c.Type != SignerRemote: + return errors.Errorf(".type must be one of [%q, %q], got %q", SignerLocal, SignerRemote, c.Type) + case c.Type == SignerLocal && c.File == "": + return errors.New(".file required for local signer") + case c.Type == SignerRemote && c.GRPC == "": + return errors.New(".grpc required for remote signer") + case c.Type == SignerRemote && c.RemoteKeyID == "": + return errors.New(".remoteKeyId required for remote signer") + } + + if c.Type == SignerLocal { + path, err := fsutil.ExpandHome(c.File) + if err != nil { + return errors.Wrap(err, ".file") + } + + fallbacks := fsutil.KeyFileFallbacks(path) + + if err := fileExistsInAny(fallbacks...); err != nil { + return errors.Wrapf(err, ".file %s", path) + } + } + + return nil +} + +func fileExistsInAny(path ...string) error { + for _, p := range path { + if err := fileExists(p); err == nil { + return nil + } + } + + return errors.New("file not found") +} + +func fileExists(path string) error { + info, err := os.Stat(path) + if err != nil { + return err + } + + if info.IsDir() { + return errors.Errorf("path is a directory") + } + + return nil +} + +// Validate validates the relayer config. Allows empty blocks. +func (c RelayerConfig) Validate() error { + if c.DispatchPollInterval != nil && *c.DispatchPollInterval <= 0 { + return errors.New(".dispatchPollInterval must be positive") + } + if err := c.validateChainOverrides(); err != nil { + return err + } + + return c.validateConnections() +} + +func (c RelayerConfig) validateChainOverrides() error { + chainIDs := make(map[string]struct{}) + + for _, chain := range c.ChainOverrides { + if err := chain.Validate(); err != nil { + return errors.Wrapf(err, ".chainOverrides[%s]", chain.ChainID) + } + + if _, ok := chainIDs[chain.ChainID]; ok { + return errors.Errorf(".chainOverrides duplicate chainId: %q", chain.ChainID) + } + chainIDs[chain.ChainID] = struct{}{} + } + + return nil +} + +func (c RelayerConfig) validateConnections() error { + aliases := make(map[string]struct{}) + clientEnds := make(map[string]struct{}) + + for _, conn := range c.Connections { + if err := conn.Validate(); err != nil { + return errors.Wrapf(err, ".connections[%s]", conn.Alias) + } + + if _, ok := aliases[conn.Alias]; ok { + return errors.Errorf(".connections duplicate alias: %q", conn.Alias) + } + aliases[conn.Alias] = struct{}{} + + for _, end := range []ClientEnd{conn.ClientA, conn.ClientB} { + key := end.ChainID + "/" + end.ClientID + if _, ok := clientEnds[key]; ok { + return errors.Errorf(".connections duplicate client %q on chain %q", end.ClientID, end.ChainID) + } + clientEnds[key] = struct{}{} + } + } + + return nil +} + +func (c ConnectionConfig) Validate() error { + if c.Alias == "" { + return errors.New(".alias required") + } + + if err := c.ClientA.Validate(); err != nil { + return errors.Wrap(err, ".clientA") + } + if err := c.ClientB.Validate(); err != nil { + return errors.Wrap(err, ".clientB") + } + + if c.ClientA.ChainID != "" && c.ClientA.ChainID == c.ClientB.ChainID { + return errors.New(".clientA and .clientB must be on different chains") + } + + return nil +} + +func (c ClientEnd) Validate() error { + switch { + case c.ChainID == "": + return errors.New(".chainId required") + case c.ClientID == "": + return errors.New(".clientId required") + case c.Signer == "": + return errors.New(".signer required") + case c.Type != ClientTypeAttestation: + return errors.Errorf(".type unknown client type: %q", c.Type) + } + + return nil +} + +func (c RelayerChainOverride) Validate() error { + switch { + case c.ChainID == "": + return errors.New(".chainId required") + case c.TxSubmissionDelay != nil && *c.TxSubmissionDelay < 0: + return errors.New(".txSubmissionDelay must not be negative") + case c.PacketBatchSize != nil && *c.PacketBatchSize <= 0: + return errors.New(".packetBatchSize must be positive") + case c.PacketBatchTimeout != nil && *c.PacketBatchTimeout <= 0: + return errors.New(".packetBatchTimeout must be positive") + } + + if c.EVM != nil { + if err := c.EVM.Validate(); err != nil { + return errors.Wrap(err, ".evm") + } + } + + return nil +} + +func (c RelayerEVMConfig) Validate() error { + switch { + case c.GasFeeCapMultiplier != nil && *c.GasFeeCapMultiplier <= 0: + return errors.New(".gasFeeCapMultiplier must be positive") + case c.GasTipCapMultiplier != nil && *c.GasTipCapMultiplier <= 0: + return errors.New(".gasTipCapMultiplier must be positive") + } + + return nil +} diff --git a/link/config/yaml.go b/link/config/yaml.go new file mode 100644 index 000000000..60abedd17 --- /dev/null +++ b/link/config/yaml.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "os" + + "github.com/goccy/go-yaml" + "github.com/pkg/errors" +) + +// LoadOptions controls config decoding and validation. +type LoadOptions struct { + SkipValidation bool + DisallowUnknownFields bool +} + +// LoadFile loads a config file after expanding environment variables. +func LoadFile(path string, opts LoadOptions) (Config, error) { + config := Default() + + bz, err := os.ReadFile(path) + if err != nil { + return Config{}, err + } + + expanded := os.ExpandEnv(string(bz)) + + decodeOpts := []yaml.DecodeOption{} + if opts.DisallowUnknownFields { + decodeOpts = append(decodeOpts, yaml.DisallowUnknownField()) + } + + if err := yaml.UnmarshalWithOptions([]byte(expanded), &config, decodeOpts...); err != nil { + return Config{}, err + } + + if !opts.SkipValidation { + if err := config.Validate(); err != nil { + return Config{}, errors.Wrap(err, "validation failed") + } + } + + return config, nil +} + +// MarshalYAML encodes v using Link's canonical YAML codec. +func MarshalYAML(v any) ([]byte, error) { + return yaml.Marshal(v) +} + +// MarshalYAMLWithComments encodes v with line comments keyed by YAML path. +func MarshalYAMLWithComments(v any, comments map[string]string) ([]byte, error) { + cm := make(yaml.CommentMap, len(comments)) + for path, text := range comments { + cm[path] = []*yaml.Comment{yaml.LineComment(" " + text)} + } + return yaml.MarshalWithOptions(v, yaml.WithComment(cm)) +} diff --git a/link/internal/bootstrap/bootstrap.go b/link/internal/bootstrap/bootstrap.go index 3abb7c57f..d09c63422 100644 --- a/link/internal/bootstrap/bootstrap.go +++ b/link/internal/bootstrap/bootstrap.go @@ -6,8 +6,8 @@ import ( "context" "log/slog" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/dispatch" "github.com/cosmos/ibc/link/internal/relay/pipeline" "github.com/cosmos/ibc/link/internal/relay/proofgen" diff --git a/link/internal/chains/client.go b/link/internal/chains/client.go index b22c8a29e..366300458 100644 --- a/link/internal/chains/client.go +++ b/link/internal/chains/client.go @@ -8,8 +8,8 @@ import ( "github.com/pkg/errors" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains/evm" - "github.com/cosmos/ibc/link/internal/config" v2 "github.com/cosmos/ibc/link/internal/types/v2" ) diff --git a/link/internal/config/config.go b/link/internal/config/config.go deleted file mode 100644 index 016aba5ab..000000000 --- a/link/internal/config/config.go +++ /dev/null @@ -1,738 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Package config contains config and flag parsing logic -package config - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/goccy/go-yaml" - "github.com/pkg/errors" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - - "github.com/cosmos/ibc/link/internal/network" -) - -// Database type -const ( - DBTypeSQLite = "sqlite" - DBTypePostgres = "postgres" -) - -// Signer type. Local represents a private key file. Remote connects to cosmos/KMS. -const ( - SignerLocal = "local" - SignerRemote = "remote" -) - -const sqliteInMemory = ":memory:" - -// Config represents a config file -// Should only contain `camelCase` keywords -type Config struct { - Server ServerConfig `yaml:"server"` - DB DBConfig `yaml:"db"` - Chains []ChainConfig `yaml:"chains"` - Relayer RelayerConfig `yaml:"relayer"` - Attestors Attestors `yaml:"attestors"` - Signers Signers `yaml:"signers"` -} - -// ServerConfig config for RPC server for both relayer and attestor -type ServerConfig struct { - ListenAddress string `yaml:"listenAddr"` -} - -// DBConfig config for database storage. -type DBConfig struct { - Type string `yaml:"type"` - URL string `yaml:"url"` -} - -// Attestors is the list of attestors, used both by the relayer -// (to resolve who to query) and the attestor binary (to know what it -// serves locally). -type Attestors []AttestorConfig - -// AttestorConfig describes one attestor, either run by this process -// (type: local) or reachable over gRPC (type: remote). -type AttestorConfig struct { - // ChainID is the chain this attestor watches. - ChainID string `yaml:"chainId,omitempty"` - - // Name is the attestor's own self-reported identity. Not required unique. - Name string `yaml:"name"` - - Type AttestorType `yaml:"type"` - - // Signer required for type: local only -- the signer used to sign - // attestations. - Signer string `yaml:"signer"` - - // FinalityOffset local only. Zero attests up to the chain's "finalized" - // tag; n > 0 attests up to "latest" - n instead. - FinalityOffset uint `yaml:"finalityOffset"` - - // GRPC required for type: remote only. Bare host:port. - GRPC string `yaml:"grpc,omitempty"` -} - -// Signers is the list of configured signer backends. -type Signers []SignerConfig - -// SignerConfig represents a single signer configuration in the config. -type SignerConfig struct { - // Alias unique name for a signer - Alias string `yaml:"alias"` - - // Type [local, remote] - Type string `yaml:"type"` - - // File key file path for a local signer - File string `yaml:"file,omitempty"` - - // GRPC address for a remote signer - GRPC string `yaml:"grpc,omitempty"` - - // RemoteKeyID KMS key ID for a remote signer - RemoteKeyID string `yaml:"remoteKeyId,omitempty"` -} - -// ChainType the execution environment of a chain. -type ChainType string - -// Chain types -const ( - ChainTypeEVM ChainType = "evm" -) - -// ChainConfig chain information shared by the attestor and relayer. -type ChainConfig struct { - ChainID string `yaml:"chainId"` - EVM *EVMChainConfig `yaml:"evm,omitempty"` - - // Deployer optional signer alias used by `ibc deploy` for this chain. - Deployer string `yaml:"deployer,omitempty"` -} - -// Type returns the chain type implied by the configured settings. -func (c ChainConfig) Type() ChainType { - if c.EVM != nil { - return ChainTypeEVM - } - - return "" -} - -// EVMChainConfig EVM-specific chain details. -type EVMChainConfig struct { - RPC string `yaml:"rpc"` - ICS26Router string `yaml:"ics26Router"` -} - -// DefaultConfig sample config using default values and Sqlite. -func DefaultConfig() Config { - return Config{ - Server: ServerConfig{ - ListenAddress: "0.0.0.0:3000", - }, - DB: DBConfig{ - Type: DBTypeSQLite, - URL: "ibc.db", - }, - Chains: []ChainConfig{}, - Relayer: RelayerConfig{ - ChainOverrides: []RelayerChainOverride{}, - Connections: []ConnectionConfig{}, - }, - Attestors: Attestors{}, - Signers: Signers{}, - } -} - -// LoadFromFile loads Config from file with optional validation. -// Note: supports ENV variables expansion! -func LoadFromFile(path string, validate, restrictUnknownFields bool) (Config, error) { - config := DefaultConfig() - - bz, err := os.ReadFile(path) - if err != nil { - return Config{}, err - } - - // substitute ENV variables - expanded := os.ExpandEnv(string(bz)) - - opts := []yaml.DecodeOption{} - if restrictUnknownFields { - opts = append(opts, yaml.DisallowUnknownField()) - } - - err = yaml.UnmarshalWithOptions([]byte(expanded), &config, opts...) - if err != nil { - return Config{}, err - } - - if validate { - if err := config.Validate(); err != nil { - return Config{}, errors.Wrap(err, "validation failed") - } - } - - return config, nil -} - -func (c Config) Validate() error { - if err := c.Server.Validate(); err != nil { - return errors.Wrap(err, ".server") - } - - if err := c.DB.Validate(); err != nil { - return errors.Wrap(err, ".db") - } - - chainIDs := make(map[string]struct{}) - for _, chain := range c.Chains { - if err := chain.Validate(); err != nil { - return errors.Wrapf(err, ".chains[%s]", chain.ChainID) - } - - if _, ok := chainIDs[chain.ChainID]; ok { - return errors.Wrapf(errors.Errorf("duplicate chainId: %q", chain.ChainID), ".chains") - } - chainIDs[chain.ChainID] = struct{}{} - } - - if err := c.Relayer.Validate(); err != nil { - return errors.Wrap(err, ".relayer") - } - - if err := c.Attestors.Validate(); err != nil { - return errors.Wrap(err, ".attestors") - } - - if err := c.Signers.Validate(); err != nil { - return errors.Wrap(err, ".signers") - } - - return c.crossValidate() -} - -func (c Config) crossValidate() error { - signerSet := make(map[string]struct{}, len(c.Signers)) - for _, signer := range c.Signers { - signerSet[signer.Alias] = struct{}{} - } - - for i, a := range c.Attestors { - if a.Type != AttestorTypeLocal { - continue - } - if _, exists := signerSet[a.Signer]; !exists { - return errors.Errorf(".attestors[%d].signer references unknown signer: %q", i, a.Signer) - } - } - - for _, chain := range c.Chains { - if chain.Deployer == "" { - continue - } - if _, exists := signerSet[chain.Deployer]; !exists { - return errors.Errorf(".chains[%s].deployer references unknown signer: %q", chain.ChainID, chain.Deployer) - } - } - - if err := c.validateChainReferences(); err != nil { - return err - } - - if err := c.validateConnectionSigners(signerSet); err != nil { - return errors.Wrap(err, ".relayer.connections") - } - - return nil -} - -type namedClientEnd struct { - label string - cfg ClientEnd -} - -func connectionEnds(conn ConnectionConfig) []namedClientEnd { - return []namedClientEnd{{"clientA", conn.ClientA}, {"clientB", conn.ClientB}} -} - -// ChainSignerPair one (chain, signer alias) pair a client end submits with. -type ChainSignerPair struct { - ChainID string - SignerAlias string -} - -// RelayerChainSignerPairs resolves the unique (chain, signer) pairs across -// every configured connection's two client ends. -func RelayerChainSignerPairs(c Config) []ChainSignerPair { - seen := make(map[ChainSignerPair]struct{}) - - var pairs []ChainSignerPair - - for _, conn := range c.Relayer.Connections { - for _, end := range []ClientEnd{conn.ClientA, conn.ClientB} { - pair := ChainSignerPair{ChainID: end.ChainID, SignerAlias: end.Signer} - if _, dup := seen[pair]; dup { - continue - } - - seen[pair] = struct{}{} - pairs = append(pairs, pair) - } - } - - return pairs -} - -// validateChainReferences ensures chains referenced by the relayer config are -// declared in the top-level chains block. -func (c Config) validateChainReferences() error { - for _, chain := range c.Relayer.ChainOverrides { - if _, ok := c.Chain(chain.ChainID); chain.ChainID != "" && !ok { - return errors.Errorf(".chainOverrides[%s] chainId not declared in top-level chains", chain.ChainID) - } - } - - for _, conn := range c.Relayer.Connections { - for _, end := range connectionEnds(conn) { - if _, ok := c.Chain(end.cfg.ChainID); end.cfg.ChainID != "" && !ok { - return errors.Errorf( - ".connections[%s].%s chainId %q not declared in top-level chains", - conn.Alias, end.label, end.cfg.ChainID, - ) - } - } - } - - return nil -} - -// validateConnectionSigners ensures every client end's signer resolves to a -// configured signer. -func (c Config) validateConnectionSigners(signerSet map[string]struct{}) error { - for _, conn := range c.Relayer.Connections { - for _, end := range connectionEnds(conn) { - if _, exists := signerSet[end.cfg.Signer]; !exists { - return errors.Errorf( - "connection %q %s references unknown signer %q", - conn.Alias, end.label, end.cfg.Signer, - ) - } - } - } - - return nil -} - -func (c Config) Chain(chainID string) (ChainConfig, bool) { - for _, chain := range c.Chains { - if chain.ChainID == chainID { - return chain, true - } - } - - return ChainConfig{}, false -} - -// Signer returns the signer with the given alias. -func (c Config) Signer(alias string) (SignerConfig, bool) { - for _, signer := range c.Signers { - if signer.Alias == alias { - return signer, true - } - } - - return SignerConfig{}, false -} - -// AttestorByName returns the configured attestor with the given name. -func (c Config) AttestorByName(name string) (AttestorConfig, bool) { - for _, attestor := range c.Attestors { - if attestor.Name == name { - return attestor, true - } - } - - return AttestorConfig{}, false -} - -// AttestorsForChain returns every configured attestor watching chainID. -func (c Config) AttestorsForChain(chainID string) []AttestorConfig { - var attestors []AttestorConfig - for _, attestor := range c.Attestors { - if attestor.ChainID == chainID { - attestors = append(attestors, attestor) - } - } - - return attestors -} - -func (c ChainConfig) Validate() error { - if c.ChainID == "" { - return errors.New(".chainId required") - } - - if c.Type() == ChainTypeEVM && c.EVM.RPC == "" { - return errors.New(".evm.rpc required") - } - - return nil -} - -func (c Config) StoreToFile(path string) error { - return c.store(path, nil) -} - -// StoreToFileWithComments writes c to path as YAML, with a TODO comment -// attached to every field CollectComments flags as left for the operator to -// fill in. -func (c Config) StoreToFileWithComments(path string) error { - return c.store(path, CollectComments(c)) -} - -func (c Config) store(path string, comments map[string]string) error { - if err := EnsureDirectory(path); err != nil { - return err - } - - bz, err := yaml.MarshalWithOptions(c, yaml.WithComment(toCommentMap(comments))) - if err != nil { - return err - } - - return os.WriteFile(path, bz, 0o644) -} - -// toCommentMap converts comments (YAML path -> text) into a yaml.CommentMap -// of line comments, as PrintYAMLWithComments/store both need. -func toCommentMap(comments map[string]string) yaml.CommentMap { - cm := make(yaml.CommentMap, len(comments)) - for path, text := range comments { - cm[path] = []*yaml.Comment{yaml.LineComment(" " + text)} - } - return cm -} - -func (c ServerConfig) Validate() error { - if err := network.ValidateListenAddr(c.ListenAddress); err != nil { - return errors.Wrapf(err, ".listenAddr %q", c.ListenAddress) - } - - return nil -} - -func (c DBConfig) Validate() error { - switch { - case c.Type != DBTypeSQLite && c.Type != DBTypePostgres: - return errors.Errorf(".type must be one of [%q, %q], got %q", DBTypeSQLite, DBTypePostgres, c.Type) - case c.Type == DBTypeSQLite && c.URL == sqliteInMemory: - return errors.New(".url must not be :memory: for sqlite") - case c.URL == "": - return errors.New(".url must not be empty") - } - - return nil -} - -// Label returns a human-readable label for the DB config. -func (c DBConfig) Label() string { - if c.Type != DBTypeSQLite { - return c.Type - } - - // sqlite case - path := c.URL - if abs, err := filepath.Abs(path); err == nil { - path = abs - } - - return path -} - -// DBConfigFromURL infers DB type from a CLI database URL override. -func DBConfigFromURL(url string) (DBConfig, error) { - db := DBConfig{ - URL: url, - Type: dbTypeFromURL(url), - } - - return db, db.Validate() -} - -// Validate validates the attestors list. Allows empty. -func (a Attestors) Validate() error { - localNames := make(map[string]struct{}) - // keyed by chainId+signer: the same signer backing one operator's local - // attestor on two different chains is fine, but reusing it for two - // attestors on the same chain is always a redundant duplicate. - localChainSigners := make(map[string]struct{}) - for i, attestor := range a { - if err := attestor.Validate(); err != nil { - return errors.Wrapf(err, "[%d]", i) - } - - if attestor.Type != AttestorTypeLocal { - continue - } - - if _, exists := localNames[attestor.Name]; exists { - return errors.Errorf("duplicate local attestor name: %q", attestor.Name) - } - localNames[attestor.Name] = struct{}{} - - chainSigner := attestor.ChainID + "/" + attestor.Signer - if _, exists := localChainSigners[chainSigner]; exists { - return errors.Errorf("duplicate local attestor signer %q on chain %q", attestor.Signer, attestor.ChainID) - } - localChainSigners[chainSigner] = struct{}{} - } - - return nil -} - -func (c AttestorConfig) Validate() error { - switch { - case c.Name == "": - return errors.New(".name required") - case c.Type != AttestorTypeLocal && c.Type != AttestorTypeRemote: - return errors.Errorf(".type unknown attestor type: %q", c.Type) - } - - switch c.Type { - case AttestorTypeLocal: - switch { - case c.ChainID == "": - return errors.New(".chainId required for local attestors") - case c.Signer == "": - return errors.New(".signer required for local attestors") - case c.GRPC != "": - return errors.New(".grpc must not be set for local attestors") - } - case AttestorTypeRemote: - switch { - case c.GRPC == "": - return errors.New(".grpc required for remote attestors") - case strings.Contains(c.GRPC, "://"): - return errors.Errorf(".grpc must be a bare host:port, not a URL: %q", c.GRPC) - case c.ChainID != "": - return errors.New(".chainId must not be set for remote attestors") - case c.Signer != "": - return errors.New(".signer must not be set for remote attestors") - case c.FinalityOffset != 0: - return errors.New(".finalityOffset must not be set for remote attestors") - } - } - - return nil -} - -func (c Signers) Validate() error { - set := make(map[string]struct{}) - - for i, signer := range c { - if err := signer.Validate(); err != nil { - return errors.Wrapf(err, ".signers[%d]", i) - } - - if _, exists := set[signer.Alias]; exists { - return errors.Errorf(".signers duplicate alias: %q", signer.Alias) - } - - set[signer.Alias] = struct{}{} - } - - return nil -} - -func (c SignerConfig) Validate() error { - switch { - case c.Alias == "": - return errors.New(".alias required") - case c.Type == "": - return errors.New(".type required") - case c.Type != SignerLocal && c.Type != SignerRemote: - return errors.Errorf(".type must be one of [%q, %q], got %q", SignerLocal, SignerRemote, c.Type) - case c.Type == SignerLocal && c.File == "": - return errors.New(".file required for local signer") - case c.Type == SignerRemote && c.GRPC == "": - return errors.New(".grpc required for remote signer") - case c.Type == SignerRemote && c.RemoteKeyID == "": - return errors.New(".remoteKeyId required for remote signer") - } - - if c.Type == SignerLocal { - path, err := ExpandHome(c.File) - if err != nil { - return errors.Wrap(err, ".file") - } - - fallbacks := KeyFileFallbacks(path) - - if err := fileExistsInAny(fallbacks...); err != nil { - return errors.Wrapf(err, ".file %s", path) - } - } - - return nil -} - -// KeyFileFallbacks returns the paths tried for a local signer key file. -func KeyFileFallbacks(keyPath string) []string { - fallbacks := []string{keyPath} - - // absolute path, no fallbacks needed - if filepath.IsAbs(keyPath) { - return fallbacks - } - - // forgot to add .json extension - if !strings.HasSuffix(keyPath, ".json") { - keyPath = fmt.Sprintf("%s.json", keyPath) - - fallbacks = append(fallbacks, keyPath) - } - - // forgot to add keys/ directory - if !strings.Contains(keyPath, "keys/") { - keyPath = filepath.Join("keys", keyPath) - - fallbacks = append(fallbacks, keyPath) - } - - return fallbacks -} - -// PrintJSON prints anything as JSON to stdout. -func PrintJSON(v any) error { - bz, err := json.MarshalIndent(v, "", " ") - if err != nil { - return err - } - - fmt.Println(string(bz)) - - return nil -} - -// PrintProtoJSON prints a protobuf message as JSON to stdout, rendering enum -// fields by name -func PrintProtoJSON(msg proto.Message) error { - bz, err := protojson.MarshalOptions{Indent: " ", UseProtoNames: true, EmitUnpopulated: true}.Marshal(msg) - if err != nil { - return err - } - - fmt.Println(string(bz)) - - return nil -} - -// PrintYAML prints anything as YAML to stdout. -func PrintYAML(v any) error { - bz, err := yaml.Marshal(v) - if err != nil { - return err - } - - fmt.Println(string(bz)) - - return nil -} - -// PrintYAMLWithComments prints v as YAML to stdout, attaching a line comment -// to every field addressed by a YAML path in comments, keyed as -// "$.relayer.connections[0].clientA.signer". -func PrintYAMLWithComments(v any, comments map[string]string) error { - bz, err := yaml.MarshalWithOptions(v, yaml.WithComment(toCommentMap(comments))) - if err != nil { - return err - } - - fmt.Println(string(bz)) - - return nil -} - -const finalityOffsetTODO = `TODO: set appropriately. 0 defaults to chain finality` - -// CollectComments builds TODO comments for every field in cfg that's left -// for the operator to fill in by hand, keyed by YAML path for -// PrintYAMLWithComments/StoreToFileWithComments. -func CollectComments(cfg Config) map[string]string { - comments := map[string]string{} - - for i, chain := range cfg.Chains { - if chain.EVM != nil && chain.EVM.ICS26Router == "" { - path := fmt.Sprintf("$.chains[%d].evm.ics26Router", i) - comments[path] = "TODO: fill in" - } - } - - for i, conn := range cfg.Relayer.Connections { - if conn.ClientA.Signer == "" { - path := fmt.Sprintf("$.relayer.connections[%d].clientA.signer", i) - comments[path] = "TODO: signers[] alias that submits relay txs on chainA" - } - if conn.ClientB.Signer == "" { - path := fmt.Sprintf("$.relayer.connections[%d].clientB.signer", i) - comments[path] = "TODO: signers[] alias that submits relay txs on chainB" - } - } - - for i, attestor := range cfg.Attestors { - if attestor.Type != AttestorTypeLocal { - continue - } - if attestor.Signer == "" { - path := fmt.Sprintf("$.attestors[%d].signer", i) - comments[path] = "TODO: signers[] alias backing this attestor's key" - } - path := fmt.Sprintf("$.attestors[%d].finalityOffset", i) - comments[path] = finalityOffsetTODO - } - - return comments -} - -func dbTypeFromURL(raw string) string { - if strings.HasPrefix(raw, "postgres://") || strings.HasPrefix(raw, "postgresql://") { - return DBTypePostgres - } - - return DBTypeSQLite -} - -func fileExistsInAny(path ...string) error { - for _, p := range path { - if err := fileExists(p); err == nil { - return nil - } - } - - return errors.New("file not found") -} - -func fileExists(path string) error { - info, err := os.Stat(path) - if err != nil { - return err - } - - if info.IsDir() { - return errors.Errorf("path is a directory") - } - - return nil -} diff --git a/link/internal/config/flags.go b/link/internal/config/flags.go deleted file mode 100644 index 70b0992d6..000000000 --- a/link/internal/config/flags.go +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "os" - "path/filepath" - "strings" - - "github.com/spf13/cobra" -) - -// FlagSet composed set of cli args parsed into a nice struct. -type FlagSet struct { - // Home IBC home directory where files are stored - Home string - Config string - DB string - Quiet bool - LogJSON bool - - skipConfigValidation bool -} - -// DefaultFlagSet returns the default flag set. -func DefaultFlagSet() FlagSet { - return FlagSet{ - Home: "~/.ibc", - Config: "ibc.yml", - DB: "", - Quiet: false, - - skipConfigValidation: false, - } -} - -// DeclarePersistentFlags declares the persistent flags for the command. -func DeclarePersistentFlags(cmd *cobra.Command, flags *FlagSet) { - pf := cmd.PersistentFlags() - - pf.StringVarP(&flags.Home, "home", "", flags.Home, "IBC home directory") - pf.StringVarP(&flags.Config, "config", "", flags.Config, "Config file relative to home") - pf.StringVarP(&flags.DB, "db", "", flags.DB, "Database URL override") - pf.BoolVarP(&flags.Quiet, "quiet", "q", flags.Quiet, "Quiet mode") - pf.BoolVarP(&flags.LogJSON, "log-json", "", flags.LogJSON, "Enable JSON logging") -} - -func (fs *FlagSet) ConfigPath() (string, error) { - home, err := ExpandHome(fs.Home) - if err != nil { - return "", err - } - - return filepath.Abs(filepath.Join(home, fs.Config)) -} - -func (fs *FlagSet) ValidateConfig() bool { - return !fs.skipConfigValidation -} - -func (fs *FlagSet) SkipConfigValidation() { - fs.skipConfigValidation = true -} - -// ExpandHome converts path with ~ to an absolute path. -func ExpandHome(path string) (string, error) { - if path != "~" && !strings.HasPrefix(path, "~/") { - return path, nil - } - - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - - if path == "~" { - return home, nil - } - - return filepath.Join(home, strings.TrimPrefix(path, "~/")), nil -} - -// Given a path to a file or a directory, ensure the directory exists. -func EnsureDirectory(directoryOrFile string) error { - dir := filepath.Dir(directoryOrFile) - if dir == "." { - return nil - } - - return os.MkdirAll(dir, 0o755) -} diff --git a/link/internal/config/relayer.go b/link/internal/config/relayer.go deleted file mode 100644 index e0ea9e183..000000000 --- a/link/internal/config/relayer.go +++ /dev/null @@ -1,225 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package config - -import ( - "time" - - "github.com/pkg/errors" -) - -// ClientType the light client type. -type ClientType string - -// Client types -const ( - ClientTypeAttestation ClientType = "attestation" -) - -// AttestorType how an attestor is reached. -type AttestorType string - -// Attestor types -const ( - AttestorTypeRemote AttestorType = "remote" - AttestorTypeLocal AttestorType = "local" -) - -// RelayerConfig the relayer block of the config. -type RelayerConfig struct { - DispatchPollInterval *time.Duration `yaml:"dispatchPollInterval,omitempty"` - ChainOverrides []RelayerChainOverride `yaml:"chainOverrides"` - Connections []ConnectionConfig `yaml:"connections"` -} - -// RelayerChainOverride relay settings for one chain. -type RelayerChainOverride struct { - ChainID string `yaml:"chainId"` - EVM *RelayerEVMConfig `yaml:"evm,omitempty"` - TxSubmissionDelay *time.Duration `yaml:"txSubmissionDelay,omitempty"` - PacketBatchSize *int `yaml:"packetBatchSize,omitempty"` - PacketBatchTimeout *time.Duration `yaml:"packetBatchTimeout,omitempty"` -} - -// RelayerEVMConfig EVM relaying settings. -type RelayerEVMConfig struct { - GasFeeCapMultiplier *float64 `yaml:"gasFeeCapMultiplier,omitempty"` - GasTipCapMultiplier *float64 `yaml:"gasTipCapMultiplier,omitempty"` -} - -// ConnectionConfig one bidirectional IBC connection the relayer actively -// relays, in both directions. ClientA's counterparty is simply ClientB (and -// vice versa). -type ConnectionConfig struct { - Alias string `yaml:"alias"` - ClientA ClientEnd `yaml:"clientA"` - ClientB ClientEnd `yaml:"clientB"` -} - -// ClientEnd one side of a connection: a light client on chainId, -// tracking the connection's other end as its counterparty -type ClientEnd struct { - ChainID string `yaml:"chainId"` - Signer string `yaml:"signer"` - ClientID string `yaml:"clientId"` - Type ClientType `yaml:"type"` - - // AutoRelay configures auto-relay for packets flowing FROM this end's - // chain TOWARD the counterparty end. - AutoRelay AutoRelayConfig `yaml:"autoRelay,omitempty"` -} - -// AutoRelayConfig automatic relaying settings. -type AutoRelayConfig struct { - Enabled *bool `yaml:"enabled,omitempty"` - // Lookback the number of blocks the relayer looks back from the latest - // block to check for packets to relay. - Lookback uint64 `yaml:"lookback,omitempty"` -} - -// ChainOverride returns the relay settings override for a chain. -func (c RelayerConfig) ChainOverride(chainID string) (RelayerChainOverride, bool) { - for _, override := range c.ChainOverrides { - if override.ChainID == chainID { - return override, true - } - } - - return RelayerChainOverride{}, false -} - -// ClientEnd returns the client end matching (chainID, clientID) in any -// configured connection, along with its counterparty -func (c RelayerConfig) ClientEnd(chainID, clientID string) (end, counterparty ClientEnd, ok bool) { - for _, conn := range c.Connections { - switch { - case conn.ClientA.ChainID == chainID && conn.ClientA.ClientID == clientID: - return conn.ClientA, conn.ClientB, true - case conn.ClientB.ChainID == chainID && conn.ClientB.ClientID == clientID: - return conn.ClientB, conn.ClientA, true - } - } - - return ClientEnd{}, ClientEnd{}, false -} - -// Validate validates the relayer config. Allows empty blocks. -func (c RelayerConfig) Validate() error { - if c.DispatchPollInterval != nil && *c.DispatchPollInterval <= 0 { - return errors.New(".dispatchPollInterval must be positive") - } - if err := c.validateChainOverrides(); err != nil { - return err - } - - return c.validateConnections() -} - -func (c RelayerConfig) validateChainOverrides() error { - chainIDs := make(map[string]struct{}) - - for _, chain := range c.ChainOverrides { - if err := chain.Validate(); err != nil { - return errors.Wrapf(err, ".chainOverrides[%s]", chain.ChainID) - } - - if _, ok := chainIDs[chain.ChainID]; ok { - return errors.Errorf(".chainOverrides duplicate chainId: %q", chain.ChainID) - } - chainIDs[chain.ChainID] = struct{}{} - } - - return nil -} - -func (c RelayerConfig) validateConnections() error { - aliases := make(map[string]struct{}) - clientEnds := make(map[string]struct{}) - - for _, conn := range c.Connections { - if err := conn.Validate(); err != nil { - return errors.Wrapf(err, ".connections[%s]", conn.Alias) - } - - if _, ok := aliases[conn.Alias]; ok { - return errors.Errorf(".connections duplicate alias: %q", conn.Alias) - } - aliases[conn.Alias] = struct{}{} - - for _, end := range []ClientEnd{conn.ClientA, conn.ClientB} { - key := end.ChainID + "/" + end.ClientID - if _, ok := clientEnds[key]; ok { - return errors.Errorf(".connections duplicate client %q on chain %q", end.ClientID, end.ChainID) - } - clientEnds[key] = struct{}{} - } - } - - return nil -} - -func (c ConnectionConfig) Validate() error { - if c.Alias == "" { - return errors.New(".alias required") - } - - if err := c.ClientA.Validate(); err != nil { - return errors.Wrap(err, ".clientA") - } - if err := c.ClientB.Validate(); err != nil { - return errors.Wrap(err, ".clientB") - } - - if c.ClientA.ChainID != "" && c.ClientA.ChainID == c.ClientB.ChainID { - return errors.New(".clientA and .clientB must be on different chains") - } - - return nil -} - -func (c ClientEnd) Validate() error { - switch { - case c.ChainID == "": - return errors.New(".chainId required") - case c.ClientID == "": - return errors.New(".clientId required") - case c.Signer == "": - return errors.New(".signer required") - case c.Type != ClientTypeAttestation: - return errors.Errorf(".type unknown client type: %q", c.Type) - } - - return nil -} - -func (c RelayerChainOverride) Validate() error { - switch { - case c.ChainID == "": - return errors.New(".chainId required") - case c.TxSubmissionDelay != nil && *c.TxSubmissionDelay < 0: - return errors.New(".txSubmissionDelay must not be negative") - case c.PacketBatchSize != nil && *c.PacketBatchSize <= 0: - return errors.New(".packetBatchSize must be positive") - case c.PacketBatchTimeout != nil && *c.PacketBatchTimeout <= 0: - return errors.New(".packetBatchTimeout must be positive") - } - - if c.EVM != nil { - if err := c.EVM.Validate(); err != nil { - return errors.Wrap(err, ".evm") - } - } - - return nil -} - -func (c RelayerEVMConfig) Validate() error { - switch { - case c.GasFeeCapMultiplier != nil && *c.GasFeeCapMultiplier <= 0: - return errors.New(".gasFeeCapMultiplier must be positive") - case c.GasTipCapMultiplier != nil && *c.GasTipCapMultiplier <= 0: - return errors.New(".gasTipCapMultiplier must be positive") - } - - return nil -} diff --git a/link/internal/fsutil/path.go b/link/internal/fsutil/path.go new file mode 100644 index 000000000..8b72dea3e --- /dev/null +++ b/link/internal/fsutil/path.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package fsutil contains shared filesystem path helpers. +package fsutil + +import ( + "os" + "path/filepath" + "strings" +) + +// ExpandHome converts a path beginning with ~ to an absolute path. +func ExpandHome(path string) (string, error) { + if path != "~" && !strings.HasPrefix(path, "~/") { + return path, nil + } + + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + + if path == "~" { + return home, nil + } + + return filepath.Join(home, strings.TrimPrefix(path, "~/")), nil +} + +// EnsureDirectory creates the parent directory of a file path when needed. +func EnsureDirectory(path string) error { + dir := filepath.Dir(path) + if dir == "." { + return nil + } + + return os.MkdirAll(dir, 0o755) +} + +// KeyFileFallbacks returns the paths tried for a local signer key file. +func KeyFileFallbacks(keyPath string) []string { + fallbacks := []string{keyPath} + + if filepath.IsAbs(keyPath) { + return fallbacks + } + + if !strings.HasSuffix(keyPath, ".json") { + keyPath += ".json" + fallbacks = append(fallbacks, keyPath) + } + + if !strings.Contains(keyPath, "keys/") { + fallbacks = append(fallbacks, filepath.Join("keys", keyPath)) + } + + return fallbacks +} diff --git a/link/internal/livevalidate/connections.go b/link/internal/livevalidate/connections.go index 10c45c73a..490a71a4c 100644 --- a/link/internal/livevalidate/connections.go +++ b/link/internal/livevalidate/connections.go @@ -7,8 +7,8 @@ import ( "github.com/pkg/errors" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" ) // validateConnectionsLive queries each configured connection's two chains to diff --git a/link/internal/livevalidate/connections_test.go b/link/internal/livevalidate/connections_test.go index 90337047f..7444d0c79 100644 --- a/link/internal/livevalidate/connections_test.go +++ b/link/internal/livevalidate/connections_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/tests/mocks" ) diff --git a/link/internal/livevalidate/quorum.go b/link/internal/livevalidate/quorum.go index a980bd204..c9a672374 100644 --- a/link/internal/livevalidate/quorum.go +++ b/link/internal/livevalidate/quorum.go @@ -7,8 +7,8 @@ import ( "github.com/pkg/errors" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/proofgen" "github.com/cosmos/ibc/link/internal/service/attestor" "github.com/cosmos/ibc/link/internal/service/signer" diff --git a/link/internal/livevalidate/quorum_test.go b/link/internal/livevalidate/quorum_test.go index 483ad8d89..0a35cdebd 100644 --- a/link/internal/livevalidate/quorum_test.go +++ b/link/internal/livevalidate/quorum_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/internal/tests/mocks" ) diff --git a/link/internal/livevalidate/validate.go b/link/internal/livevalidate/validate.go index fe78e23b8..1b62642ce 100644 --- a/link/internal/livevalidate/validate.go +++ b/link/internal/livevalidate/validate.go @@ -9,8 +9,8 @@ import ( "github.com/pkg/errors" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/store" ) diff --git a/link/internal/relay/dispatch/dispatcher_test.go b/link/internal/relay/dispatch/dispatcher_test.go index d94284fba..bc7abea95 100644 --- a/link/internal/relay/dispatch/dispatcher_test.go +++ b/link/internal/relay/dispatch/dispatcher_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/relay/pipeline" "github.com/cosmos/ibc/link/internal/relay/processors" "github.com/cosmos/ibc/link/internal/store" diff --git a/link/internal/relay/dispatch/pipeline_set.go b/link/internal/relay/dispatch/pipeline_set.go index 7d642fadf..428f5617a 100644 --- a/link/internal/relay/dispatch/pipeline_set.go +++ b/link/internal/relay/dispatch/pipeline_set.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/relay/pipeline" "github.com/cosmos/ibc/link/internal/relay/processors" ) diff --git a/link/internal/relay/pipeline/opts.go b/link/internal/relay/pipeline/opts.go index 9c5bd1440..30b652736 100644 --- a/link/internal/relay/pipeline/opts.go +++ b/link/internal/relay/pipeline/opts.go @@ -5,7 +5,7 @@ package pipeline import ( "time" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/relay/processors" ) diff --git a/link/internal/relay/proofgen/attestation/resolve.go b/link/internal/relay/proofgen/attestation/resolve.go index 9d9260194..80a539e73 100644 --- a/link/internal/relay/proofgen/attestation/resolve.go +++ b/link/internal/relay/proofgen/attestation/resolve.go @@ -9,8 +9,8 @@ import ( "github.com/pkg/errors" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/attestor" ) diff --git a/link/internal/relay/proofgen/attestation/resolve_test.go b/link/internal/relay/proofgen/attestation/resolve_test.go index 22f44251a..11c5c393e 100644 --- a/link/internal/relay/proofgen/attestation/resolve_test.go +++ b/link/internal/relay/proofgen/attestation/resolve_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/attestor" "github.com/cosmos/ibc/link/internal/tests/mocks" ) diff --git a/link/internal/relay/proofgen/proofgen.go b/link/internal/relay/proofgen/proofgen.go index 9f7d59489..1a02c4369 100644 --- a/link/internal/relay/proofgen/proofgen.go +++ b/link/internal/relay/proofgen/proofgen.go @@ -12,8 +12,8 @@ import ( "github.com/pkg/errors" channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/proofgen/attestation" "github.com/cosmos/ibc/link/internal/service/attestor" v2 "github.com/cosmos/ibc/link/internal/types/v2" diff --git a/link/internal/relay/proofgen/proofgen_test.go b/link/internal/relay/proofgen/proofgen_test.go index 963af37fe..96a419e82 100644 --- a/link/internal/relay/proofgen/proofgen_test.go +++ b/link/internal/relay/proofgen/proofgen_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/attestor" "github.com/cosmos/ibc/link/internal/tests/mocks" ) diff --git a/link/internal/relay/txbuilder/txbuilder.go b/link/internal/relay/txbuilder/txbuilder.go index 9dd125419..94f8d0158 100644 --- a/link/internal/relay/txbuilder/txbuilder.go +++ b/link/internal/relay/txbuilder/txbuilder.go @@ -8,7 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/relay/txbuilder/evm" v2 "github.com/cosmos/ibc/link/internal/types/v2" ) diff --git a/link/internal/service/attestor/local.go b/link/internal/service/attestor/local.go index 296b0b7c9..d6a5e7e19 100644 --- a/link/internal/service/attestor/local.go +++ b/link/internal/service/attestor/local.go @@ -14,8 +14,8 @@ import ( hostv2 "github.com/cosmos/ibc-go/v11/modules/core/24-host/v2" "github.com/cosmos/ibc/link/attestor/evm" "github.com/cosmos/ibc/link/attestor/evm/ibc" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/signer" v2 "github.com/cosmos/ibc/link/internal/types/v2" ) diff --git a/link/internal/service/attestor/local_test.go b/link/internal/service/attestor/local_test.go index 7179a8b48..b15ef377a 100644 --- a/link/internal/service/attestor/local_test.go +++ b/link/internal/service/attestor/local_test.go @@ -20,9 +20,9 @@ import ( hostv2 "github.com/cosmos/ibc-go/v11/modules/core/24-host/v2" attestorevm "github.com/cosmos/ibc/link/attestor/evm" "github.com/cosmos/ibc/link/attestor/evm/ibc" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" "github.com/cosmos/ibc/link/internal/chains/evm/contracts/ics26router" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/internal/tests/mocks" v2 "github.com/cosmos/ibc/link/internal/types/v2" diff --git a/link/internal/service/attestor/resolve.go b/link/internal/service/attestor/resolve.go index d7221d135..494fd5ec5 100644 --- a/link/internal/service/attestor/resolve.go +++ b/link/internal/service/attestor/resolve.go @@ -9,8 +9,8 @@ import ( "github.com/pkg/errors" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/signer" ) diff --git a/link/internal/service/attestor/resolve_test.go b/link/internal/service/attestor/resolve_test.go index 202ea2ae6..371c2b8a6 100644 --- a/link/internal/service/attestor/resolve_test.go +++ b/link/internal/service/attestor/resolve_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/service/signer" ) diff --git a/link/internal/service/attestor/service_test.go b/link/internal/service/attestor/service_test.go index 4f2132284..c51b6eba3 100644 --- a/link/internal/service/attestor/service_test.go +++ b/link/internal/service/attestor/service_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" proto "github.com/cosmos/ibc/link/api/v2/attestor" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/service/signer" v2 "github.com/cosmos/ibc/link/internal/types/v2" ) diff --git a/link/internal/service/relayer/service.go b/link/internal/service/relayer/service.go index 724fdf4a7..7f4a5bb6b 100644 --- a/link/internal/service/relayer/service.go +++ b/link/internal/service/relayer/service.go @@ -17,8 +17,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/chains" - "github.com/cosmos/ibc/link/internal/config" "github.com/cosmos/ibc/link/internal/relay/dispatch" "github.com/cosmos/ibc/link/internal/store" v2 "github.com/cosmos/ibc/link/internal/types/v2" diff --git a/link/internal/service/relayer/service_test.go b/link/internal/service/relayer/service_test.go index f1167c57f..e667e81b9 100644 --- a/link/internal/service/relayer/service_test.go +++ b/link/internal/service/relayer/service_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" channeltypesv2 "github.com/cosmos/ibc-go/v11/modules/core/04-channel/v2/types" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/store" "github.com/cosmos/ibc/link/internal/tests/mocks" v2 "github.com/cosmos/ibc/link/internal/types/v2" diff --git a/link/internal/service/signer/local.go b/link/internal/service/signer/local.go index e41c3b0b1..6c8cebdfe 100644 --- a/link/internal/service/signer/local.go +++ b/link/internal/service/signer/local.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/internal/fsutil" "github.com/cosmos/ibc/link/keyfile" ) @@ -29,7 +29,7 @@ type PersistedLocalKey struct { func KeyFilePath(homePath, keyName string) (string, error) { filename := keyName + ".json" path := filepath.Join(homePath, "/keys", filepath.Clean(filename)) - return config.ExpandHome(path) + return fsutil.ExpandHome(path) } func GenerateLocalKey(keyType keyfile.Type) (LocalKey, error) { diff --git a/link/internal/service/signer/signer.go b/link/internal/service/signer/signer.go index 52afc5802..eae47ee9c 100644 --- a/link/internal/service/signer/signer.go +++ b/link/internal/service/signer/signer.go @@ -10,7 +10,8 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/pkg/errors" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" + "github.com/cosmos/ibc/link/internal/fsutil" "github.com/cosmos/ibc/link/keyfile" ) @@ -67,12 +68,12 @@ func (s *Set) Get(alias string) (Signer, bool) { func NewSignerFromConfig(ctx context.Context, cfg config.SignerConfig) (signer Signer, alias string, err error) { switch cfg.Type { case config.SignerLocal: - path, err := config.ExpandHome(cfg.File) + path, err := fsutil.ExpandHome(cfg.File) if err != nil { return nil, "", errors.Wrap(err, "expand local signer file") } - s, err := LocalKeyFromFile(config.KeyFileFallbacks(path)...) + s, err := LocalKeyFromFile(fsutil.KeyFileFallbacks(path)...) return s, cfg.Alias, err case config.SignerRemote: @@ -112,12 +113,12 @@ func EVMAddressOf(cfg config.SignerConfig) (string, error) { return "", errors.Errorf("cannot derive an address for remote signer %q", cfg.Alias) } - path, err := config.ExpandHome(cfg.File) + path, err := fsutil.ExpandHome(cfg.File) if err != nil { return "", err } - key, err := LocalKeyFromFile(config.KeyFileFallbacks(path)...) + key, err := LocalKeyFromFile(fsutil.KeyFileFallbacks(path)...) if err != nil { return "", errors.Wrapf(err, "signer %q", cfg.Alias) } diff --git a/link/internal/service/signer/signer_test.go b/link/internal/service/signer/signer_test.go index 72feea4d7..dcda35267 100644 --- a/link/internal/service/signer/signer_test.go +++ b/link/internal/service/signer/signer_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/keyfile" ) diff --git a/link/internal/store/migrations.go b/link/internal/store/migrations.go index 74a0643e8..1b1311f03 100644 --- a/link/internal/store/migrations.go +++ b/link/internal/store/migrations.go @@ -15,7 +15,7 @@ import ( "github.com/pkg/errors" migrate "github.com/rubenv/sql-migrate" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" ) const migrationTemplate = `-- SPDX-License-Identifier: Apache-2.0 diff --git a/link/internal/store/store.go b/link/internal/store/store.go index fc1871b65..7a99d0a5a 100644 --- a/link/internal/store/store.go +++ b/link/internal/store/store.go @@ -11,7 +11,7 @@ import ( pgx "github.com/jackc/pgx/v5" "github.com/pkg/errors" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" ) // Store a unified, database-agnostic API for persistence. diff --git a/link/internal/store/store_postgres.go b/link/internal/store/store_postgres.go index 20c7e43ec..2ddf207f4 100644 --- a/link/internal/store/store_postgres.go +++ b/link/internal/store/store_postgres.go @@ -15,7 +15,7 @@ import ( "github.com/pkg/errors" migrate "github.com/rubenv/sql-migrate" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/store/repository/postgres" ) diff --git a/link/internal/store/store_sqlite.go b/link/internal/store/store_sqlite.go index b0100e18c..f07a7652d 100644 --- a/link/internal/store/store_sqlite.go +++ b/link/internal/store/store_sqlite.go @@ -13,7 +13,8 @@ import ( "github.com/pkg/errors" migrate "github.com/rubenv/sql-migrate" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" + "github.com/cosmos/ibc/link/internal/fsutil" reposqlite "github.com/cosmos/ibc/link/internal/store/repository/sqlite" //nolint:blank-imports // SQL driver @@ -83,7 +84,7 @@ func NewSqliteWithOptions(path string, connectionOpts map[string]string) (*Sqlit } // sqlite can auto-create db files, but not nested directories. - if err = config.EnsureDirectory(path); err != nil { + if err = fsutil.EnsureDirectory(path); err != nil { return nil, errors.Wrapf(err, "ensure directory for %s", path) } diff --git a/link/internal/txsubmitter/txsubmitter.go b/link/internal/txsubmitter/txsubmitter.go index e05e86f46..c9b296892 100644 --- a/link/internal/txsubmitter/txsubmitter.go +++ b/link/internal/txsubmitter/txsubmitter.go @@ -9,7 +9,7 @@ import ( "github.com/cockroachdb/errors" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/service/signer" "github.com/cosmos/ibc/link/internal/txsubmitter/evm" v2 "github.com/cosmos/ibc/link/internal/types/v2" diff --git a/link/scripts/migratenew.go b/link/scripts/migratenew.go index 739b73c41..9570fb354 100644 --- a/link/scripts/migratenew.go +++ b/link/scripts/migratenew.go @@ -7,7 +7,7 @@ import ( "os" "path/filepath" - "github.com/cosmos/ibc/link/internal/config" + "github.com/cosmos/ibc/link/config" "github.com/cosmos/ibc/link/internal/store" )