Skip to content

Commit 214aff9

Browse files
authored
add extra validations to client creation in read-only mode (#1280)
1 parent fc79d0e commit 214aff9

File tree

7 files changed

+140
-4
lines changed

7 files changed

+140
-4
lines changed

seth/.changeset/v1.50.6.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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

seth/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -813,11 +813,12 @@ With `SETH_LOG_LEVEL=trace` we will also log to console all traffic between Seth
813813

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

822823
The easiest way to enable read-only mode is to client via `ClientBuilder`:
823824
```go
@@ -831,3 +832,10 @@ The easiest way to enable read-only mode is to client via `ClientBuilder`:
831832
```
832833

833834
when builder is called with `WithReadOnlyMode()` it will disable all the operations mentioned above and all the configuration settings related to them.
835+
836+
Additionally, when the client is build anc `cfg.ReadOnly = true` is set, we will validate that:
837+
* no addresses and private keys are passed
838+
* no ephemeral addresses are to be created
839+
* RPC health check is disabled
840+
* pending nonce protection is disabled
841+
* gas bumping is disabled

seth/client.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@ const (
3131
ErrCreateNonceManager = "failed to create nonce manager"
3232
ErrCreateTracer = "failed to create tracer"
3333
ErrReadContractMap = "failed to read deployed contract map"
34-
ErrNoKeyLoaded = "failed to load private key"
3534
ErrRpcHealthCheckFailed = "RPC health check failed ¯\\_(ツ)_/¯"
3635
ErrContractDeploymentFailed = "contract deployment failed"
36+
ErrNoPksEphemeralMode = "no private keys loaded, cannot fund ephemeral addresses"
37+
38+
ErrReadOnlyWithPrivateKeys = "read-only mode is enabled, but you tried to load private keys"
39+
ErrReadOnlyEphemeralKeys = "ephemeral mode is not supported in read-only mode"
40+
ErrReadOnlyGasBumping = "gas bumping is not supported in read-only mode"
41+
ErrReadOnlyRpcHealth = "RPC health check is not supported in read-only mode"
42+
ErrReadOnlyPendingNonce = "pending nonce protection is not supported in read-only mode"
3743

3844
ContractMapFilePattern = "deployed_contracts_%s_%s.toml"
3945
RevertedTransactionsFilePattern = "reverted_transactions_%s_%s.json"
@@ -231,6 +237,11 @@ func NewClientRaw(
231237
if len(cfg.Network.URLs) > 1 {
232238
L.Warn().Msg("Multiple RPC URLs provided, only the first one will be used")
233239
}
240+
241+
if cfg.ReadOnly && (len(addrs) > 0 || len(pkeys) > 0) {
242+
return nil, errors.New(ErrReadOnlyWithPrivateKeys)
243+
}
244+
234245
ctx, cancel := context.WithTimeout(context.Background(), cfg.Network.DialTimeout.Duration())
235246
defer cancel()
236247
rpcClient, err := rpc.DialOptions(ctx,
@@ -303,6 +314,9 @@ func NewClientRaw(
303314
}
304315

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

329+
if cfg.PendingNonceProtectionEnabled && cfg.ReadOnly {
330+
return nil, errors.New(ErrReadOnlyPendingNonce)
331+
}
332+
315333
cfg.setEphemeralAddrs()
316334

317335
L.Info().
@@ -324,7 +342,10 @@ func NewClientRaw(
324342

325343
if cfg.ephemeral {
326344
if len(c.Addresses) == 0 {
327-
return nil, errors.New("no private keys loaded, cannot fund ephemeral addresses")
345+
return nil, errors.New(ErrNoPksEphemeralMode)
346+
}
347+
if cfg.ReadOnly {
348+
return nil, errors.New(ErrReadOnlyEphemeralKeys)
328349
}
329350
gasPrice, err := c.GetSuggestedLegacyFees(context.Background(), Priority_Standard)
330351
if err != nil {
@@ -392,6 +413,10 @@ func NewClientRaw(
392413
}
393414
}
394415

416+
if c.Cfg.GasBump != nil && c.Cfg.GasBump.Retries != 0 && c.Cfg.ReadOnly {
417+
return nil, errors.New(ErrReadOnlyGasBumping)
418+
}
419+
395420
// 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)
396421
if c.Cfg.GasBump != nil && c.Cfg.GasBump.StrategyFn == nil {
397422
if c.Cfg.GasBumpRetries() != 0 {

seth/client_builder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ func (c *ClientBuilder) handleReadOnlyMode() {
393393
c.config.PendingNonceProtectionEnabled = false
394394
c.config.CheckRpcHealthOnStart = false
395395
c.config.EphemeralAddrs = nil
396+
c.readonly = true
396397
if c.config.Network != nil {
397398
c.config.Network.GasPriceEstimationEnabled = false
398399
c.config.Network.PrivateKeys = []string{}

seth/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type Config struct {
6767
CheckRpcHealthOnStart bool `toml:"check_rpc_health_on_start"`
6868
BlockStatsConfig *BlockStatsConfig `toml:"block_stats"`
6969
GasBump *GasBumpConfig `toml:"gas_bump"`
70+
ReadOnly bool `toml:"read_only"`
7071
}
7172

7273
type GasBumpConfig struct {

seth/config_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,98 @@ func TestConfigAppendPkToInactiveNetwork(t *testing.T) {
465465
require.Equal(t, 0, len(cfg.Networks[0].PrivateKeys), "network should have 0 pks")
466466
require.Equal(t, []string{"pk"}, cfg.Networks[1].PrivateKeys, "network should have 1 pk")
467467
}
468+
469+
func TestConfig_ReadOnly_WithPk(t *testing.T) {
470+
cfg := seth.Config{
471+
ReadOnly: true,
472+
Network: &seth.Network{
473+
Name: "some_other",
474+
URLs: []string{"ws://localhost:8546"},
475+
},
476+
}
477+
478+
addrs := []common.Address{common.HexToAddress("0xb794f5ea0ba39494ce839613fffba74279579268")}
479+
480+
_, err := seth.NewClientRaw(&cfg, addrs, nil)
481+
require.Error(t, err, "succeeded in creating client")
482+
require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message")
483+
484+
privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
485+
require.NoError(t, err, "failed to parse private key")
486+
487+
pks := []*ecdsa.PrivateKey{privateKey}
488+
_, err = seth.NewClientRaw(&cfg, nil, pks)
489+
require.Error(t, err, "succeeded in creating client")
490+
require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message")
491+
492+
_, err = seth.NewClientRaw(&cfg, addrs, pks)
493+
require.Error(t, err, "succeeded in creating client")
494+
require.Equal(t, seth.ErrReadOnlyWithPrivateKeys, err.Error(), "expected different error message")
495+
}
496+
497+
func TestConfig_ReadOnly_GasBumping(t *testing.T) {
498+
cfg := seth.Config{
499+
ReadOnly: true,
500+
Network: &seth.Network{
501+
Name: "some_other",
502+
URLs: []string{"ws://localhost:8546"},
503+
DialTimeout: &seth.Duration{D: 10 * time.Second},
504+
},
505+
GasBump: &seth.GasBumpConfig{
506+
Retries: uint(1),
507+
},
508+
}
509+
510+
_, err := seth.NewClientRaw(&cfg, nil, nil)
511+
require.Error(t, err, "succeeded in creating client")
512+
require.Equal(t, seth.ErrReadOnlyGasBumping, err.Error(), "expected different error message")
513+
}
514+
515+
func TestConfig_ReadOnly_RpcHealth(t *testing.T) {
516+
cfg := seth.Config{
517+
ReadOnly: true,
518+
CheckRpcHealthOnStart: true,
519+
Network: &seth.Network{
520+
Name: "some_other",
521+
URLs: []string{"ws://localhost:8546"},
522+
DialTimeout: &seth.Duration{D: 10 * time.Second},
523+
},
524+
}
525+
526+
_, err := seth.NewClientRaw(&cfg, nil, nil)
527+
require.Error(t, err, "succeeded in creating client")
528+
require.Equal(t, seth.ErrReadOnlyRpcHealth, err.Error(), "expected different error message")
529+
}
530+
531+
func TestConfig_ReadOnly_PendingNonce(t *testing.T) {
532+
cfg := seth.Config{
533+
ReadOnly: true,
534+
PendingNonceProtectionEnabled: true,
535+
Network: &seth.Network{
536+
Name: "some_other",
537+
URLs: []string{"ws://localhost:8546"},
538+
DialTimeout: &seth.Duration{D: 10 * time.Second},
539+
},
540+
}
541+
542+
_, err := seth.NewClientRaw(&cfg, nil, nil)
543+
require.Error(t, err, "succeeded in creating client")
544+
require.Equal(t, seth.ErrReadOnlyPendingNonce, err.Error(), "expected different error message")
545+
}
546+
547+
func TestConfig_ReadOnly_EphemeralKeys(t *testing.T) {
548+
ten := int64(10)
549+
cfg := seth.Config{
550+
ReadOnly: true,
551+
EphemeralAddrs: &ten,
552+
Network: &seth.Network{
553+
Name: "some_other",
554+
URLs: []string{"ws://localhost:8546"},
555+
DialTimeout: &seth.Duration{D: 10 * time.Second},
556+
},
557+
}
558+
559+
_, err := seth.NewClientRaw(&cfg, nil, nil)
560+
require.Error(t, err, "succeeded in creating client")
561+
require.Equal(t, seth.ErrNoPksEphemeralMode, err.Error(), "expected different error message")
562+
}

seth/seth.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ experiments_enabled = ["slow_funds_return", "eip_1559_fee_equalizer"]
5555
# to make sure transaction can be submitted and mined
5656
check_rpc_health_on_start = false
5757

58+
# when enabled, upon creation Seth will validate that there are no private keys set, that node RPC health check is disabled
59+
# and that gas bumping is disabled, since all of these operations are "write" operations. This is useful for running Seth
60+
# only for tracing, when you want to make sure that no transactions are sent to the network.
61+
read_only = false
62+
5863
[gas_bumps]
5964
# when > 0 then we will bump gas price for transactions that are stuck in the mempool
6065
# by default the bump step is controlled by gas_price_estimation_tx_priority (check readme.md for more details)

0 commit comments

Comments
 (0)