diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index d090ccb5..c0796f28 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -7,12 +7,12 @@ jobs: steps: - uses: actions/setup-go@v5 with: - go-version: 1.24.2 + go-version: 1.25.3 - uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v7 with: - version: v2.1.6 + version: v2.12.2 args: --timeout=5m0s diff --git a/Dockerfile b/Dockerfile index d6252374..5af3424c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ============================ Build Stage ============================ -FROM --platform=$BUILDPLATFORM golang:1.24.2-alpine3.20 AS build +FROM --platform=$BUILDPLATFORM golang:1.25.3-alpine3.21 AS build LABEL org.opencontainers.image.source="https://github.com/bandprotocol/falcon" diff --git a/README.md b/README.md index 38cbe25e..0aefb8a8 100644 --- a/README.md +++ b/README.md @@ -111,11 +111,11 @@ sudo apt-get update && \ sudo apt-get upgrade -y && \ sudo apt-get install -y build-essential curl wget jq ``` -- Install Go 1.24.2 +- Install Go 1.25.3 ```shell -# Install Go 1.24.2 -wget https://go.dev/dl/go1.24.2.linux-amd64.tar.gz -tar xf go1.24.2.linux-amd64.tar.gz +# Install Go 1.25.3 +wget https://go.dev/dl/go1.25.3.linux-amd64.tar.gz +tar xf go1.25.3.linux-amd64.tar.gz sudo mv go /usr/local/go # Set Go path to $PATH variable @@ -247,38 +247,30 @@ Enter your private key If you already have a mnemonic and want to retrieve key from it, you can choose `Mnemonic` option. ``` Enter your mnemonic -> Enter a coin type Coin type number for HD derivation (default: 60; leave empty to use default) -> Enter an account Account number in the HD derivation path (default: 0; leave empty to use default) -> Enter an index Index number for the specific address within an account in the HD derivation path (default: 0; leave empty to use default) -> ``` If you want to generate a new address, choose the `Generate new address` option. ``` Enter a coin type Coin type number for HD derivation (default: 60; leave empty to use default) -> Enter an account Account number in the HD derivation path (default: 0; leave empty to use default) -> Enter an index Index number for the specific address within an account in the HD derivation path (default: 0; leave empty to use default) -> ``` ### 7. Check that the keys for the configured chains are funded - You can query the balance of each configured key by running: ``` shell falcon q balance testkey @@ -288,7 +280,7 @@ Starts all tunnels that `falcon query tunnels` can query ``` shell falcon start ``` -> NOTE: You can choose which tunnels do you want to relay. +NOTE: You can choose which tunnels do you want to relay. ``` shell falcon start 1 2 3 ``` @@ -296,7 +288,6 @@ falcon start 1 2 3 ## Generate Go Protobuf Code Falcon uses gRPC and Protocol Buffers for its internal APIs. If you modify any `.proto` files under the `proto/` directory, regenerate the Go code by running: - ```sh make proto ``` @@ -320,8 +311,8 @@ go install github.com/pressly/goose/v3/cmd/goose@latest ``` #### Run -> Use `-dir` before the driver/DSN. -> Supported drivers: `postgres`, `sqlite`. +Use `-dir` before the driver/DSN. +Supported drivers: `postgres`, `sqlite`. ##### Up (apply all pending) ```sh diff --git a/go.mod b/go.mod index 3e1fc6ca..4c39d95d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/bandprotocol/falcon -go 1.24.3 +go 1.25.3 require ( cosmossdk.io/math v1.4.0 @@ -30,7 +30,7 @@ require ( go.uber.org/mock v0.6.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.46.0 - google.golang.org/grpc v1.77.0 + google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 @@ -156,7 +156,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/pgx/v5 v5.9.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -241,9 +241,9 @@ require ( go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.48.0 // indirect diff --git a/go.sum b/go.sum index 99b004dc..d0f4acfd 100644 --- a/go.sum +++ b/go.sum @@ -621,8 +621,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/pgx/v5 v5.9.0 h1:T/dI+2TvmI2H8s/KH1/lXIbz1CUFk3gn5oTjr0/mBsE= +github.com/jackc/pgx/v5 v5.9.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= @@ -1094,16 +1094,16 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1488,8 +1488,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/relayer/chains/evm/client.go b/relayer/chains/evm/client.go index 89e85fb7..ba1b6326 100644 --- a/relayer/chains/evm/client.go +++ b/relayer/chains/evm/client.go @@ -51,10 +51,11 @@ type Client interface { // Client is the struct that handles interactions with the EVM chain. type client struct { - ChainName string - Endpoints []string - QueryTimeout time.Duration - ExecuteTimeout time.Duration + ChainName string + Endpoints []string + QueryTimeout time.Duration + ExecuteTimeout time.Duration + BlockConfirmation uint64 Log logger.Logger @@ -90,13 +91,14 @@ func dialEVMEndpoint(ctx context.Context, endpoint string) (*ethclient.Client, e // NewClient creates a new EVM client from config file and load keys. func NewClient(chainName string, cfg *EVMChainProviderConfig, log logger.Logger, alert alert.Alert) *client { return &client{ - ChainName: chainName, - Endpoints: cfg.Endpoints, - QueryTimeout: cfg.QueryTimeout, - ExecuteTimeout: cfg.ExecuteTimeout, - Log: log.With("chain_name", chainName), - alert: alert, - clients: NewEVMClients(), + ChainName: chainName, + Endpoints: cfg.Endpoints, + QueryTimeout: cfg.QueryTimeout, + ExecuteTimeout: cfg.ExecuteTimeout, + BlockConfirmation: cfg.BlockConfirmation, + Log: log.With("chain_name", chainName), + alert: alert, + clients: NewEVMClients(), } } @@ -504,7 +506,10 @@ func (c *client) BroadcastTx(ctx context.Context, tx *gethtypes.Transaction) (st return tx.Hash().Hex(), nil } -// getClientWithMaxHeight connects to the endpoint that has the highest block height. +// getClientWithMaxHeight selects an endpoint by config order within the confirmed +// block range. It collects block heights from all endpoints in parallel, then +// picks the first endpoint (in config order) whose block height is within +// [maxHeight - BlockConfirmation, maxHeight]. func (c *client) getClientWithMaxHeight(ctx context.Context) (ClientConnectionResult, error) { ch := make(chan ClientConnectionResult, len(c.Endpoints)) @@ -554,16 +559,34 @@ func (c *client) getClientWithMaxHeight(ctx context.Context) (ClientConnectionRe }(endpoint) } - var result ClientConnectionResult + // Collect all results into a map and track the maximum block height. + resultMap := make(map[string]ClientConnectionResult, len(c.Endpoints)) + var maxHeight uint64 for i := 0; i < len(c.Endpoints); i++ { r := <-ch if r.Client != nil { - if r.BlockHeight > result.BlockHeight || (r.Endpoint == c.clients.GetSelectedEndpoint() && r.BlockHeight == result.BlockHeight) { - result = r + resultMap[r.Endpoint] = r + if r.BlockHeight > maxHeight { + maxHeight = r.BlockHeight } } } + // Determine the minimum acceptable block height based on BlockConfirmation. + var minHeight uint64 + if maxHeight >= c.BlockConfirmation { + minHeight = maxHeight - c.BlockConfirmation + } + + // Pick the first endpoint in config order that is within the confirmed range. + var result ClientConnectionResult + for _, endpoint := range c.Endpoints { + if r, ok := resultMap[endpoint]; ok && r.BlockHeight >= minHeight { + result = r + break + } + } + if result.Client == nil { alert.HandleAlert( c.alert, diff --git a/relayer/chains/flow/client.go b/relayer/chains/flow/client.go index 0b809997..693873ea 100644 --- a/relayer/chains/flow/client.go +++ b/relayer/chains/flow/client.go @@ -47,10 +47,11 @@ var _ Client = (*client)(nil) // client is the concrete implementation that handles Flow HTTP interactions. type client struct { - ChainName string - Endpoints []string - QueryTimeout time.Duration - ExecuteTimeout time.Duration + ChainName string + Endpoints []string + QueryTimeout time.Duration + ExecuteTimeout time.Duration + BlockConfirmation uint64 Log logger.Logger alert alert.Alert @@ -61,17 +62,19 @@ type client struct { // NewClient creates a new Flow client from config. func NewClient(chainName string, cfg *FlowChainProviderConfig, log logger.Logger, a alert.Alert) Client { return &client{ - ChainName: chainName, - Endpoints: cfg.Endpoints, - QueryTimeout: cfg.QueryTimeout, - ExecuteTimeout: cfg.ExecuteTimeout, - Log: log.With("chain_name", chainName), - alert: a, - clients: NewFlowClients(), + ChainName: chainName, + Endpoints: cfg.Endpoints, + QueryTimeout: cfg.QueryTimeout, + ExecuteTimeout: cfg.ExecuteTimeout, + BlockConfirmation: 5, + Log: log.With("chain_name", chainName), + alert: a, + clients: NewFlowClients(), } } -// Connect connects to all endpoints and selects the one with the highest block height. +// Connect connects to all endpoints and selects the first eligible one by config order +// within the confirmed block range. func (c *client) Connect(_ context.Context) error { var wg sync.WaitGroup for _, endpoint := range c.Endpoints { @@ -123,7 +126,10 @@ func (c *client) Connect(_ context.Context) error { return nil } -// getClientWithMaxHeight selects the endpoint with the highest sealed block height. +// getClientWithMaxHeight selects an endpoint by config order within the confirmed +// block range. It collects block heights from all endpoints in parallel, then +// picks the first endpoint (in config order) whose block height is within +// [maxHeight - BlockConfirmation, maxHeight]. func (c *client) getClientWithMaxHeight() (ClientConnectionResult, error) { ch := make(chan ClientConnectionResult, len(c.Endpoints)) @@ -163,17 +169,34 @@ func (c *client) getClientWithMaxHeight() (ClientConnectionResult, error) { }(endpoint) } - var result ClientConnectionResult + // Collect all results into a map and track the maximum block height. + resultMap := make(map[string]ClientConnectionResult, len(c.Endpoints)) + var maxHeight uint64 for range c.Endpoints { r := <-ch if r.Client != nil { - if r.BlockHeight > result.BlockHeight || - (r.Endpoint == c.clients.GetSelectedEndpoint() && r.BlockHeight == result.BlockHeight) { - result = r + resultMap[r.Endpoint] = r + if r.BlockHeight > maxHeight { + maxHeight = r.BlockHeight } } } + // Determine the minimum acceptable block height based on BlockConfirmation. + var minHeight uint64 + if maxHeight >= c.BlockConfirmation { + minHeight = maxHeight - c.BlockConfirmation + } + + // Pick the first endpoint in config order that is within the confirmed range. + var result ClientConnectionResult + for _, endpoint := range c.Endpoints { + if r, ok := resultMap[endpoint]; ok && r.BlockHeight >= minHeight { + result = r + break + } + } + if result.Client == nil { alert.HandleAlert( c.alert, diff --git a/relayer/chains/icon/client.go b/relayer/chains/icon/client.go index 1d4fa221..899543bb 100644 --- a/relayer/chains/icon/client.go +++ b/relayer/chains/icon/client.go @@ -38,8 +38,9 @@ type Client interface { // Client is the struct that handles interactions with the Icon chain. type client struct { - ChainName string - Endpoints []string + ChainName string + Endpoints []string + BlockConfirmation uint64 Log logger.Logger @@ -50,11 +51,12 @@ type client struct { // NewClient creates a new Icon client from config file and load keys. func NewClient(chainName string, cfg *IconChainProviderConfig, log logger.Logger, alert alert.Alert) *client { return &client{ - ChainName: chainName, - Endpoints: cfg.Endpoints, - Log: log.With("chain_name", chainName), - alert: alert, - clients: NewIconClients(), + ChainName: chainName, + Endpoints: cfg.Endpoints, + BlockConfirmation: 5, + Log: log.With("chain_name", chainName), + alert: alert, + clients: NewIconClients(), } } @@ -111,7 +113,10 @@ func (c *client) StartLivelinessCheck(ctx context.Context, interval time.Duratio } } -// getClientWithMaxHeight connects to the endpoint that has the highest block height. +// getClientWithMaxHeight selects an endpoint by config order within the confirmed +// block range. It collects block heights from all endpoints in parallel, then +// picks the first endpoint (in config order) whose block height is within +// [maxHeight - BlockConfirmation, maxHeight]. func (c *client) getClientWithMaxHeight() (ClientConnectionResult, error) { ch := make(chan ClientConnectionResult, len(c.Endpoints)) @@ -158,17 +163,34 @@ func (c *client) getClientWithMaxHeight() (ClientConnectionResult, error) { }(endpoint) } - var result ClientConnectionResult + // Collect all results into a map and track the maximum block height. + resultMap := make(map[string]ClientConnectionResult, len(c.Endpoints)) + var maxHeight uint64 for i := 0; i < len(c.Endpoints); i++ { r := <-ch if r.Client != nil { - if r.BlockHeight > result.BlockHeight || - (r.Endpoint == c.clients.GetSelectedEndpoint() && r.BlockHeight == result.BlockHeight) { - result = r + resultMap[r.Endpoint] = r + if r.BlockHeight > maxHeight { + maxHeight = r.BlockHeight } } } + // Determine the minimum acceptable block height based on BlockConfirmation. + var minHeight uint64 + if maxHeight >= c.BlockConfirmation { + minHeight = maxHeight - c.BlockConfirmation + } + + // Pick the first endpoint in config order that is within the confirmed range. + var result ClientConnectionResult + for _, endpoint := range c.Endpoints { + if r, ok := resultMap[endpoint]; ok && r.BlockHeight >= minHeight { + result = r + break + } + } + if result.Client == nil { alert.HandleAlert( c.alert, diff --git a/relayer/chains/secret/client.go b/relayer/chains/secret/client.go index 5ca4fc2a..cabdd1ae 100644 --- a/relayer/chains/secret/client.go +++ b/relayer/chains/secret/client.go @@ -58,9 +58,10 @@ type typesBlockResult struct { var _ Client = (*client)(nil) type client struct { - chainName string - endpoints []string - denom string + chainName string + endpoints []string + denom string + blockConfirmation int64 log logger.Logger alert alert.Alert @@ -69,12 +70,13 @@ type client struct { func NewClient(chainName string, cpc *SecretChainProviderConfig, log logger.Logger, alert alert.Alert) *client { return &client{ - chainName: chainName, - endpoints: cpc.Endpoints, - denom: cpc.Denom, - log: log.With("chain_name", chainName), - alert: alert, - clients: chains.NewClientPool[sdkclient.Context](), + chainName: chainName, + endpoints: cpc.Endpoints, + denom: cpc.Denom, + blockConfirmation: 5, + log: log.With("chain_name", chainName), + alert: alert, + clients: chains.NewClientPool[sdkclient.Context](), } } @@ -337,7 +339,10 @@ func (c *client) GetBlockByHeight(ctx context.Context, height *big.Int) (*typesB return &typesBlockResult{Time: resBlock.Block.Time.UTC()}, nil } -// getClientWithMaxHeight connects to the endpoint that has the highest block height. +// getClientWithMaxHeight selects an endpoint by config order within the confirmed +// block range. It collects block heights from all endpoints in parallel, then +// picks the first endpoint (in config order) whose block height is within +// [maxHeight - blockConfirmation, maxHeight]. func (c *client) getClientWithMaxHeight(ctx context.Context) (ClientConnectionResult, error) { ch := make(chan ClientConnectionResult, len(c.endpoints)) @@ -402,16 +407,34 @@ func (c *client) getClientWithMaxHeight(ctx context.Context) (ClientConnectionRe }(endpoint) } - var result ClientConnectionResult + // Collect all results into a map and track the maximum block height. + resultMap := make(map[string]ClientConnectionResult, len(c.endpoints)) + var maxHeight int64 for i := 0; i < len(c.endpoints); i++ { r := <-ch if r.Client != nil { - if r.BlockHeight > result.BlockHeight || (r.Endpoint == c.clients.GetSelectedEndpoint() && r.BlockHeight == result.BlockHeight) { - result = r + resultMap[r.Endpoint] = r + if r.BlockHeight > maxHeight { + maxHeight = r.BlockHeight } } } + // Determine the minimum acceptable block height based on blockConfirmation. + var minHeight int64 + if maxHeight >= c.blockConfirmation { + minHeight = maxHeight - c.blockConfirmation + } + + // Pick the first endpoint in config order that is within the confirmed range. + var result ClientConnectionResult + for _, endpoint := range c.endpoints { + if r, ok := resultMap[endpoint]; ok && r.BlockHeight >= minHeight { + result = r + break + } + } + if result.Client == nil { alert.HandleAlert( c.alert, diff --git a/relayer/chains/soroban/client.go b/relayer/chains/soroban/client.go index 4087d4e6..1db81a43 100644 --- a/relayer/chains/soroban/client.go +++ b/relayer/chains/soroban/client.go @@ -46,9 +46,10 @@ type Client interface { } type client struct { - ChainName string - HorizonEndpoints []string - QueryTimeout time.Duration + ChainName string + HorizonEndpoints []string + QueryTimeout time.Duration + BlockConfirmation uint64 Log logger.Logger alert alert.Alert @@ -58,16 +59,18 @@ type client struct { func NewClient(chainName string, cfg *SorobanChainProviderConfig, log logger.Logger, alert alert.Alert) Client { return &client{ - ChainName: chainName, - HorizonEndpoints: cfg.HorizonEndpoints, - QueryTimeout: cfg.QueryTimeout, - Log: log.With("chain_name", chainName), - alert: alert, - clients: NewHorizonClients(), + ChainName: chainName, + HorizonEndpoints: cfg.HorizonEndpoints, + QueryTimeout: cfg.QueryTimeout, + BlockConfirmation: 5, + Log: log.With("chain_name", chainName), + alert: alert, + clients: NewHorizonClients(), } } -// Connect connects to all Horizon endpoints in parallel and selects the one with the highest ledger. +// Connect connects to all Horizon endpoints in parallel and selects the first eligible +// one by config order within the confirmed ledger range. func (c *client) Connect(ctx context.Context) error { var wg sync.WaitGroup for _, endpoint := range c.HorizonEndpoints { @@ -151,7 +154,10 @@ func (c *client) StartLivelinessCheck(ctx context.Context, interval time.Duratio } } -// getClientWithMaxLedger returns the connected client with the highest ingested ledger sequence. +// getClientWithMaxLedger selects an endpoint by config order within the confirmed +// ledger range. It collects ledger sequences from all endpoints in parallel, then +// picks the first endpoint (in config order) whose sequence is within +// [maxLedger - BlockConfirmation, maxLedger]. func (c *client) getClientWithMaxLedger(ctx context.Context) (ClientConnectionResult, error) { ch := make(chan ClientConnectionResult, len(c.HorizonEndpoints)) @@ -198,17 +204,34 @@ func (c *client) getClientWithMaxLedger(ctx context.Context) (ClientConnectionRe }(endpoint) } - var result ClientConnectionResult + // Collect all results into a map and track the maximum ledger sequence. + resultMap := make(map[string]ClientConnectionResult, len(c.HorizonEndpoints)) + var maxLedger uint64 for i := 0; i < len(c.HorizonEndpoints); i++ { r := <-ch if r.Client != nil { - if r.LedgerSequence > result.LedgerSequence || - (r.Endpoint == c.clients.GetSelectedEndpoint() && r.LedgerSequence == result.LedgerSequence) { - result = r + resultMap[r.Endpoint] = r + if r.LedgerSequence > maxLedger { + maxLedger = r.LedgerSequence } } } + // Determine the minimum acceptable ledger sequence based on BlockConfirmation. + var minLedger uint64 + if maxLedger >= c.BlockConfirmation { + minLedger = maxLedger - c.BlockConfirmation + } + + // Pick the first endpoint in config order that is within the confirmed range. + var result ClientConnectionResult + for _, endpoint := range c.HorizonEndpoints { + if r, ok := resultMap[endpoint]; ok && r.LedgerSequence >= minLedger { + result = r + break + } + } + if result.Client == nil { alert.HandleAlert( c.alert, diff --git a/relayer/chains/xrpl/client.go b/relayer/chains/xrpl/client.go index 81219922..fbc1b61b 100644 --- a/relayer/chains/xrpl/client.go +++ b/relayer/chains/xrpl/client.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/big" + "net/http" "sync" "time" @@ -22,6 +23,11 @@ import ( const RippleEpochOffset = 946684800 +// idleConnTimeout is shorter than the typical load-balancer idle timeout +// (e.g. AWS ALB default 60s) to prevent "connection reset by peer" errors +// from stale keep-alive connections being reused after the LB closes them. +const idleConnTimeout = 30 * time.Second + // XRPLClients holds XRPL RPC clients and the selected endpoint. type XRPLClients = chains.ClientPool[rpc.Client] @@ -46,8 +52,9 @@ var _ Client = (*client)(nil) // client is the concrete implementation that handles XRPL JSON-RPC interactions. type client struct { - ChainName string - Endpoints []string + ChainName string + Endpoints []string + BlockConfirmation uint32 Log logger.Logger alert alert.Alert @@ -64,11 +71,12 @@ type TxResult struct { // NewClient creates a new XRPL client from config. func NewClient(chainName string, cfg *XRPLChainProviderConfig, log logger.Logger, alert alert.Alert) Client { return &client{ - ChainName: chainName, - Endpoints: cfg.Endpoints, - Log: log.With("chain_name", chainName), - alert: alert, - clients: NewXRPLClients(), + ChainName: chainName, + Endpoints: cfg.Endpoints, + BlockConfirmation: 5, + Log: log.With("chain_name", chainName), + alert: alert, + clients: NewXRPLClients(), } } @@ -79,7 +87,8 @@ type ClientConnectionResult struct { LedgerIndex uint32 } -// Connect selects a responsive endpoint with the highest ledger index. +// Connect connects to all endpoints and selects the first eligible one by config order +// within the confirmed ledger range. func (c *client) Connect(_ context.Context) error { var wg sync.WaitGroup for _, endpoint := range c.Endpoints { @@ -91,7 +100,11 @@ func (c *client) Connect(_ context.Context) error { wg.Add(1) go func(endpoint string) { defer wg.Done() - opts := []rpc.ConfigOpt{} + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.IdleConnTimeout = idleConnTimeout + opts := []rpc.ConfigOpt{ + rpc.WithHTTPClient(&http.Client{Transport: transport}), + } cfg, err := rpc.NewClientConfig(endpoint, opts...) if err != nil { c.Log.Warn("XRPL endpoint config error", "endpoint", endpoint, err) @@ -133,7 +146,10 @@ func (c *client) Connect(_ context.Context) error { return nil } -// getClientWithMaxHeight connects to the endpoint that has the highest ledger index. +// getClientWithMaxHeight selects an endpoint by config order within the confirmed +// ledger range. It collects ledger indices from all endpoints in parallel, then +// picks the first endpoint (in config order) whose ledger index is within +// [maxLedger - BlockConfirmation, maxLedger]. func (c *client) getClientWithMaxHeight() (ClientConnectionResult, error) { ch := make(chan ClientConnectionResult, len(c.Endpoints)) @@ -170,17 +186,34 @@ func (c *client) getClientWithMaxHeight() (ClientConnectionResult, error) { }(endpoint) } - var result ClientConnectionResult + // Collect all results into a map and track the maximum ledger index. + resultMap := make(map[string]ClientConnectionResult, len(c.Endpoints)) + var maxLedger uint32 for range c.Endpoints { r := <-ch if r.Client != nil { - if r.LedgerIndex > result.LedgerIndex || - (r.Endpoint == c.clients.GetSelectedEndpoint() && r.LedgerIndex == result.LedgerIndex) { - result = r + resultMap[r.Endpoint] = r + if r.LedgerIndex > maxLedger { + maxLedger = r.LedgerIndex } } } + // Determine the minimum acceptable ledger index based on BlockConfirmation. + var minLedger uint32 + if maxLedger >= c.BlockConfirmation { + minLedger = maxLedger - c.BlockConfirmation + } + + // Pick the first endpoint in config order that is within the confirmed range. + var result ClientConnectionResult + for _, endpoint := range c.Endpoints { + if r, ok := resultMap[endpoint]; ok && r.LedgerIndex >= minLedger { + result = r + break + } + } + if result.Client == nil { alert.HandleAlert( c.alert,