Skip to content

Commit

Permalink
add extra validations to client creation in read-only mode (#1280)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tofel authored Oct 29, 2024
1 parent fc79d0e commit 214aff9
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 4 deletions.
1 change: 1 addition & 0 deletions seth/.changeset/v1.50.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Adds `read-only` mode that can be useful if we are interested only in tracing and want to make sure that no write operations can be executed
12 changes: 10 additions & 2 deletions seth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -813,11 +813,12 @@ With `SETH_LOG_LEVEL=trace` we will also log to console all traffic between Seth

### Read-only mode
It's possible to use Seth in read-only mode only for transaction confirmation and tracing. Following operations will fail:
* contract deployment
* gas estimations (we need the pk/address to check nonce)
* contract deployment (we need a pk to sign the transaction)
* new transaction options (we need the pk/address to check nonce)
* RPC health check (we need a pk to send a transaction to ourselves)
* pending nonce protection (we need an address to check pending transactions)
* ephemeral keys (we need a pk to fund them)
* gas bumping (we need a pk to sign the transaction)

The easiest way to enable read-only mode is to client via `ClientBuilder`:
```go
Expand All @@ -831,3 +832,10 @@ The easiest way to enable read-only mode is to client via `ClientBuilder`:
```

when builder is called with `WithReadOnlyMode()` it will disable all the operations mentioned above and all the configuration settings related to them.

Additionally, when the client is build anc `cfg.ReadOnly = true` is set, we will validate that:
* no addresses and private keys are passed
* no ephemeral addresses are to be created
* RPC health check is disabled
* pending nonce protection is disabled
* gas bumping is disabled
29 changes: 27 additions & 2 deletions seth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ const (
ErrCreateNonceManager = "failed to create nonce manager"
ErrCreateTracer = "failed to create tracer"
ErrReadContractMap = "failed to read deployed contract map"
ErrNoKeyLoaded = "failed to load private key"
ErrRpcHealthCheckFailed = "RPC health check failed ¯\\_(ツ)_/¯"
ErrContractDeploymentFailed = "contract deployment failed"
ErrNoPksEphemeralMode = "no private keys loaded, cannot fund ephemeral addresses"

ErrReadOnlyWithPrivateKeys = "read-only mode is enabled, but you tried to load private keys"
ErrReadOnlyEphemeralKeys = "ephemeral mode is not supported in read-only mode"
ErrReadOnlyGasBumping = "gas bumping is not supported in read-only mode"
ErrReadOnlyRpcHealth = "RPC health check is not supported in read-only mode"
ErrReadOnlyPendingNonce = "pending nonce protection is not supported in read-only mode"

ContractMapFilePattern = "deployed_contracts_%s_%s.toml"
RevertedTransactionsFilePattern = "reverted_transactions_%s_%s.json"
Expand Down Expand Up @@ -231,6 +237,11 @@ func NewClientRaw(
if len(cfg.Network.URLs) > 1 {
L.Warn().Msg("Multiple RPC URLs provided, only the first one will be used")
}

if cfg.ReadOnly && (len(addrs) > 0 || len(pkeys) > 0) {
return nil, errors.New(ErrReadOnlyWithPrivateKeys)
}

ctx, cancel := context.WithTimeout(context.Background(), cfg.Network.DialTimeout.Duration())
defer cancel()
rpcClient, err := rpc.DialOptions(ctx,
Expand Down Expand Up @@ -303,6 +314,9 @@ func NewClientRaw(
}

if cfg.CheckRpcHealthOnStart {
if cfg.ReadOnly {
return nil, errors.New(ErrReadOnlyRpcHealth)
}
if c.NonceManager == nil {
L.Debug().Msg("Nonce manager is not set, RPC health check will be skipped. Client will most probably fail on first transaction")
} else {
Expand All @@ -312,6 +326,10 @@ func NewClientRaw(
}
}

if cfg.PendingNonceProtectionEnabled && cfg.ReadOnly {
return nil, errors.New(ErrReadOnlyPendingNonce)
}

cfg.setEphemeralAddrs()

L.Info().
Expand All @@ -324,7 +342,10 @@ func NewClientRaw(

if cfg.ephemeral {
if len(c.Addresses) == 0 {
return nil, errors.New("no private keys loaded, cannot fund ephemeral addresses")
return nil, errors.New(ErrNoPksEphemeralMode)
}
if cfg.ReadOnly {
return nil, errors.New(ErrReadOnlyEphemeralKeys)
}
gasPrice, err := c.GetSuggestedLegacyFees(context.Background(), Priority_Standard)
if err != nil {
Expand Down Expand Up @@ -392,6 +413,10 @@ func NewClientRaw(
}
}

if c.Cfg.GasBump != nil && c.Cfg.GasBump.Retries != 0 && c.Cfg.ReadOnly {
return nil, errors.New(ErrReadOnlyGasBumping)
}

// if gas bumping is enabled, but no strategy is set, we set the default one; otherwise we set the no-op strategy (defensive programming to avoid NPE)
if c.Cfg.GasBump != nil && c.Cfg.GasBump.StrategyFn == nil {
if c.Cfg.GasBumpRetries() != 0 {
Expand Down
1 change: 1 addition & 0 deletions seth/client_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ func (c *ClientBuilder) handleReadOnlyMode() {
c.config.PendingNonceProtectionEnabled = false
c.config.CheckRpcHealthOnStart = false
c.config.EphemeralAddrs = nil
c.readonly = true
if c.config.Network != nil {
c.config.Network.GasPriceEstimationEnabled = false
c.config.Network.PrivateKeys = []string{}
Expand Down
1 change: 1 addition & 0 deletions seth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Config struct {
CheckRpcHealthOnStart bool `toml:"check_rpc_health_on_start"`
BlockStatsConfig *BlockStatsConfig `toml:"block_stats"`
GasBump *GasBumpConfig `toml:"gas_bump"`
ReadOnly bool `toml:"read_only"`
}

type GasBumpConfig struct {
Expand Down
95 changes: 95 additions & 0 deletions seth/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,98 @@ func TestConfigAppendPkToInactiveNetwork(t *testing.T) {
require.Equal(t, 0, len(cfg.Networks[0].PrivateKeys), "network should have 0 pks")
require.Equal(t, []string{"pk"}, cfg.Networks[1].PrivateKeys, "network should have 1 pk")
}

func TestConfig_ReadOnly_WithPk(t *testing.T) {
cfg := seth.Config{
ReadOnly: true,
Network: &seth.Network{
Name: "some_other",
URLs: []string{"ws://localhost:8546"},
},
}

addrs := []common.Address{common.HexToAddress("0xb794f5ea0ba39494ce839613fffba74279579268")}

_, err := seth.NewClientRaw(&cfg, addrs, nil)
require.Error(t, err, "succeeded in creating client")
require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message")

privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
require.NoError(t, err, "failed to parse private key")

pks := []*ecdsa.PrivateKey{privateKey}
_, err = seth.NewClientRaw(&cfg, nil, pks)
require.Error(t, err, "succeeded in creating client")
require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message")

_, err = seth.NewClientRaw(&cfg, addrs, pks)
require.Error(t, err, "succeeded in creating client")
require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message")
}

func TestConfig_ReadOnly_GasBumping(t *testing.T) {
cfg := seth.Config{
ReadOnly: true,
Network: &seth.Network{
Name: "some_other",
URLs: []string{"ws://localhost:8546"},
DialTimeout: &seth.Duration{D: 10 * time.Second},
},
GasBump: &seth.GasBumpConfig{
Retries: uint(1),
},
}

_, err := seth.NewClientRaw(&cfg, nil, nil)
require.Error(t, err, "succeeded in creating client")
require.Equal(t, seth.ErrReadOnlyGasBumping, err.Error(), "expected different error message")
}

func TestConfig_ReadOnly_RpcHealth(t *testing.T) {
cfg := seth.Config{
ReadOnly: true,
CheckRpcHealthOnStart: true,
Network: &seth.Network{
Name: "some_other",
URLs: []string{"ws://localhost:8546"},
DialTimeout: &seth.Duration{D: 10 * time.Second},
},
}

_, err := seth.NewClientRaw(&cfg, nil, nil)
require.Error(t, err, "succeeded in creating client")
require.Equal(t, seth.ErrReadOnlyRpcHealth, err.Error(), "expected different error message")
}

func TestConfig_ReadOnly_PendingNonce(t *testing.T) {
cfg := seth.Config{
ReadOnly: true,
PendingNonceProtectionEnabled: true,
Network: &seth.Network{
Name: "some_other",
URLs: []string{"ws://localhost:8546"},
DialTimeout: &seth.Duration{D: 10 * time.Second},
},
}

_, err := seth.NewClientRaw(&cfg, nil, nil)
require.Error(t, err, "succeeded in creating client")
require.Equal(t, seth.ErrReadOnlyPendingNonce, err.Error(), "expected different error message")
}

func TestConfig_ReadOnly_EphemeralKeys(t *testing.T) {
ten := int64(10)
cfg := seth.Config{
ReadOnly: true,
EphemeralAddrs: &ten,
Network: &seth.Network{
Name: "some_other",
URLs: []string{"ws://localhost:8546"},
DialTimeout: &seth.Duration{D: 10 * time.Second},
},
}

_, err := seth.NewClientRaw(&cfg, nil, nil)
require.Error(t, err, "succeeded in creating client")
require.Equal(t, seth.ErrNoPksEphemeralMode, err.Error(), "expected different error message")
}
5 changes: 5 additions & 0 deletions seth/seth.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ experiments_enabled = ["slow_funds_return", "eip_1559_fee_equalizer"]
# to make sure transaction can be submitted and mined
check_rpc_health_on_start = false

# when enabled, upon creation Seth will validate that there are no private keys set, that node RPC health check is disabled
# and that gas bumping is disabled, since all of these operations are "write" operations. This is useful for running Seth
# only for tracing, when you want to make sure that no transactions are sent to the network.
read_only = false

[gas_bumps]
# when > 0 then we will bump gas price for transactions that are stuck in the mempool
# by default the bump step is controlled by gas_price_estimation_tx_priority (check readme.md for more details)
Expand Down

0 comments on commit 214aff9

Please sign in to comment.