Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: network abstraction and transaction builder #190

Merged
merged 65 commits into from
Mar 11, 2024
Merged

Conversation

prestwich
Copy link
Member

@prestwich prestwich commented Feb 6, 2024

Motivation

We want a transaction builder that can build one of several strongly-typed consensus transactions for signing to implement send_transaction. The intended flow is:

  • User specifies fields they want to add to their transaction
  • They pass it off to a signer, where the concrete transaction type is resolved from the builder (e.g. legacy, EIP-1559, ..)
  • The transaction is signed and sent

This is made complicated due to our network abstraction:

  • Different networks have a different set of transactions
  • Different networks can have different signature schemes, and some are valid for the same transaction types, i.e. our current associated type Signature for transactions is not a good abstraction

This flow should also work server side, i.e. if I get a TransactionRequest over the wire from a client, I should be able to resolve it to a consensus type transaction. I should also be able to take an already-signed RLP-encoded transaction and decode it into one of the valid transaction types.

This PR intends to:

  • Add a builder
  • Complete the request → signer flow, including adding send_transaction in the provider and the alloy-contract call builder
  • Remove the temporary provider (and related code) and fully transition to the network-parameterized provider type

Solution

  • Adds a TransactionBuilder trait and implements this for TransactionRequest
  • Adds an Ethereum network with all types defined
  • Removes the temporary provider, replacing it with a new, network-parameterized provider
  • Adjusts alloy-contract to work with the new provider
  • Moves a lot of types around, because a lot of the abstractions we made had not been put together, and could not without circular dependencies
  • Splits Transaction into Transaction and SignableTransaction, since not all transactions are signable (e.g. Optimism deposit tx's)
  • Moves the associated Signature type in Transaction into a parameter instead
  • Adds 3 new signer traits: TxSigner, TxSignerSync and NetworkSigner
    • TxSigner is essentially ripped out of the Signer trait and should be implemented for all signers, even if the signer does not care about transactions.
    • It is needed for e.g. Ledger/Trezor where tx info is needed. A blanket implementation is not provided because it isn't allowed.
    • These two traits essentially state that a signer can sign any transaction with the given Signature. Signer implementors need to implement this and Signer.
    • NetworkSigner instead is implemented by network crate authors. This trait is implemented for a wrapper type and essentially states that the signer can sign any transaction for the given network. (See EthereumSigner)
  • Finish up the ProviderBuilder abstraction and add a SignerLayer to add signing on top of an existing provider

Companion PRs:

Things left:

  • Possibly improve the ergonomics of ProviderBuilder a bit

Follow ups

Breaking changes

Anything that uses alloy-contract, alloy-provider, or alloy-consensus likely broke with this PR as the API changed.

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

@prestwich prestwich added enhancement New feature or request debt Tech debt which needs to be addressed labels Feb 6, 2024
@prestwich prestwich self-assigned this Feb 6, 2024
crates/consensus/src/lib.rs Outdated Show resolved Hide resolved
crates/consensus/src/transaction/builder.rs Outdated Show resolved Hide resolved
crates/signer/src/signer.rs Outdated Show resolved Hide resolved
crates/signer/src/signer.rs Show resolved Hide resolved
Comment on lines 5 to 12
/// The TypedTransaction enum represents all Ethereum transaction request types.
///
/// Its variants correspond to specific allowed transactions:
/// 1. Legacy (pre-EIP2718) [`TxLegacy`]
/// 2. EIP2930 (state access lists) [`TxEip2930`]
/// 3. EIP1559 [`TxEip1559`]
/// 4. EIP4844 [`TxEip4844`]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TypedTransaction {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this impl RLP?

Copy link
Member

@onbjerg onbjerg Mar 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsure, do we need it? cc @mattsse perhaps

@onbjerg onbjerg force-pushed the prestwich/tx-builder branch 2 times, most recently from d0a8838 to 3300b0c Compare February 13, 2024 14:12
@onbjerg onbjerg changed the title Wip: Tx Builder and signing refactor Wip: Tx Builder, provider and signing refactor Feb 15, 2024
crates/consensus/src/transaction/builder.rs Outdated Show resolved Hide resolved
crates/consensus/src/transaction/eip1559.rs Outdated Show resolved Hide resolved
crates/consensus/src/transaction/eip4844.rs Show resolved Hide resolved
crates/consensus/src/transaction/legacy.rs Outdated Show resolved Hide resolved
Comment on lines +57 to +67
// todo: block responses are ethereum only, so we need to include this in here too, or make `Block`
// generic over tx/header type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's do as follow-up IMO when we're adding the 2nd network?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crates/network/src/transaction/builder.rs Outdated Show resolved Hide resolved
Comment on lines 64 to 42
// todo: move
/// A signer capable of signing any transaction for the given network.
#[async_trait]
pub trait NetworkSigner<N: Network> {
/// Asynchronously sign an unsigned transaction.
async fn sign(&self, tx: N::UnsignedTx) -> alloy_signer::Result<N::TxEnvelope>;
}

// todo: move
/// An async signer capable of signing any [SignableTransaction] for the given [Signature] type.
#[async_trait]
pub trait TxSigner<Signature> {
/// Asynchronously sign an unsigned transaction.
async fn sign_transaction(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> alloy_signer::Result<Signature>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference between these 2?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetworkSigner is a signer capable of signing any transaction with any signature type for a given network, TxSigner is a signer capable of signing any transaction regardless of network for a given signature type.

The idea is to have fewer generics for signers

crates/providers/src/lib.rs Outdated Show resolved Hide resolved
key: U256,
tag: Option<BlockId>,
) -> TransportResult<StorageValue> {
self.client.prepare("eth_getStorageAt", (address, key, tag.unwrap_or_default())).await
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally find it weird we're calling this self.client.prepare when it's sending the request, shouldnt it be self.client.prepare(...).send().await or self.client.request(...)?

Copy link
Member

@onbjerg onbjerg Mar 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's called prepare because it doesn't do any serialization or really anything at all until awaited. The send method in your example is equiv to awaiting it

crates/providers/src/lib.rs Outdated Show resolved Hide resolved
@@ -258,81 +257,6 @@ mod tests {
assert_eq!(recovered2, address);
}

#[tokio::test]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was moved to the network crate

crates/signer/src/signer.rs Outdated Show resolved Hide resolved
crates/signer-trezor/src/signer.rs Show resolved Hide resolved
crates/signer-ledger/src/signer.rs Show resolved Hide resolved
///
/// Returns `None` if the transaction is not a contract creation (the `to` field is set), or if
/// the `from` or `nonce` fields are not set.
pub fn calculate_create_address(&self) -> Option<Address> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is now part of the Builder trait, so removed it here. can add it back if we want

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it for convenience, it's not strictly necessary but it's nice to have

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

builder trait should cover us here

crates/network/src/ethereum/mod.rs Outdated Show resolved Hide resolved
}

/// Build a legacy transaction.
fn build_legacy(request: TransactionRequest) -> Result<TxLegacy, TransactionBuilderError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I'd have these on TransactionRequest, but that would cause a circular dep (alloy-network depends on rpc-types -> rpc-types would need to depend on alloy-network for TransactionBuilderError)

Err(RpcError::Transport(err)) if retries > 0 && err.recoverable() => {
debug!(number, %err, "failed to fetch block, retrying");
retries -= 1;
continue;
}
Ok(None) if retries > 0 => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the rpc can respond with the block not existing, which can be the case if we are querying the latest block but the node doesn't have it yet. in that case i think we retry imo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

crates/providers/src/new.rs Outdated Show resolved Hide resolved
crates/providers/src/new.rs Outdated Show resolved Hide resolved
@onbjerg onbjerg changed the title Wip: Tx Builder, provider and signing refactor Network abstraction and transaction builder Mar 8, 2024
@onbjerg
Copy link
Member

onbjerg commented Mar 11, 2024

rebased ptal @gakonst @DaniPopes, after merge I will create all the follow up issues

@DaniPopes
Copy link
Member

I realized we're not running doctests in CI because when I ran cargo t --all-features it failed on a doctest, @onbjerg can you please look into it? I couldn't figure it out

@onbjerg
Copy link
Member

onbjerg commented Mar 11, 2024

created all the follow up issues, will look into doctest

@onbjerg
Copy link
Member

onbjerg commented Mar 11, 2024

@DaniPopes we are, it just showed up later. i ran cargo t --all-features and got the same error as ci (which is fixed now)

@DaniPopes
Copy link
Member

Yes, because I added it in 81c98a5 😄

@onbjerg onbjerg merged commit 05d0d0e into main Mar 11, 2024
15 checks passed
@onbjerg onbjerg deleted the prestwich/tx-builder branch March 11, 2024 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
debt Tech debt which needs to be addressed enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants