Skip to content

Timeout-aware, auto-retrying, failover-ready RPC provider wrapper for Alloy built in Rust

License

Notifications You must be signed in to change notification settings

OpenZeppelin/Robust-Provider

Repository files navigation

Robust Provider

License

Robust, retrying wrapper around Alloy providers.

Currently production-ready for supported features. The API surface is still evolving and future feature additions may introduce breaking changes and/or changes in assumptions.

About

Robust Provider is a Rust library that wraps Alloy providers with production-ready resilience features. It adds automatic retries, timeouts, and transparent failover between multiple RPC endpoints - making it ideal for applications that need reliable blockchain connectivity.


Table of Contents


Features

  • Bounded timeouts - per-call timeouts prevent indefinite hangs on unresponsive RPC endpoints.
  • Exponential backoff retries - automatic retry with configurable backoff for transient failures.
  • Transparent failover - seamlessly switch from a primary provider to one or more fallback providers.
  • Resilient subscriptions - WebSocket block subscriptions with automatic reconnection and lag detection.

Quick Start

Add robust-provider to your Cargo.toml:

[dependencies]
robust-provider = "0.2.0"

Create a robust provider with automatic retries and fallback:

use alloy::providers::{Provider, ProviderBuilder};
use robust_provider::RobustProviderBuilder;
use std::time::Duration;
use tokio_stream::StreamExt;

async fn run() -> anyhow::Result<()> {
    let ws = ProviderBuilder::new().connect("ws://localhost:8545").await?;
    let ws_fallback = ProviderBuilder::new().connect("ws://localhost:8546").await?;

    let robust = RobustProviderBuilder::new(ws)
        .fallback(ws_fallback)
        .call_timeout(Duration::from_secs(30))
        .subscription_timeout(Duration::from_secs(120))
        .build()
        .await?;

    // Make RPC calls with automatic retries and fallback
    let block_number = robust.get_block_number().await?;
    println!("Current block: {}", block_number);

    // Create subscriptions that automatically reconnect on failure
    let sub = robust.subscribe_blocks().await?;
    let mut stream = sub.into_stream();
    while let Some(response) = stream.next().await {
        match response {
            Ok(block) => println!("New block: {:?}", block),
            Err(e) => println!("Got error: {:?}", e),
        }
    }

    Ok(())
}

Usage

Building a Provider

RobustProviderBuilder provides a fluent API for constructing a RobustProvider with custom settings:

use alloy::providers::ProviderBuilder;
use robust_provider::RobustProviderBuilder;
use std::time::Duration;

// Standard configuration with retries
let provider = ProviderBuilder::new().connect("ws://localhost:8545").await?;
let robust = RobustProviderBuilder::new(provider)
    .call_timeout(Duration::from_secs(30))
    .max_retries(3)
    .build()
    .await?;

// With multiple fallback providers
let primary = ProviderBuilder::new().connect("ws://primary:8545").await?;
let fallback_1 = ProviderBuilder::new().connect("ws://fallback1:8545").await?;
let fallback_2 = ProviderBuilder::new().connect_http("http://fallback2:8545".parse()?);

let robust = RobustProviderBuilder::new(primary)
    .fallback(fallback_1)
    .fallback(fallback_2)
    .build()
    .await?;

Configuration Options

Option Default Description
call_timeout 60s Maximum time for RPC operations before timing out
subscription_timeout 120s Maximum time to wait for subscription messages
max_retries 3 Number of retry attempts before failing over
min_delay 1s Base delay for exponential backoff between retries
reconnect_interval 30s Interval between primary provider reconnection attempts (for subscription)
subscription_buffer_capacity 128 Buffer size for subscription streams

Subscriptions

RobustSubscription wraps Alloy's block subscriptions with automatic failover and reconnection:

let robust = RobustProviderBuilder::new(provider)
    .fallback(fallback)
    .subscription_timeout(Duration::from_secs(120))
    .reconnect_interval(Duration::from_secs(30))
    .build()
    .await?;

let subscription = robust.subscribe_blocks().await?;
let mut stream = subscription.into_stream();

while let Some(result) = stream.next().await {
    match result {
        Ok(block) => {
            println!("Block {}: {}", block.number, block.hash);
        }
        Err(e) => {
            // Errors are propagated but the stream continues
            // (except for Closed errors which terminate the stream)
            eprintln!("Subscription error: {:?}", e);
        }
    }
}

Subscription behaviour:

  • If no block arrives within subscription_timeout, the provider automatically fails over to fallbacks.
  • While on a fallback, the subscription periodically attempts to reconnect to the primary provider (every reconnect_interval).
  • When a fallback fails, the primary is tried first before moving to the next fallback.
  • The Lagged error indicates the consumer is not keeping pace with incoming blocks.

Provider Conversion

The library provides two conversion traits for flexible provider handling:

IntoRootProvider

Converts various types into an Alloy RootProvider. Implementations are provided for:

  • RobustProvider
  • RootProvider
  • &str (connection URL)
  • Url
  • FillProvider
  • CacheProvider
  • DynProvider
  • CallBatchProvider

IntoRobustProvider

Converts any IntoRootProvider type directly into a RobustProvider with default settings:

use robust_provider::IntoRobustProvider;

// Convert a URL directly to a RobustProvider
let robust: RobustProvider<Ethereum> = "ws://localhost:8545".into().await?;

// Or convert an existing provider
let provider = ProviderBuilder::new().connect("ws://localhost:8545").await?;
let robust: RobustProvider<Ethereum> = provider.into().await?;

Testing

Run the test suite:

cargo nextest run

The tests use local Anvil instances to verify retry logic, failover behaviour, and subscription resilience.


RPC Endpoint Coverage

⚠️ Work In Progress ⚠️

This library is under active development and many RPC endpoints have not been implemented yet. If you encounter a missing endpoint, see the Extensibility section below for how to make raw RPC calls.


Extensibility

The library exposes try_operation_with_failover, allowing you to wrap any RPC call with the full retry and failover logic, even for endpoints that haven't been explicitly implemented:

use alloy::providers::Provider;

// Use try_operation_with_failover to call any RPC method with full resilience
let block = robust
    .try_operation_with_failover(
        |provider| async move { provider.get_block_by_number(0.into()).await },
        false, 
    )
    .await?;

This gives you the same automatic retries, timeouts, and failover behaviour for any RPC method supported by your node.

About

Timeout-aware, auto-retrying, failover-ready RPC provider wrapper for Alloy built in Rust

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages