From a52084f293d99cad4ff3f49c8ae4b3870bc8a6d3 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Wed, 8 Jan 2025 17:01:51 -0500 Subject: [PATCH 1/5] wip --- .../11103-forwarder-contract/design.md | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 in-progress/11103-forwarder-contract/design.md diff --git a/in-progress/11103-forwarder-contract/design.md b/in-progress/11103-forwarder-contract/design.md new file mode 100644 index 0000000..32f76ab --- /dev/null +++ b/in-progress/11103-forwarder-contract/design.md @@ -0,0 +1,171 @@ +# Forwarder Contract + +| | | +| -------------------- | ---------------------------------------------------------------------------------- | +| Issue | [Forwarder Contract](https://github.com/AztecProtocol/aztec-packages/issues/11103) | +| Owners | @just-mitch | +| Approvers | @LHerskind @PhilWindle | +| Target Approval Date | 2025-01-20 | + +## Executive Summary + +Add a forwarder contract that allows the sequencer client to take multiple actions in the same L1 transaction. + +Adjust the sequencer client to batch its actions into a single L1 transaction. + +## Introduction + +Within the same L1 transaction, we cannot make blob transactions and regular transactions from the same address. + +We must be able to do that though, since we want to be able to do things like: + +- propose and l2 block +- vote in the governance proposer contract +- claim an epoch proof quote + all in the same L1 block. + +### Goals + +- Allow the sequencer client to take multiple actions in the same L1 transaction +- No changes to governance/staking +- Under 50 gas overhead per L2 transaction when operating at 10TPS + +### Non-goals + +- Support multiple actions for the prover node + +## Interface + +Node operators will need to deploy a forwarder contract. + +When an attester deposits into the staking contract, the forwarder contract of the node operator will be specified as the proposer. + +The Aztec Labs sequencer client implementation will need to be updated to use the forwarder contract; this involves refactoring `yarn-project/sequencer-client/src/publisher/l1-publisher.ts`. + +## Implementation + +### Forwarder Contract + +It is straightforward. + +```solidity +import {Address} from "@oz/utils/Address.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; + +contract Forwarder is Ownable { + using Address for address; + + constructor(address __owner) Ownable(__owner) {} + + function forward(address[] calldata _to, bytes[] calldata _data) external onlyOwner { + require(_to.length == _data.length); + for (uint256 i = 0; i < _to.length; i++) { + _to[i].functionCall(_data[i]); + } + } +} +``` + +### Refactoring L1 Publisher + +L1 publisher will be broken into two classes: + +- `SequencerL1API` +- `ProverL1API` + +Under the hood, both of these will use the `L1TxUtils` to create and send L1 transactions. + +### ProverL1API + +The `ProverL1API` will have the functions within `l1-publisher.ts` that are related to the prover node, and have the same interface/semantics as the current `L1Publisher`. As an aside, this means `@aztec/prover-node` should no longer have a dependency on the `@aztec/sequencer-client` package. + +### SequencerL1API + +The `SequencerL1API` will have many of the same functions currently within the `l1-publisher.ts`, but will have different semantics. + +The `SequencerL1API` will have + +- knowledge of the sequencer's forwarder contract +- knowledge of L1 slot boundaries +- `queuedRequests: L1TxRequest[]` +- a work loop + +The `Sequencer` uses the `SequencerL1API` to make calls to: + +- propose an l2 block +- cast a governance proposal vote +- cast a slashing vote +- claim an epoch proof quote + +These requests will be added to the `queuedRequests` list. + +The work loop will wait for a configurable amount of time (e.g. 6 seconds) into each L1 slot. + +If there are any queued requests, it will send them to the forwarder contract, and flush the `queuedRequests` list. + +### Gas + +Once the L2 block body is removed from calldata, the "static" arguments to call the propose function should be under 1KB. + +Operating at 10TPS, this would mean an overhead of under 3 gas per L2 transaction. + +Unfortunately, the current design to support forced inclusion requires a hash for each transaction in the proposal args. + +This means that the overhead per L2 transaction will be ~35 gas, which is still under 50 gas, but a significant portion of the overall target of 500 gas per L2 transaction. + +### Alternative solutions + +The original problem was voting at the same time as proposing an L2 block. + +The sequencer client could have done the voting in its first L1 slot available, and delayed production of the L2 block until the next L1 slot. + +This is unacceptable since the L2 blocks should eventually be published in the _first_ L1 slot available, to give the greatest chance of getting the L2 block included within our L2 slot. + +Alternatively, the EmpireBase contract could have an additional address specified by validators, specifying a separate address that would be used for governance voting. + +This seemed more complex, and has a similar problem when considering the flow where a proposer tries to claim an epoch proof quote instead of building a block (because there were no transactions at the start of the slot), but then a transaction became available, and they tried to build/propose an L2 block in the same slot; delays or other queueing in the sequencer client would be required regardless. + +## Change Set + +Fill in bullets for each area that will be affected by this change. + +- [ ] Cryptography +- [ ] Noir +- [ ] Aztec.js +- [ ] PXE +- [ ] Aztec.nr +- [ ] Enshrined L2 Contracts +- [ ] Private Kernel Circuits +- [ ] Sequencer +- [ ] AVM +- [ ] Public Kernel Circuits +- [ ] Rollup Circuits +- [ ] L1 Contracts +- [ ] Prover +- [ ] Economics +- [ ] P2P Network +- [ ] DevOps + +## Test Plan + +Outline what unit and e2e tests will be written. Describe the logic they cover and any mock objects used. + +## Documentation Plan + +Identify changes or additions to the user documentation or protocol spec. + +## Rejection Reason + +If the design is rejected, include a brief explanation of why. + +## Abandonment Reason + +If the design is abandoned mid-implementation, include a brief explanation of why. + +## Implementation Deviations + +If the design is implemented, include a brief explanation of deviations to the original design. + +## Disclaimer + +The information set out herein is for discussion purposes only and does not represent any binding indication or commitment by Aztec Labs and its employees to take any action whatsoever, including relating to the structure and/or any potential operation of the Aztec protocol or the protocol roadmap. In particular: (i) nothing in these projects, requests, or comments is intended to create any contractual or other form of legal relationship with Aztec Labs or third parties who engage with this AztecProtocol GitHub account (including, without limitation, by responding to a conversation or submitting comments) (ii) by engaging with any conversation or request, the relevant persons are consenting to Aztec Labs’ use and publication of such engagement and related information on an open-source basis (and agree that Aztec Labs will not treat such engagement and related information as confidential), and (iii) Aztec Labs is not under any duty to consider any or all engagements, and that consideration of such engagements and any decision to award grants or other rewards for any such engagement is entirely at Aztec Labs’ sole discretion. Please do not rely on any information on this account for any purpose - the development, release, and timing of any products, features, or functionality remains subject to change and is currently entirely hypothetical. Nothing on this account should be treated as an offer to sell any security or any other asset by Aztec Labs or its affiliates, and you should not rely on any content or comments for advice of any kind, including legal, investment, financial, tax, or other professional advice. From 4a43961f0a25a66636253fc08e67fd39418c4f79 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Wed, 8 Jan 2025 19:32:46 -0500 Subject: [PATCH 2/5] rename --- .../11103-forwarder-contract/design.md | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/in-progress/11103-forwarder-contract/design.md b/in-progress/11103-forwarder-contract/design.md index 32f76ab..23ddf0b 100644 --- a/in-progress/11103-forwarder-contract/design.md +++ b/in-progress/11103-forwarder-contract/design.md @@ -70,27 +70,30 @@ contract Forwarder is Ownable { L1 publisher will be broken into two classes: -- `SequencerL1API` -- `ProverL1API` +- within `@aztec/sequencer-client`, there will be a `L1TxManager` +- within `@aztec/prover-node`, there will be a `L1TxPublisher` Under the hood, both of these will use the `L1TxUtils` to create and send L1 transactions. -### ProverL1API +### ProverNode `L1TxPublisher` -The `ProverL1API` will have the functions within `l1-publisher.ts` that are related to the prover node, and have the same interface/semantics as the current `L1Publisher`. As an aside, this means `@aztec/prover-node` should no longer have a dependency on the `@aztec/sequencer-client` package. +The `ProverNode` will have a `L1TxPublisher` that has the functions within `l1-publisher.ts` that are related to the prover node, and have the same interface/semantics as the current `L1Publisher`. As an aside, this means `@aztec/prover-node` should no longer have a dependency on the `@aztec/sequencer-client` package. -### SequencerL1API +In essence, this class is an API for L1 transactions for the prover node, and a simple wrapper around the `L1TxUtils` class. -The `SequencerL1API` will have many of the same functions currently within the `l1-publisher.ts`, but will have different semantics. +### SequencerClient `L1TxManager` -The `SequencerL1API` will have +The `SequencerClient` will have a `L1TxManager` that has many of the same functions currently within the `l1-publisher.ts`, but will have different semantics. + +The `L1TxManager` will have: -- knowledge of the sequencer's forwarder contract -- knowledge of L1 slot boundaries - `queuedRequests: L1TxRequest[]` - a work loop +- knowledge of the sequencer's forwarder contract +- knowledge of L1 slot boundaries +- knowledge of successful L1 transactions per L2 slot -The `Sequencer` uses the `SequencerL1API` to make calls to: +The `Sequencer` uses its `L1TxManager` to make calls to: - propose an l2 block - cast a governance proposal vote From 71e7a5943c6219bdd5f57c058aa9deaab1aa2bd6 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Fri, 10 Jan 2025 10:41:07 -0500 Subject: [PATCH 3/5] stats, cancellation, startup --- .../11103-forwarder-contract/design.md | 73 +++++++++++++------ 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/in-progress/11103-forwarder-contract/design.md b/in-progress/11103-forwarder-contract/design.md index 23ddf0b..cd2cc1a 100644 --- a/in-progress/11103-forwarder-contract/design.md +++ b/in-progress/11103-forwarder-contract/design.md @@ -4,8 +4,8 @@ | -------------------- | ---------------------------------------------------------------------------------- | | Issue | [Forwarder Contract](https://github.com/AztecProtocol/aztec-packages/issues/11103) | | Owners | @just-mitch | -| Approvers | @LHerskind @PhilWindle | -| Target Approval Date | 2025-01-20 | +| Approvers | @LHerskind @PhilWindle @spalladino @spypsy | +| Target Approval Date | 2025-01-15 | ## Executive Summary @@ -15,9 +15,9 @@ Adjust the sequencer client to batch its actions into a single L1 transaction. ## Introduction -Within the same L1 transaction, we cannot make blob transactions and regular transactions from the same address. +Within the same L1 transaction, one cannot make blob transactions and regular transactions from the same address. -We must be able to do that though, since we want to be able to do things like: +However, aztec node operators must be able to do things like: - propose and l2 block - vote in the governance proposer contract @@ -66,6 +66,10 @@ contract Forwarder is Ownable { } ``` +Note: this requires all the actions to succeed, so the sender must be sure that, e.g. a failed governance vote will not prevent the L2 block from being proposed. + +Note: this implementation is not technically part of the protocol, and as such will live in `l1-contracts/src/periphery`. + ### Refactoring L1 Publisher L1 publisher will be broken into two classes: @@ -100,12 +104,49 @@ The `Sequencer` uses its `L1TxManager` to make calls to: - cast a slashing vote - claim an epoch proof quote +The return type of each of these functions will be a `Promise`, which will be resolved when the bundled transaction is included in an L1 block. + These requests will be added to the `queuedRequests` list. -The work loop will wait for a configurable amount of time (e.g. 6 seconds) into each L1 slot. +The work loop will wait for a configurable amount of time (e.g. 6 seconds) into each L1 slot. This will be exposed as an environment variable `sequencer.l1TxSubmissionDeadline`. If there are any queued requests, it will send them to the forwarder contract, and flush the `queuedRequests` list. +### L1PublishBlockStats + +Since one `L1PublishBlockStats` can be used for multiple actions, its constituent `event` will be changed to `actions`, which will be a `string[]`. + +The sequencer's `L1TxManager` and the prover node's `L1TxPublisher` will populate this array and record the metric. + +### Cancellation/Resend + +A complication is that ethereum nodes make replacement of blob transactions expensive, and cancelation impossible, as they operate under the assumption that rollups seldom/never need to replace/cancel blob transactions. + +See [geth's blob pool](https://github.com/ethereum/go-ethereum/blob/581e2140f22566655aa8fb2d1e9a6c4a740d3be1/core/txpool/blobpool/blobpool.go) for details/constraints. + +This is not true for Aztec's decentralized sequencer set with strict L1 timeliness requirements on L2 blocks. + +So a concern is the following scenario: + +- proposer A submits a tx with nonce 1 (with a blob) that is not priced aggressively enough +- Tx1 sits in the blob pool, but is not included in an L1 block +- proposer A tries to submit another transaction, but needs to know to use Tx2 +- Tx1 needs to be replaced with a higher fee, but it will revert if the network is in a different L2 slot and the bundle contained a proposal + +This is addressed by: + +- Upgrading viem to at least v2.15.0 to use their nonceManager to be aware of pending nonces +- Aggressive pricing of blob transactions +- The L1TxUtils will be able to speed up Tx1 (even if it reverts), which should unblock Tx2 + +A different approach would be to have the sequencer client maintain a pool of available forwarder contracts, and use one until it gets stuck, then switch to the next one: presumably by the time the sequencer client gets to the original forwarder contract, the blob pool will have been cleared. + +Broader changes about changing the timeliness requirements are not in scope for this change. + +### Setup + +There will be an optional environment variable `sequencer.forwarderContractAddress` that will be used to specify the forwarder contract address. To improve UX, there will be a separate environment variable `sequencer.deployForwarderContract` that will default to `true` and be used to specify whether the forwarder contract should be deployed. If so, the Aztec Labs implementation of the forwarder contract will be deployed and used by the sequencer. + ### Gas Once the L2 block body is removed from calldata, the "static" arguments to call the propose function should be under 1KB. @@ -139,35 +180,23 @@ Fill in bullets for each area that will be affected by this change. - [ ] Aztec.nr - [ ] Enshrined L2 Contracts - [ ] Private Kernel Circuits -- [ ] Sequencer +- [x] Sequencer - [ ] AVM - [ ] Public Kernel Circuits - [ ] Rollup Circuits -- [ ] L1 Contracts -- [ ] Prover +- [x] L1 Contracts +- [x] Prover - [ ] Economics - [ ] P2P Network - [ ] DevOps ## Test Plan -Outline what unit and e2e tests will be written. Describe the logic they cover and any mock objects used. +The primary test is [cluster governance upgrade](https://github.com/AztecProtocol/aztec-packages/issues/9638), ensuring that block production does not stall (as it currently does). ## Documentation Plan -Identify changes or additions to the user documentation or protocol spec. - -## Rejection Reason - -If the design is rejected, include a brief explanation of why. - -## Abandonment Reason - -If the design is abandoned mid-implementation, include a brief explanation of why. - -## Implementation Deviations - -If the design is implemented, include a brief explanation of deviations to the original design. +No plans to document this as yet: the node operator guide effectively does not exist. ## Disclaimer From 9e811d1925a5453740f70314928696ba13d833a6 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Mon, 13 Jan 2025 16:41:58 -0500 Subject: [PATCH 4/5] updates based on review --- .../11103-forwarder-contract/design.md | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/in-progress/11103-forwarder-contract/design.md b/in-progress/11103-forwarder-contract/design.md index cd2cc1a..9a8f424 100644 --- a/in-progress/11103-forwarder-contract/design.md +++ b/in-progress/11103-forwarder-contract/design.md @@ -79,6 +79,8 @@ L1 publisher will be broken into two classes: Under the hood, both of these will use the `L1TxUtils` to create and send L1 transactions. +The publisher had also had responsibilities as a "getter" of different information on L1. This will be refactored into classes specific to the individual contracts that are being queried, e.g. `yarn-project/ethereum/src/contracts/rollup.ts` has a `Rollup` class that is responsible for getting information from the rollup contract. + ### ProverNode `L1TxPublisher` The `ProverNode` will have a `L1TxPublisher` that has the functions within `l1-publisher.ts` that are related to the prover node, and have the same interface/semantics as the current `L1Publisher`. As an aside, this means `@aztec/prover-node` should no longer have a dependency on the `@aztec/sequencer-client` package. @@ -92,31 +94,25 @@ The `SequencerClient` will have a `L1TxManager` that has many of the same functi The `L1TxManager` will have: - `queuedRequests: L1TxRequest[]` -- a work loop - knowledge of the sequencer's forwarder contract -- knowledge of L1 slot boundaries -- knowledge of successful L1 transactions per L2 slot -The `Sequencer` uses its `L1TxManager` to make calls to: +It will have an interface of: + +```typescript +interface L1TxManager { + addRequest(request: L1TxRequest): void; + sendRequests(): Promise; +} +``` + +The `Sequencer` uses its `L1TxManager.addRequest()` to push requests to the `queuedRequests` list whenever it wants to: - propose an l2 block - cast a governance proposal vote - cast a slashing vote - claim an epoch proof quote -The return type of each of these functions will be a `Promise`, which will be resolved when the bundled transaction is included in an L1 block. - -These requests will be added to the `queuedRequests` list. - -The work loop will wait for a configurable amount of time (e.g. 6 seconds) into each L1 slot. This will be exposed as an environment variable `sequencer.l1TxSubmissionDeadline`. - -If there are any queued requests, it will send them to the forwarder contract, and flush the `queuedRequests` list. - -### L1PublishBlockStats - -Since one `L1PublishBlockStats` can be used for multiple actions, its constituent `event` will be changed to `actions`, which will be a `string[]`. - -The sequencer's `L1TxManager` and the prover node's `L1TxPublisher` will populate this array and record the metric. +At end of every iteration of the Sequencer's work loop, it will await a call to `L1TxManager.sendRequests()`, which will send the queued requests to the forwarder contract, and flush the `queuedRequests` list. ### Cancellation/Resend @@ -139,13 +135,11 @@ This is addressed by: - Aggressive pricing of blob transactions - The L1TxUtils will be able to speed up Tx1 (even if it reverts), which should unblock Tx2 -A different approach would be to have the sequencer client maintain a pool of available forwarder contracts, and use one until it gets stuck, then switch to the next one: presumably by the time the sequencer client gets to the original forwarder contract, the blob pool will have been cleared. - -Broader changes about changing the timeliness requirements are not in scope for this change. - ### Setup -There will be an optional environment variable `sequencer.forwarderContractAddress` that will be used to specify the forwarder contract address. To improve UX, there will be a separate environment variable `sequencer.deployForwarderContract` that will default to `true` and be used to specify whether the forwarder contract should be deployed. If so, the Aztec Labs implementation of the forwarder contract will be deployed and used by the sequencer. +There will be an optional environment variable `sequencer.custForwarderContractAddress` that can be used to specify a custom forwarder contract address. + +If this is not set, the sequencer will deploy the Aztec Labs implementation of the forwarder contract, using the Universal Deterministic Deployer, supplying the sequencer's address as the deployment salt, and the sequencer's address as the owner. ### Gas @@ -157,6 +151,10 @@ Unfortunately, the current design to support forced inclusion requires a hash fo This means that the overhead per L2 transaction will be ~35 gas, which is still under 50 gas, but a significant portion of the overall target of 500 gas per L2 transaction. +### Future work + +For more robust cancellation, the sequencer client could maintain a pool of available EOAs, each of which are "owners"/"authorized senders" on its forwarder contract, and use one until it gets stuck, then switch to the next one: presumably by the time the sequencer client gets to the original EOA, the blob pool will have been cleared. + ### Alternative solutions The original problem was voting at the same time as proposing an L2 block. From 9c9e893f822cc9e502b2ac72ad0238e6ebe04e13 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Wed, 15 Jan 2025 12:25:37 -0500 Subject: [PATCH 5/5] update based on comments. --- in-progress/11103-forwarder-contract/design.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/in-progress/11103-forwarder-contract/design.md b/in-progress/11103-forwarder-contract/design.md index 9a8f424..516ceb5 100644 --- a/in-progress/11103-forwarder-contract/design.md +++ b/in-progress/11103-forwarder-contract/design.md @@ -15,11 +15,11 @@ Adjust the sequencer client to batch its actions into a single L1 transaction. ## Introduction -Within the same L1 transaction, one cannot make blob transactions and regular transactions from the same address. +Within the same L1 block, one cannot make blob transactions and regular transactions from the same address. However, aztec node operators must be able to do things like: -- propose and l2 block +- propose an l2 block - vote in the governance proposer contract - claim an epoch proof quote all in the same L1 block. @@ -100,7 +100,7 @@ It will have an interface of: ```typescript interface L1TxManager { - addRequest(request: L1TxRequest): void; + addRequest(request: L1TxRequest, validThroughL2Slot: number): void; sendRequests(): Promise; } ``` @@ -137,19 +137,18 @@ This is addressed by: ### Setup -There will be an optional environment variable `sequencer.custForwarderContractAddress` that can be used to specify a custom forwarder contract address. +There will be an optional environment variable `sequencer.customForwarderContractAddress` that can be used to specify a custom forwarder contract address. If this is not set, the sequencer will deploy the Aztec Labs implementation of the forwarder contract, using the Universal Deterministic Deployer, supplying the sequencer's address as the deployment salt, and the sequencer's address as the owner. ### Gas Once the L2 block body is removed from calldata, the "static" arguments to call the propose function should be under 1KB. +But including commitee ECDSA signatures, this goes to ~7KB. -Operating at 10TPS, this would mean an overhead of under 3 gas per L2 transaction. - -Unfortunately, the current design to support forced inclusion requires a hash for each transaction in the proposal args. +Operating at 10TPS, this means an overhead of under (16 gas/B _ 8KB) / (10 transactions/s _ 36s) = 355 gas per L2 transaction. -This means that the overhead per L2 transaction will be ~35 gas, which is still under 50 gas, but a significant portion of the overall target of 500 gas per L2 transaction. +However, after the committee signatures convert to BLS, the calldata will drop to 1KB total, so the overhead will drop to (16 gas/B _ 1KB) / (10 transactions/s _ 36s) = 44 gas per L2 transaction. ### Future work