Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f8bec03
feat(rpc): add rpc pool for endpoint fallback
SeUkKim Jul 24, 2025
0bb912a
chore: fix typo
SeUkKim Jul 24, 2025
46f740a
chore: sort imports.
SeUkKim Jul 24, 2025
a447897
wip(rpc): apply feedbacks
SeUkKim Jul 24, 2025
eac5605
refactor(rpc): deduplicate retry logic in ExecuteWithFallback using t…
SeUkKim Jul 24, 2025
54e90de
refactor(config): rename RPCAddress to RPCAddresses for clarity
SeUkKim Jul 24, 2025
79a23d6
chore: add missed files
SeUkKim Jul 24, 2025
a81e174
fix(rpc): add mutex to ensure tread-safe access to HTTP client.
SeUkKim Jul 24, 2025
a531045
fix(rpc): use pointer receivers for RCPClient methods
SeUkKim Jul 24, 2025
ccfe38e
chore: fix lint error
SeUkKim Jul 25, 2025
a78ae66
refactor(rpcclient): pass rpc-timeout via context instead of env var
SeUkKim Jul 25, 2025
44aa89a
chore(challenger): update comments to reflect challenger
SeUkKim Jul 25, 2025
7547280
feat(rpcclient): implement advanced RPC pool with health scoring
SeUkKim Aug 6, 2025
7f7520d
fix(rpcclient): fix race condition when accessing client score
SeUkKim Aug 6, 2025
dc458b2
fix(rpcpool): delete invalid endpoints from the slice instead of mark…
SeUkKim Aug 14, 2025
395f427
docs(rpcpool): add a more descriptive comment for function
SeUkKim Aug 14, 2025
d721e33
test(rpcpool): add and fix tests.
SeUkKim Aug 14, 2025
c3f4e03
fix(rpcpool): mark client healthy for retry rather than recreating it
SeUkKim Aug 18, 2025
f46d2f5
style(rpcpool): format code with gofmt
SeUkKim Aug 18, 2025
114601b
fix(rpcpool): fix data race
SeUkKim Aug 18, 2025
ea3bf90
refactor(rpcpool): refactor client-health and scoring methods to take…
SeUkKim Aug 18, 2025
1980ef5
style(rpcpool): format code with gofmt
SeUkKim Aug 18, 2025
dd3db87
test(rpcpool): use fully qualified urls in tests.
SeUkKim Aug 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,51 @@ Challenger node types:
- host
- child
```

## RPC Pool Configuration

To enhance reliability, the bots support multiple RPC endpoints for L1, L2, and DA nodes. This allows for automatic fallback and retries if an endpoint becomes unavailable.

### Endpoint Configuration

In your configuration file (`~/.opinit/[bot-name].json`), you can specify a list of RPC addresses for each node:

```json
{
"l1_node": {
"chain_id": "testnet-l1-1",
"bech32_prefix": "init",
"rpc_addresses": [
"tcp://doi-rpc:26657",
"tcp://another-l1-rpc:26657"
]
},
"l2_node": {
"chain_id": "testnet-l2-1",
"bech32_prefix": "init",
"rpc_addresses": [
"tcp://moro-rpc:27657",
"tcp://another-l2-rpc:27657"
]
},
"da_node": {
"chain_id": "testnet-l1-1",
"bech32_prefix": "init",
"rpc_addresses": [
"tcp://rene-rpc:26657",
"tcp://another-da-rpc:26657"
]
}
}
```

The bot will try the endpoints in the order they are listed. If a request to the first endpoint fails, it will automatically fall back to the next one in the list.

### Timeout Configuration

You can configure the timeout for each RPC request using the `RPC_TIMEOUT_SECONDS` environment variable. The value is in seconds. If the variable is not set, the default timeout is 5 seconds.

```bash
export RPC_TIMEOUT_SECONDS=10
opinitd start [bot-name]
```
10 changes: 8 additions & 2 deletions challenger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ To configure the Challenger, fill in the values in the `~/.opinit/challenger.jso
"l1_node": {
"chain_id": "testnet-l1-1",
"bech32_prefix": "init",
"rpc_address": "tcp://localhost:26657",
"rpc_addresses": [
"tcp://doi-rpc:26657",
"tcp://localhost:26657"
],
},
"l2_node": {
"chain_id": "testnet-l2-1",
"bech32_prefix": "init",
"rpc_address": "tcp://localhost:27657",
"rpc_addresses": [
"tcp://moro-rpc:26657",
"tcp://localhost:27657"
],
},
// DisableAutoSetL1Height is the flag to disable the automatic setting of the l1 height.
// If it is false, it will finds the optimal height and sets l1_start_height automatically
Expand Down
43 changes: 38 additions & 5 deletions challenger/challenger.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ import (

var _ bottypes.Bot = &Challenger{}

// Executor charges the execution of the bridge between the host and the child chain
// - relay l1 deposit messages to l2
// - generate l2 output root and submit to l1
// Challenger is responsible for:
// Verifying that the MsgInitiateTokenDeposit event is properly relayed to MsgFinalizeTokenDeposit
// Checking whether MsgInitiateTokenDeposit was relayed on time
// Verifying that the Oracle data is properly relayed to MsgUpdateOracle
// Checking whether Oracle was relayed on time
// Verifying that the OutputRoot submitted with MsgProposeOutput is correct
// Checking whether next MsgProposeOutput was submitted on time
type Challenger struct {
host *host.Host
child *child.Child
Expand Down Expand Up @@ -84,6 +88,12 @@ func NewChallenger(cfg *challengertypes.Config, db types.DB, sv *server.Server)
}

func (c *Challenger) Initialize(ctx types.Context) error {
// Initialize the child's query client first so we can query bridge info
err := c.child.InitializeQueryClient(ctx)
if err != nil {
return errors.Wrap(err, "failed to initialize child query client")
}

childBridgeInfo, err := c.child.QueryBridgeInfo(ctx)
if err != nil {
return err
Expand All @@ -92,6 +102,12 @@ func (c *Challenger) Initialize(ctx types.Context) error {
return errors.New("bridge info is not set")
}

// Initialize the host's query client so we can query bridge config
err = c.host.InitializeQueryClient(ctx)
if err != nil {
return errors.Wrap(err, "failed to initialize host query client")
}

bridgeInfo, err := c.host.QueryBridgeConfig(ctx, childBridgeInfo.BridgeId)
if err != nil {
return err
Expand Down Expand Up @@ -179,8 +195,25 @@ func (c *Challenger) Start(ctx types.Context) error {
return c.challengeHandler(ctx)
})

c.host.Start(ctx)
c.child.Start(ctx)
// Start components in separate goroutines to ensure proper shutdown handling
ctx.ErrGrp().Go(func() (err error) {
defer func() {
ctx.Logger().Info("host stopped")
}()
c.host.Start(ctx)
<-ctx.Done()
return nil
})

ctx.ErrGrp().Go(func() (err error) {
defer func() {
ctx.Logger().Info("child stopped")
}()
c.child.Start(ctx)
<-ctx.Done()
return nil
})

return ctx.ErrGrp().Wait()
}

Expand Down
16 changes: 8 additions & 8 deletions challenger/types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
)

type NodeConfig struct {
ChainID string `json:"chain_id"`
Bech32Prefix string `json:"bech32_prefix"`
RPCAddress string `json:"rpc_address"`
ChainID string `json:"chain_id"`
Bech32Prefix string `json:"bech32_prefix"`
RPCAddresses []string `json:"rpc_addresses"`
}

func (nc NodeConfig) Validate() error {
Expand All @@ -20,7 +20,7 @@ func (nc NodeConfig) Validate() error {
if nc.Bech32Prefix == "" {
return errors.New("bech32 prefix is required")
}
if nc.RPCAddress == "" {
if len(nc.RPCAddresses) == 0 {
return errors.New("RPC address is required")
}
return nil
Expand Down Expand Up @@ -66,13 +66,13 @@ func DefaultConfig() *Config {
L1Node: NodeConfig{
ChainID: "testnet-l1-1",
Bech32Prefix: "init",
RPCAddress: "tcp://localhost:26657",
RPCAddresses: []string{"tcp://localhost:26657"},
},

L2Node: NodeConfig{
ChainID: "testnet-l2-1",
Bech32Prefix: "init",
RPCAddress: "tcp://localhost:27657",
RPCAddresses: []string{"tcp://localhost:27657"},
},
DisableAutoSetL1Height: false,
L1StartHeight: 1,
Expand Down Expand Up @@ -114,7 +114,7 @@ func (cfg Config) Validate() error {
func (cfg Config) L1NodeConfig() nodetypes.NodeConfig {
nc := nodetypes.NodeConfig{
ChainID: cfg.L1Node.ChainID,
RPC: cfg.L1Node.RPCAddress,
RPC: cfg.L1Node.RPCAddresses,
ProcessType: nodetypes.PROCESS_TYPE_DEFAULT,
Bech32Prefix: cfg.L1Node.Bech32Prefix,
}
Expand All @@ -124,7 +124,7 @@ func (cfg Config) L1NodeConfig() nodetypes.NodeConfig {
func (cfg Config) L2NodeConfig() nodetypes.NodeConfig {
nc := nodetypes.NodeConfig{
ChainID: cfg.L2Node.ChainID,
RPC: cfg.L2Node.RPCAddress,
RPC: cfg.L2Node.RPCAddresses,
ProcessType: nodetypes.PROCESS_TYPE_DEFAULT,
Bech32Prefix: cfg.L2Node.Bech32Prefix,
}
Expand Down
11 changes: 9 additions & 2 deletions cmd/opinitd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,14 @@ v0.1.9-2: Fill block hash of finalized tree
return err
}

rpcTimeout, err := cmd.Flags().GetDuration(flagRPCTimeout)
if err != nil {
return err
}

baseCtx := types.NewContext(cmdCtx, ctx.logger.Named(string(bottypes.BotTypeExecutor)), ctx.homePath).
WithPollingInterval(interval)
WithPollingInterval(interval).
WithRPCTimeout(rpcTimeout)

configPath, err := getConfigPath(cmd, ctx.homePath, string(bottypes.BotTypeExecutor))
if err != nil {
Expand All @@ -79,7 +85,7 @@ v0.1.9-2: Fill block hash of finalized tree
return err
}

rpcClient, err := rpcclient.NewRPCClient(cdc, l2Config.RPC)
rpcClient, err := rpcclient.NewRPCClient(baseCtx, cdc, l2Config.RPC, baseCtx.Logger().Named("migration-rpcclient"))
if err != nil {
return err
}
Expand All @@ -106,5 +112,6 @@ v0.1.9-2: Fill block hash of finalized tree
}
cmd = configFlag(ctx.v, cmd)
cmd.Flags().Duration(flagPollingInterval, 100*time.Millisecond, "Polling interval in milliseconds")
cmd.Flags().Duration(flagRPCTimeout, 5*time.Second, "RPC timeout duration")
return cmd
}
10 changes: 9 additions & 1 deletion cmd/opinitd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

const (
flagPollingInterval = "polling-interval"
flagRPCTimeout = "rpc-timeout"
)

func startCmd(cmdCtx *cmdContext) *cobra.Command {
Expand Down Expand Up @@ -62,9 +63,15 @@ Currently supported bots:
return err
}

rpcTimeout, err := cmd.Flags().GetDuration(flagRPCTimeout)
if err != nil {
return err
}

baseCtx := types.NewContext(ctx, cmdCtx.logger.Named(string(botType)), cmdCtx.homePath).
WithErrGrp(errGrp).
WithPollingInterval(interval)
WithPollingInterval(interval).
WithRPCTimeout(rpcTimeout)
err = bot.Initialize(baseCtx)
if err != nil {
return err
Expand All @@ -75,6 +82,7 @@ Currently supported bots:

cmd = configFlag(cmdCtx.v, cmd)
cmd.Flags().Duration(flagPollingInterval, 100*time.Millisecond, "Polling interval in milliseconds")
cmd.Flags().Duration(flagRPCTimeout, 5*time.Second, "RPC timeout duration")
return cmd
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/opinitd/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func QueryBridgeId(ctx types.Context, cfg *executortypes.Config) (uint64, error)
return 0, err
}

l2RpcClient, err := rpcclient.NewRPCClient(cdc, l2Config.RPC)
l2RpcClient, err := rpcclient.NewRPCClient(ctx, cdc, l2Config.RPC, ctx.Logger().Named("l2-rpcclient"))
if err != nil {
return 0, err
}
Expand All @@ -247,7 +247,7 @@ func l1ProposerAccount(ctx types.Context, cfg *executortypes.Config, bridgeId ui
return nil, err
}

rpcClient, err := rpcclient.NewRPCClient(cdc, l1Config.RPC)
rpcClient, err := rpcclient.NewRPCClient(ctx, cdc, l1Config.RPC, ctx.Logger().Named("l1-rpcclient"))
if err != nil {
return nil, err
}
Expand All @@ -272,7 +272,7 @@ func l2BroadcasterAccount(ctx types.Context, cfg *executortypes.Config) (*broadc
return nil, err
}

rpcClient, err := rpcclient.NewRPCClient(cdc, l2Config.RPC)
rpcClient, err := rpcclient.NewRPCClient(ctx, cdc, l2Config.RPC, ctx.Logger().Named("l2-rpcclient"))
if err != nil {
return nil, err
}
Expand Down
6 changes: 3 additions & 3 deletions e2e/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ func (op OPTestHelper) ExecutorConfig() *executortypes.Config {
L1Node: executortypes.NodeConfig{
ChainID: op.Initia.Config().ChainID,
Bech32Prefix: op.Initia.Config().Bech32Prefix,
RPCAddress: fmt.Sprintf("http://%s:26657", op.Initia.GetFullNode().HostName()),
RPCAddresses: []string{fmt.Sprintf("http://%s:26657", op.Initia.GetFullNode().HostName())},
GasPrice: op.Initia.Config().GasPrices,
GasAdjustment: op.Initia.Config().GasAdjustment,
TxTimeout: 60,
Expand All @@ -693,7 +693,7 @@ func (op OPTestHelper) ExecutorConfig() *executortypes.Config {
L2Node: executortypes.NodeConfig{
ChainID: op.Minitia.Config().ChainID,
Bech32Prefix: op.Minitia.Config().Bech32Prefix,
RPCAddress: fmt.Sprintf("http://%s:26657", op.Minitia.GetFullNode().HostName()),
RPCAddresses: []string{fmt.Sprintf("http://%s:26657", op.Minitia.GetFullNode().HostName())},
GasPrice: "",
GasAdjustment: op.Minitia.Config().GasAdjustment,
TxTimeout: 60,
Expand All @@ -702,7 +702,7 @@ func (op OPTestHelper) ExecutorConfig() *executortypes.Config {
DANode: executortypes.NodeConfig{
ChainID: op.DA.Config().ChainID,
Bech32Prefix: op.DA.Config().Bech32Prefix,
RPCAddress: fmt.Sprintf("http://%s:26657", op.DA.GetFullNode().HostName()),
RPCAddresses: []string{fmt.Sprintf("http://%s:26657", op.DA.GetFullNode().HostName())},
GasPrice: op.DA.Config().GasPrices,
GasAdjustment: op.DA.Config().GasAdjustment,
TxTimeout: 60,
Expand Down
17 changes: 13 additions & 4 deletions executor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,32 @@ To configure the Executor, fill in the values in the `~/.opinit/executor.json` f
"l1_node": {
"chain_id": "testnet-l1-1",
"bech32_prefix": "init",
"rpc_address": "tcp://localhost:26657",
"rpc_addresses": [
"tcp://doi-rpc.com",
"tcp://localhost:26657"
],
"gas_price": "0.15uinit",
"gas_adjustment": 1.5,
"tx_timeout": 60
},
"l2_node": {
"chain_id": "testnet-l2-1",
"bech32_prefix": "init",
"rpc_address": "tcp://localhost:27657",
"rpc_addresses": [
"tcp://moro-rpc:27657",
"tcp://localhost:27657"
],
"gas_price": "",
"gas_adjustment": 1.5,
"tx_timeout": 60
},
"da_node": {
"chain_id": "testnet-l1-1",
"bech32_prefix": "init",
"rpc_address": "tcp://localhost:26657",
"rpc_addresses": [
"tcp://rene-rpc:26657",
"tcp://localhost:26657"
],
"gas_price": "0.15uinit",
"gas_adjustment": 1.5,
"tx_timeout": 60
Expand Down Expand Up @@ -320,7 +329,7 @@ If the batch info registered in the chain is changed to change the account or DA

```go
{
RPCAddress string `json:"rpc_address"`
RPCAddresses []string `json:"rpc_addresses"`
GasPrice string `json:"gas_price"`
GasAdjustment string `json:"gas_adjustment"`
ChainID string `json:"chain_id"`
Expand Down
10 changes: 8 additions & 2 deletions executor/batchsubmitter/batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,10 @@ func TestFinalizeBatch(t *testing.T) {
require.NoError(t, err)

mockCaller := mockclient.NewMockCaller()
rpcClient := rpcclient.NewRPCClientWithClient(appCodec, client.NewWithCaller(mockCaller))
testLogger, _ := zap.NewDevelopment()
testCtx := types.NewContext(context.Background(), testLogger, "/tmp").WithRPCTimeout(5 * time.Second)
rpcClient, err := rpcclient.NewRPCClientWithClient(testCtx, appCodec, client.NewWithCaller(mockCaller), []string{"http://localhost:26657"}, testLogger)
require.NoError(t, err)
batchNode := node.NewTestNode(nodetypes.NodeConfig{}, batchDB, appCodec, txConfig, rpcClient, nil)

hostCdc, _, err := hostprovider.GetCodec("init")
Expand Down Expand Up @@ -753,7 +756,10 @@ func TestSubmitGenesis(t *testing.T) {
require.NoError(t, err)

mockCaller := mockclient.NewMockCaller()
rpcClient := rpcclient.NewRPCClientWithClient(appCodec, client.NewWithCaller(mockCaller))
testLogger, _ := zap.NewDevelopment()
testCtx := types.NewContext(context.Background(), testLogger, "/tmp").WithRPCTimeout(5 * time.Second)
rpcClient, err := rpcclient.NewRPCClientWithClient(testCtx, appCodec, client.NewWithCaller(mockCaller), []string{"http://localhost:26657"}, testLogger)
require.NoError(t, err)
batchNode := node.NewTestNode(nodetypes.NodeConfig{}, batchDB, appCodec, txConfig, rpcClient, nil)

hostCdc, _, err := hostprovider.GetCodec("init")
Expand Down
5 changes: 4 additions & 1 deletion executor/batchsubmitter/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ func TestRawBlockHandler(t *testing.T) {
require.NoError(t, err)

mockCaller := mockclient.NewMockCaller()
rpcClient := rpcclient.NewRPCClientWithClient(appCodec, client.NewWithCaller(mockCaller))
testLogger, _ := zap.NewDevelopment()
testCtx := types.NewContext(context.Background(), testLogger, "/tmp").WithRPCTimeout(5 * time.Second)
rpcClient, err := rpcclient.NewRPCClientWithClient(testCtx, appCodec, client.NewWithCaller(mockCaller), []string{"http://localhost:26657"}, testLogger)
require.NoError(t, err)
batchNode := node.NewTestNode(nodetypes.NodeConfig{}, batchDB, appCodec, txConfig, rpcClient, nil)

hostCdc, _, err := hostprovider.GetCodec("init")
Expand Down
Loading
Loading