Paradigm

Releasing Alloy

Jun 18, 2024 | Georgios Konstantopoulos, James Prestwich, Matthias Seitz, DaniPopes, Yash Atreya, Zerosnacks, Enrique Ortiz

Contents

The story so far

One year ago, we announced Alloy, our full rewrite of the low-level building blocks for interacting with EVM blockchains, from the RPC types, down to the ABI encoders and primitive types.

Today, we’re releasing Alloy 0.1 (crates), the first release of the Alloy project including all the necessary utilities for interacting with an EVM-based blockchain. This release includes multiple API improvements and new features which we are excited to finally share with our users. As part of this release, we’re also officially stopping maintenance of ethers-rs, and we’re encouraging everyone to move to Alloy.

While we are only now releasing Alloy as the top-level package including all JSON-RPC functionality, the packages inside of Alloy Core with low-level type & ABI coders have been released for a few months now, and many of them have >1 million all time downloads already!

To help users get up to speed with Alloy, we’re publishing the Alloy Book (including an ethers to alloy migration guide), the Alloy and Alloy Core docs, as well as a lot of examples showing all sorts of ways you can consume Alloy.

Alloy is already integrated in many Rust codebases, as well as all the projects we’re working on: Revm, Foundry and Reth. We’ve been using it in production ourselves for months, and we’re excited for it to be a performant and stable foundation for the Rust Ethereum ecosystem, as we envisioned in our original announcement.

With that out of the way, let’s dive in!

What are the highlights of the Alloy release?

Alloy is written from the bottom-up to be performant, safe to use, and intuitive. We took our learnings from 4 years of ethers-rs development and put them all into designing a great client-side library for interacting with EVM-based blockchains.

This took a lot of time, 1 year since we announced the Alloy project, but we’re proud of the result.

Today we are excited to reveal some exciting new features co-architected and co-developed by our core team alongside James Prestwich who’s been maintaining Alloy with the rest of us over the last year. Big shoutout & thank you to James for pushing us to go beyond what ethers-rs allowed us before, and for being a core input to the successful delivery of today’s release.

Reworked Provider Middleware architecture

The most important abstraction in a RPC client library is the Provider, which lets you interact with the network. Users commonly want to extend basic provider operations: different gas estimations, gas escalators, alternative signing pipelines, and more. You want to do this in a modular way which empowers the developer to extend the behavior of the core library, instead of having to modify it.

In ethers-rs, we had defined the Middleware as the one-stop shop for modifying your RPC client’s behavior. If you wanted nonce management, it’s a middleware. Gas escalation, it’s a middleware. Signing transactions? Middleware. Flashbots bundles? Middleware. This approach worked because it gave us a lot of flexibility to override things as we wanted, but it also was too error prone, and had poor developer experience (e.g. expecting devs to panic when a function that’s required by the Middleware trait does not make sense to implement).

Alloy redesigned that abstraction from the ground up. We split the Middleware in 3 overridable abstractions:

  • Transport Layers: The Transport is the “wire” that carries over your data to the node. This now implement’s Tower’s Service Layers which allows tapping into the Tower Ecosystem for common middleware such as request retries, rate limits and more.
  • Provider Layers: Modeled after Tower’s Layers this allows overriding various Provider functionalities, e.g. if you wanted to hijack the get_storage_at method, you’d implement a ProviderLayer.
  • Fillers: This is probably the most exciting abstraction, Fillers handle everything about the transaction’s lifecycle. A user generally submits a partially populated transaction, and the provider is responsible for figuring out what data is missing and filling it in. Fillers can be installed independent of order from each other, solving a major footgun from ethers-rs. We provide the .with_recommended_fillers() method which aggregates commonly used Fillers via multiple JoinFill ProviderLayers for convenience:
    • Wallet Filler: Signs a transaction with a credential from a wide suite: a local private key parsed from a hex string, a 12/24-word mnemonic, a keystore file, a hardware wallet like Trezor or Ledger, or even from a productionized key management system such as YubiHSM, AWS KMS or GCP KMS.
    • Nonce Filler: Automatically manages nonces across all accounts.
    • Gas Filler: Handles calculating the gas price and the gas limit for each transaction.
    • ChainId Filler: Embeds the correct chain ID into transactions depending on which chain is connected.

Putting it all together, the stack of features we have can be seen below:

This stack is bundled and exposed to the user via the ProviderBuilder which has a very ergonomic API:

// Create a signer from a random private key.
let signer = PrivateKeySigner::random();

let provider = ProviderBuilder::new()
   // configures all the fillers
  .with_recommended_fillers()
   // sets the signer, allows configuring more than 1 signer which will be picked based on your transaction's `from` field.
  .wallet(EthereumWallet::from(signer))
  // connects to the chain
  // can also use `.on_http`, `.on_ws`, or `.on_ipc` for no dyn dispatch
  // can also use `.on_anvil` for local testing
  // can also use `.on_client` for configuring auth options e.g. bearer auth.
  .on_builtin("ws://localhost:8545")
  .await?;

let tx = TransactionRequest::new().with_to(...).with_value(...);
let receipt = provider.send_transaction(tx).await?.get_receipt().await?;
// do something with the receipt

Oh, we also made sure the Provider and Signers are object-safe, to make it easier to avoid generics in your code by Boxing them. For more on how to consume providers, see the book.

RPC Types Abstraction

The world is going multichain, and that means more differences in RPC types! That was one of the most painful things in ethers-rs where we supported e.g. Celo-related fields with a feature flag. This meant that if you wanted to have a type-safe connection to both Celo and Ethereum you had to choose between importing the library twice, or expecting that the Celo fields would be None in all cases in Ethereum. This is a problem that we set out to fix.

In Alloy, we define the Network trait which defines the “shape” of every network for all its RPC requests and responses where each type must implement certain traits, e.g. Eip2718Envelope, TxReceipt, TransactionBuilder, summarized below:

pub trait Network {
    /// The network transaction type enum.
    type TxType
    /// The network transaction envelope type.
    type TxEnvelope: Eip2718Envelope + Debug;
    /// An enum over the various transaction types.
    type UnsignedTx: From<Self::TxEnvelope>;
    /// The network receipt envelope type.
    type ReceiptEnvelope: Eip2718Envelope + TxReceipt;
    /// The network header type.
    type Header;
    /// The JSON body of a transaction request.
    type TransactionRequest: RpcObject + TransactionBuilder<Self> + Debug + From<Self::TxEnvelope> + From<Self::UnsignedTx>;
    /// The JSON body of a transaction response.
    type TransactionResponse: RpcObject + TransactionResponse;
    /// The JSON body of a transaction receipt.
    type ReceiptResponse: RpcObject + ReceiptResponse;
    /// The JSON body of a header response.
    type HeaderResponse: RpcObject;
}

This allows us to import the library once without any feature flags, and depending on what network we specify we get different type abstractions. This is great! Type-safety, without redundant overhead! This is also the approach we’re taking in Reth for allowing any developer to build a chain with custom RPC types on the server side, such as a network with native account abstraction.

We provide two network implementations Ethereum and AnyNetwork. Ethereum contains all the types you know and love. AnyNetwork however, wraps every RPC type with the WithOtherFields<T> type which acts as a catch-all for any RPC response fields that do not match the Ethereum structure.

A developer can choose their Network implementation using the .network::() method on the ProviderBuilder. This allows us to support more networks than just Ethereum in a principled way, without burdening the core maintenance process. All you need to do is implement the network trait and all its associated types, import it in your code, and you’re done!

Follow our work on defining the OpStackNetwork in the op-alloy crate, and reach out if you want to implement your own network!

The sol! Macro

We first talked about the sol macro in our initial post. It is a rework of our previous abigen macro, which was used to generate type-safe bindings to a contract’s ABI. The sol macro is not a compiler, but it is a complete representation of the Solidity type system in Rust, which means you can just paste Solidity in it, and it’ll codegen bindings for it, even allowing support for custom types!

The sol macro also codegens JSON RPC bindings via the #[sol(rpc)] attribute. A deployer method is also generated if you pass it the #[sol(bytecode = "...")] attribute. For example, the code below would generate a Counter::deploy function as well as a Counter::increment(&self) method which you can use to increment the counter.

sol! {
    // solc v0.8.26; solc a.sol --via-ir --optimize --bin
    #[sol(rpc, bytecode="608080...")]
    contract Counter {
        uint256 public number;

        function increment() public {
            number++;
        }
    }
}

To learn more about the sol macro, check the page on the book and its detailed documentation. Interacting with a smart contract is similar to ethers-rs (note, no more Arcs!), with minor underlying changes in the API for fetching a transaction’s receipt.

let provider = ProviderBuilder::new().on_builtin("...").await?;

// Deploy the contract.
let contract = Counter::deploy(&provider).await?;
println!("Deployed contract at address: {}", contract.address());

let receipt = contract.setNumber(U256::from(42)).send().await?.get_receipt().await?;
println!("Receipt: {receipt}");

// Increment the number to 43 (without waiting for the receipt)
let tx_hash = contract.increment().send().await?.watch().await?;
println!("Incremented number: {tx_hash}");

// Retrieve the number, which should be 43.
let number = contract.number().await?.number.to_string();
println!("Retrieved number: {number}");

The sol macro’s functionality is also integrated with Foundry in forge bind for generating type-safe bindings for all your client-side integrations. Check out the updated Foundry Rust template if that’s of interest to you!

Extensive documentation and tests

We want our users to be equipped with high-level tutorials & examples for consuming the project, as a library. To achieve that, we provide a large surface area of documentation:

  • Alloy Docs: Rustdoc documentation for each function on the Alloy repository.
  • Alloy Core Docs: Rustdoc documentation for each function on the Alloy Core repository.
  • Alloy Examples: Wide range of code examples on how to consume Alloy in your day to day.
  • Alloy Book: Tutorials and long form writeups about all things Alloy.

Making the docs excellent is a top priority for us, please open issues on the book with more tutorials you’d like to see.

What is next for Alloy?

Today’s 0.1 release marks Alloy at feature parity with ethers-rs and beyond, while also being performant, well-tested and well-documented. We think Alloy is the tool of choice for power users, yet it is simple and intuitive enough for anyone to work with.

Our next priority is the 1.0 release, which means we’ll be polishing our APIs and working towards proper stability. While we don’t offer any formal stability guarantees, most of the APIs are baked, and we do not expect large changes.

To help achieve Alloy’s long term success, we’re looking to add 1 full-time staff-level engineer to the Alloy team who will help drive the day to day of the project, as well as help us grow the contributor base. If the above sounds interesting, please reach out to georgios@paradigm.xyz.

We’re excited for every ethers-rs user to port their code to use Alloy, as well as new services to be built with it! Go read the docs of alloy-core and alloy, the examples, and the book!

Until then, see you on Github!

Disclaimer: This post is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment recommendations. This post reflects the current opinions of the authors and is not made on behalf of Paradigm or its affiliates and does not necessarily reflect the opinions of Paradigm, its affiliates or individuals associated with Paradigm. The opinions reflected herein are subject to change without being updated.