From 402545851b64eff6dc00c7f318106ae0d60b4316 Mon Sep 17 00:00:00 2001 From: Alex | Interchain Labs Date: Wed, 14 May 2025 17:08:07 -0400 Subject: [PATCH] feat: scoped config (#24668) --- CHANGELOG.md | 2 + types/config.go | 110 +++++++++++++++++++++---------------------- types/config_test.go | 56 +++++++++++++++++----- 3 files changed, 101 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e1f0dfce..2098f56c0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,10 +39,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] ### Features + * (server) [#24720](https://github.com/cosmos/cosmos-sdk/pull/24720) add `verbose_log_level` flag for configuring the log level when switching to verbose logging mode during sensitive operations (such as chain upgrades). ### Improvements +* (types) [#24668](https://github.com/cosmos/cosmos-sdk/pull/24668) Scope the global config to a particular binary so that multiple SDK binaries can be properly run on the same machine. * (baseapp) [#24655](https://github.com/cosmos/cosmos-sdk/pull/24655) Add mutex locks for `state` and make `lastCommitInfo` atomic to prevent race conditions between `Commit` and `CreateQueryContext`. * (proto) [#24161](https://github.com/cosmos/cosmos-sdk/pull/24161) Remove unnecessary annotations from `x/staking` authz proto. diff --git a/types/config.go b/types/config.go index 0a080ca7d1..1c61968bb0 100644 --- a/types/config.go +++ b/types/config.go @@ -3,16 +3,17 @@ package types import ( "context" "fmt" + "os" "sync" "github.com/cosmos/cosmos-sdk/version" ) -// DefaultKeyringServiceName defines a default service name for the keyring. -const DefaultKeyringServiceName = "cosmos" +const ( + DefaultKeyringServiceName = "cosmos" + EnvConfigScope = "COSMOS_SDK_CONFIG_SCOPE" +) -// Config is the structure that holds the SDK configuration parameters. -// This could be used to initialize certain configuration parameters for the SDK. type Config struct { fullFundraiserPath string bech32AddressPrefix map[string]string @@ -20,7 +21,6 @@ type Config struct { addressVerifier func([]byte) error mtx sync.RWMutex - // SLIP-44 related purpose uint32 coinType uint32 @@ -28,12 +28,32 @@ type Config struct { sealedch chan struct{} } -// cosmos-sdk wide global singleton var ( - sdkConfig *Config - initConfig sync.Once + configRegistry = make(map[string]*Config) + registryMutex sync.Mutex ) +// getConfigKey returns a unique config scope identifier. +// It uses ENV override, or defaults to "hostname|binary|pid". +func getConfigKey() string { + if id := os.Getenv(EnvConfigScope); id != "" { + return id + } + + exe, errExec := os.Executable() + host, errHost := os.Hostname() + pid := os.Getpid() + + if errExec != nil { + exe = "unknown-exe" + } + if errHost != nil { + host = "unknown-host" + } + + return fmt.Sprintf("%s|%s|%d", host, exe, pid) +} + // NewConfig returns a new Config with default values. func NewConfig() *Config { return &Config{ @@ -47,74 +67,59 @@ func NewConfig() *Config { "consensus_pub": Bech32PrefixConsPub, }, fullFundraiserPath: FullFundraiserPath, - - purpose: Purpose, - coinType: CoinType, - txEncoder: nil, + purpose: Purpose, + coinType: CoinType, } } -// GetConfig returns the config instance for the SDK. +// GetConfig returns a per-scope config instance. func GetConfig() *Config { - initConfig.Do(func() { - sdkConfig = NewConfig() - }) - return sdkConfig -} + key := getConfigKey() -// GetSealedConfig returns the config instance for the SDK if/once it is sealed. -func GetSealedConfig(ctx context.Context) (*Config, error) { - config := GetConfig() - select { - case <-config.sealedch: - return config, nil - case <-ctx.Done(): - return nil, ctx.Err() + registryMutex.Lock() + defer registryMutex.Unlock() + + if cfg, exists := configRegistry[key]; exists { + return cfg } + + cfg := NewConfig() + configRegistry[key] = cfg + + return cfg } func (config *Config) assertNotSealed() { config.mtx.RLock() defer config.mtx.RUnlock() - if config.sealed { panic("Config is sealed") } } -// SetBech32PrefixForAccount builds the Config with Bech32 addressPrefix and publKeyPrefix for accounts -// and returns the config instance func (config *Config) SetBech32PrefixForAccount(addressPrefix, pubKeyPrefix string) { config.assertNotSealed() config.bech32AddressPrefix["account_addr"] = addressPrefix config.bech32AddressPrefix["account_pub"] = pubKeyPrefix } -// SetBech32PrefixForValidator builds the Config with Bech32 addressPrefix and publKeyPrefix for validators -// -// and returns the config instance func (config *Config) SetBech32PrefixForValidator(addressPrefix, pubKeyPrefix string) { config.assertNotSealed() config.bech32AddressPrefix["validator_addr"] = addressPrefix config.bech32AddressPrefix["validator_pub"] = pubKeyPrefix } -// SetBech32PrefixForConsensusNode builds the Config with Bech32 addressPrefix and publKeyPrefix for consensus nodes -// and returns the config instance func (config *Config) SetBech32PrefixForConsensusNode(addressPrefix, pubKeyPrefix string) { config.assertNotSealed() config.bech32AddressPrefix["consensus_addr"] = addressPrefix config.bech32AddressPrefix["consensus_pub"] = pubKeyPrefix } -// SetTxEncoder builds the Config with TxEncoder used to marshal StdTx to bytes func (config *Config) SetTxEncoder(encoder TxEncoder) { config.assertNotSealed() config.txEncoder = encoder } -// SetAddressVerifier builds the Config with the provided function for verifying that addresses -// have the correct format func (config *Config) SetAddressVerifier(addressVerifier func([]byte) error) { config.assertNotSealed() config.addressVerifier = addressVerifier @@ -140,81 +145,64 @@ func (config *Config) SetCoinType(coinType uint32) { config.coinType = coinType } -// Seal seals the config such that the config state could not be modified further func (config *Config) Seal() *Config { config.mtx.Lock() + defer config.mtx.Unlock() if config.sealed { - config.mtx.Unlock() return config } - // signal sealed after state exposed/unlocked config.sealed = true - config.mtx.Unlock() close(config.sealedch) return config } -// GetBech32AccountAddrPrefix returns the Bech32 prefix for account address func (config *Config) GetBech32AccountAddrPrefix() string { return config.bech32AddressPrefix["account_addr"] } -// GetBech32ValidatorAddrPrefix returns the Bech32 prefix for validator address func (config *Config) GetBech32ValidatorAddrPrefix() string { return config.bech32AddressPrefix["validator_addr"] } -// GetBech32ConsensusAddrPrefix returns the Bech32 prefix for consensus node address func (config *Config) GetBech32ConsensusAddrPrefix() string { return config.bech32AddressPrefix["consensus_addr"] } -// GetBech32AccountPubPrefix returns the Bech32 prefix for account public key func (config *Config) GetBech32AccountPubPrefix() string { return config.bech32AddressPrefix["account_pub"] } -// GetBech32ValidatorPubPrefix returns the Bech32 prefix for validator public key func (config *Config) GetBech32ValidatorPubPrefix() string { return config.bech32AddressPrefix["validator_pub"] } -// GetBech32ConsensusPubPrefix returns the Bech32 prefix for consensus node public key func (config *Config) GetBech32ConsensusPubPrefix() string { return config.bech32AddressPrefix["consensus_pub"] } -// GetTxEncoder return function to encode transactions func (config *Config) GetTxEncoder() TxEncoder { return config.txEncoder } -// GetAddressVerifier returns the function to verify that addresses have the correct format func (config *Config) GetAddressVerifier() func([]byte) error { return config.addressVerifier } -// GetPurpose returns the BIP-0044 Purpose code on the config. func (config *Config) GetPurpose() uint32 { return config.purpose } -// GetCoinType returns the BIP-0044 CoinType code on the config. func (config *Config) GetCoinType() uint32 { return config.coinType } -// GetFullFundraiserPath returns the BIP44Prefix. -// -// Deprecated: This method is supported for backward compatibility only and will be removed in a future release. Use GetFullBIP44Path instead. func (config *Config) GetFullFundraiserPath() string { return config.fullFundraiserPath } -// GetFullBIP44Path returns the BIP44Prefix. func (config *Config) GetFullBIP44Path() string { return fmt.Sprintf("m/%d'/%d'/0'/0/0", config.purpose, config.coinType) } @@ -225,3 +213,15 @@ func KeyringServiceName() string { } return version.Name } + +// Optional: expose sealed config with timeout + +func GetSealedConfig(ctx context.Context) (*Config, error) { + config := GetConfig() + select { + case <-config.sealedch: + return config, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} diff --git a/types/config_test.go b/types/config_test.go index df281b1f85..be77573bcc 100644 --- a/types/config_test.go +++ b/types/config_test.go @@ -1,12 +1,11 @@ -package types_test +package types import ( "errors" + "strings" "testing" "github.com/stretchr/testify/suite" - - sdk "github.com/cosmos/cosmos-sdk/types" ) type configTestSuite struct { @@ -17,8 +16,8 @@ func TestConfigTestSuite(t *testing.T) { suite.Run(t, new(configTestSuite)) } -func (s *contextTestSuite) TestConfig_SetPurpose() { - config := sdk.NewConfig() +func (s *configTestSuite) TestConfig_SetPurpose() { + config := NewConfig() config.SetPurpose(44) s.Require().Equal(uint32(44), config.GetPurpose()) @@ -30,7 +29,7 @@ func (s *contextTestSuite) TestConfig_SetPurpose() { } func (s *configTestSuite) TestConfig_SetCoinType() { - config := sdk.NewConfig() + config := NewConfig() config.SetCoinType(1) s.Require().Equal(uint32(1), config.GetCoinType()) config.SetCoinType(99) @@ -42,19 +41,19 @@ func (s *configTestSuite) TestConfig_SetCoinType() { func (s *configTestSuite) TestConfig_SetTxEncoder() { mockErr := errors.New("test") - config := sdk.NewConfig() + config := NewConfig() s.Require().Nil(config.GetTxEncoder()) - encFunc := sdk.TxEncoder(func(tx sdk.Tx) ([]byte, error) { return nil, nil }) + encFunc := TxEncoder(func(tx Tx) ([]byte, error) { return nil, mockErr }) config.SetTxEncoder(encFunc) - _, err := config.GetTxEncoder()(sdk.Tx(nil)) - s.Require().Error(mockErr, err) + _, err := config.GetTxEncoder()(Tx(nil)) + s.Require().Equal(mockErr, err) config.Seal() s.Require().Panics(func() { config.SetTxEncoder(encFunc) }) } func (s *configTestSuite) TestConfig_SetFullFundraiserPath() { - config := sdk.NewConfig() + config := NewConfig() config.SetFullFundraiserPath("test/path") s.Require().Equal("test/path", config.GetFullFundraiserPath()) @@ -66,5 +65,38 @@ func (s *configTestSuite) TestConfig_SetFullFundraiserPath() { } func (s *configTestSuite) TestKeyringServiceName() { - s.Require().Equal(sdk.DefaultKeyringServiceName, sdk.KeyringServiceName()) + s.Require().Equal(DefaultKeyringServiceName, KeyringServiceName()) +} + +func (s *configTestSuite) TestConfig_ScopePerBinary_DefaultBehavior() { + cfg1 := GetConfig() + cfg2 := GetConfig() + s.Require().Equal(cfg1, cfg2, "configs should be identical in same binary by default") +} + +func (s *configTestSuite) TestConfig_ScopePerBinary_EnvOverride() { + s.T().Setenv(EnvConfigScope, "test-scope-A") + cfgA := GetConfig() + + s.T().Setenv(EnvConfigScope, "test-scope-B") + cfgB := GetConfig() + + s.Require().NotEqual(cfgA, cfgB, "configs should differ for different env scopes") +} + +func (s *configTestSuite) TestConfig_ScopePerBinary_EnvRestoration() { + envKey := EnvConfigScope + + s.T().Setenv(envKey, "test-scope-Restore") + cfg1 := GetConfig() + + s.T().Setenv(envKey, "test-scope-Restore") + cfg2 := GetConfig() + + s.Require().Equal(cfg1, cfg2, "config should remain stable with same env scope") +} + +func (s *configTestSuite) TestConfig_ScopeKeyFormat() { + key := getConfigKey() + s.Require().True(strings.Count(key, "|") == 2, "scope key should have 2 pipe separators") }