From d11fbfeeca8a3b6743ab7d88edb52fc60ac09700 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 3 Mar 2025 08:56:25 -0800 Subject: [PATCH] feat(zetaclient): add dedicated restricted addresses config file (#3600) (#3614) * feat(zetaclient): add dedicated config file restricted addresses * use errors * use bg. errors should not be fatal to the main prodcess * changelog --- changelog.md | 7 + cmd/zetaclientd/start.go | 12 +- contrib/localnet/scripts/start-zetaclientd.sh | 3 + .../chains/bitcoin/observer/event_test.go | 4 +- .../chains/evm/observer/inbound_test.go | 14 +- .../chains/evm/observer/outbound_test.go | 2 +- .../chains/solana/observer/inbound_test.go | 4 +- zetaclient/compliance/compliance_test.go | 10 +- zetaclient/config/config.go | 120 ++++++++++++++++-- zetaclient/config/types.go | 3 +- zetaclient/types/event_test.go | 2 +- 11 files changed, 153 insertions(+), 28 deletions(-) diff --git a/changelog.md b/changelog.md index da04a948b5..b6b23b3fef 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # CHANGELOG +## v28.1.0 + +This is a zetaclient only release. + +### Features +* [3600](https://github.com/zeta-chain/node/pull/3600) - add dedicated zetaclient restricted addresses config. This file will be automatically reloaded when it changes without needing to restart zetaclient. + ## v28.0.0 v28 is based on the release/v27 branch rather than develop diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 80dbf283d4..9dd1904e74 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -10,6 +10,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/zeta-chain/node/pkg/bg" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/graceful" @@ -58,6 +59,15 @@ func Start(_ *cobra.Command, _ []string) error { appContext := zctx.New(cfg, passes.relayerKeys(), logger.Std) ctx := zctx.WithAppContext(context.Background(), appContext) + err = config.LoadRestrictedAddressesConfig(cfg, globalOpts.ZetacoreHome) + if err != nil { + logger.Std.Err(err).Msg("loading restricted addresses config") + } else { + bg.Work(ctx, func(ctx context.Context) error { + return config.WatchRestrictedAddressesConfig(ctx, cfg, globalOpts.ZetacoreHome, logger.Std) + }, bg.WithName("watch_restricted_addresses_config"), bg.WithLogger(logger.Std)) + } + telemetry, err := startTelemetry(ctx, cfg) if err != nil { return errors.Wrap(err, "unable to start telemetry") @@ -77,7 +87,7 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to update app context") } - log.Info().Msgf("Config is updated from zetacore\n %s", cfg.StringMasked()) + log.Debug().Msgf("Config is updated from zetacore\n %s", cfg.StringMasked()) granteePubKeyBech32, err := resolveObserverPubKeyBech32(cfg, passes.hotkey) if err != nil { diff --git a/contrib/localnet/scripts/start-zetaclientd.sh b/contrib/localnet/scripts/start-zetaclientd.sh index 1627c1cd10..4fb2290b21 100755 --- a/contrib/localnet/scripts/start-zetaclientd.sh +++ b/contrib/localnet/scripts/start-zetaclientd.sh @@ -113,4 +113,7 @@ if [[ -f /root/zetaclient-config-overlay.json ]]; then mv /tmp/merged_config.json /root/.zetacored/config/zetaclient_config.json fi +# ensure restricted addresses config is initialized to avoid log spam +echo "[]" > ~/.zetacored/config/zetaclient_restricted_addresses.json + zetaclientd-supervisor start < /root/password.file \ No newline at end of file diff --git a/zetaclient/chains/bitcoin/observer/event_test.go b/zetaclient/chains/bitcoin/observer/event_test.go index d52d428795..ee153651f3 100644 --- a/zetaclient/chains/bitcoin/observer/event_test.go +++ b/zetaclient/chains/bitcoin/observer/event_test.go @@ -46,7 +46,7 @@ func Test_Category(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct { @@ -312,7 +312,7 @@ func Test_IsEventProcessable(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct { diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index b3c5420d9e..dab029520e 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -296,21 +296,21 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { t.Run("should return nil msg if sender is restricted", func(t *testing.T) { sender := event.ZetaTxSenderAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{sender} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { receiver := clienttypes.BytesToEthHex(event.DestinationAddress) cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) t.Run("should return nil msg if txOrigin is restricted", func(t *testing.T) { txOrigin := event.SourceTxOriginAddress.Hex() cfg.ComplianceConfig.RestrictedAddresses = []string{txOrigin} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgForZetaSentEvent(ob.appContext, event) require.Nil(t, msg) }) @@ -343,14 +343,14 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { }) t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{sender.Hex()} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) t.Run("should return nil msg if receiver is restricted", func(t *testing.T) { receiver := clienttypes.BytesToEthHex(event.Recipient) cfg.ComplianceConfig.RestrictedAddresses = []string{receiver} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) require.Nil(t, msg) }) @@ -400,7 +400,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { }) t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{tx.From} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgForTokenSentToTSS( tx, ethcommon.HexToAddress(tx.From), @@ -414,7 +414,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { message := hex.EncodeToString(ethcommon.HexToAddress(testutils.OtherAddress1).Bytes()) txCopy.Input = message // use other address as receiver cfg.ComplianceConfig.RestrictedAddresses = []string{testutils.OtherAddress1} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgForTokenSentToTSS( txCopy, ethcommon.HexToAddress(txCopy.From), diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 8b36c2b16e..a555b86e54 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -71,7 +71,7 @@ func Test_IsOutboundProcessed(t *testing.T) { ComplianceConfig: config.ComplianceConfig{}, } cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.InboundParams.Sender} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // post outbound vote continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index 8c96410bd4..20827c6e3e 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -153,7 +153,7 @@ func Test_BuildInboundVoteMsgFromEvent(t *testing.T) { // restrict sender cfg.ComplianceConfig.RestrictedAddresses = []string{sender} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) msg := ob.BuildInboundVoteMsgFromEvent(event) require.Nil(t, msg) @@ -173,7 +173,7 @@ func Test_IsEventProcessable(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct { diff --git a/zetaclient/compliance/compliance_test.go b/zetaclient/compliance/compliance_test.go index fd86cab2ea..9bb12a1a3e 100644 --- a/zetaclient/compliance/compliance_test.go +++ b/zetaclient/compliance/compliance_test.go @@ -23,29 +23,29 @@ func TestCctxRestricted(t *testing.T) { t.Run("should return true if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.InboundParams.Sender} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) require.True(t, IsCctxRestricted(cctx)) }) t.Run("should return true if receiver is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{cctx.GetCurrentOutboundParam().Receiver} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) require.True(t, IsCctxRestricted(cctx)) }) t.Run("should return false if sender and receiver are not restricted", func(t *testing.T) { // restrict other address cfg.ComplianceConfig.RestrictedAddresses = []string{"0x27104b8dB4aEdDb054fCed87c346C0758Ff5dFB1"} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) require.False(t, IsCctxRestricted(cctx)) }) t.Run("should be able to restrict coinbase address", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{ethcommon.Address{}.String()} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) cctx.InboundParams.Sender = ethcommon.Address{}.String() require.True(t, IsCctxRestricted(cctx)) }) t.Run("should ignore empty address", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{""} - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) cctx.InboundParams.Sender = "" require.False(t, IsCctxRestricted(cctx)) }) diff --git a/zetaclient/config/config.go b/zetaclient/config/config.go index 8bd3e9eff9..3714627d33 100644 --- a/zetaclient/config/config.go +++ b/zetaclient/config/config.go @@ -2,17 +2,24 @@ package config import ( + "context" "encoding/json" "fmt" "os" "path/filepath" "strings" + "sync" + "github.com/fsnotify/fsnotify" "github.com/pkg/errors" + "github.com/rs/zerolog" ) // restrictedAddressBook is a map of restricted addresses var restrictedAddressBook = map[string]bool{} +var restrictedAddressBookLock sync.RWMutex + +const restrictedAddressesPath string = "zetaclient_restricted_addresses.json" // filename is config file name for ZetaClient const filename string = "zetaclient_config.json" @@ -45,9 +52,9 @@ func Save(config *Config, path string) error { } // Load loads ZetaClient config from a filepath -func Load(path string) (Config, error) { +func Load(basePath string) (Config, error) { // retrieve file - file := filepath.Join(path, folder, filename) + file := filepath.Join(basePath, folder, filename) file, err := filepath.Abs(file) if err != nil { return Config{}, err @@ -76,19 +83,114 @@ func Load(path string) (Config, error) { // fields sanitization cfg.TssPath = GetPath(cfg.TssPath) cfg.PreParamsPath = GetPath(cfg.PreParamsPath) - cfg.ZetaCoreHome = path - - // load compliance config - LoadComplianceConfig(cfg) + cfg.ZetaCoreHome = basePath return cfg, nil } -// LoadComplianceConfig loads compliance data (restricted addresses) from config -func LoadComplianceConfig(cfg Config) { +// SetRestrictedAddressesFromConfig loads compliance data (restricted addresses) from config. +func SetRestrictedAddressesFromConfig(cfg Config) { restrictedAddressBook = cfg.GetRestrictedAddressBook() } +func getRestrictedAddressAbsPath(basePath string) (string, error) { + file := filepath.Join(basePath, folder, restrictedAddressesPath) + file, err := filepath.Abs(file) + if err != nil { + return "", errors.Wrapf(err, "absolute path conversion for %s", file) + } + return file, nil +} + +func loadRestrictedAddressesConfig(cfg Config, file string) error { + input, err := os.ReadFile(file) // #nosec G304 + if err != nil { + return errors.Wrapf(err, "reading file %s", file) + } + addresses := []string{} + err = json.Unmarshal(input, &addresses) + if err != nil { + return errors.Wrap(err, "invalid json") + } + + restrictedAddressBookLock.Lock() + defer restrictedAddressBookLock.Unlock() + + // Clear the existing map, load addresses from main config, then load addresses + // from dedicated config file + SetRestrictedAddressesFromConfig(cfg) + for _, addr := range cfg.ComplianceConfig.RestrictedAddresses { + restrictedAddressBook[strings.ToLower(addr)] = true + } + return nil +} + +// LoadRestrictedAddressesConfig loads the restricted addresses from the config file +func LoadRestrictedAddressesConfig(cfg Config, basePath string) error { + file, err := getRestrictedAddressAbsPath(basePath) + if err != nil { + return errors.Wrap(err, "getting restricted address path") + } + return loadRestrictedAddressesConfig(cfg, file) +} + +// WatchRestrictedAddressesConfig monitors the restricted addresses config file +// for changes and reloads it when necessary +func WatchRestrictedAddressesConfig(ctx context.Context, cfg Config, basePath string, logger zerolog.Logger) error { + file, err := getRestrictedAddressAbsPath(basePath) + if err != nil { + return errors.Wrap(err, "getting restricted address path") + } + watcher, err := fsnotify.NewWatcher() + if err != nil { + return errors.Wrap(err, "creating file watcher") + } + defer watcher.Close() + + // Watch the config directory + // If you only watch the file, the watch will be disconnected if/when + // the config is recreated. + dir := filepath.Dir(file) + err = watcher.Add(dir) + if err != nil { + return errors.Wrapf(err, "watching directory %s", dir) + } + + for { + select { + case <-ctx.Done(): + return nil + + case event, ok := <-watcher.Events: + if !ok { + return nil + } + + if event.Name != file { + continue + } + + // only reload on create or write + if event.Op&(fsnotify.Write|fsnotify.Create) == 0 { + continue + } + + logger.Info().Msg("restricted addresses config updated") + + err := loadRestrictedAddressesConfig(cfg, file) + if err != nil { + logger.Err(err).Msg("load restricted addresses config") + } + + case err, ok := <-watcher.Errors: + if !ok { + return nil + } + return errors.Wrap(err, "watcher error") + } + } +} + // GetPath returns the absolute path of the input path func GetPath(inputPath string) string { path := strings.Split(inputPath, "/") @@ -109,6 +211,8 @@ func GetPath(inputPath string) string { // ContainRestrictedAddress returns true if any one of the addresses is restricted // Note: the addrs can contains both ETH and BTC addresses func ContainRestrictedAddress(addrs ...string) bool { + restrictedAddressBookLock.RLock() + defer restrictedAddressBookLock.RUnlock() for _, addr := range addrs { if addr != "" && restrictedAddressBook[strings.ToLower(addr)] { return true diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index 7e1fdddfdd..4069041246 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -63,7 +63,8 @@ type TONConfig struct { // ComplianceConfig is the config for compliance type ComplianceConfig struct { - LogPath string `json:"LogPath"` + LogPath string `json:"LogPath"` + // Deprecated: use the separate restricted addresses config RestrictedAddresses []string `json:"RestrictedAddresses" mask:"zero"` } diff --git a/zetaclient/types/event_test.go b/zetaclient/types/event_test.go index 87e95943ec..7eb81407ef 100644 --- a/zetaclient/types/event_test.go +++ b/zetaclient/types/event_test.go @@ -63,7 +63,7 @@ func Test_Catetory(t *testing.T) { cfg := config.Config{ ComplianceConfig: sample.ComplianceConfig(), } - config.LoadComplianceConfig(cfg) + config.SetRestrictedAddressesFromConfig(cfg) // test cases tests := []struct {