Skip to content

Commit

Permalink
feat(zetaclient): add dedicated restricted addresses config file (#3600
Browse files Browse the repository at this point in the history
…) (#3614)

* feat(zetaclient): add dedicated config file restricted addresses

* use errors

* use bg. errors should not be fatal to the main prodcess

* changelog
  • Loading branch information
gartnera authored Mar 3, 2025
1 parent 6f56f46 commit d11fbfe
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 28 deletions.
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 11 additions & 1 deletion cmd/zetaclientd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions contrib/localnet/scripts/start-zetaclientd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions zetaclient/chains/bitcoin/observer/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 7 additions & 7 deletions zetaclient/chains/evm/observer/inbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion zetaclient/chains/evm/observer/outbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions zetaclient/chains/solana/observer/inbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions zetaclient/compliance/compliance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
Expand Down
120 changes: 112 additions & 8 deletions zetaclient/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, "/")
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion zetaclient/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
2 changes: 1 addition & 1 deletion zetaclient/types/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit d11fbfe

Please sign in to comment.