├── .github └── workflows │ ├── benchmarks.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── bin └── varro.rs ├── contracts └── L2OutputOracle.json ├── logo ├── blurred.png ├── colosseum.jpeg ├── refcell.png └── varro.png ├── src ├── builder.rs ├── cli.rs ├── client.rs ├── common.rs ├── config.rs ├── errors.rs ├── lib.rs ├── macros.rs ├── metrics.rs ├── pool.rs ├── proposals.rs ├── rollup.rs └── telemetry.rs ├── tests └── l1_client.rs └── varrup ├── docker-compose.yml ├── install ├── start-varro.sh ├── varro-env.env └── varrup /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: benchmarks 2 | 3 | on: 4 | workflow_bench: 5 | 6 | env: 7 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 8 | GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} 9 | 10 | jobs: 11 | benches: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: nightly 19 | override: true 20 | components: rustfmt 21 | - uses: Swatinem/rust-cache@v2 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | command: bench 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-mac-arm: 8 | runs-on: macos-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v1 12 | 13 | - name: install rust nightly 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: nightly 17 | default: true 18 | override: true 19 | 20 | - name: install target 21 | run: rustup target add aarch64-apple-darwin 22 | 23 | - uses: Swatinem/rust-cache@v2 24 | 25 | - name: build 26 | run: cargo build --package cli --release --target aarch64-apple-darwin 27 | 28 | - name: archive 29 | run: gtar -czvf "archon_darwin_arm64.tar.gz" -C ./target/aarch64-apple-darwin/release archon 30 | 31 | - name: generate tag name 32 | id: tag 33 | run: | 34 | echo "::set-output name=release_tag::nightly-${GITHUB_SHA}" 35 | 36 | - name: release 37 | uses: softprops/action-gh-release@v1 38 | with: 39 | tag_name: ${{ steps.tag.outputs.release_tag }} 40 | prerelease: true 41 | files: | 42 | archon_darwin_arm64.tar.gz 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | build-mac-amd: 47 | runs-on: macos-latest 48 | steps: 49 | - name: checkout 50 | uses: actions/checkout@v1 51 | 52 | - name: install rust nightly 53 | uses: actions-rs/toolchain@v1 54 | with: 55 | toolchain: nightly 56 | default: true 57 | override: true 58 | 59 | - name: install target 60 | run: rustup target add x86_64-apple-darwin 61 | 62 | - uses: Swatinem/rust-cache@v2 63 | 64 | - name: build 65 | run: cargo build --package cli --release --target x86_64-apple-darwin 66 | 67 | - name: archive 68 | run: gtar -czvf "archon_darwin_amd64.tar.gz" -C ./target/x86_64-apple-darwin/release archon 69 | 70 | - name: generate tag name 71 | id: tag 72 | run: | 73 | echo "::set-output name=release_tag::nightly-${GITHUB_SHA}" 74 | 75 | - name: release 76 | uses: softprops/action-gh-release@v1 77 | with: 78 | tag_name: ${{ steps.tag.outputs.release_tag }} 79 | prerelease: true 80 | files: | 81 | archon_darwin_amd64.tar.gz 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | 85 | build-linux-arm: 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: checkout 89 | uses: actions/checkout@v1 90 | 91 | - name: install rust nightly 92 | uses: actions-rs/toolchain@v1 93 | with: 94 | toolchain: nightly 95 | default: true 96 | override: true 97 | 98 | - name: install target 99 | run: rustup target add aarch64-unknown-linux-gnu 100 | 101 | - name: install dependencies 102 | run: | 103 | sudo apt-get update -y 104 | sudo apt-get install -y gcc-aarch64-linux-gnu 105 | echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV 106 | 107 | - uses: Swatinem/rust-cache@v2 108 | 109 | - name: build 110 | run: cargo build --package cli --release --target aarch64-unknown-linux-gnu 111 | 112 | - name: archive 113 | run: tar -czvf "archon_linux_arm64.tar.gz" -C ./target/aarch64-unknown-linux-gnu/release archon 114 | 115 | - name: generate tag name 116 | id: tag 117 | run: | 118 | echo "::set-output name=release_tag::nightly-${GITHUB_SHA}" 119 | 120 | - name: release 121 | uses: softprops/action-gh-release@v1 122 | with: 123 | tag_name: ${{ steps.tag.outputs.release_tag }} 124 | prerelease: true 125 | files: | 126 | archon_linux_arm64.tar.gz 127 | env: 128 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 129 | 130 | build-linux-amd: 131 | runs-on: ubuntu-latest 132 | steps: 133 | - name: checkout 134 | uses: actions/checkout@v1 135 | 136 | - name: install rust nightly 137 | uses: actions-rs/toolchain@v1 138 | with: 139 | toolchain: nightly 140 | default: true 141 | override: true 142 | 143 | - name: install target 144 | run: rustup target add x86_64-unknown-linux-gnu 145 | 146 | - uses: Swatinem/rust-cache@v2 147 | 148 | - name: build 149 | run: cargo build --package cli --release --target x86_64-unknown-linux-gnu 150 | 151 | - name: archive 152 | run: tar -czvf "archon_linux_amd64.tar.gz" -C ./target/x86_64-unknown-linux-gnu/release archon 153 | 154 | - name: generate tag name 155 | id: tag 156 | run: | 157 | echo "::set-output name=release_tag::nightly-${GITHUB_SHA}" 158 | 159 | - name: release 160 | uses: softprops/action-gh-release@v1 161 | with: 162 | tag_name: ${{ steps.tag.outputs.release_tag }} 163 | prerelease: true 164 | files: | 165 | archon_linux_amd64.tar.gz 166 | env: 167 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 168 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | L1_RPC_URL: ${{ secrets.L1_RPC_URL }} 11 | L2_RPC_URL: ${{ secrets.L2_RPC_URL }} 12 | ROLLUP_NODE_RPC_URL: ${{ secrets.ROLLUP_NODE_RPC_URL }} 13 | 14 | jobs: 15 | check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: nightly 23 | override: true 24 | - uses: Swatinem/rust-cache@v2 25 | - uses: actions-rs/cargo@v1 26 | with: 27 | command: check 28 | 29 | test: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | toolchain: nightly 37 | override: true 38 | - uses: Swatinem/rust-cache@v2 39 | - uses: actions-rs/cargo@v1 40 | with: 41 | command: test 42 | args: --all 43 | 44 | fmt: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: actions-rs/toolchain@v1 49 | with: 50 | profile: minimal 51 | toolchain: nightly 52 | override: true 53 | components: rustfmt 54 | - uses: Swatinem/rust-cache@v2 55 | - uses: actions-rs/cargo@v1 56 | with: 57 | command: fmt 58 | args: --all -- --check 59 | 60 | clippy: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v3 64 | - uses: actions-rs/toolchain@v1 65 | with: 66 | profile: minimal 67 | toolchain: nightly 68 | override: true 69 | components: clippy 70 | - uses: Swatinem/rust-cache@v2 71 | - uses: actions-rs/cargo@v1 72 | with: 73 | command: clippy 74 | args: --all -- -D warnings 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 90 # changed 2 | normalize_comments = true # changed 3 | imports_layout = "Vertical" # changed 4 | imports_granularity = "Crate" # changed 5 | trailing_semicolon = false # changed 6 | edition = "2021" # changed 7 | use_try_shorthand = true # changed 8 | use_field_init_shorthand = true # changed 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "varro" 3 | version = "0.1.0-alpha" 4 | edition = "2021" 5 | authors = ["refcell"] 6 | description = "A persistent, robust, and composable proposal service for rollup stacks." 7 | repository = "https://github.com/refcell/varro" 8 | license = "MIT" 9 | keywords = ["op-stack", "optimism", "varro", "node", "ethereum"] 10 | exclude = [ "logo", "varrup" ] 11 | 12 | [[bin]] 13 | name = "varro" 14 | path = "bin/varro.rs" 15 | 16 | [lib] 17 | crate-type = ["cdylib", "rlib"] 18 | 19 | [profile.release] 20 | strip = true 21 | opt-level = "z" 22 | lto = true 23 | codegen-units = 1 24 | panic = "abort" 25 | 26 | [dependencies] 27 | tokio = { version = "1.25.0", features = ["full"] } 28 | async-trait = "0.1.64" 29 | futures = "0.3.26" 30 | eyre = "0.6.8" 31 | hex = "0.4.3" 32 | libflate = "1.2.0" 33 | 34 | # Logging Telemetry 35 | chrono = "0.4.22" 36 | tracing = "0.1.36" 37 | ansi_term = "0.12.1" 38 | tracing-log = "0.1.3" 39 | tracing-subscriber = { version = "0.3.16", features = ["fmt", "env-filter", "ansi"] } 40 | 41 | # Serialization 42 | serde = { version = "1.0.152", features = [ "derive" ] } 43 | serde_json = "1.0.93" 44 | 45 | # Backend Crates 46 | sled = "0.34.7" 47 | uuid = { version = "1.3.0", features = ["v4"] } 48 | bytes = "1.4.0" 49 | reqwest = "0.11.14" 50 | jsonwebtoken = "8.2.0" 51 | rand = "0.8.5" 52 | home = "0.5.4" 53 | 54 | # CLI 55 | figment = { version = "0.10.8", features = ["toml", "env"] } 56 | ctrlc = "3.2.3" 57 | clap = { version = "3.2.18", features = ["derive", "env"] } 58 | dirs = "4.0.0" 59 | thiserror = "1.0.39" 60 | flate2 = { version = "1.0.25", features = ["zlib"] } 61 | once_cell = "1.17.1" 62 | 63 | # Ethers 64 | ethers-core = "1.0.2" 65 | ethers-providers = "1.0.2" 66 | ethers-middleware = "1.0.2" 67 | ethers-signers = "1.0.2" 68 | ethers-contract = { version = "1.0.2", features = ["abigen"] } 69 | toml = "0.7.3" 70 | tokio-stream = "0.1.12" 71 | 72 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 73 | tracing-test = "0.2.4" 74 | criterion = { version = "0.4", features = [ "async_tokio", "plotters" ]} 75 | plotters = "0.3.4" 76 | tempfile = "3.4.0" 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 refcell.eth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![build](https://github.com/refcell/varro/actions/workflows/test.yml/badge.svg)](https://github.com/refcell/varro/actions/workflows/test.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) [![varro](https://img.shields.io/crates/v/varro.svg)](https://crates.io/crates/varro) 4 | 5 | `varro` is a persistent, robust, and composable proposal service for rollup stacks written in pure rust. 6 | 7 | > **Note** 8 | > 9 | > Varro is primarily tested against the [op-stack](https://stack.optimism.io/). 10 | 11 | ## Quickstart 12 | 13 | Varro provides an easy to use installer called `varrup`. 14 | 15 | To install `varro`, run: `curl https://raw.githubusercontent.com/refcell/varro/main/varrup/install | bash`. 16 | 17 | Additionally, you can run varro for [ethereum-optimism](https://github.com/ethereum-optimism) using the provided scripts in the `scripts` directory. 18 | 19 | ## Architecture 20 | 21 | At it's core, `varro` is a client. In [./src/client.rs](./src/client.rs), there is an `Varro` struct that is the core client/driver of the batcher. 22 | 23 | The `Varro` struct first _builds_ "stages" and then executes them as asynchronous threads. These stages split up the transformation of data types and handles channel transitions and metrics. 24 | 25 | The primary entrypoint for `varro` is [./src/client/rs](./src/client.rs) which is the `Varro` struct. `Varro` exposes methods for constructing proposals. 26 | 27 | ## Configuration 28 | 29 | The `varro` cli maintains a verbose menu for running a proposal service. To see a list of all available commands, run `varro --help`. This will print output similar to the following: 30 | 31 | ```bash 32 | 33 | ``` 34 | 35 | ### Environment Variables 36 | 37 | The following environment variables are the default values for `varro`'s configuration. 38 | 39 | They can be overridden by setting the environment variable in the shell before running `varro`, or setting the associated flags when running the `varro` cli. 40 | 41 | ```env 42 | 43 | ``` 44 | 45 | ## Specifications 46 | 47 | // TODO: 48 | 49 | ## Why "Varro"? 50 | 51 | The term "varro" comes from [Marcus Terentius Varro](https://en.wikipedia.org/wiki/Marcus_Terentius_Varro), an ancient Roman polymath and prolific author. Referred to the "great third light of Rome", Varro mirrors the role of the `varro` service in the rollup stack, handling the "inscription" of outputs to the settlement layer (eg, posting L2 output proposals to Ethereum). 52 | 53 | ## FAQ 54 | 55 | // TODO: 56 | 57 | ## Contributing 58 | 59 | All contributions are welcome. Before opening a PR, please submit an issue detailing the bug or feature. When opening a PR, please ensure that your contribution builds on the nightly rust toolchain, has been linted with `cargo fmt`, and contains tests when applicable. 60 | 61 | ## Disclaimer 62 | 63 | _This code is being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the code. It has not been audited and as such there can be no assurance it will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information. Nothing in this repo should be construed as investment advice or legal advice for any particular facts or circumstances and is not meant to replace competent counsel. It is strongly advised for you to contact a reputable attorney in your jurisdiction for any questions or concerns with respect thereto. The authors is not liable for any use of the foregoing, and users should proceed with caution and use at their own risk._ 64 | -------------------------------------------------------------------------------- /bin/varro.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use eyre::Result; 3 | 4 | use varro::{ 5 | builder::VarroBuilder, 6 | cli::Cli, 7 | config::Config, 8 | telemetry, 9 | }; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | telemetry::init(false)?; 14 | telemetry::register_shutdown(); 15 | 16 | let cli = Cli::parse(); 17 | let config = Config::try_from(cli)?; 18 | let builder = VarroBuilder::try_from(config)?; 19 | let mut varro = builder.build()?; 20 | match varro.start().await { 21 | Ok(_) => Ok(()), 22 | Err(e) => { 23 | tracing::error!(target: "varro", "Varro exited with error: {}", e); 24 | Err(e) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /contracts/L2OutputOracle.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "_submissionInterval", 7 | "type": "uint256" 8 | }, 9 | { 10 | "internalType": "uint256", 11 | "name": "_l2BlockTime", 12 | "type": "uint256" 13 | }, 14 | { 15 | "internalType": "uint256", 16 | "name": "_startingBlockNumber", 17 | "type": "uint256" 18 | }, 19 | { 20 | "internalType": "uint256", 21 | "name": "_startingTimestamp", 22 | "type": "uint256" 23 | }, 24 | { 25 | "internalType": "address", 26 | "name": "_proposer", 27 | "type": "address" 28 | }, 29 | { 30 | "internalType": "address", 31 | "name": "_challenger", 32 | "type": "address" 33 | } 34 | ], 35 | "stateMutability": "nonpayable", 36 | "type": "constructor" 37 | }, 38 | { 39 | "anonymous": false, 40 | "inputs": [ 41 | { 42 | "indexed": false, 43 | "internalType": "uint8", 44 | "name": "version", 45 | "type": "uint8" 46 | } 47 | ], 48 | "name": "Initialized", 49 | "type": "event" 50 | }, 51 | { 52 | "anonymous": false, 53 | "inputs": [ 54 | { 55 | "indexed": true, 56 | "internalType": "bytes32", 57 | "name": "outputRoot", 58 | "type": "bytes32" 59 | }, 60 | { 61 | "indexed": true, 62 | "internalType": "uint256", 63 | "name": "l2OutputIndex", 64 | "type": "uint256" 65 | }, 66 | { 67 | "indexed": true, 68 | "internalType": "uint256", 69 | "name": "l2BlockNumber", 70 | "type": "uint256" 71 | }, 72 | { 73 | "indexed": false, 74 | "internalType": "uint256", 75 | "name": "l1Timestamp", 76 | "type": "uint256" 77 | } 78 | ], 79 | "name": "OutputProposed", 80 | "type": "event" 81 | }, 82 | { 83 | "anonymous": false, 84 | "inputs": [ 85 | { 86 | "indexed": true, 87 | "internalType": "uint256", 88 | "name": "prevNextOutputIndex", 89 | "type": "uint256" 90 | }, 91 | { 92 | "indexed": true, 93 | "internalType": "uint256", 94 | "name": "newNextOutputIndex", 95 | "type": "uint256" 96 | } 97 | ], 98 | "name": "OutputsDeleted", 99 | "type": "event" 100 | }, 101 | { 102 | "inputs": [], 103 | "name": "CHALLENGER", 104 | "outputs": [ 105 | { 106 | "internalType": "address", 107 | "name": "", 108 | "type": "address" 109 | } 110 | ], 111 | "stateMutability": "view", 112 | "type": "function" 113 | }, 114 | { 115 | "inputs": [], 116 | "name": "L2_BLOCK_TIME", 117 | "outputs": [ 118 | { 119 | "internalType": "uint256", 120 | "name": "", 121 | "type": "uint256" 122 | } 123 | ], 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "inputs": [], 129 | "name": "PROPOSER", 130 | "outputs": [ 131 | { 132 | "internalType": "address", 133 | "name": "", 134 | "type": "address" 135 | } 136 | ], 137 | "stateMutability": "view", 138 | "type": "function" 139 | }, 140 | { 141 | "inputs": [], 142 | "name": "SUBMISSION_INTERVAL", 143 | "outputs": [ 144 | { 145 | "internalType": "uint256", 146 | "name": "", 147 | "type": "uint256" 148 | } 149 | ], 150 | "stateMutability": "view", 151 | "type": "function" 152 | }, 153 | { 154 | "inputs": [ 155 | { 156 | "internalType": "uint256", 157 | "name": "_l2BlockNumber", 158 | "type": "uint256" 159 | } 160 | ], 161 | "name": "computeL2Timestamp", 162 | "outputs": [ 163 | { 164 | "internalType": "uint256", 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "stateMutability": "view", 170 | "type": "function" 171 | }, 172 | { 173 | "inputs": [ 174 | { 175 | "internalType": "uint256", 176 | "name": "_l2OutputIndex", 177 | "type": "uint256" 178 | } 179 | ], 180 | "name": "deleteL2Outputs", 181 | "outputs": [], 182 | "stateMutability": "nonpayable", 183 | "type": "function" 184 | }, 185 | { 186 | "inputs": [ 187 | { 188 | "internalType": "uint256", 189 | "name": "_l2OutputIndex", 190 | "type": "uint256" 191 | } 192 | ], 193 | "name": "getL2Output", 194 | "outputs": [ 195 | { 196 | "components": [ 197 | { 198 | "internalType": "bytes32", 199 | "name": "outputRoot", 200 | "type": "bytes32" 201 | }, 202 | { 203 | "internalType": "uint128", 204 | "name": "timestamp", 205 | "type": "uint128" 206 | }, 207 | { 208 | "internalType": "uint128", 209 | "name": "l2BlockNumber", 210 | "type": "uint128" 211 | } 212 | ], 213 | "internalType": "struct Types.OutputProposal", 214 | "name": "", 215 | "type": "tuple" 216 | } 217 | ], 218 | "stateMutability": "view", 219 | "type": "function" 220 | }, 221 | { 222 | "inputs": [ 223 | { 224 | "internalType": "uint256", 225 | "name": "_l2BlockNumber", 226 | "type": "uint256" 227 | } 228 | ], 229 | "name": "getL2OutputAfter", 230 | "outputs": [ 231 | { 232 | "components": [ 233 | { 234 | "internalType": "bytes32", 235 | "name": "outputRoot", 236 | "type": "bytes32" 237 | }, 238 | { 239 | "internalType": "uint128", 240 | "name": "timestamp", 241 | "type": "uint128" 242 | }, 243 | { 244 | "internalType": "uint128", 245 | "name": "l2BlockNumber", 246 | "type": "uint128" 247 | } 248 | ], 249 | "internalType": "struct Types.OutputProposal", 250 | "name": "", 251 | "type": "tuple" 252 | } 253 | ], 254 | "stateMutability": "view", 255 | "type": "function" 256 | }, 257 | { 258 | "inputs": [ 259 | { 260 | "internalType": "uint256", 261 | "name": "_l2BlockNumber", 262 | "type": "uint256" 263 | } 264 | ], 265 | "name": "getL2OutputIndexAfter", 266 | "outputs": [ 267 | { 268 | "internalType": "uint256", 269 | "name": "", 270 | "type": "uint256" 271 | } 272 | ], 273 | "stateMutability": "view", 274 | "type": "function" 275 | }, 276 | { 277 | "inputs": [ 278 | { 279 | "internalType": "uint256", 280 | "name": "_startingBlockNumber", 281 | "type": "uint256" 282 | }, 283 | { 284 | "internalType": "uint256", 285 | "name": "_startingTimestamp", 286 | "type": "uint256" 287 | } 288 | ], 289 | "name": "initialize", 290 | "outputs": [], 291 | "stateMutability": "nonpayable", 292 | "type": "function" 293 | }, 294 | { 295 | "inputs": [], 296 | "name": "latestBlockNumber", 297 | "outputs": [ 298 | { 299 | "internalType": "uint256", 300 | "name": "", 301 | "type": "uint256" 302 | } 303 | ], 304 | "stateMutability": "view", 305 | "type": "function" 306 | }, 307 | { 308 | "inputs": [], 309 | "name": "latestOutputIndex", 310 | "outputs": [ 311 | { 312 | "internalType": "uint256", 313 | "name": "", 314 | "type": "uint256" 315 | } 316 | ], 317 | "stateMutability": "view", 318 | "type": "function" 319 | }, 320 | { 321 | "inputs": [], 322 | "name": "nextBlockNumber", 323 | "outputs": [ 324 | { 325 | "internalType": "uint256", 326 | "name": "", 327 | "type": "uint256" 328 | } 329 | ], 330 | "stateMutability": "view", 331 | "type": "function" 332 | }, 333 | { 334 | "inputs": [], 335 | "name": "nextOutputIndex", 336 | "outputs": [ 337 | { 338 | "internalType": "uint256", 339 | "name": "", 340 | "type": "uint256" 341 | } 342 | ], 343 | "stateMutability": "view", 344 | "type": "function" 345 | }, 346 | { 347 | "inputs": [ 348 | { 349 | "internalType": "bytes32", 350 | "name": "_outputRoot", 351 | "type": "bytes32" 352 | }, 353 | { 354 | "internalType": "uint256", 355 | "name": "_l2BlockNumber", 356 | "type": "uint256" 357 | }, 358 | { 359 | "internalType": "bytes32", 360 | "name": "_l1BlockHash", 361 | "type": "bytes32" 362 | }, 363 | { 364 | "internalType": "uint256", 365 | "name": "_l1BlockNumber", 366 | "type": "uint256" 367 | } 368 | ], 369 | "name": "proposeL2Output", 370 | "outputs": [], 371 | "stateMutability": "payable", 372 | "type": "function" 373 | }, 374 | { 375 | "inputs": [], 376 | "name": "startingBlockNumber", 377 | "outputs": [ 378 | { 379 | "internalType": "uint256", 380 | "name": "", 381 | "type": "uint256" 382 | } 383 | ], 384 | "stateMutability": "view", 385 | "type": "function" 386 | }, 387 | { 388 | "inputs": [], 389 | "name": "startingTimestamp", 390 | "outputs": [ 391 | { 392 | "internalType": "uint256", 393 | "name": "", 394 | "type": "uint256" 395 | } 396 | ], 397 | "stateMutability": "view", 398 | "type": "function" 399 | }, 400 | { 401 | "inputs": [], 402 | "name": "version", 403 | "outputs": [ 404 | { 405 | "internalType": "string", 406 | "name": "", 407 | "type": "string" 408 | } 409 | ], 410 | "stateMutability": "view", 411 | "type": "function" 412 | } 413 | ] -------------------------------------------------------------------------------- /logo/blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/refcell/varro/9cd8f8d901410effa122c3cadd54ff083cb22ee9/logo/blurred.png -------------------------------------------------------------------------------- /logo/colosseum.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/refcell/varro/9cd8f8d901410effa122c3cadd54ff083cb22ee9/logo/colosseum.jpeg -------------------------------------------------------------------------------- /logo/refcell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/refcell/varro/9cd8f8d901410effa122c3cadd54ff083cb22ee9/logo/refcell.png -------------------------------------------------------------------------------- /logo/varro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/refcell/varro/9cd8f8d901410effa122c3cadd54ff083cb22ee9/logo/varro.png -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use ethers_core::types::{ 4 | Address, 5 | H256, 6 | }; 7 | use eyre::Result; 8 | 9 | use crate::{ 10 | client::Varro, 11 | config::Config, 12 | metrics::Metrics, 13 | rollup::RollupNode, 14 | L1Client, 15 | OutputOracleContract, 16 | }; 17 | 18 | /// VarroBuilder 19 | /// 20 | /// A builder for the [Varro] client. 21 | #[derive(Debug, Default, Clone)] 22 | pub struct VarroBuilder { 23 | /// An L1 [L1Client] 24 | pub l1_client: Option, 25 | /// A [RollupNode] client 26 | pub rollup_node: Option, 27 | /// An output oracle contract 28 | pub output_oracle: Option>, 29 | /// Whether to use non-finalized L1 data to propose L2 blocks. 30 | pub allow_non_finalized: Option, 31 | /// The proposer 32 | pub proposer: Option
, 33 | /// The proposer's private key used to send the output transactions. 34 | pub output_private_key: Option, 35 | /// The polling interval 36 | pub polling_interval: Option, 37 | /// An _optional_ metrics server 38 | pub metrics: Option, 39 | /// The transaction backoff 40 | pub tx_backoff: Option, 41 | } 42 | 43 | impl TryFrom for VarroBuilder { 44 | type Error = eyre::Report; 45 | 46 | fn try_from(conf: Config) -> std::result::Result { 47 | let output_oracle_contract = conf.get_output_oracle_contract()?; 48 | Ok(Self { 49 | l1_client: Some(conf.get_l1_client()?), 50 | rollup_node: Some(conf.get_rollup_node_client()?), 51 | output_oracle: Some(output_oracle_contract), 52 | allow_non_finalized: Some(conf.allow_non_finalized), 53 | proposer: Some(conf.output_oracle_address), 54 | output_private_key: Some(conf.get_output_private_key()?), 55 | polling_interval: Some(conf.polling_interval), 56 | metrics: None, 57 | tx_backoff: Some(conf.resubmission_timeout), 58 | }) 59 | } 60 | } 61 | 62 | impl VarroBuilder { 63 | /// Creates a new [VarroBuilder]. 64 | pub fn new() -> Self { 65 | Self::default() 66 | } 67 | 68 | /// Sets the metrics server for the [Varro] client. 69 | pub fn with_metrics(&mut self, metrics: Metrics) -> &mut Self { 70 | self.metrics = Some(metrics); 71 | self 72 | } 73 | 74 | /// Sets the L1 client for the [Varro] client. 75 | pub fn with_l1_client(&mut self, l1_client: L1Client) -> &mut Self { 76 | self.l1_client = Some(l1_client); 77 | self 78 | } 79 | 80 | /// Sets the rollup node for the [Varro] client. 81 | pub fn with_rollup_node(&mut self, rollup_node: RollupNode) -> &mut Self { 82 | self.rollup_node = Some(rollup_node); 83 | self 84 | } 85 | 86 | /// Sets the output oracle for the [Varro] client. 87 | pub fn with_output_oracle(&mut self, output_oracle: OutputOracleContract) -> &mut Self { 88 | self.output_oracle = Some(output_oracle); 89 | self 90 | } 91 | 92 | /// Sets whether to use non-finalized L1 data to propose L2 blocks. 93 | pub fn with_allow_non_finalized(&mut self, allow_non_finalized: bool) -> &mut Self { 94 | self.allow_non_finalized = Some(allow_non_finalized); 95 | self 96 | } 97 | 98 | /// Sets the proposer for the [Varro] client. 99 | pub fn with_proposer(&mut self, proposer: Address) -> &mut Self { 100 | self.proposer = Some(proposer); 101 | self 102 | } 103 | 104 | /// Sets the proposer's private key used to send the output transactions. 105 | /// for the [Varro] client. 106 | pub fn with_output_private_key(&mut self, output_private_key: H256) -> &mut Self { 107 | self.output_private_key = Some(output_private_key); 108 | self 109 | } 110 | 111 | /// Sets the polling interval for the [Varro] client. 112 | pub fn with_polling_interval(&mut self, polling_interval: Duration) -> &mut Self { 113 | self.polling_interval = Some(polling_interval); 114 | self 115 | } 116 | 117 | /// Sets the transaction backoff for the [Varro] client. 118 | pub fn with_tx_backoff(&mut self, tx_backoff: Duration) -> &mut Self { 119 | self.tx_backoff = Some(tx_backoff); 120 | self 121 | } 122 | 123 | /// Builds the [Varro] client. 124 | pub fn build(self) -> Result { 125 | Ok(Varro::try_from(self)?) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Write, 4 | str::FromStr, 5 | time::Duration, 6 | }; 7 | 8 | use crate::{ 9 | config::Config, 10 | errors::ConfigError, 11 | extract_env, 12 | }; 13 | use clap::Parser; 14 | use dirs::home_dir; 15 | use ethers_core::types::Address; 16 | 17 | /// The [Varro] CLI 18 | #[derive(Parser)] 19 | pub struct Cli { 20 | /// The HTTP provider URL for L1. 21 | #[clap(short = 'l', long, env = "L1_RPC_URL")] 22 | l1_client_rpc_url: Option, 23 | /// The HTTP provider URL for the rollup node. 24 | #[clap(short = 'r', long, env = "ROLLUP_NODE_RPC_URL")] 25 | rollup_node_rpc_url: Option, 26 | /// The L2OutputOracle contract address. 27 | #[clap(short = 's', long, env = "OUTPUT_ORACLE_ADDRESS")] 28 | output_oracle_address: Option, 29 | /// The delay between querying L2 for more blocks and creating a new batch. 30 | #[clap(short = 'i', long, default_value = "2")] 31 | polling_interval: u64, 32 | /// The number of confirmations which to wait after appending new batches. 33 | #[clap(short = 'c', long, default_value = "2")] 34 | num_confirmation: u64, 35 | /// The number of [VarroError::NonceTooLow] errors required to give up on a 36 | /// tx at a particular nonce without receiving confirmation. 37 | #[clap(short = 'a', long, default_value = "2")] 38 | safe_abort_nonce_too_low: u64, 39 | /// The time to wait before resubmitting a transaction. 40 | #[clap(short = 'r', long, default_value = "2")] 41 | resubmission_timeout: u64, 42 | /// The HD seed used to derive the wallet private keys for both 43 | /// the sequencer and proposer. Must be used in conjunction with 44 | /// SequencerHDPath and ProposerHDPath. 45 | /// 46 | /// If not provided, a new mnemonic will be generated. 47 | #[clap(short = 'm', long, env = "MNEMONIC")] 48 | mnemonic: Option, 49 | /// The private key used for the L2 Output transactions. 50 | #[clap(short = 'k', long, env = "OUTPUT_PRIVATE_KEY")] 51 | output_private_key: Option, 52 | /// The derivation path used to obtain the private key for the output transactions. 53 | #[clap(short = 'p', long, default_value = "m/44'/60'/0'/0/0")] 54 | output_hd_path: String, 55 | /// Whether to use non-finalized L1 data to propose L2 blocks. 56 | #[clap(short = 'n', long)] 57 | allow_non_finalized: bool, 58 | } 59 | 60 | impl TryFrom for Config { 61 | type Error = ConfigError; 62 | 63 | fn try_from(cli: Cli) -> Result { 64 | let l1_client_rpc_url = 65 | cli.l1_client_rpc_url.unwrap_or(extract_env!("L1_RPC_URL")); 66 | let rollup_node_rpc_url = cli 67 | .rollup_node_rpc_url 68 | .unwrap_or(extract_env!("ROLLUP_NODE_RPC_URL")); 69 | let output_oracle_address = cli 70 | .output_oracle_address 71 | .unwrap_or(extract_env!("OUTPUT_ORACLE_ADDRESS")); 72 | let polling_interval = Duration::from_secs(cli.polling_interval); 73 | let num_confirmation = cli.num_confirmation; 74 | let safe_abort_nonce_too_low = cli.safe_abort_nonce_too_low; 75 | let resubmission_timeout = Duration::from_secs(cli.resubmission_timeout); 76 | let mnemonic = cli.mnemonic.unwrap_or_else(|| extract_env!("MNEMONIC")); 77 | let output_private_key = cli 78 | .output_private_key 79 | .unwrap_or_else(|| extract_env!("OUTPUT_PRIVATE_KEY")); 80 | let output_hd_path = cli.output_hd_path; 81 | let allow_non_finalized = cli.allow_non_finalized; 82 | 83 | let config = Config { 84 | l1_client_rpc_url, 85 | rollup_node_rpc_url, 86 | output_oracle_address: Address::from_str(&output_oracle_address).map_err( 87 | |_| ConfigError::InvalidOutputOracleAddress(output_oracle_address), 88 | )?, 89 | polling_interval, 90 | num_confirmation, 91 | safe_abort_nonce_too_low, 92 | resubmission_timeout, 93 | mnemonic, 94 | output_private_key, 95 | output_hd_path, 96 | allow_non_finalized, 97 | }; 98 | 99 | // Save the config to disk 100 | let config_path = home_dir().unwrap().join(".varro/varro.toml"); 101 | let mut file = File::create(&config_path).map_err(|_| { 102 | ConfigError::TomlFileCreation(config_path.to_string_lossy().to_string()) 103 | })?; 104 | file.write_all( 105 | toml::to_string(&config) 106 | .map_err(|_| ConfigError::ConfigTomlConversion)? 107 | .as_bytes(), 108 | ) 109 | .map_err(|_| ConfigError::TomlFileWrite)?; 110 | 111 | Ok(config) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::Arc, 3 | time::Duration, 4 | }; 5 | 6 | use eyre::Result; 7 | use futures::lock::Mutex; 8 | use tokio::task::JoinHandle; 9 | 10 | use ethers_core::types::{ 11 | Address, 12 | H256, 13 | }; 14 | 15 | use tokio::sync::mpsc::{ 16 | unbounded_channel, 17 | UnboundedReceiver, 18 | UnboundedSender, 19 | }; 20 | 21 | use crate::{ 22 | builder::VarroBuilder, 23 | metrics::Metrics, 24 | pool::TransactionPool, 25 | prelude::VarroBuilderError, 26 | rollup::{ 27 | OutputResponse, 28 | RollupNode, 29 | SyncStatus, 30 | }, 31 | L1Client, 32 | OutputOracleContract, 33 | proposals::ProposalManager, 34 | }; 35 | 36 | /// Varro 37 | /// 38 | /// This is the primary varro client, responsible for orchestrating proposal submission. 39 | /// 40 | /// The [Varro] client should be constructed using the [crate::builder::VarroBuilder]. 41 | /// The builder provides an ergonomic way to construct the [Varro] client, with sensible defaults. 42 | #[derive(Debug)] 43 | pub struct Varro { 44 | /// An L1 [L1Client] 45 | l1_client: Arc>, 46 | /// A [RollupNode] client 47 | rollup_node: Arc>, 48 | /// The output oracle contract address 49 | output_oracle_address: Address, 50 | /// An output oracle contract 51 | output_oracle: OutputOracleContract, 52 | /// Whether to use non-finalized L1 data to propose L2 blocks. 53 | allow_non_finalized: bool, 54 | /// The proposer 55 | #[allow(dead_code)] 56 | proposer: Address, 57 | /// The proposer's private key used to send the output transactions. 58 | output_private_key: H256, 59 | /// The polling interval 60 | polling_interval: Duration, 61 | /// An _optional_ metrics server 62 | metrics: Option, 63 | /// The proposal sender sends new [OutputResponse]. 64 | proposal_sender: UnboundedSender, 65 | /// The proposal receiver listens for new [OutputResponse]. 66 | proposal_receiver: UnboundedReceiver, 67 | /// The tx_pool sender sends new [OutputResponse]. 68 | tx_pool_sender: UnboundedSender, 69 | /// The tx_pool receiver listens for new [OutputResponse]. 70 | tx_pool_receiver: Option>, 71 | /// The transaction pool 72 | transaction_pool: Arc>, 73 | /// The transaction pool task handle 74 | tx_pool_task: Option>>, 75 | /// A handle for the metrics server 76 | metrics_handle: Option>>, 77 | /// A handle for the spawned [ProposalManager] task 78 | proposal_manager_handle: Option>>, 79 | /// A list of output roots submitted to the [TransactionPool]. 80 | roots: Vec>, 81 | /// A backoff time for sending transaction to the [TransactionPool] 82 | tx_backoff: Duration, 83 | } 84 | 85 | impl TryFrom for Varro { 86 | type Error = VarroBuilderError; 87 | 88 | fn try_from(mut builder: VarroBuilder) -> std::result::Result { 89 | let l1_client = builder 90 | .l1_client 91 | .take() 92 | .ok_or(VarroBuilderError::MissingL1Client)?; 93 | let rollup_node = builder 94 | .rollup_node 95 | .take() 96 | .ok_or(VarroBuilderError::MissingRollupNode)?; 97 | let output_oracle = builder 98 | .output_oracle 99 | .take() 100 | .ok_or(VarroBuilderError::MissingOutputOracle)?; 101 | let proposer = builder 102 | .proposer 103 | .take() 104 | .ok_or(VarroBuilderError::MissingProposer)?; 105 | let output_private_key = builder 106 | .output_private_key 107 | .take() 108 | .ok_or(VarroBuilderError::MissingOutputPrivateKey)?; 109 | let polling_interval = builder 110 | .polling_interval 111 | .take() 112 | .ok_or(VarroBuilderError::MissingPollingInterval)?; 113 | let allow_non_finalized = match builder.allow_non_finalized { 114 | Some(allow_non_finalized) => allow_non_finalized, 115 | None => { 116 | tracing::warn!(target: "varro=client", "VarroBuilder was not provided 'allow_non_finalized', defaulting to false"); 117 | false 118 | } 119 | }; 120 | let tx_backoff = builder.tx_backoff.take().ok_or(VarroBuilderError::MissingTxBackoff)?; 121 | let output_oracle_address = output_oracle.address(); 122 | let metrics = builder.metrics.take(); 123 | 124 | let (proposal_sender, proposal_receiver) = unbounded_channel(); 125 | let (tx_pool_sender, tx_pool_receiver) = unbounded_channel(); 126 | 127 | Ok(Varro { 128 | l1_client: Arc::new(Mutex::new(l1_client)), 129 | rollup_node: Arc::new(Mutex::new(rollup_node)), 130 | output_oracle_address, 131 | output_oracle, 132 | allow_non_finalized, 133 | proposer, 134 | output_private_key, 135 | polling_interval, 136 | metrics, 137 | proposal_sender, 138 | proposal_receiver, 139 | tx_pool_sender, 140 | tx_pool_receiver: Some(tx_pool_receiver), 141 | transaction_pool: Arc::new(Mutex::new(TransactionPool::new())), 142 | tx_pool_task: None, 143 | metrics_handle: None, 144 | proposal_manager_handle: None, 145 | roots: vec![], 146 | tx_backoff 147 | }) 148 | } 149 | } 150 | 151 | impl Varro { 152 | /// Starts the [Varro] client. 153 | pub async fn start(&mut self) { 154 | // Get the L1 Chain ID 155 | // let l1_chain_id = self.l1_client.lock().await.get_chainid().await?; 156 | 157 | // Start a metrics server 158 | self.start_metrics_server().await; 159 | 160 | // Spawn the transaction pool task 161 | self.start_transaction_pool().await; 162 | 163 | // Start the proposal manager to listen for new proposals 164 | self.start_proposal_manager().await; 165 | 166 | // Listen for new proposals and submit them to the transaction pool 167 | // This will block until the proposal receiver channel is closed 168 | self.listen_for_proposals().await; 169 | } 170 | } 171 | 172 | impl Varro { 173 | /// Derives the block number from a [RollupNode]'s [SyncStatus]. 174 | pub fn get_block_number(allow_non_finalized: bool, sync_status: SyncStatus) -> u64 { 175 | // If we're not allowed to use non-finalized data, 176 | // then we can only use the finalized l2 block number 177 | if !allow_non_finalized { 178 | return sync_status.finalized_l2 179 | } 180 | 181 | // If "safe" blocks are allowed, use the l2 safe head 182 | sync_status.safe_l2 183 | } 184 | 185 | /// Spawns the transaction pool task 186 | async fn start_transaction_pool(&mut self) { 187 | let output_private_key = self.output_private_key; 188 | let l1_client = Arc::clone(&self.l1_client); 189 | let tx_pool_receiver = self.tx_pool_receiver.take(); 190 | let tx_pool = Arc::clone(&self.transaction_pool); 191 | let tx_pool_handle = tokio::spawn(async move { 192 | tx_pool 193 | .lock() 194 | .await 195 | .start(l1_client, output_private_key, tx_pool_receiver) 196 | .await 197 | }); 198 | self.tx_pool_task = Some(tx_pool_handle); 199 | } 200 | 201 | /// Shuts down associated tasks. 202 | pub async fn shutdown(&mut self) { 203 | // Shutdown the transaction pool 204 | if let Some(tx_pool_task) = &self.tx_pool_task { 205 | tx_pool_task.abort() 206 | } 207 | 208 | // Shutdown the metrics server 209 | if let Some(metrics_handle) = self.metrics_handle.take() { 210 | metrics_handle.abort() 211 | } 212 | } 213 | 214 | /// Starts the [ProposalManager] task 215 | async fn start_proposal_manager(&mut self) { 216 | let l1_client = Arc::clone(&self.l1_client); 217 | let rollup_node = Arc::clone(&self.rollup_node); 218 | let output_oracle_address = self.output_oracle_address; 219 | let allow_non_finalized = self.allow_non_finalized; 220 | let polling_interval = self.polling_interval; 221 | let proposal_sender = self.proposal_sender.clone(); 222 | let proposer = self.proposer.clone(); 223 | let proposal_manager_handle = tokio::spawn(async move { 224 | ProposalManager::start( 225 | l1_client, 226 | rollup_node, 227 | output_oracle_address, 228 | allow_non_finalized, 229 | polling_interval, 230 | proposal_sender, 231 | proposer, 232 | ) 233 | .await 234 | }); 235 | self.proposal_manager_handle = Some(proposal_manager_handle); 236 | } 237 | 238 | /// Starts the [Metrics] server 239 | async fn start_metrics_server(&mut self) { 240 | // Spawn the metrics server 241 | if let Some(mut metrics) = self.metrics.take() { 242 | let metrics_handle = tokio::task::spawn(async move { 243 | tracing::info!(target: "varro=client", "starting metrics server..."); 244 | metrics.serve().await 245 | }); 246 | self.metrics_handle = Some(metrics_handle); 247 | } 248 | } 249 | 250 | /// Listens for new proposals and submits them to the transaction pool. 251 | pub async fn listen_for_proposals(&mut self) { 252 | while let Some(proposal) = self.proposal_receiver.recv().await { 253 | tracing::debug!(target: "varro=client", "received proposal: {:?}", proposal); 254 | self.submit_proposal(proposal).await; 255 | } 256 | tracing::warn!(target: "varro=client", "proposal receiver channel closed, varro client shutting down..."); 257 | } 258 | 259 | /// Submits an [OutputResponse] to the [TransactionPool]. 260 | pub async fn submit_proposal(&mut self, proposal: OutputResponse) { 261 | tracing::info!(target: "varro=client", "received proposal: {:?}", proposal); 262 | tracing::info!(target: "varro=client", "submitting to transaction pool..."); 263 | 264 | // Check if the proposal is already being sent 265 | if self.roots.contains(&proposal.output_root) { 266 | tracing::warn!(target: "varro=client", "proposal already being sent, skipping..."); 267 | return; 268 | } 269 | 270 | // Track that we already submitted this proposal 271 | self.roots.push(proposal.output_root.clone()); 272 | 273 | // Send proposal to transaction pool with backoff retries 274 | // This is to gracefully handle when the transaction pool is full 275 | while self.tx_pool_sender.send(proposal.clone()).is_err() { 276 | tracing::warn!(target: "varro=client", "transaction pool is full, retrying..."); 277 | tokio::time::sleep(self.tx_backoff).await; 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | //! Common Types and Utilies for Varro 3 | 4 | use ethers_contract::abigen; 5 | use eyre::Result; 6 | 7 | pub use ethers_providers::{ 8 | Http, 9 | Middleware, 10 | Provider, 11 | }; 12 | 13 | abigen!(OutputOracleContract, "contracts/L2OutputOracle.json"); 14 | 15 | pub use OutputOracleContract; 16 | 17 | /// An enum of supported output versions for the [crate::rollup::OutputResponse] version. 18 | #[derive(Debug, Clone)] 19 | pub enum SupportedOutputVersion { 20 | /// Output version 1 21 | V1, 22 | } 23 | 24 | impl SupportedOutputVersion { 25 | /// Checks that the given output version is equal to the provided bytes version. 26 | pub fn equals(&self, other: &[u8]) -> bool { 27 | match (self, other.is_empty()) { 28 | // V1 is the only supported version for now - an empty Vec 29 | (Self::V1, true) => true, 30 | _ => false, 31 | } 32 | } 33 | } 34 | 35 | // TODO: Can we just use the Middleware trait from ethers? 36 | /// An L1Client is an [Http] [Provider] for Ethereum Mainnet. 37 | pub type L1Client = Provider; 38 | 39 | // TODO: properly construct an OutputOracle Contract 40 | /// An OutputOracle is a contract that provides the next block number to use for the next proposal. 41 | #[derive(Debug, Default, Clone)] 42 | pub struct OutputOracle {} 43 | 44 | impl OutputOracle { 45 | /// Get the next block number to use for the next proposal. 46 | pub async fn get_next_block_number(&self) -> Result { 47 | Ok(0) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | str::FromStr, 3 | time::Duration, 4 | }; 5 | 6 | use ethers_core::types::{ 7 | Address, 8 | H256, 9 | }; 10 | use ethers_providers::{ 11 | Http, 12 | Provider, 13 | }; 14 | use eyre::Result; 15 | use serde::{ 16 | Deserialize, 17 | Serialize, 18 | }; 19 | 20 | use crate::{ 21 | errors::ConfigError, 22 | extract_env, 23 | rollup::RollupNode, 24 | OutputOracleContract, 25 | }; 26 | 27 | /// A system configuration 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct Config { 30 | /// The L1 client rpc url. 31 | pub l1_client_rpc_url: String, 32 | /// The L2 client rollup node rpc url. 33 | /// Note: This is not explicitly the same as an "L2 client rpc url". 34 | /// The Rollup Node may expose a [RollupRPC] interface on a different port. 35 | /// This is the url that should be used for this field. 36 | pub rollup_node_rpc_url: String, 37 | /// The L2OutputOracle contract address. 38 | pub output_oracle_address: Address, 39 | /// The delay between querying L2 for more blocks and creating a new batch. 40 | pub polling_interval: Duration, 41 | /// The number of confirmations which to wait after appending new batches. 42 | pub num_confirmation: u64, 43 | /// The number of [VarroError::NonceTooLow] errors required to give up on a 44 | /// tx at a particular nonce without receiving confirmation. 45 | pub safe_abort_nonce_too_low: u64, 46 | /// The time to wait before resubmitting a transaction. 47 | pub resubmission_timeout: Duration, 48 | /// The HD seed used to derive the wallet private keys for both 49 | /// the sequencer and proposer. Must be used in conjunction with 50 | /// SequencerHDPath and ProposerHDPath. 51 | /// 52 | /// If not provided, a new mnemonic will be generated. 53 | pub mnemonic: String, 54 | /// The private key used for the L2 Output transactions. 55 | pub output_private_key: String, 56 | /// The HD path used to derive the wallet private key for the sequencer. 57 | pub output_hd_path: String, 58 | /// Whether to use non-finalized L1 data to propose L2 blocks. 59 | pub allow_non_finalized: bool, 60 | } 61 | 62 | impl Default for Config { 63 | fn default() -> Self { 64 | Self { 65 | l1_client_rpc_url: extract_env!("L1_CLIENT_RPC_URL"), 66 | rollup_node_rpc_url: extract_env!("ROLLUP_NODE_RPC_URL"), 67 | output_oracle_address: Address::from_str(&extract_env!( 68 | "OUTPUT_ORACLE_ADDRESS" 69 | )) 70 | .expect("invalid output oracle address"), 71 | polling_interval: Duration::from_secs(5), 72 | num_confirmation: 1, 73 | safe_abort_nonce_too_low: 3, 74 | resubmission_timeout: Duration::from_secs(60), 75 | mnemonic: extract_env!("MNEMONIC"), 76 | output_private_key: extract_env!("OUTPUT_PRIVATE_KEY"), 77 | output_hd_path: String::from("m/44'/60'/0'/0/0"), 78 | allow_non_finalized: false, 79 | } 80 | } 81 | } 82 | 83 | impl Config { 84 | /// Parses the output private key string into a 32-byte hash 85 | pub fn get_output_private_key(&self) -> Result { 86 | Ok(H256::from_str(&self.output_private_key).map_err(|_| { 87 | ConfigError::InvalidOutputPrivateKey(self.output_private_key.clone()) 88 | })?) 89 | } 90 | 91 | /// Returns the output oracle address 92 | pub fn get_output_oracle_address(&self) -> Address { 93 | self.output_oracle_address 94 | } 95 | 96 | /// Constructs an L1 [Provider] using the configured L1 client rpc url. 97 | pub fn get_l1_client(&self) -> Result> { 98 | Ok(Provider::::try_from(&self.l1_client_rpc_url) 99 | .map_err(|_| ConfigError::InvalidL1ClientUrl)?) 100 | } 101 | 102 | /// Constructs a Rollup Node [RollupNode] using the configured rollup node rpc url. 103 | pub fn get_rollup_node_client(&self) -> Result { 104 | Ok(RollupNode::try_from(self.rollup_node_rpc_url.clone()) 105 | .map_err(|_| ConfigError::InvalidRollupNodeRpcUrl)?) 106 | } 107 | 108 | /// Get the output oracle contract 109 | pub fn get_output_oracle_contract(&self) -> Result>> { 110 | let l1_client = self.get_l1_client()?; 111 | let output_oracle = OutputOracleContract::new( 112 | self.get_output_oracle_address(), 113 | l1_client.into(), 114 | ); 115 | Ok(output_oracle) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// [crate::client::Varro] Error 4 | #[derive(Debug, Error)] 5 | pub enum VarroError { 6 | /// Missing client 7 | #[error("missing client")] 8 | MissingClient, 9 | } 10 | 11 | /// A [crate::rollup::RollupNode] Error 12 | #[derive(Debug, Error)] 13 | pub enum RollupNodeError { 14 | /// Failed to create a new rollup node 15 | /// from the given rpc url. 16 | #[error("failed to create a new rollup node from the given rpc url: {0}")] 17 | RollupNodeInvalidUrl(String), 18 | } 19 | 20 | /// A [crate::config::Config] Error 21 | #[derive(Debug, Error)] 22 | pub enum ConfigError { 23 | /// L1 Client URL is invalid 24 | #[error("l1 client url is invalid")] 25 | InvalidL1ClientUrl, 26 | /// Rollup Node RPC URL is invalid 27 | #[error("rollup node rpc url is invalid")] 28 | InvalidRollupNodeRpcUrl, 29 | /// Failed to create toml file on disk 30 | #[error("failed to create toml file at {0}")] 31 | TomlFileCreation(String), 32 | /// Failed to translate the [crate::config::Config] to a toml object 33 | #[error("failed to translate the config to a toml object")] 34 | ConfigTomlConversion, 35 | /// Failed to write the [crate::config::Config] to the toml file 36 | #[error("failed to write the config to the toml file")] 37 | TomlFileWrite, 38 | /// An invalid output oracle address was provided 39 | #[error("an invalid output oracle address was provided: {0}")] 40 | InvalidOutputOracleAddress(String), 41 | /// Failed to parse the given output private key as a 32 byte hex string. 42 | #[error("failed to parse the given output private key as a 32 byte hex string: {0}")] 43 | InvalidOutputPrivateKey(String), 44 | } 45 | 46 | /// A [crate::client::VarroBuilder] Error 47 | #[derive(Debug, Error)] 48 | pub enum VarroBuilderError { 49 | /// An [crate::L1Client] is required to build a [Varro] client. 50 | #[error("an l1 client is required to build a varro client")] 51 | MissingL1Client, 52 | /// A [crate::rollup::RollupNode] is required to build a [Varro] client. 53 | #[error("an rollup node is required to build a varro client")] 54 | MissingRollupNode, 55 | /// An [crate::output::OutputOracle] is required to build a [Varro] client. 56 | #[error("an output oracle is required to build a varro client")] 57 | MissingOutputOracle, 58 | /// A [crate::proposer::Proposer] is required to build a [Varro] client. 59 | #[error("a proposer is required to build a varro client")] 60 | MissingProposer, 61 | /// An output private key is required to build a [Varro] client. 62 | #[error("an output private key is required to build a varro client")] 63 | MissingOutputPrivateKey, 64 | /// A polling interval is required to build a [Varro] client. 65 | #[error("a polling interval is required to build a varro client")] 66 | MissingPollingInterval, 67 | /// A backoff is required for resubmission of transactions 68 | #[error("a backoff is required for resubmission of transactions")] 69 | MissingTxBackoff, 70 | } 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc=include_str!("../README.md")] 2 | #![warn(missing_docs)] 3 | #![warn(unused_extern_crates)] 4 | #![forbid(unsafe_code)] 5 | #![forbid(where_clauses_object_safety)] 6 | 7 | mod common; 8 | pub use common::*; 9 | 10 | pub mod telemetry; 11 | pub use telemetry::*; 12 | 13 | /// The core [Varro] client 14 | pub mod client; 15 | 16 | /// A Builder for the [Varro] client 17 | pub mod builder; 18 | 19 | /// Pool contains the logic to manage proposal transactions 20 | pub mod pool; 21 | 22 | /// Proposals holds the [ProposalManager] which 23 | /// listens for new [OutputResponse] proposals 24 | pub mod proposals; 25 | 26 | /// Configuration 27 | pub mod config; 28 | 29 | /// CLI parsing 30 | pub mod cli; 31 | 32 | /// The Rollup Node 33 | pub mod rollup; 34 | 35 | /// Common Errors 36 | pub mod errors; 37 | 38 | /// The metrics server 39 | pub mod metrics; 40 | 41 | /// Common internal macros 42 | pub(crate) mod macros; 43 | 44 | /// Re-export Archon Types 45 | pub mod prelude { 46 | pub use crate::{ 47 | builder::*, 48 | client::*, 49 | common::*, 50 | config::*, 51 | errors::*, 52 | metrics::*, 53 | telemetry::*, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | /// This macro is used to read the value of an environment variable. 2 | /// If the environment variable is not set, the macro will panic. 3 | #[macro_export] 4 | macro_rules! extract_env { 5 | ($a:expr) => { 6 | std::env::var($a).unwrap_or_else(|_| panic!("{} is not set", $a)) 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | use eyre::Result; 2 | use std::{ 3 | io::prelude::*, 4 | net::{ 5 | TcpListener, 6 | TcpStream, 7 | }, 8 | }; 9 | 10 | /// Metrics 11 | /// 12 | /// Serves metrics for the [crate::client::Varro] client. 13 | #[derive(Debug, Default, Clone)] 14 | pub struct Metrics {} 15 | 16 | impl Metrics { 17 | /// Constructs a new [Metrics] instance 18 | pub fn new() -> Self { 19 | Self {} 20 | } 21 | 22 | /// Serve a [TcpListener] to provide [crate::client::Varro] metrics. 23 | pub async fn serve(&mut self) -> Result<()> { 24 | let addr = "127.0.0.1:8082".to_string(); 25 | let listener = TcpListener::bind(&addr) 26 | .map_err(|_| eyre::eyre!("Metrics failed to bind to {}", addr))?; 27 | for stream in listener.incoming().flatten() { 28 | self.handle_connection(stream)?; 29 | } 30 | Ok(()) 31 | } 32 | 33 | /// Handle an incoming connection. 34 | pub fn handle_connection(&self, mut stream: TcpStream) -> Result<()> { 35 | let mut buffer = [0; 1024]; 36 | let read_bytes = stream.read(&mut buffer)?; 37 | println!( 38 | "Request with {} bytes: {}", 39 | read_bytes, 40 | String::from_utf8_lossy(&buffer[..]) 41 | ); 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/pool.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_variables)] 3 | 4 | use ethers_middleware::SignerMiddleware; 5 | use ethers_signers::LocalWallet; 6 | use futures::lock::Mutex; 7 | use std::sync::Arc; 8 | 9 | use crate::{ 10 | rollup::OutputResponse, 11 | L1Client, 12 | OutputOracle, 13 | OutputOracleContract, 14 | }; 15 | use ethers_providers::Middleware; 16 | use ethers_core::{types::{ 17 | TransactionReceipt, 18 | H256, Address, U256, transaction::eip2718::TypedTransaction, 19 | }, k256::ecdsa::SigningKey}; 20 | use eyre::Result; 21 | use tokio::{ 22 | sync::mpsc::UnboundedReceiver, 23 | task::JoinHandle, 24 | }; 25 | 26 | /// TransactionPool 27 | /// 28 | /// The transaction pool is responsible for managing and sending [crate::rollup::OutputResponse] 29 | /// proposal transactions to the L1 network. 30 | #[derive(Debug, Default)] 31 | pub struct TransactionPool { 32 | /// The L1 client 33 | pub l1_client: Option, 34 | /// The output oracle 35 | pub output_oracle: Option, 36 | /// Transaction Construction Join Handles 37 | pub tx_construction_handles: Arc>>>>, 38 | /// The core transaction pool task 39 | pub core_task: Option>>, 40 | } 41 | 42 | impl TransactionPool { 43 | /// Constructs a new [TransactionPool]. 44 | pub fn new() -> Self { 45 | Self::default() 46 | } 47 | 48 | /// Starts the transaction pool in a new thread. 49 | pub async fn start( 50 | &mut self, 51 | l1_client: Arc>, 52 | private_key: H256, 53 | receiver: Option>, 54 | ) -> Result<()> { 55 | let handles = Arc::clone(&self.tx_construction_handles); 56 | let mut receiver = 57 | receiver.ok_or(eyre::eyre!("Missing output response receiver"))?; 58 | // Spawn transaction pool in a separate task 59 | let main_handle = tokio::task::spawn(async move { 60 | while let Some(proposal) = receiver.recv().await { 61 | let l1_client = Arc::clone(&l1_client); 62 | let sub_task = tokio::task::spawn(async move { 63 | let tx = TransactionPool::construct_proposal_tx( 64 | proposal, 65 | Arc::clone(&l1_client), 66 | &private_key, 67 | ) 68 | .await?; 69 | let receipt = TransactionPool::send_transaction( 70 | Arc::clone(&l1_client), 71 | &private_key, 72 | tx, 73 | ) 74 | .await?; 75 | tracing::info!(target: "varro=pool", "Sent transaction: {:?}", receipt); 76 | Ok(()) 77 | }); 78 | handles.lock().await.push(sub_task); 79 | } 80 | Ok(()) 81 | }); 82 | self.core_task = Some(main_handle); 83 | 84 | Ok(()) 85 | } 86 | 87 | /// Constructs a [TypedTransaction] for a given [OutputResponse] proposal. 88 | /// 89 | /// Returns an [eyre::Result] containing the constructed [TypedTransaction]. 90 | pub async fn construct_proposal_tx( 91 | proposal: OutputResponse, 92 | l1_client: Arc>, 93 | private_key: &H256, 94 | ) -> Result { 95 | tracing::debug!(target: "varro=pool", "Constructing proposal: {:?}", proposal); 96 | 97 | let unwrapped_client = l1_client.lock().await; 98 | let signing_key = SigningKey::from_bytes(&private_key.0)?; 99 | let wallet = LocalWallet::from(signing_key); 100 | let client = SignerMiddleware::new(unwrapped_client.to_owned(), wallet); 101 | let client = Arc::new(client); 102 | 103 | // TODO: get the output oracle address from the config (pass it into this function) 104 | let output_oracle_address = Address::zero(); 105 | let output_oracle_contract = OutputOracleContract::new(output_oracle_address, client); 106 | 107 | let tx = output_oracle_contract.propose_l2_output( 108 | H256::from_slice(proposal.output_root.as_slice()).into(), 109 | U256::from(proposal.block_ref.number), 110 | // TODO: `current_l1` should contain the l1 hash and the l1 number 111 | // TODO: this should be the current l1 hash 112 | // proposal.sync_status.current_l1, 113 | H256::zero().into(), 114 | U256::from(proposal.sync_status.current_l1), 115 | ); 116 | 117 | Ok(tx.tx) 118 | } 119 | 120 | /// Sends a [TypedTransaction] to the L1 network through an [L1Client]. 121 | /// 122 | /// Returns an [eyre::Result] containing the [TransactionReceipt] for the sent transaction. 123 | pub async fn send_transaction( 124 | l1_client: Arc>, 125 | private_key: &H256, 126 | tx: TypedTransaction, 127 | ) -> Result> { 128 | tracing::debug!(target: "varro=pool", "Sending typed transaction: {:?}", tx); 129 | 130 | let unwrapped_client = l1_client.lock().await; 131 | let signing_key = SigningKey::from_bytes(&private_key.0)?; 132 | let wallet = LocalWallet::from(signing_key); 133 | let client = SignerMiddleware::new(unwrapped_client.to_owned(), wallet); 134 | let client = Arc::new(client); 135 | 136 | let receipt = client.send_transaction(tx, None).await?; 137 | let receipt = receipt.confirmations(1).await?; 138 | Ok(receipt) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/proposals.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::time::Duration; 3 | 4 | use eyre::Result; 5 | use tokio::task::JoinHandle; 6 | use tokio_stream::StreamExt; 7 | use tokio_stream::wrappers::IntervalStream; 8 | 9 | /// ProposalManager 10 | /// 11 | /// Queries for [OutputResponse] proposals on a periodic basis, 12 | /// sending them back to a receiver channel. 13 | #[derive(Debug)] 14 | pub struct ProposalManager { 15 | /// The interval at which to query for new proposals. 16 | polling_interval: Duration, 17 | /// 18 | } 19 | 20 | impl ProposalManager { 21 | pub fn new() -> Self { 22 | Self { 23 | polling_interval: Duration::from_secs(5), 24 | } 25 | } 26 | 27 | pub async fn start(&self) -> JoinHandle> { 28 | // TODO: pull variables out of state 29 | let interval = self.polling_interval; 30 | 31 | tokio::spawn(async move { 32 | tracing::info!(target: "varro=proposals", "starting proposer with polling interval: {:?}", interval); 33 | let mut stream = IntervalStream::new(tokio::time::interval(interval)); 34 | while let Some(ts) = stream.next().await { 35 | tracing::debug!(target: "varro=proposals", "polling interval triggered at {:?}", ts); 36 | tracing::debug!(target: "varr=proposals", "spawning proposal task in new thread for chain ID: {}", l1_chain_id.as_u64()); 37 | let _handle = ProposalManager::spawn_proposal_task( 38 | // l1_chain_id.as_u64(), 39 | allow_non_finalized, 40 | Arc::clone(&rollup_node), 41 | l1_client, 42 | output_oracle_address, 43 | proposal_sender.clone(), 44 | ); 45 | } 46 | Ok(()) 47 | }) 48 | } 49 | 50 | /// Spawns a new proposal task 51 | pub fn spawn_proposal_task( 52 | allow_non_finalized: bool, 53 | rollup_node: Arc>, 54 | l1_client: Arc>, 55 | output_oracle_address: Address, 56 | proposal_sender: UnboundedSender, 57 | ) -> JoinHandle> { 58 | // Spawn a new task to create the next proposal 59 | let l1_client = l1_client.clone(); 60 | tokio::task::spawn(async move { 61 | let next_block_number = { 62 | let locked = l1_client.lock().await.to_owned().clone(); 63 | let output_oracle: OutputOracleContract = OutputOracleContract::new( 64 | output_oracle_address, 65 | locked.into(), 66 | ); 67 | // Get the next block number to use from the output oracle contract 68 | let next_block_number: U256 = output_oracle.next_block_number().await?.clone(); 69 | next_block_number 70 | }; 71 | // let locked_output_oracle = cloned_output_oracle.lock().map_err(|_| eyre::eyre!("Failed to lock output oracle"))?; 72 | // let pinned: OutputOracleContract = locked_output_oracle.to_owned(); 73 | // pinned 74 | // let next_block_number = { 75 | // }; 76 | 77 | // Get the rollup node's sync status 78 | let sync_status = { 79 | let locked_rollup_node = rollup_node.lock().await; 80 | locked_rollup_node.sync_status().await? 81 | }; 82 | 83 | // Figure out which block number to use 84 | let block_number = Varro::get_block_number(allow_non_finalized, sync_status); 85 | 86 | // We should not be submitting a block in the future 87 | if block_number < next_block_number.as_u64() { 88 | tracing::info!(target: "varro=client", "proposer submission interval has not elapsed, current block number: {}, next block number: {}", block_number, next_block_number); 89 | return Ok(()) 90 | } 91 | 92 | // Get the rollup node output at the given block number 93 | let output = { 94 | let locked_rollup_node = rollup_node.lock().await; 95 | locked_rollup_node 96 | .output_at_block(next_block_number.as_u64()) 97 | .await? 98 | }; 99 | 100 | // Validate the output version 101 | if SupportedOutputVersion::V1.equals(&output.version) { 102 | tracing::warn!(target: "varro=client", "output version is not not supported, skipping proposal on L2 block {}", output.block_ref.number); 103 | return Ok(()) 104 | } 105 | 106 | // Validate that the block number is correct 107 | if output.block_ref.number != next_block_number.as_u64() { 108 | tracing::warn!(target: "varro=client", "output block number does not match expected block number, skipping proposal on L2 block {}", output.block_ref.number); 109 | return Ok(()) 110 | } 111 | 112 | // Only allow proposals for finalized blocks and safe, if allowed 113 | let is_finalized = output.block_ref.number <= output.sync_status.finalized_l2; 114 | let is_safe_and_accepted = allow_non_finalized 115 | && output.block_ref.number <= output.sync_status.safe_l2; 116 | if !is_finalized && !is_safe_and_accepted { 117 | tracing::warn!(target: "varro=client", "output block is not finalized or safe, skipping proposal on L2 block {}", output.block_ref.number); 118 | return Ok(()) 119 | } 120 | 121 | // Reaching this point means we can bubble up the valid proposal through the channel 122 | Ok(proposal_sender.send(output)?) 123 | }) 124 | } 125 | } -------------------------------------------------------------------------------- /src/rollup.rs: -------------------------------------------------------------------------------- 1 | //! Rollup 2 | //! 3 | //! Encapsulates logic for interacting with a rollup node. 4 | 5 | use ethers_core::types::{ 6 | BlockId, 7 | H256, 8 | }; 9 | use ethers_providers::{ 10 | Http, 11 | Provider, 12 | }; 13 | use eyre::Result; 14 | use serde::{ 15 | Deserialize, 16 | Serialize, 17 | }; 18 | 19 | use crate::errors::RollupNodeError; 20 | 21 | /// A Rollup Node 22 | #[derive(Debug, Clone, Default)] 23 | pub struct RollupNode { 24 | /// The rollup node's URL. 25 | pub client: Option>, 26 | } 27 | 28 | impl TryFrom for RollupNode { 29 | type Error = RollupNodeError; 30 | 31 | fn try_from(url: String) -> Result { 32 | Self::new(url.as_ref()).map_err(|_| Self::Error::RollupNodeInvalidUrl(url)) 33 | } 34 | } 35 | 36 | impl RollupNode { 37 | /// Creates a new rollup node. 38 | pub fn new(l2_url: &str) -> Result { 39 | let client = Provider::::try_from(l2_url)?; 40 | Ok(Self { 41 | client: Some(client), 42 | }) 43 | } 44 | 45 | /// Fetches the output of the rollup node as a [`OutputResponse`]. 46 | pub async fn output_at_block(&self, block_num: u64) -> Result { 47 | let output = self 48 | .client 49 | .as_ref() 50 | .unwrap() 51 | .request("optimism_outputAtBlock", vec![block_num]) 52 | .await?; 53 | Ok(output) 54 | } 55 | 56 | /// Fetches the sync status of the rollup node as a [`SyncStatus`]. 57 | /// 58 | /// This should be called synchronously with the driver event loop 59 | /// to avoid retrieval of an inconsistent status. 60 | pub async fn sync_status(&self) -> Result { 61 | let empty_params: Vec = Vec::new(); 62 | let sync_status = self 63 | .client 64 | .as_ref() 65 | .unwrap() 66 | .request("optimism_syncStatus", empty_params) 67 | .await?; 68 | Ok(sync_status) 69 | } 70 | 71 | /// Fetches the rollup-node's config as a [`serde_json::Value`]. 72 | pub async fn rollup_config(&self) -> Result { 73 | let empty_params: Vec = Vec::new(); 74 | let config = self 75 | .client 76 | .as_ref() 77 | .unwrap() 78 | .request("optimism_rollupConfig", empty_params) 79 | .await?; 80 | Ok(config) 81 | } 82 | 83 | /// Fetches the rollup-node's version as a [`String`]. 84 | pub async fn version(&self) -> Result { 85 | let empty_params: Vec = Vec::new(); 86 | let version = self 87 | .client 88 | .as_ref() 89 | .unwrap() 90 | .request("optimism_version", empty_params) 91 | .await?; 92 | Ok(version) 93 | } 94 | } 95 | 96 | /// The current sync status of a rollup node. 97 | #[derive( 98 | Debug, 99 | Clone, 100 | Serialize, 101 | Deserialize, 102 | Default, 103 | Copy, 104 | PartialEq, 105 | Eq, 106 | PartialOrd, 107 | Ord, 108 | Hash, 109 | )] 110 | pub struct SyncStatus { 111 | /// The current L1 block number. 112 | pub current_l1: u64, 113 | /// The current L1 finalized block number. 114 | pub current_l1_finalized: u64, 115 | /// The current L1 head block number. 116 | pub head_l1: u64, 117 | /// The current L1 safe block number. 118 | pub safe_l1: u64, 119 | /// The current L1 finalized block number. 120 | pub finalized_l1: u64, 121 | /// The current L2 head block number. 122 | pub unsafe_l2: u64, 123 | /// The current L2 safe block number. 124 | pub safe_l2: u64, 125 | /// The current L2 finalized block number. 126 | pub finalized_l2: u64, 127 | } 128 | 129 | /// The rollup node's OutputResponse. 130 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 131 | pub struct OutputResponse { 132 | /// The output response version 133 | pub version: Vec, 134 | /// The output response output root 135 | pub output_root: Vec, 136 | /// The output response block ref 137 | pub block_ref: L2BlockRef, 138 | /// The output response withdrawal storage root 139 | pub withdrawal_storage_root: H256, 140 | /// The output response state root 141 | pub state_root: H256, 142 | /// The output response sync status 143 | pub sync_status: SyncStatus, 144 | } 145 | 146 | /// The rollup node's L2BlockRef. 147 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 148 | pub struct L2BlockRef { 149 | /// The L2 block ref hash 150 | pub hash: H256, 151 | /// The L2 block ref number 152 | pub number: u64, 153 | /// The L2 block ref parent hash 154 | pub parent_hash: H256, 155 | /// The L2 block ref time 156 | pub time: u64, 157 | /// The L2 block ref L1 origin 158 | #[serde(rename = "l1origin")] 159 | pub l1_origin: BlockId, 160 | /// The L2 block ref sequence number 161 | pub sequence_number: u64, 162 | } 163 | -------------------------------------------------------------------------------- /src/telemetry.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs, missing_debug_implementations)] 2 | 3 | //! Telemetry module 4 | //! 5 | //! This module encompasses telemetry for `varro`. 6 | //! Core components are described below. 7 | //! 8 | //! ### Logging 9 | //! 10 | //! Logging is constructed using the [tracing](https://crates.io/crates/tracing) crate. 11 | //! The `tracing` crate is a framework for instrumenting Rust programs to collect 12 | //! structured, event-based diagnostic information. You can use the [logging::init] function 13 | //! to initialize a global logger, passing in a boolean `verbose` parameter. This function 14 | //! will return an error if a logger has already been initialized. 15 | //! 16 | //! ### Shutdown 17 | //! 18 | //! The shutdown module provides a [shutdown::register_shutdown] function which will 19 | //! register a ctrl-c handler to gracefully shutdown the running thread. 20 | 21 | use std::{ 22 | env::current_dir, 23 | path::Path, 24 | }; 25 | 26 | use eyre::Result; 27 | use tracing::{ 28 | subscriber::set_global_default, 29 | Level, 30 | Subscriber, 31 | }; 32 | use tracing_log::LogTracer; 33 | use tracing_subscriber::{ 34 | layer::SubscriberExt, 35 | EnvFilter, 36 | Layer, 37 | Registry, 38 | }; 39 | 40 | use ansi_term::Colour::{ 41 | Blue, 42 | Cyan, 43 | Purple, 44 | Red, 45 | Yellow, 46 | }; 47 | 48 | /// Registers a ctrl-c handler to gracefully shutdown the driver 49 | pub fn register_shutdown() { 50 | ctrlc::set_handler(move || { 51 | println!(); 52 | tracing::info!(target: "varro", "shutting down..."); 53 | std::process::exit(0); 54 | }) 55 | .expect("failed to register shutdown handler"); 56 | } 57 | 58 | /// Configure logging telemetry 59 | pub fn init(verbose: bool) -> Result<()> { 60 | let subscriber = match verbose { 61 | true => get_subscriber("varro=debug".into()), 62 | false => get_subscriber("varro=info".into()), 63 | }; 64 | init_subscriber(subscriber) 65 | } 66 | 67 | /// Subscriber Composer 68 | /// 69 | /// Builds a subscriber with multiple layers into a [tracing](https://crates.io/crates/tracing) subscriber. 70 | pub fn get_subscriber(env_filter: String) -> impl Subscriber + Sync + Send { 71 | let env_filter = 72 | EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter)); 73 | let formatting_layer = AsniTermLayer; 74 | Registry::default().with(env_filter).with(formatting_layer) 75 | } 76 | 77 | /// Globally registers a subscriber. 78 | /// This will error if a subscriber has already been registered. 79 | pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) -> Result<()> { 80 | LogTracer::init().map_err(|_| eyre::eyre!("Failed to set logger"))?; 81 | set_global_default(subscriber).map_err(|_| eyre::eyre!("Failed to set subscriber")) 82 | } 83 | 84 | /// The AnsiVisitor 85 | #[derive(Debug)] 86 | pub struct AnsiVisitor; 87 | 88 | impl tracing::field::Visit for AnsiVisitor { 89 | fn record_f64(&mut self, _: &tracing::field::Field, value: f64) { 90 | println!("{value}") 91 | } 92 | 93 | fn record_i64(&mut self, _: &tracing::field::Field, value: i64) { 94 | println!("{value}") 95 | } 96 | 97 | fn record_u64(&mut self, _: &tracing::field::Field, value: u64) { 98 | println!("{value}") 99 | } 100 | 101 | fn record_bool(&mut self, _: &tracing::field::Field, value: bool) { 102 | println!("{value}") 103 | } 104 | 105 | fn record_str(&mut self, _: &tracing::field::Field, value: &str) { 106 | println!("{value}") 107 | } 108 | 109 | fn record_error( 110 | &mut self, 111 | _: &tracing::field::Field, 112 | value: &(dyn std::error::Error + 'static), 113 | ) { 114 | println!("{value}") 115 | } 116 | 117 | fn record_debug(&mut self, _: &tracing::field::Field, value: &dyn std::fmt::Debug) { 118 | println!("{value:?}") 119 | } 120 | } 121 | 122 | /// An Ansi Term layer for tracing 123 | #[derive(Debug)] 124 | pub struct AsniTermLayer; 125 | 126 | impl Layer for AsniTermLayer 127 | where 128 | S: tracing::Subscriber, 129 | { 130 | fn on_event( 131 | &self, 132 | event: &tracing::Event<'_>, 133 | _ctx: tracing_subscriber::layer::Context<'_, S>, 134 | ) { 135 | // Print the timestamp 136 | let utc: chrono::DateTime = chrono::Utc::now(); 137 | print!("[{}] ", Cyan.paint(utc.to_rfc2822())); 138 | 139 | // Print the level prefix 140 | match *event.metadata().level() { 141 | Level::ERROR => { 142 | eprint!("{}: ", Red.paint("ERROR")); 143 | } 144 | Level::WARN => { 145 | print!("{}: ", Yellow.paint("WARN")); 146 | } 147 | Level::INFO => { 148 | print!("{}: ", Blue.paint("INFO")); 149 | } 150 | Level::DEBUG => { 151 | print!("DEBUG: "); 152 | } 153 | Level::TRACE => { 154 | print!("{}: ", Purple.paint("TRACE")); 155 | } 156 | } 157 | 158 | print!("{} ", Purple.paint(event.metadata().target())); 159 | 160 | let original_location = event 161 | .metadata() 162 | .name() 163 | .split(' ') 164 | .last() 165 | .unwrap_or_default(); 166 | let relative_path = current_dir() 167 | .unwrap_or_default() 168 | .to_string_lossy() 169 | .to_string(); 170 | 171 | // Remove common prefixes from the location and relative path 172 | let location_path = std::path::Path::new(original_location); 173 | let relative_path_path = std::path::Path::new(&relative_path); 174 | let common_prefix = location_path 175 | .ancestors() 176 | .collect::>() 177 | .iter() 178 | .cloned() 179 | .rev() 180 | .zip( 181 | relative_path_path 182 | .ancestors() 183 | .collect::>() 184 | .iter() 185 | .cloned() 186 | .rev(), 187 | ) 188 | .take_while(|(a, b)| a == b) 189 | .last() 190 | .map(|(a, _)| a) 191 | .unwrap_or_else(|| std::path::Path::new("")); 192 | let location = location_path 193 | .strip_prefix(common_prefix) 194 | .unwrap_or(location_path) 195 | .to_str() 196 | .unwrap_or(original_location); 197 | let location = location.strip_prefix('/').unwrap_or(location); 198 | print!("at {} ", Cyan.paint(location.to_string())); 199 | 200 | let mut visitor = AnsiVisitor; 201 | event.record(&mut visitor); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tests/l1_client.rs: -------------------------------------------------------------------------------- 1 | use ethers_core::types::{ 2 | BlockId, 3 | BlockNumber, 4 | }; 5 | use ethers_providers::{ 6 | Http, 7 | Middleware, 8 | Provider, 9 | }; 10 | 11 | /// Requires the following environment variables to be set: 12 | /// - L1_RPC_URL 13 | #[tokio::test] 14 | async fn test_l1_client() { 15 | let l1_rpc_url = std::env::var("L1_RPC_URL").unwrap(); 16 | let provider = Provider::::try_from(l1_rpc_url).unwrap(); 17 | println!("Constructed provider: {:?}", provider); 18 | let l1_tip = provider 19 | .get_block(BlockId::Number(BlockNumber::Latest)) 20 | .await 21 | .unwrap(); 22 | println!("L1 tip: {:?}", l1_tip); 23 | } 24 | -------------------------------------------------------------------------------- /varrup/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.4" 3 | x-logging: &logging 4 | logging: 5 | driver: json-file 6 | options: 7 | max-size: 10m 8 | max-file: "3" 9 | 10 | services: 11 | op-batcher: 12 | stop_grace_period: 3m 13 | restart: unless-stopped 14 | entrypoint: /scripts/start-varro.sh 15 | env_file: 16 | - ./varro-env.env 17 | - .env 18 | ports: 19 | - "6061:6060" 20 | - "7301:7300" 21 | - "6545:8545" 22 | volumes: 23 | - ./:/scripts 24 | <<: *logging 25 | 26 | volumes: 27 | scripts: 28 | -------------------------------------------------------------------------------- /varrup/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | REPO="refcell/varro" 5 | NAME=varr 6 | 7 | INSTALLER_NAME=${NAME}up 8 | 9 | DIR=$HOME/.$NAME 10 | BIN_DIR=$DIR/bin 11 | 12 | BIN_URL="https://raw.githubusercontent.com/$REPO/master/$INSTALLER_NAME/$INSTALLER_NAME" 13 | BIN_PATH="$BIN_DIR/$INSTALLER_NAME" 14 | 15 | mkdir -p $BIN_DIR 16 | curl -# -L $BIN_URL -o $BIN_PATH 17 | chmod +x $BIN_PATH 18 | 19 | case $SHELL in 20 | */zsh) 21 | PROFILE=$HOME/.zshrc 22 | PREF_SHELL=zsh 23 | ;; 24 | */bash) 25 | PROFILE=$HOME/.bashrc 26 | PREF_SHELL=bash 27 | ;; 28 | */fish) 29 | PROFILE=$HOME/.config/fish/config.fish 30 | PREF_SHELL=fish 31 | ;; 32 | *) 33 | echo "$INSTALLER_NAME: could not detect shell, manually add ${BIN_DIR} to your PATH." 34 | exit 1 35 | esac 36 | 37 | if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then 38 | echo >> $PROFILE && echo "export PATH=\"\$PATH:$BIN_DIR\"" >> $PROFILE 39 | fi 40 | 41 | echo && echo "Detected your preferred shell is ${PREF_SHELL} and added ${INSTALLER_NAME} to PATH. Run 'source ${PROFILE}' or start a new terminal session to use $INSTALLER_NAME." 42 | echo "Then, simply run '$INSTALLER_NAME' to install $NAME." 43 | -------------------------------------------------------------------------------- /varrup/start-varro.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | varro \ 4 | # TODO: parameterize 5 | # --proposer-address "0x87A159604e2f18B01a080F672ee011F39777E640" \ 6 | # --batcher-inbox "0xff00000000000000000000000000000000042069" \ 7 | # --l2-client-rpc-url $L2_RPC_URL \ 8 | # --l1-client-rpc-url $L1_RPC_URL \ 9 | # --data-availability-layer mainnet \ 10 | # --network optimism \ 11 | # --polling-interval 5 \ 12 | # --sequencer-private-key "0xa0bba68a40ddd0b573c344de2e7dd597af69b3d90e30a87ec91fa0547ddb6ab8" \ 13 | # --proposer-private-key "0x4a6e5ceb37cd67ed8e740cc25b0ee6d11f6cfabe366daad1c908dec1d178bc72" \ 14 | # --batcher-address "0x87A159604e2f18B01a080F672ee011F39777E640" \ 15 | # --sequencer-address "0xf4031e0983177452c9e7F27f46ff6bB9CA5933E1" \ 16 | # --batcher-private-key "0x4a6e5ceb37cd67ed8e740cc25b0ee6d11f6cfabe366daad1c908dec1d178bc72" 17 | -------------------------------------------------------------------------------- /varrup/varro-env.env: -------------------------------------------------------------------------------- 1 | OP_BATCHER_L1_ETH_RPC: http://l1:8545 2 | OP_BATCHER_L2_ETH_RPC: http://l2:8545 3 | OP_BATCHER_ROLLUP_RPC: http://op-node:8545 4 | OP_BATCHER_MAX_CHANNEL_DURATION: 1 5 | OP_BATCHER_MAX_L1_TX_SIZE_BYTES: 120000 6 | OP_BATCHER_TARGET_L1_TX_SIZE_BYTES: 100000 7 | OP_BATCHER_TARGET_NUM_FRAMES: 1 8 | OP_BATCHER_APPROX_COMPR_RATIO: 0.4 9 | OP_BATCHER_SUB_SAFETY_MARGIN: 4 # SWS is 15, ChannelTimeout is 40 10 | OP_BATCHER_POLL_INTERVAL: 1s 11 | OP_BATCHER_NUM_CONFIRMATIONS: 1 12 | OP_BATCHER_SAFE_ABORT_NONCE_TOO_LOW_COUNT: 3 13 | OP_BATCHER_RESUBMISSION_TIMEOUT: 30s 14 | OP_BATCHER_MNEMONIC: test test test test test test test test test test test junk 15 | OP_BATCHER_SEQUENCER_HD_PATH: "m/44'/60'/0'/0/2" 16 | OP_BATCHER_PPROF_ENABLED: "true" 17 | OP_BATCHER_METRICS_ENABLED: "true" 18 | OP_BATCHER_RPC_ENABLE_ADMIN: "true" -------------------------------------------------------------------------------- /varrup/varrup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | REPO="refcell/varro" 5 | NAME=varr 6 | 7 | DIR=$HOME/.$NAME 8 | BIN_DIR=$DIR/bin 9 | 10 | rm -f $BIN_DIR/$NAME 11 | 12 | TAG=$(curl https://api.github.com/repos/$REPO/releases/latest | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$') 13 | 14 | PLATFORM="$(uname -s)" 15 | case $PLATFORM in 16 | Linux) 17 | PLATFORM="linux" 18 | ;; 19 | Darwin) 20 | PLATFORM="darwin" 21 | ;; 22 | *) 23 | err "unsupported platform: $PLATFORM" 24 | ;; 25 | esac 26 | 27 | ARCHITECTURE="$(uname -m)" 28 | if [ "${ARCHITECTURE}" = "x86_64" ]; then 29 | # Redirect stderr to /dev/null to avoid printing errors if non Rosetta. 30 | if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then 31 | ARCHITECTURE="arm64" # Rosetta. 32 | else 33 | ARCHITECTURE="amd64" # Intel. 34 | fi 35 | elif [ "${ARCHITECTURE}" = "arm64" ] ||[ "${ARCHITECTURE}" = "aarch64" ] ; then 36 | ARCHITECTURE="arm64" # Arm. 37 | else 38 | ARCHITECTURE="amd64" # Amd. 39 | fi 40 | 41 | TARBALL_URL="https://github.com/$REPO/releases/download/${TAG}/${NAME}_${PLATFORM}_${ARCHITECTURE}.tar.gz" 42 | curl -L $TARBALL_URL | tar -xzC $BIN_DIR 43 | 44 | echo "Installed $NAME" 45 | --------------------------------------------------------------------------------