blocklag

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 12 Imported by: 0

README

failnext-blocklag

failnext-blocklag is a small Go companion library for failnext that detects relative block-height lag across a fixed set of Ethereum HTTP JSON-RPC providers.

An RPC provider can return HTTP 200 while several blocks behind its peers. blocklag checks their reported heights directly with eth_blockNumber and supplies eligibility through failnext.Config.Eligible.

blocklag performs the block-height checks; failnext handles endpoint selection and HTTP failover. failnext itself does not detect blockchain lag or stale state.

Quick start

Use the same endpoints for monitoring and failover:

endpoints := []failnext.Endpoint{
    {ID: "primary", URL: "https://provider-a.example/rpc"},
    {ID: "backup", URL: "https://provider-b.example/rpc"},
}

monitor, err := blocklag.New(blocklag.Config{
    Endpoints: endpoints,
    MaxLag:    3,
})
if err != nil {
    return err
}

// The application decides when to refresh eligibility.
checkCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := monitor.Check(checkCtx); err != nil {
    // A partially successful check may still publish new eligibility.
    log.Printf("block-lag check: %v", err)
}

tr, err := failnext.New(failnext.Config{
    Endpoints: endpoints,
    Eligible:  monitor.Eligible,
})
if err != nil {
    return err
}

httpClient := &http.Client{Transport: tr}
// Use httpClient with your HTTP / JSON-RPC application client.

This excerpt assumes the necessary imports, an existing ctx, and a surrounding function returning error.

Ethereum JSON-RPC reads use HTTP POST. Allow failover only for operations that are safe to repeat, using failnext.WithFailoverAllowed(ctx); eligibility does not grant failover permission or make a request replayable.

How it works

Each Check(ctx) probes every configured URL directly and concurrently with one eth_blockNumber HTTP POST per provider, outside failnext. A probe succeeds only with HTTP 200 and a valid JSON-RPC 2.0 response containing the expected ID and a canonical hexadecimal block number fitting uint64. Probes do not follow redirects or retry; response bodies are limited to 4 KiB and closed.

The reference is the highest successfully observed block in that check. A successful provider is eligible when:

highestObservedBlock - providerBlock <= MaxLag

The threshold is inclusive: MaxLag: 0 accepts only providers at the highest observed height. With at least one successful probe, Check publishes a complete eligibility snapshot. Eligible(id) reads that snapshot without network I/O.

There is no background polling loop. The application calls Check on its own schedule and provides a suitable context deadline.

Failure behavior
Check result Eligibility state Returned error
All probes succeed Publish the new comparison nil
Some probes succeed Publish a new snapshot; failed providers are ineligible Error identifying failed endpoints
All probes fail Retain the previous snapshot Error
Cancellation observed before publication Retain the previous snapshot Context error

Even one successful probe is enough to publish a snapshot; that provider is eligible. Before the first published check, all configured endpoints are eligible (fail-open), while unknown IDs are ineligible. A later successful check can restore previously ineligible providers.

A Check error does not necessarily mean eligibility was unchanged. Partial failures publish state and return an error. Multiple probe errors are joined, with endpoint IDs for attribution.

Concurrency and limits

Check calls are serialized; a waiting call can be canceled. Probes within a check run concurrently. Eligible is safe for concurrent use and never waits for network probes. Separate Eligible calls can observe different snapshots if publication occurs between them, even during one failnext request; failnext retains each eligibility decision for that request.

Published eligibility does not expire. If checks stop or every subsequent check fails, the last snapshot remains in use.

Relative, not canonical, freshness. If all providers are equally stale, blocklag cannot detect it; an unusually high reported block can make others appear lagging. Configure endpoints for the same intended Ethereum-compatible chain. The library does not validate chain IDs, forks, block hashes, finality, or reported heights against an independent source.

Eligibility is not routing. failnext still applies its own endpoint priority, cooldown, exclusions, failover permission, triggers, and replay rules. An in-flight request may already have evaluated eligibility before a new snapshot was published.

blocklag.Config.Base is used for monitoring probes only and defaults to http.DefaultTransport. A custom transport must preserve the probed destination, honor the request context, and must not reroute through failnext. Its resources belong to the caller.

There is no automatic scheduling, TTL, retry/backoff policy, scoring, metrics, service discovery, or WebSocket support.

Examples

ExampleMonitor is a runnable local-server example of JSON-RPC POST integration with failnext:

go test -run '^ExampleMonitor$' -v

For ongoing monitoring with go-ethereum, see examples/goethereum/continuous. It checks eligibility every 15 seconds and reads the latest block every 10 seconds, with separate operation timeouts and signal-based shutdown.

Configure two HTTP RPC URLs on the same chain. From the repository root:

export ETH_RPC_PRIMARY_URL='https://your-primary.example/rpc'
export ETH_RPC_BACKUP_URL='https://your-backup.example/rpc'
cd examples/goethereum
go run ./continuous

The go-ethereum example is a separate Go 1.25+ module, so go-ethereum is not a dependency of blocklag. It uses a local replace for blocklag; run go mod tidy inside examples/goethereum to resolve module checksums.

Installation

Requires Go 1.24 or newer.

go get github.com/yermakovsa/failnext-blocklag

License

MIT. See LICENSE.

Documentation

Overview

Package blocklag is a Go companion library for failnext that detects relative block-height lag across a fixed set of Ethereum HTTP JSON-RPC providers.

Call Monitor.Check to query providers directly with eth_blockNumber, then pass Monitor.Eligible to failnext.Config.Eligible.

blocklag determines provider eligibility from relative block-height lag; failnext handles endpoint selection and HTTP failover. failnext itself does not detect blockchain lag or stale state.

Checks are initiated by the caller; no background polling occurs. Eligibility reflects the last published comparison, not verified canonical-chain freshness.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Endpoints is the fixed set of Ethereum HTTP JSON-RPC destinations.
	// Each endpoint needs a nonempty, unique ID and an absolute HTTP or HTTPS
	// URL with a host and no fragment. Endpoints should be on the same
	// Ethereum-compatible chain.
	Endpoints []failnext.Endpoint

	// MaxLag is the largest permitted difference from the highest successfully
	// observed block in each Check. The limit is inclusive; zero is valid.
	MaxLag uint64

	// Base performs blocklag's direct HTTP probes, not failnext's application
	// requests. If nil, http.DefaultTransport is used. A custom transport must
	// preserve the requested destination, honor request cancellation, and must
	// not route probes through failnext.
	Base http.RoundTripper
}

Config configures a Monitor.

type Monitor

type Monitor struct {
	// contains filtered or unexported fields
}

Monitor maintains eligibility based on the last published comparison of block heights for a fixed set of Ethereum RPC endpoints. Construct it with New; the zero value is not usable.

Example
// Local test servers stand in for two Ethereum HTTP RPC providers.
provider := func(block, balance string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var request struct {
			Method string `json:"method"`
		}
		if r.Method != http.MethodPost || json.NewDecoder(r.Body).Decode(&request) != nil {
			http.Error(w, "invalid RPC request", http.StatusBadRequest)
			return
		}

		var result string
		switch request.Method {
		case "eth_blockNumber":
			result = block
		case "eth_getBalance":
			result = balance
		default:
			http.Error(w, "unexpected RPC method", http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":1,"result":%q}`, result)
	})
}

primary := httptest.NewServer(provider("0x64", "0x0")) // Block 100.
defer primary.Close()
backup := httptest.NewServer(provider("0x6e", "0x2a")) // Block 110.
defer backup.Close()

endpoints := []failnext.Endpoint{
	{ID: "primary", URL: primary.URL},
	{ID: "backup", URL: backup.URL},
}
monitor, err := blocklag.New(blocklag.Config{
	Endpoints: endpoints,
	MaxLag:    3,
})
if err != nil {
	panic(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := monitor.Check(ctx); err != nil {
	panic(err)
}

fmt.Println("primary eligible:", monitor.Eligible("primary"))
fmt.Println("backup eligible:", monitor.Eligible("backup"))

tr, err := failnext.New(failnext.Config{
	Endpoints: endpoints,
	Eligible:  monitor.Eligible,
})
if err != nil {
	panic(err)
}
client := &http.Client{Transport: tr}
defer client.CloseIdleConnections()

// Ethereum JSON-RPC reads use POST. Allow failover only when the
// application knows the operation is safe to repeat.
readCtx := failnext.WithFailoverAllowed(ctx)
req, err := http.NewRequestWithContext(readCtx, http.MethodPost, endpoints[0].URL,
	strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0x0000000000000000000000000000000000000000","latest"]}`))
if err != nil {
	panic(err)
}
req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
	panic(err)
}
defer resp.Body.Close()
var rpcResult struct {
	Result string `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&rpcResult); err != nil {
	panic(err)
}
fmt.Println("balance:", rpcResult.Result)
Output:
primary eligible: false
backup eligible: true
balance: 0x2a

func New

func New(cfg Config) (*Monitor, error)

New validates the configuration and constructs a Monitor. It copies the endpoint slice so later caller modifications do not affect it. All configured endpoints are initially eligible.

func (*Monitor) Check

func (m *Monitor) Check(ctx context.Context) error

Check probes all configured providers directly with eth_blockNumber, outside failnext, and publishes their relative eligibility. Provider probes run concurrently, but overlapping calls to Check are serialized.

A successful probe is eligible when its height is within MaxLag blocks of the highest successfully observed height. If at least one probe succeeds, Check publishes a complete new snapshot; failed probes are ineligible. Partial probe failure still returns an error, even though state is updated. Probe errors identify failed endpoint IDs; multiple errors are joined.

If every probe fails or cancellation is observed before publication, the previous snapshot is retained. A waiting Check can be canceled without starting probes. The caller owns the checking schedule; published eligibility never expires.

func (*Monitor) Eligible

func (m *Monitor) Eligible(id failnext.EndpointID) bool

Eligible reports whether id is eligible according to the latest published snapshot. All configured endpoints are initially eligible; unknown IDs are always ineligible. It performs no network I/O and is safe for concurrent use. Separate calls may observe different snapshots if Check publishes between them. It can be used directly as failnext.Config.Eligible. It only supplies eligibility; failnext still performs endpoint selection and HTTP failover.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL