├── .env.example ├── .github └── workflows │ └── pr.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── client-executor │ ├── Cargo.toml │ └── src │ │ ├── anchor.rs │ │ ├── errors.rs │ │ ├── io.rs │ │ └── lib.rs └── host-executor │ ├── Cargo.toml │ ├── src │ ├── anchor_builder.rs │ ├── beacon │ │ ├── client.rs │ │ ├── mod.rs │ │ └── signed_beacon_block.rs │ ├── errors.rs │ ├── events.rs │ ├── lib.rs │ ├── sketch.rs │ ├── sketch_builder.rs │ └── test.rs │ └── tests │ └── anchor.rs ├── examples ├── events │ ├── client │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── main.rs │ └── host │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src │ │ └── main.rs ├── example-deploy │ └── host │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs ├── multiplexer │ ├── ZkOracleHelper.sol │ ├── client │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── host │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src │ │ └── main.rs ├── uniswap │ ├── client │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── contracts │ │ ├── .gitignore │ │ ├── README.md │ │ ├── foundry.toml │ │ ├── remappings.txt │ │ ├── src │ │ │ ├── UniswapCall.sol │ │ │ └── fixtures │ │ │ │ └── plonk-fixture.json │ │ └── test │ │ │ └── UniswapCall.t.sol │ └── host │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src │ │ ├── basic.rs │ │ └── onchain_verify.rs └── verify-quorum │ ├── SimpleStaking.sol │ ├── client │ ├── Cargo.toml │ └── src │ │ └── main.rs │ └── host │ ├── Cargo.toml │ ├── build.rs │ └── src │ └── main.rs ├── rust-toolchain └── rustfmt.toml /.env.example: -------------------------------------------------------------------------------- 1 | ETH_RPC_URL= 2 | ETH_SEPOLIA_RPC_URL= 3 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | examples: 14 | name: Test ${{ matrix.example }} example 15 | runs-on: 16 | [ 17 | runs-on, 18 | cpu=64, 19 | ram=256, 20 | family=m7i+m7a, 21 | hdd=80, 22 | image=ubuntu22-full-x64, 23 | spot=false, 24 | "run-id=${{ github.run_id }}", 25 | ] 26 | strategy: 27 | matrix: 28 | example: 29 | - uniswap-basic 30 | - multiplexer 31 | - verify-quorum 32 | - example-deploy 33 | env: 34 | CARGO_NET_GIT_FETCH_WITH_CLI: "true" 35 | steps: 36 | - name: Checkout sources 37 | uses: actions/checkout@v4 38 | 39 | - name: "Install sp1up" 40 | run: | 41 | curl -L https://sp1.succinct.xyz | bash 42 | echo "$HOME/.sp1/bin" >> $GITHUB_PATH 43 | 44 | - name: "Install SP1 toolchain" 45 | run: | 46 | sp1up 47 | 48 | - name: "Set up RPC env" 49 | run: | 50 | echo "ETH_RPC_URL=${{secrets.ETH_RPC_URL}}" >> $GITHUB_ENV 51 | echo "BEACON_RPC_URL=${{secrets.BEACON_RPC_URL}}" >> $GITHUB_ENV 52 | echo "ETH_SEPOLIA_RPC_URL=${{secrets.ETH_SEPOLIA_RPC_URL}}" >> $GITHUB_ENV 53 | 54 | - name: Run ${{ matrix.example }} 55 | uses: actions-rs/cargo@v1 56 | with: 57 | command: run 58 | args: 59 | --release --bin ${{ matrix.example }} --ignore-rust-version 60 | env: 61 | RUSTFLAGS: -Copt-level=3 -Coverflow-checks=y -Cdebuginfo=0 -C target-cpu=native 62 | RUST_BACKTRACE: full 63 | RUST_LOG: info 64 | 65 | test-e2e: 66 | name: E2E tests 67 | runs-on: ["runs-on", "runner=32cpu-linux-x64", "run-id=${{ github.run_id }}"] 68 | env: 69 | CARGO_NET_GIT_FETCH_WITH_CLI: "true" 70 | steps: 71 | - name: "Checkout sources" 72 | uses: actions/checkout@v4 73 | 74 | - name: "Set up RPC env" 75 | run: | 76 | echo "ETH_RPC_URL=${{secrets.ETH_RPC_URL}}" >> $GITHUB_ENV 77 | echo "BEACON_RPC_URL=${{secrets.BEACON_RPC_URL}}" >> $GITHUB_ENV 78 | echo "ETH_SEPOLIA_RPC_URL=${{secrets.ETH_SEPOLIA_RPC_URL}}" >> $GITHUB_ENV 79 | 80 | - name: "Run integration test" 81 | run: | 82 | SP1_DEV=1 RUST_LOG=info cargo test -p sp1-cc-host-executor --release -- --nocapture 83 | 84 | test-uniswap-forge: 85 | name: Test uniswap forge 86 | runs-on: ubuntu-latest 87 | steps: 88 | - uses: actions/checkout@v4 89 | with: 90 | submodules: recursive 91 | 92 | - name: Install Foundry 93 | uses: foundry-rs/foundry-toolchain@v1 94 | with: 95 | version: nightly 96 | 97 | - name: Run Forge build 98 | run: | 99 | cd examples/uniswap/contracts 100 | forge --version 101 | forge build --sizes 102 | id: build 103 | 104 | - name: Run Forge tests 105 | run: | 106 | cd examples/uniswap/contracts 107 | forge test -vvv 108 | id: test 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo build 2 | **/target 3 | 4 | # Cargo config 5 | .cargo 6 | 7 | # Profile-guided optimization 8 | /tmp 9 | pgo-data.profdata 10 | 11 | # MacOS nuisances 12 | .DS_Store 13 | 14 | # Proofs 15 | **/proof-with-pis.bin 16 | **/proof-with-io.bin 17 | 18 | # Env 19 | .env 20 | 21 | # SP1 ELF files 22 | **/riscv32im-succinct-zkvm-elf 23 | 24 | **.csv 25 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/uniswap/contracts/lib/forge-std"] 2 | path = examples/uniswap/contracts/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "examples/uniswap/contracts/lib/sp1-contracts"] 5 | path = examples/uniswap/contracts/lib/sp1-contracts 6 | url = https://github.com/succinctlabs/sp1-contracts 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.inlineSuggest.enabled": true, 3 | "[rust]": { 4 | "editor.defaultFormatter": "rust-lang.rust-analyzer", 5 | "editor.formatOnSave": true, 6 | "editor.hover.enabled": true 7 | }, 8 | "editor.rulers": [ 9 | 100 10 | ], 11 | "rust-analyzer.check.overrideCommand": [ 12 | "cargo", 13 | "clippy", 14 | "--workspace", 15 | "--message-format=json", 16 | "--all-features", 17 | "--all-targets", 18 | "--", 19 | "-A", 20 | "incomplete-features" 21 | ], 22 | "rust-analyzer.linkedProjects": [ 23 | "Cargo.toml", 24 | ], 25 | "rust-analyzer.showUnlinkedFileNotification": false, 26 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "examples/example-deploy/host", 4 | "examples/uniswap/host", 5 | "examples/uniswap/client", 6 | "examples/multiplexer/host", 7 | "examples/multiplexer/client", 8 | "examples/events/host", 9 | "examples/events/client", 10 | "examples/verify-quorum/host", 11 | "examples/verify-quorum/client", 12 | "crates/client-executor", 13 | "crates/host-executor", 14 | ] 15 | exclude = [] 16 | resolver = "2" 17 | 18 | [workspace.package] 19 | edition = "2021" 20 | license = "MIT OR Apache-2.0" 21 | authors = ["yuwen01"] 22 | exclude = ["**/target"] 23 | version = "0.1.0" 24 | 25 | [workspace.dependencies] 26 | eyre = "0.6" 27 | tracing = { version = "0.1.40", default-features = false } 28 | tokio = { version = "1.21", default-features = false, features = [ 29 | "rt", 30 | "rt-multi-thread", 31 | ] } 32 | serde_json = "1.0.94" 33 | serde = { version = "1.0", default-features = false, features = ["derive"] } 34 | reqwest = "0.12.15" 35 | url = "2.3" 36 | hex-literal = "0.4.1" 37 | bincode = "1.3.3" 38 | dotenv = "0.15.0" 39 | thiserror = "2.0.12" 40 | 41 | # workspace 42 | sp1-cc-client-executor = {path = "./crates/client-executor"} 43 | sp1-cc-host-executor = {path = "./crates/host-executor"} 44 | 45 | # sp1 46 | sp1-sdk = "5.0.0" 47 | sp1-zkvm = "5.0.0" 48 | sp1-build = "5.0.0" 49 | 50 | # rsp 51 | rsp-rpc-db = { git = "https://github.com/succinctlabs/rsp", rev = "34f9ee4898e6727ddc9ce09fea0674bac7aefe8c" } 52 | rsp-witness-db = { git = "https://github.com/succinctlabs/rsp", rev = "34f9ee4898e6727ddc9ce09fea0674bac7aefe8c" } 53 | rsp-primitives = { git = "https://github.com/succinctlabs/rsp", rev = "34f9ee4898e6727ddc9ce09fea0674bac7aefe8c" } 54 | rsp-client-executor = { git = "https://github.com/succinctlabs/rsp", rev = "34f9ee4898e6727ddc9ce09fea0674bac7aefe8c" } 55 | rsp-mpt = { git = "https://github.com/succinctlabs/rsp", rev = "34f9ee4898e6727ddc9ce09fea0674bac7aefe8c" } 56 | 57 | # rsp-rpc-db = { path = "../rsp/crates/storage/rpc-db" } 58 | # rsp-witness-db = { path = "../rsp/crates/storage/witness-db" } 59 | # rsp-primitives = { path = "../rsp/crates/primitives"} 60 | # rsp-client-executor = {path = "../rsp/crates/executor/client"} 61 | # rsp-mpt = { path = "../rsp/crates/mpt"} 62 | 63 | # reth 64 | reth-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false, features = [ 65 | "alloy-compat", 66 | "std", 67 | ] } 68 | reth-codecs = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 69 | reth-consensus = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 70 | reth-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 71 | reth-revm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false, features = [ 72 | "std", 73 | ] } 74 | reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false, features = [ 75 | "std", 76 | ] } 77 | reth-storage-errors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false, features = [ 78 | "std", 79 | ] } 80 | reth-trie = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 81 | reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 82 | reth-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 83 | reth-execution-errors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 84 | reth-execution-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 85 | reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 86 | reth-errors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 87 | reth-ethereum-consensus = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.10", default-features = false } 88 | 89 | # revm 90 | revm = { version = "22.0.1", features = [ 91 | "std", 92 | ], default-features = false } 93 | revm-primitives = { version = "18.0.0", features = [ 94 | "std", 95 | ], default-features = false } 96 | 97 | # alloy 98 | alloy-primitives = "1.0" 99 | alloy-consensus = { version = "0.14.0", default-features = false } 100 | alloy-contract = { version = "0.14.0", default-features = false } 101 | alloy-eips = { version = "0.14.0", default-features = false } 102 | alloy-node-bindings = { version = "0.14.0", default-features = false } 103 | alloy-provider = { version = "0.14.0", default-features = false, features = [ 104 | "reqwest", 105 | ] } 106 | alloy-rpc-types = { version = "0.14.0", default-features = false, features = [ 107 | "eth", 108 | ] } 109 | alloy-rpc-types-eth = { version = "0.14.0", default-features = false } 110 | alloy-serde = { version = "0.14.0" } 111 | alloy-transport = { version = "0.14.0" } 112 | 113 | alloy-rlp = "0.3.10" 114 | alloy-trie = "0.8.1" 115 | alloy-sol-types = { version = "1.0" } 116 | alloy-sol-macro = { version = "1.0" } 117 | alloy = { version = "0.14.0" } 118 | 119 | alloy-evm = { version = "0.4.0", default-features = false } 120 | 121 | sha2 = "0.10.8" 122 | beacon-api-client = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "ba43147eb71b07e21e156e2904549405f87bc9a6" } 123 | ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "ba43147eb71b07e21e156e2904549405f87bc9a6" } 124 | async-trait = "0.1.88" 125 | 126 | [workspace.lints] 127 | rust.missing_debug_implementations = "warn" 128 | rust.unreachable_pub = "warn" 129 | rust.unused_must_use = "deny" 130 | rust.rust_2018_idioms = { level = "deny", priority = -1 } 131 | rustdoc.all = "warn" 132 | 133 | [patch.crates-io] 134 | sha2-v0-10-8 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha2", tag = "patch-sha2-0.10.8-sp1-4.0.0" } 135 | sha3-v0-10-8 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha3", tag = "patch-sha3-0.10.8-sp1-4.0.0" } 136 | crypto-bigint = { git = "https://github.com/sp1-patches/RustCrypto-bigint", tag = "patch-0.5.5-sp1-4.0.0" } 137 | tiny-keccak = { git = "https://github.com/sp1-patches/tiny-keccak", tag = "patch-2.0.2-sp1-4.0.0" } 138 | secp256k1 = { git = "https://github.com/sp1-patches/rust-secp256k1", tag = "patch-0.30.0-sp1-5.0.0" } 139 | 140 | # TODO: Remove once https://github.com/ralexstokes/ethereum-consensus/pull/419 is merged. 141 | [patch."https://github.com/ralexstokes/ethereum-consensus"] 142 | ethereum-consensus = { git = "https://github.com/leruaa/ethereum-consensus", branch = "electra-fixes" } 143 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Succinct Labs 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Succinct Labs 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SP1 Contract Calls 2 | 3 | Generates zero-knowledge proofs of Ethereum smart contract execution. 4 | 5 | > [!CAUTION] 6 | > 7 | > This repository is not meant for production usage. 8 | 9 | ## Overview 10 | 11 | This library (`sp1-contract-call`, or `sp1-cc` for short), provides developers with a simple interface to efficiently generate a ZKP of Ethereum smart contract execution offchain, that can be verified cheaply onchain for ~280k gas. This enables developers to verifiably run very expensive Solidity smart contract calls and be able to use this information in their onchain applications. Developers simply specific their Solidity function interface in Rust using the [`alloy_sol_macro`](https://docs.rs/alloy-sol-macro/latest/alloy_sol_macro/) library and can write an SP1 program to generate these proofs. Let's check out an example below: 12 | 13 | ### Client 14 | 15 | First, we create a Rust program that runs the Solidity smart contract call, using the `alloy_sol_macro` interface, the contract address and the caller address. This is known as a "client" program and it is run inside SP1 to generate a ZKP of the smart contract call's execution. 16 | 17 | In this example, we use the `slot0` function to fetch the current price of the UNI/WETH pair on the UniswapV3 pool. Note that we abi encode the `public_values` -- this is to make it easy later to use those public values on chain. The code below is taken from [`examples/uniswap/client/src/main.rs`](./examples/uniswap/client/src/main.rs) which contains all of the code needed for the SP1 client program. 18 | 19 | ```rs 20 | sol! { 21 | /// Simplified interface of the IUniswapV3PoolState interface. 22 | interface IUniswapV3PoolState { 23 | function slot0( 24 | ) external view returns (uint160 sqrtPriceX96, ...); 25 | } 26 | } 27 | 28 | /// Address of Uniswap V3 pool. 29 | const CONTRACT: Address = address!("1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801"); 30 | 31 | ... 32 | 33 | let state_sketch_bytes = sp1_zkvm::io::read::>(); 34 | let state_sketch = bincode::deserialize::(&state_sketch_bytes).unwrap(); 35 | 36 | // Initialize the client executor with the state sketch. 37 | // This step also validates all of the storage against the provided state root. 38 | let executor = ClientExecutor::new(state_sketch).unwrap(); 39 | 40 | // Execute the slot0 call using the client executor. 41 | let slot0_call = IUniswapV3PoolState::slot0Call {}; 42 | let call = ContractInput::new_call(CONTRACT, Address::default(), slot0_call); 43 | let public_vals = executor.execute(call).unwrap(); 44 | 45 | // Commit the abi-encoded output. 46 | sp1_zkvm::io::commit_slice(&public_vals.abi_encode()); 47 | ``` 48 | 49 | ### Host 50 | 51 | Under the hood, the SP1 client program uses the executor from the `sp1-cc-client-executor` library, which requires storage slots and merkle proof information to correctly and verifiably run the smart contract execution. 52 | 53 | The "host" program is code that is run outside of the zkVM & is responsible for fetching all of the witness data that is needed for the client program. This witness data includes storage slots, account information & merkle proofs that the client program verifies. 54 | 55 | You can see in the host example code below that we run the exact same contract call with the host executor (instead of the client executor), and the host executor will fetch all relevant information as its executing. When we call `finalize()` on the host executor, it prepares all of the data it has gathered during contract call execution and then prepares it for input into the client program. 56 | 57 | ```rs 58 | ... 59 | 60 | // Prepare the host executor. 61 | // 62 | // Use `RPC_URL` to get all of the necessary state for the smart contract call. 63 | let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| panic!("Missing RPC_URL")); 64 | let provider = ReqwestProvider::new_http(Url::parse(&rpc_url)?); 65 | let mut host_executor = HostExecutor::new(provider.clone(), block_number).await?; 66 | 67 | // Keep track of the block hash. We'll later validate the client's execution against this. 68 | let block_hash = host_executor.header.hash_slow(); 69 | 70 | // Make the call to the slot0 function. 71 | let slot0_call = IUniswapV3PoolState::slot0Call {}; 72 | let _price_x96_bytes = host_executor 73 | .execute(ContractInput::new_call(CONTRACT, Address::default(), slot0_call)) 74 | .await?; 75 | 76 | // Now that we've executed all of the calls, get the `EVMStateSketch` from the host executor. 77 | let input = host_executor.finalize().await?; 78 | 79 | // Feed the sketch into the client. 80 | let input_bytes = bincode::serialize(&input)?; 81 | let mut stdin = SP1Stdin::new(); 82 | stdin.write(&input_bytes); 83 | 84 | // Now we can call the client program. 85 | 86 | ... 87 | 88 | ``` 89 | 90 | After running the client program in the host, we generate a proof that can easily be verified on chain. In addition, the public values associated with our proof are abi-encoded, which allows us to use the output of the contract call on chain. Here is part of a sample contract that verifies this proof; check out [`examples/uniswap/contracts`](./examples/uniswap/contracts/) for more details. 91 | 92 | ```sol 93 | /// @title SP1 UniswapCall. 94 | /// @notice This contract implements a simple example of verifying the proof of call to a smart 95 | /// contract. 96 | contract UniswapCall { 97 | /// @notice The address of the SP1 verifier contract. 98 | /// @dev This can either be a specific SP1Verifier for a specific version, or the 99 | /// SP1VerifierGateway which can be used to verify proofs for any version of SP1. 100 | /// For the list of supported verifiers on each chain, see: 101 | /// https://github.com/succinctlabs/sp1-contracts/tree/main/contracts/deployments 102 | address public verifier; 103 | 104 | /// @notice The verification key for the uniswapCall program. 105 | bytes32 public uniswapCallProgramVKey; 106 | 107 | constructor(address _verifier, bytes32 _uniswapCallProgramVKey) { 108 | verifier = _verifier; 109 | uniswapCallProgramVKey = _uniswapCallProgramVKey; 110 | } 111 | 112 | /// @notice The entrypoint for verifying the proof of a uniswapCall number. 113 | /// @param _proofBytes The encoded proof. 114 | /// @param _publicValues The encoded public values. 115 | function verifyUniswapCallProof(bytes calldata _publicValues, bytes calldata _proofBytes) 116 | public 117 | view 118 | returns (uint160) 119 | { 120 | ISP1Verifier(verifier).verifyProof(uniswapCallProgramVKey, _publicValues, _proofBytes); 121 | ContractPublicValues memory publicValues = abi.decode(_publicValues, (ContractPublicValues)); 122 | uint160 sqrtPriceX96 = abi.decode(publicValues.contractOutput, (uint160)); 123 | return sqrtPriceX96; 124 | } 125 | } 126 | ``` 127 | ## Running examples 128 | 129 | To use SP1-contract-call, you must first have Rust installed and SP1 installed to build the client programs. In addition, you need to set the `ETH_RPC_URL` and `ETH_SEPOLIA_RPC_URL` environment variables. You can do this manually by running the following: 130 | 131 | ``` 132 | export ETH_RPC_URL=[YOUR ETH RPC URL HERE] 133 | export ETH_SEPOLIA_RPC_URL=[YOUR ETH SEPOLIA RPC URL HERE] 134 | ``` 135 | 136 | Alternatively, you can use a `.env` file (see [example](./.env.example)). 137 | 138 | Then, from the root directory of the repository, run 139 | 140 | ```RUST_LOG=info cargo run --bin [example] --release``` 141 | 142 | where `[example]` is one of the following 143 | * `uniswap-basic` 144 | * Fetches the price of the UNI / WETH pair on Uniswap V3. By default, this does not generate a proof. 145 | * Running `RUST_LOG=info cargo run --bin [example] --release -- --prove` will generate a plonk proof. This requires 146 | significant computational resources, so we recommend using the [SP1 Prover network](https://docs.succinct.xyz/docs/generating-proofs/prover-network). 147 | * Outputs a file called [plonk-fixture.json](examples/uniswap/contracts/src/fixtures/plonk-fixture.json), which contains everything you need to verify the proof on chain. 148 | * `uniswap-onchain-verify` 149 | * Fetches the price of the WETH / USDC pair on Uniswap V3 on Sepolia. 150 | * This example demonstrate on-chain verification, with the following variations: 151 | * By default, the `blockhash()` opcode is used, allowing to verify up to 256 blocks old. 152 | * If you provides a Beacon RPC endpoint with the `--beacon-sepolia-rpc-url` argument, the proof will be verified on chain with the beacon root using [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788), up to 8191 blocks old (~27h). 153 | * The window can even be extended up to the Cancun hardfork by chaining beacon roots (see the `--reference-block` argument). 154 | * The contract can be found at the [contracts](./examples/uniswap/contracts/) directory. 155 | * `multiplexer` 156 | * Calls a contract that fetches the prices of many different collateral assets. 157 | * The source code of this contract is found [here](./examples/multiplexer/ZkOracleHelper.sol). 158 | * Due to the size of this program, it's recommended to use the [SP1 Prover network](https://docs.succinct.xyz/docs/generating-proofs/prover-network) to generate proofs for this example. 159 | * `verify-quorum` 160 | * Calls a contract that verifies several ECDSA signatures on chain, and sums the stake for the addresses corresponding to valid signatures. 161 | * `example-deploy` 162 | * Demonstrates how to simulate a contract creation transaction on SP1-CC. 163 | * `events` 164 | * Demonstrates how to prefetch log events in the host and send them to the client as inputs. 165 | 166 | ## Acknowledgments 167 | 168 | * [Unstable.Money](https://www.unstable.money/): Developed the smart contract featured in the `multiplexer` example. 169 | * [SP1](https://github.com/succinctlabs/sp1): A fast, feature-complete zkVM for developers that can prove the execution of arbitrary Rust (or any LLVM-compiled) program. 170 | 171 | -------------------------------------------------------------------------------- /crates/client-executor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sp1-cc-client-executor" 3 | description = "" 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [dependencies] 12 | eyre.workspace = true 13 | serde.workspace = true 14 | sha2.workspace = true 15 | serde_with = "3.12.0" 16 | thiserror.workspace = true 17 | 18 | # rsp 19 | rsp-witness-db.workspace = true 20 | rsp-primitives.workspace = true 21 | rsp-client-executor.workspace = true 22 | rsp-mpt.workspace = true 23 | 24 | # reth 25 | reth-chainspec.workspace = true 26 | reth-consensus.workspace = true 27 | reth-ethereum-consensus.workspace = true 28 | reth-evm.workspace = true 29 | reth-evm-ethereum.workspace = true 30 | reth-primitives.workspace = true 31 | 32 | # revm 33 | revm.workspace = true 34 | revm-primitives.workspace = true 35 | 36 | # alloy 37 | alloy-consensus = { workspace = true, features = ["serde", "serde-bincode-compat"] } 38 | alloy-eips.workspace = true 39 | alloy-sol-types.workspace = true 40 | alloy-primitives.workspace = true 41 | alloy-rpc-types = { workspace = true } 42 | alloy-rpc-types-eth = { workspace = true, features = ["serde"] } 43 | alloy-evm.workspace = true 44 | alloy-serde.workspace = true 45 | alloy-trie.workspace = true 46 | 47 | [dev-dependencies] 48 | -------------------------------------------------------------------------------- /crates/client-executor/src/anchor.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt::Display}; 2 | 3 | use alloy_consensus::Header; 4 | use alloy_eips::eip4788::BEACON_ROOTS_ADDRESS; 5 | use alloy_primitives::{uint, B256, U256}; 6 | use revm::DatabaseRef; 7 | use rsp_client_executor::io::TrieDB; 8 | use rsp_mpt::EthereumState; 9 | use serde::{Deserialize, Serialize}; 10 | use serde_with::serde_as; 11 | use sha2::{Digest, Sha256}; 12 | 13 | use crate::AnchorType; 14 | 15 | // https://eips.ethereum.org/EIPS/eip-4788 16 | pub const HISTORY_BUFFER_LENGTH: U256 = uint!(8191_U256); 17 | /// The generalized Merkle tree index of the `block_hash` field in the `BeaconBlock`. 18 | const BLOCK_HASH_LEAF_INDEX: usize = 6444; 19 | /// The generalized Merkle tree index of the `state_root` field in the `BeaconBlock`. 20 | const STATE_ROOT_LEAF_INDEX: usize = 6434; 21 | 22 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 23 | pub enum Anchor { 24 | Header(HeaderAnchor), 25 | Eip4788(BeaconWithHeaderAnchor), 26 | ChainedEip4788(ChainedBeaconAnchor), 27 | Consensus(BeaconWithHeaderAnchor), 28 | } 29 | 30 | impl Anchor { 31 | pub fn header(&self) -> &Header { 32 | match self { 33 | Anchor::Header(header_anchor) => &header_anchor.header, 34 | Anchor::Eip4788(beacon_anchor) | Anchor::Consensus(beacon_anchor) => { 35 | &beacon_anchor.inner.header 36 | } 37 | Anchor::ChainedEip4788(chained_anchor) => &chained_anchor.inner.inner.header, 38 | } 39 | } 40 | 41 | pub fn resolve(&self) -> ResolvedAnchor { 42 | match self { 43 | Anchor::Header(header_anchor) => ResolvedAnchor { 44 | id: U256::from(header_anchor.header.number), 45 | hash: header_anchor.header.hash_slow(), 46 | }, 47 | Anchor::Eip4788(beacon_anchor) | Anchor::Consensus(beacon_anchor) => { 48 | let block_hash = beacon_anchor.inner.header.hash_slow(); 49 | let hash = beacon_anchor.anchor.beacon_root(block_hash, BLOCK_HASH_LEAF_INDEX); 50 | 51 | ResolvedAnchor { id: beacon_anchor.id().into(), hash } 52 | } 53 | Anchor::ChainedEip4788(chained_anchor) => { 54 | // Retrieve the execution block beacon root and timestamp 55 | let mut beacon_root = chained_anchor.inner.beacon_root(); 56 | let mut timestamp = U256::from(chained_anchor.inner.id().as_timestamp().unwrap()); 57 | 58 | // Iterate over all the state anchors stating from the execution block 59 | // to the reference block 60 | for state_anchor in &chained_anchor.state_anchors { 61 | let state_root = state_anchor.state.state_root(); 62 | let current_beacon_root = 63 | get_beacon_root_from_state(&state_anchor.state, timestamp); 64 | 65 | // Verify that the previous anchor is valid wrt the current state 66 | assert_eq!(current_beacon_root, beacon_root, "Beacon root should match"); 67 | 68 | // Retrieve the beacon root and timestamp of the current state 69 | beacon_root = 70 | state_anchor.anchor.beacon_root(state_root, STATE_ROOT_LEAF_INDEX); 71 | timestamp = U256::from(state_anchor.anchor.id().as_timestamp().unwrap()); 72 | } 73 | 74 | // If the full chain is valid, return the resolved anchor containing 75 | // the reference block beacon root and timestamp 76 | ResolvedAnchor { id: timestamp, hash: beacon_root } 77 | } 78 | } 79 | } 80 | 81 | pub fn ty(&self) -> AnchorType { 82 | match self { 83 | Anchor::Header(_) => AnchorType::BlockHash, 84 | Anchor::Eip4788(_) | Anchor::ChainedEip4788(_) => AnchorType::Eip4788, 85 | Anchor::Consensus(_) => AnchorType::Consensus, 86 | } 87 | } 88 | } 89 | 90 | impl From
for Anchor { 91 | fn from(header: Header) -> Self { 92 | Self::Header(HeaderAnchor { header }) 93 | } 94 | } 95 | 96 | #[derive(Debug, Clone, Copy)] 97 | pub struct ResolvedAnchor { 98 | pub id: U256, 99 | pub hash: B256, 100 | } 101 | 102 | #[serde_as] 103 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 104 | pub struct HeaderAnchor { 105 | #[serde_as(as = "alloy_consensus::serde_bincode_compat::Header")] 106 | header: Header, 107 | } 108 | 109 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 110 | pub struct BeaconWithHeaderAnchor { 111 | inner: HeaderAnchor, 112 | anchor: BeaconAnchor, 113 | } 114 | 115 | impl BeaconWithHeaderAnchor { 116 | pub fn new(header: Header, anchor: BeaconAnchor) -> Self { 117 | Self { inner: HeaderAnchor { header }, anchor } 118 | } 119 | 120 | pub fn proof(&self) -> &[B256] { 121 | self.anchor.proof() 122 | } 123 | 124 | pub fn id(&self) -> &BeaconAnchorId { 125 | self.anchor.id() 126 | } 127 | 128 | pub fn beacon_root(&self) -> B256 { 129 | self.anchor.beacon_root(self.inner.header.hash_slow(), BLOCK_HASH_LEAF_INDEX) 130 | } 131 | } 132 | 133 | impl From for BeaconAnchor { 134 | fn from(value: BeaconWithHeaderAnchor) -> Self { 135 | value.anchor 136 | } 137 | } 138 | 139 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 140 | pub struct BeaconAnchor { 141 | proof: Vec, 142 | id: BeaconAnchorId, 143 | } 144 | 145 | impl BeaconAnchor { 146 | pub fn new(proof: Vec, id: BeaconAnchorId) -> Self { 147 | Self { proof, id } 148 | } 149 | 150 | pub fn proof(&self) -> &[B256] { 151 | &self.proof 152 | } 153 | 154 | pub fn id(&self) -> &BeaconAnchorId { 155 | &self.id 156 | } 157 | 158 | pub fn beacon_root(&self, leaf: B256, generalized_index: usize) -> B256 { 159 | rebuild_merkle_root(leaf, generalized_index, &self.proof) 160 | } 161 | } 162 | 163 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 164 | pub enum BeaconAnchorId { 165 | Timestamp(u64), 166 | Slot(u64), 167 | } 168 | 169 | impl BeaconAnchorId { 170 | pub fn as_timestamp(&self) -> Option { 171 | match self { 172 | BeaconAnchorId::Timestamp(t) => Some(*t), 173 | BeaconAnchorId::Slot(_) => None, 174 | } 175 | } 176 | } 177 | 178 | impl From<&BeaconAnchorId> for U256 { 179 | fn from(value: &BeaconAnchorId) -> Self { 180 | match value { 181 | BeaconAnchorId::Timestamp(t) => U256::from(*t), 182 | BeaconAnchorId::Slot(s) => U256::from(*s), 183 | } 184 | } 185 | } 186 | 187 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 188 | pub struct ChainedBeaconAnchor { 189 | inner: BeaconWithHeaderAnchor, 190 | state_anchors: Vec, 191 | } 192 | 193 | impl ChainedBeaconAnchor { 194 | pub fn new(inner: BeaconWithHeaderAnchor, state_anchors: Vec) -> Self { 195 | Self { inner, state_anchors } 196 | } 197 | } 198 | 199 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 200 | pub struct BeaconStateAnchor { 201 | state: EthereumState, 202 | anchor: BeaconAnchor, 203 | } 204 | 205 | impl BeaconStateAnchor { 206 | pub fn new(state: EthereumState, anchor: BeaconAnchor) -> Self { 207 | Self { state, anchor } 208 | } 209 | } 210 | 211 | #[derive(Debug, Clone, Copy)] 212 | pub enum BeaconBlockField { 213 | BlockHash, 214 | StateRoot, 215 | } 216 | 217 | impl Display for BeaconBlockField { 218 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 219 | match self { 220 | BeaconBlockField::BlockHash => write!(f, "block_hash"), 221 | BeaconBlockField::StateRoot => write!(f, "state_root"), 222 | } 223 | } 224 | } 225 | 226 | impl PartialEq for usize { 227 | fn eq(&self, other: &BeaconBlockField) -> bool { 228 | let other = usize::from(other); 229 | 230 | *self == other 231 | } 232 | } 233 | 234 | impl From<&BeaconBlockField> for usize { 235 | fn from(value: &BeaconBlockField) -> Self { 236 | match value { 237 | BeaconBlockField::BlockHash => BLOCK_HASH_LEAF_INDEX, 238 | BeaconBlockField::StateRoot => STATE_ROOT_LEAF_INDEX, 239 | } 240 | } 241 | } 242 | 243 | pub fn rebuild_merkle_root(leaf: B256, generalized_index: usize, branch: &[B256]) -> B256 { 244 | let mut current_hash = leaf; 245 | let depth = generalized_index.ilog2(); 246 | let mut index = generalized_index - (1 << depth); 247 | let mut hasher = Sha256::new(); 248 | 249 | for sibling in branch { 250 | // Determine if the current node is a left or right child 251 | let is_left = index % 2 == 0; 252 | 253 | // Combine the current hash with the sibling hash 254 | if is_left { 255 | // If current node is left child, hash(current + sibling) 256 | hasher.update(current_hash); 257 | hasher.update(sibling); 258 | } else { 259 | // If current node is right child, hash(sibling + current) 260 | hasher.update(sibling); 261 | hasher.update(current_hash); 262 | } 263 | current_hash.copy_from_slice(&hasher.finalize_reset()); 264 | 265 | // Move up to the parent level 266 | index /= 2; 267 | } 268 | 269 | current_hash 270 | } 271 | 272 | pub fn get_beacon_root_from_state(state: &EthereumState, timestamp: U256) -> B256 { 273 | let db = TrieDB::new(state, HashMap::default(), HashMap::default()); 274 | let timestamp_idx = timestamp % HISTORY_BUFFER_LENGTH; 275 | let root_idx = timestamp_idx + HISTORY_BUFFER_LENGTH; 276 | 277 | let root = db.storage_ref(BEACON_ROOTS_ADDRESS, root_idx).unwrap(); 278 | 279 | root.into() 280 | } 281 | -------------------------------------------------------------------------------- /crates/client-executor/src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum ClientError { 5 | #[error("ABI error: {0}")] 6 | ABI(#[from] alloy_sol_types::Error), 7 | #[error("RSP error: {0}")] 8 | RSP(#[from] rsp_client_executor::error::ClientError), 9 | } 10 | -------------------------------------------------------------------------------- /crates/client-executor/src/io.rs: -------------------------------------------------------------------------------- 1 | use std::iter::once; 2 | 3 | use alloy_consensus::ReceiptEnvelope; 4 | use reth_primitives::Header; 5 | use revm::state::Bytecode; 6 | use revm_primitives::{Address, HashMap, B256, U256}; 7 | use rsp_client_executor::io::WitnessInput; 8 | use rsp_mpt::EthereumState; 9 | use rsp_primitives::genesis::Genesis; 10 | use serde::{Deserialize, Serialize}; 11 | use serde_with::serde_as; 12 | 13 | use crate::Anchor; 14 | 15 | /// Information about how the contract executions accessed state, which is needed to execute the 16 | /// contract in SP1. 17 | /// 18 | /// Instead of passing in the entire state, only the state roots and merkle proofs 19 | /// for the storage slots that were modified and accessed are passed in. 20 | #[serde_as] 21 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 22 | pub struct EvmSketchInput { 23 | /// The current block anchor. 24 | pub anchor: Anchor, 25 | /// The genesis block specification. 26 | pub genesis: Genesis, 27 | /// The previous block headers starting from the most recent. These are used for calls to the 28 | /// blockhash opcode. 29 | #[serde_as(as = "Vec")] 30 | pub ancestor_headers: Vec
, 31 | /// Current block's Ethereum state. 32 | pub state: EthereumState, 33 | /// Requests to account state and storage slots. 34 | pub state_requests: HashMap>, 35 | /// Account bytecodes. 36 | pub bytecodes: Vec, 37 | /// Receipts. 38 | #[serde_as(as = "Option>")] 39 | pub receipts: Option>, 40 | } 41 | 42 | impl WitnessInput for EvmSketchInput { 43 | #[inline(always)] 44 | fn state(&self) -> &EthereumState { 45 | &self.state 46 | } 47 | 48 | #[inline(always)] 49 | fn state_anchor(&self) -> B256 { 50 | self.anchor.header().state_root 51 | } 52 | 53 | #[inline(always)] 54 | fn state_requests(&self) -> impl Iterator)> { 55 | self.state_requests.iter() 56 | } 57 | 58 | #[inline(always)] 59 | fn bytecodes(&self) -> impl Iterator { 60 | self.bytecodes.iter() 61 | } 62 | 63 | #[inline(always)] 64 | fn headers(&self) -> impl Iterator { 65 | once(self.anchor.header()).chain(self.ancestor_headers.iter()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/client-executor/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod io; 2 | use std::sync::Arc; 3 | 4 | use alloy_eips::Encodable2718; 5 | use alloy_evm::{Database, Evm, IntoTxEnv}; 6 | use alloy_primitives::Log; 7 | use alloy_rpc_types::{Filter, FilteredParams}; 8 | use alloy_sol_types::{sol, SolCall, SolEvent}; 9 | use alloy_trie::root::ordered_trie_root_with_encoder; 10 | use eyre::OptionExt; 11 | use io::EvmSketchInput; 12 | use reth_chainspec::ChainSpec; 13 | use reth_consensus::HeaderValidator; 14 | use reth_ethereum_consensus::EthBeaconConsensus; 15 | use reth_evm::{ConfigureEvm, EvmEnv}; 16 | use reth_evm_ethereum::{EthEvm, EthEvmConfig}; 17 | use reth_primitives::{Header, SealedHeader}; 18 | use revm::{ 19 | context::TxEnv, database::CacheDB, inspector::NoOpInspector, Context, MainBuilder, MainContext, 20 | }; 21 | use revm_primitives::{Address, Bytes, TxKind, B256, U256}; 22 | use rsp_client_executor::io::{TrieDB, WitnessInput}; 23 | 24 | mod anchor; 25 | pub use anchor::{ 26 | get_beacon_root_from_state, rebuild_merkle_root, Anchor, BeaconAnchor, BeaconAnchorId, 27 | BeaconBlockField, BeaconStateAnchor, BeaconWithHeaderAnchor, ChainedBeaconAnchor, HeaderAnchor, 28 | HISTORY_BUFFER_LENGTH, 29 | }; 30 | 31 | mod errors; 32 | pub use errors::ClientError; 33 | 34 | pub use rsp_primitives::genesis::Genesis; 35 | 36 | /// Input to a contract call. 37 | /// 38 | /// Can be used to call an existing contract or create a new one. If used to create a new one, 39 | #[derive(Debug, Clone)] 40 | pub struct ContractInput { 41 | /// The address of the contract to call. 42 | pub contract_address: Address, 43 | /// The address of the caller. 44 | pub caller_address: Address, 45 | /// The calldata to pass to the contract. 46 | pub calldata: ContractCalldata, 47 | } 48 | 49 | /// The type of calldata to pass to a contract. 50 | /// 51 | /// This enum is used to distinguish between contract calls and contract creations. 52 | #[derive(Debug, Clone)] 53 | pub enum ContractCalldata { 54 | Call(Bytes), 55 | Create(Bytes), 56 | } 57 | 58 | impl ContractCalldata { 59 | /// Encode the calldata as a bytes. 60 | pub fn to_bytes(&self) -> Bytes { 61 | match self { 62 | Self::Call(calldata) => calldata.clone(), 63 | Self::Create(calldata) => calldata.clone(), 64 | } 65 | } 66 | } 67 | 68 | impl ContractInput { 69 | /// Create a new contract call input. 70 | pub fn new_call( 71 | contract_address: Address, 72 | caller_address: Address, 73 | calldata: C, 74 | ) -> Self { 75 | Self { 76 | contract_address, 77 | caller_address, 78 | calldata: ContractCalldata::Call(calldata.abi_encode().into()), 79 | } 80 | } 81 | 82 | /// Creates a new contract creation input. 83 | /// 84 | /// To create a new contract, we send a transaction with TxKind Create to the 85 | /// zero address. As such, the contract address will be set to the zero address. 86 | pub fn new_create(caller_address: Address, calldata: Bytes) -> Self { 87 | Self { 88 | contract_address: Address::ZERO, 89 | caller_address, 90 | calldata: ContractCalldata::Create(calldata), 91 | } 92 | } 93 | } 94 | 95 | impl IntoTxEnv for &ContractInput { 96 | fn into_tx_env(self) -> TxEnv { 97 | TxEnv { 98 | caller: self.caller_address, 99 | data: self.calldata.to_bytes(), 100 | // Set the gas price to 0 to avoid lack of funds (0) error. 101 | gas_price: 0, 102 | kind: match self.calldata { 103 | ContractCalldata::Create(_) => TxKind::Create, 104 | ContractCalldata::Call(_) => TxKind::Call(self.contract_address), 105 | }, 106 | chain_id: None, 107 | ..Default::default() 108 | } 109 | } 110 | } 111 | 112 | sol! { 113 | #[derive(Debug)] 114 | enum AnchorType { BlockHash, Eip4788, Consensus } 115 | 116 | /// Public values of a contract call. 117 | /// 118 | /// These outputs can easily be abi-encoded, for use on-chain. 119 | #[derive(Debug)] 120 | struct ContractPublicValues { 121 | uint256 id; 122 | bytes32 anchorHash; 123 | AnchorType anchorType; 124 | address callerAddress; 125 | address contractAddress; 126 | bytes contractCalldata; 127 | bytes contractOutput; 128 | } 129 | } 130 | 131 | impl ContractPublicValues { 132 | /// Construct a new [`ContractPublicValues`] 133 | /// 134 | /// By default, commit the contract input, the output, and the block hash to public values of 135 | /// the proof. More can be committed if necessary. 136 | pub fn new( 137 | call: ContractInput, 138 | output: Bytes, 139 | id: U256, 140 | anchor: B256, 141 | anchor_type: AnchorType, 142 | ) -> Self { 143 | Self { 144 | id, 145 | anchorHash: anchor, 146 | anchorType: anchor_type, 147 | contractAddress: call.contract_address, 148 | callerAddress: call.caller_address, 149 | contractCalldata: call.calldata.to_bytes(), 150 | contractOutput: output, 151 | } 152 | } 153 | } 154 | 155 | /// An executor that executes smart contract calls inside a zkVM. 156 | #[derive(Debug)] 157 | pub struct ClientExecutor<'a> { 158 | /// The block anchor. 159 | pub anchor: &'a Anchor, 160 | /// The genesis block specification. 161 | pub chain_spec: Arc, 162 | /// The database that the executor uses to access state. 163 | pub witness_db: TrieDB<'a>, 164 | /// All logs in the block. 165 | pub logs: Vec, 166 | } 167 | 168 | impl<'a> ClientExecutor<'a> { 169 | /// Instantiates a new [`ClientExecutor`] 170 | pub fn new(state_sketch: &'a EvmSketchInput) -> Result { 171 | let chain_spec = Arc::new(ChainSpec::try_from(&state_sketch.genesis).unwrap()); 172 | let validator = EthBeaconConsensus::new(chain_spec.clone()); 173 | let header = state_sketch.anchor.header(); 174 | 175 | validator 176 | .validate_header(&SealedHeader::new_unhashed(header.clone())) 177 | .expect("the header in not valid"); 178 | 179 | assert_eq!(header.state_root, state_sketch.state.state_root(), "State root mismatch"); 180 | 181 | // verify that ancestors form a valid chain 182 | let mut previous_header = header; 183 | for ancestor in &state_sketch.ancestor_headers { 184 | let ancestor_hash = ancestor.hash_slow(); 185 | 186 | validator 187 | .validate_header(&SealedHeader::new_unhashed(ancestor.clone())) 188 | .unwrap_or_else(|_| panic!("the ancestor {} header in not valid", ancestor.number)); 189 | assert_eq!( 190 | previous_header.parent_hash, ancestor_hash, 191 | "block {} is not the parent of {}", 192 | ancestor.number, previous_header.number 193 | ); 194 | previous_header = ancestor; 195 | } 196 | 197 | if let Some(receipts) = &state_sketch.receipts { 198 | // verify the receipts root hash 199 | let root = ordered_trie_root_with_encoder(receipts, |r, out| r.encode_2718(out)); 200 | assert_eq!(state_sketch.anchor.header().receipts_root, root, "Receipts root mismatch"); 201 | } 202 | 203 | let logs = state_sketch 204 | .receipts 205 | .as_ref() 206 | .unwrap_or(&vec![]) 207 | .iter() 208 | .flat_map(|r| r.logs().to_vec()) 209 | .collect(); 210 | 211 | Ok(Self { 212 | anchor: &state_sketch.anchor, 213 | chain_spec, 214 | witness_db: state_sketch.witness_db()?, 215 | logs, 216 | }) 217 | } 218 | 219 | /// Executes the smart contract call with the given [`ContractInput`] in SP1. 220 | /// 221 | /// Storage accesses are already validated against the `witness_db`'s state root. 222 | pub fn execute(&self, call: ContractInput) -> eyre::Result { 223 | let cache_db = CacheDB::new(&self.witness_db); 224 | let mut evm = new_evm(cache_db, self.anchor.header(), U256::ZERO, self.chain_spec.clone()); 225 | let tx_output = evm.transact(&call)?; 226 | let tx_output_bytes = tx_output.result.output().ok_or_eyre("Error decoding result")?; 227 | let resolved = self.anchor.resolve(); 228 | 229 | let public_values = ContractPublicValues::new( 230 | call, 231 | tx_output_bytes.clone(), 232 | resolved.id, 233 | resolved.hash, 234 | self.anchor.ty(), 235 | ); 236 | 237 | Ok(public_values) 238 | } 239 | 240 | /// Returns the decoded logs matching the provided `filter`. 241 | /// 242 | /// To be avaliable in the client, the logs need to be prefetched in the host first. 243 | pub fn get_logs(&self, filter: Filter) -> Result>, ClientError> { 244 | let params = FilteredParams::new(Some(filter)); 245 | 246 | self.logs 247 | .iter() 248 | .filter(|log| params.filter_address(&log.address) && params.filter_topics(log.topics())) 249 | .map(|log| E::decode_log(log)) 250 | .collect::>() 251 | .map_err(Into::into) 252 | } 253 | } 254 | 255 | /// Instantiates a new EVM, which is ready to run `call`. 256 | pub fn new_evm( 257 | db: DB, 258 | header: &Header, 259 | difficulty: U256, 260 | chain_spec: Arc, 261 | ) -> EthEvm 262 | where 263 | DB: Database, 264 | { 265 | let EvmEnv { cfg_env, mut block_env, .. } = EthEvmConfig::new(chain_spec).evm_env(header); 266 | 267 | // Set the base fee to 0 to enable 0 gas price transactions. 268 | block_env.basefee = 0; 269 | block_env.difficulty = difficulty; 270 | 271 | let evm = Context::mainnet() 272 | .with_db(db) 273 | .with_cfg(cfg_env) 274 | .with_block(block_env) 275 | .modify_tx_chained(|tx_env| { 276 | tx_env.gas_limit = header.gas_limit; 277 | }) 278 | .build_mainnet_with_inspector(NoOpInspector {}); 279 | 280 | EthEvm::new(evm, false) 281 | } 282 | -------------------------------------------------------------------------------- /crates/host-executor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sp1-cc-host-executor" 3 | description = "" 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [dependencies] 12 | async-trait.workspace = true 13 | eyre.workspace = true 14 | reqwest.workspace = true 15 | serde.workspace = true 16 | serde_json.workspace = true 17 | url.workspace = true 18 | tokio.workspace = true 19 | tracing.workspace = true 20 | thiserror.workspace = true 21 | 22 | # workspace 23 | sp1-cc-client-executor.workspace = true 24 | 25 | # rsp 26 | rsp-client-executor.workspace = true 27 | rsp-rpc-db.workspace = true 28 | rsp-primitives.workspace = true 29 | rsp-mpt.workspace = true 30 | 31 | # reth 32 | reth-chainspec.workspace = true 33 | 34 | # revm 35 | revm.workspace = true 36 | revm-primitives.workspace = true 37 | 38 | # alloy 39 | alloy-consensus.workspace = true 40 | alloy-eips.workspace = true 41 | alloy-primitives.workspace = true 42 | alloy-provider.workspace = true 43 | alloy-transport.workspace = true 44 | alloy-sol-macro.workspace = true 45 | alloy-sol-types.workspace = true 46 | alloy-rpc-types.workspace = true 47 | alloy-evm.workspace = true 48 | 49 | ethereum-consensus.workspace = true 50 | 51 | [dev-dependencies] 52 | alloy-primitives.workspace = true 53 | dotenv.workspace = true 54 | tracing-subscriber = "0.3.18" 55 | bincode = "1.3.3" 56 | -------------------------------------------------------------------------------- /crates/host-executor/src/anchor_builder.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, marker::PhantomData}; 2 | 3 | use alloy_consensus::{Header, Sealed}; 4 | use alloy_eips::{eip4788::BEACON_ROOTS_ADDRESS, BlockId}; 5 | use alloy_primitives::{B256, U256}; 6 | use alloy_provider::{network::AnyNetwork, Provider}; 7 | use async_trait::async_trait; 8 | use ethereum_consensus::ssz::prelude::Prove; 9 | use rsp_mpt::EthereumState; 10 | use sp1_cc_client_executor::{ 11 | get_beacon_root_from_state, rebuild_merkle_root, Anchor, BeaconAnchor, BeaconAnchorId, 12 | BeaconBlockField, BeaconStateAnchor, BeaconWithHeaderAnchor, ChainedBeaconAnchor, 13 | HISTORY_BUFFER_LENGTH, 14 | }; 15 | use url::Url; 16 | 17 | use crate::{ 18 | beacon::{BeaconClient, SignedBeaconBlock}, 19 | HostError, 20 | }; 21 | 22 | /// Abstracts [`Anchor`] creation. 23 | #[async_trait] 24 | pub trait AnchorBuilder { 25 | async fn build + Send>(&self, block_id: B) -> Result; 26 | } 27 | 28 | #[async_trait] 29 | pub trait BeaconAnchorKind: Sized { 30 | async fn build_beacon_anchor_from_header>( 31 | header: &Sealed
, 32 | field: BeaconBlockField, 33 | beacon_anchor_builder: &BeaconAnchorBuilder, 34 | ) -> Result<(B256, BeaconAnchor), HostError>; 35 | } 36 | 37 | #[derive(Debug)] 38 | pub struct Eip4788BeaconAnchor; 39 | 40 | #[async_trait] 41 | impl BeaconAnchorKind for Eip4788BeaconAnchor { 42 | async fn build_beacon_anchor_from_header>( 43 | header: &Sealed
, 44 | field: BeaconBlockField, 45 | beacon_anchor_builder: &BeaconAnchorBuilder, 46 | ) -> Result<(B256, BeaconAnchor), HostError> { 47 | let child_header = 48 | beacon_anchor_builder.header_anchor_builder.get_header(header.number + 1).await?; 49 | assert_eq!(child_header.parent_hash, header.seal()); 50 | 51 | let beacon_root = child_header 52 | .parent_beacon_block_root 53 | .ok_or_else(|| HostError::ParentBeaconBlockRootMissing)?; 54 | 55 | let anchor = beacon_anchor_builder 56 | .build_beacon_anchor( 57 | beacon_root, 58 | BeaconAnchorId::Timestamp(child_header.timestamp), 59 | field, 60 | ) 61 | .await?; 62 | 63 | Ok((beacon_root, anchor)) 64 | } 65 | } 66 | 67 | #[derive(Debug)] 68 | pub struct ConsensusBeaconAnchor; 69 | 70 | #[async_trait] 71 | impl BeaconAnchorKind for ConsensusBeaconAnchor { 72 | async fn build_beacon_anchor_from_header>( 73 | header: &Sealed
, 74 | field: BeaconBlockField, 75 | beacon_anchor_builder: &BeaconAnchorBuilder, 76 | ) -> Result<(B256, BeaconAnchor), HostError> { 77 | let parent_root = header 78 | .parent_beacon_block_root 79 | .ok_or_else(|| HostError::ParentBeaconBlockRootMissing)?; 80 | 81 | let (beacon_root, beacon_header) = beacon_anchor_builder 82 | .client 83 | .get_header_from_parent_root(parent_root.to_string()) 84 | .await?; 85 | 86 | let anchor = beacon_anchor_builder 87 | .build_beacon_anchor( 88 | beacon_root, 89 | BeaconAnchorId::Slot(beacon_header.message.slot), 90 | field, 91 | ) 92 | .await?; 93 | 94 | Ok((beacon_root, anchor)) 95 | } 96 | } 97 | 98 | /// A builder for [`HeaderAnchor`]. 99 | #[derive(Debug)] 100 | pub struct HeaderAnchorBuilder

{ 101 | provider: P, 102 | } 103 | 104 | impl

HeaderAnchorBuilder

{ 105 | pub fn new(provider: P) -> Self { 106 | Self { provider } 107 | } 108 | } 109 | 110 | impl> HeaderAnchorBuilder

{ 111 | pub async fn get_header>( 112 | &self, 113 | block_id: B, 114 | ) -> Result, HostError> { 115 | let block_id = block_id.into(); 116 | let block = self 117 | .provider 118 | .get_block(block_id) 119 | .await? 120 | .ok_or_else(|| HostError::BlockNotFoundError(block_id))?; 121 | 122 | let header = block 123 | .header 124 | .inner 125 | .clone() 126 | .try_into_header() 127 | .map_err(|_| HostError::HeaderConversionError(block.inner.header.number))?; 128 | 129 | Ok(Sealed::new(header)) 130 | } 131 | } 132 | 133 | #[async_trait] 134 | impl> AnchorBuilder for HeaderAnchorBuilder

{ 135 | async fn build + Send>(&self, block_id: B) -> Result { 136 | let header = self.get_header(block_id).await?; 137 | 138 | Ok(header.into_inner().into()) 139 | } 140 | } 141 | 142 | /// A builder for [`BeaconAnchor`]. 143 | pub struct BeaconAnchorBuilder { 144 | header_anchor_builder: HeaderAnchorBuilder

, 145 | client: BeaconClient, 146 | phantom: PhantomData, 147 | } 148 | 149 | impl

BeaconAnchorBuilder { 150 | pub fn new(header_anchor_builder: HeaderAnchorBuilder

, cl_rpc_url: Url) -> Self { 151 | Self { header_anchor_builder, client: BeaconClient::new(cl_rpc_url), phantom: PhantomData } 152 | } 153 | 154 | pub fn into_consensus(self) -> BeaconAnchorBuilder { 155 | BeaconAnchorBuilder { 156 | header_anchor_builder: self.header_anchor_builder, 157 | client: self.client, 158 | phantom: PhantomData, 159 | } 160 | } 161 | } 162 | 163 | impl, K: BeaconAnchorKind> BeaconAnchorBuilder { 164 | pub async fn build_beacon_anchor_with_header( 165 | &self, 166 | header: &Sealed

, 167 | field: BeaconBlockField, 168 | ) -> Result { 169 | let (beacon_root, anchor) = K::build_beacon_anchor_from_header(header, field, self).await?; 170 | 171 | if matches!(field, BeaconBlockField::BlockHash) { 172 | assert!( 173 | verify_merkle_root(header.seal(), anchor.proof(), usize::from(&field), beacon_root), 174 | "the proof verification fail, field: {field}", 175 | ); 176 | } 177 | 178 | Ok(BeaconWithHeaderAnchor::new(header.clone_inner(), anchor)) 179 | } 180 | 181 | pub async fn build_beacon_anchor( 182 | &self, 183 | beacon_root: B256, 184 | id: BeaconAnchorId, 185 | field: BeaconBlockField, 186 | ) -> Result { 187 | let signed_beacon_block = self.client.get_block(beacon_root.to_string()).await?; 188 | 189 | let (proof, _) = match signed_beacon_block { 190 | SignedBeaconBlock::Deneb(signed_beacon_block) => { 191 | signed_beacon_block.message.prove(&[ 192 | "body".into(), 193 | "execution_payload".into(), 194 | field.to_string().as_str().into(), 195 | ])? 196 | } 197 | SignedBeaconBlock::Electra(signed_beacon_block) => { 198 | signed_beacon_block.message.prove(&[ 199 | "body".into(), 200 | "execution_payload".into(), 201 | field.to_string().as_str().into(), 202 | ])? 203 | } 204 | _ => unimplemented!(), 205 | }; 206 | 207 | assert!(proof.index == field, "the field leaf index is incorrect"); 208 | 209 | let proof = proof.branch.iter().map(|n| n.0.into()).collect::>(); 210 | 211 | let anchor = BeaconAnchor::new(proof, id); 212 | 213 | Ok(anchor) 214 | } 215 | } 216 | 217 | #[async_trait] 218 | impl> AnchorBuilder for BeaconAnchorBuilder { 219 | async fn build + Send>(&self, block_id: B) -> Result { 220 | let header = self.header_anchor_builder.get_header(block_id).await?; 221 | let anchor = 222 | self.build_beacon_anchor_with_header(&header, BeaconBlockField::BlockHash).await?; 223 | 224 | Ok(Anchor::Eip4788(anchor)) 225 | } 226 | } 227 | 228 | #[async_trait] 229 | impl> AnchorBuilder for BeaconAnchorBuilder { 230 | async fn build + Send>(&self, block_id: B) -> Result { 231 | let header = self.header_anchor_builder.get_header(block_id).await?; 232 | let anchor = 233 | self.build_beacon_anchor_with_header(&header, BeaconBlockField::BlockHash).await?; 234 | 235 | Ok(Anchor::Consensus(anchor)) 236 | } 237 | } 238 | 239 | impl Debug for BeaconAnchorBuilder { 240 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 241 | f.debug_struct("BeaconAnchorBuilder") 242 | .field("header_anchor_builder", &self.header_anchor_builder) 243 | .finish() 244 | } 245 | } 246 | 247 | /// A builder for [`ChainedBeaconAnchor`]. 248 | #[derive(Debug)] 249 | pub struct ChainedBeaconAnchorBuilder

{ 250 | beacon_anchor_builder: BeaconAnchorBuilder, 251 | /// The reference is a successor of the execution block. 252 | reference: BlockId, 253 | } 254 | 255 | impl

ChainedBeaconAnchorBuilder

{ 256 | pub fn new( 257 | beacon_anchor_builder: BeaconAnchorBuilder, 258 | reference: BlockId, 259 | ) -> Self { 260 | Self { beacon_anchor_builder, reference } 261 | } 262 | } 263 | 264 | impl> ChainedBeaconAnchorBuilder

{ 265 | async fn get_eip_4788_timestamp( 266 | &self, 267 | timestamp: U256, 268 | block_hash: B256, 269 | ) -> Result { 270 | let timestamp_idx = timestamp % HISTORY_BUFFER_LENGTH; 271 | let result = self 272 | .beacon_anchor_builder 273 | .header_anchor_builder 274 | .provider 275 | .get_storage_at(BEACON_ROOTS_ADDRESS, timestamp_idx) 276 | .block_id(BlockId::Hash(block_hash.into())) 277 | .await?; 278 | 279 | Ok(result) 280 | } 281 | 282 | async fn retrieve_state( 283 | &self, 284 | timestamp: U256, 285 | block_hash: B256, 286 | ) -> Result { 287 | // Compute the indexes of the two storage slots that will be queried 288 | let timestamp_idx = timestamp % HISTORY_BUFFER_LENGTH; 289 | let root_idx = timestamp_idx + HISTORY_BUFFER_LENGTH; 290 | 291 | let provider = &self.beacon_anchor_builder.header_anchor_builder.provider; 292 | 293 | let proof = provider 294 | .get_proof(BEACON_ROOTS_ADDRESS, vec![timestamp_idx.into(), root_idx.into()]) 295 | .block_id(BlockId::Hash(block_hash.into())) 296 | .await?; 297 | 298 | let state = EthereumState::from_account_proof(proof)?; 299 | 300 | Ok(state) 301 | } 302 | } 303 | 304 | #[async_trait] 305 | impl> AnchorBuilder for ChainedBeaconAnchorBuilder

{ 306 | async fn build + Send>(&self, block_id: B) -> Result { 307 | let execution_header = 308 | self.beacon_anchor_builder.header_anchor_builder.get_header(block_id).await?; 309 | let reference_header = 310 | self.beacon_anchor_builder.header_anchor_builder.get_header(self.reference).await?; 311 | assert!( 312 | execution_header.number < reference_header.number, 313 | "The execution block must be an ancestor of the reference block" 314 | ); 315 | 316 | // Build an anchor for the execution block containing the beacon root we need to verify 317 | let execution_anchor = self 318 | .beacon_anchor_builder 319 | .build_beacon_anchor_with_header(&execution_header, BeaconBlockField::BlockHash) 320 | .await?; 321 | // Build an anchor for the reference block 322 | let mut current_anchor = Some( 323 | self.beacon_anchor_builder 324 | .build_beacon_anchor_with_header(&reference_header, BeaconBlockField::StateRoot) 325 | .await? 326 | .into(), 327 | ); 328 | let mut current_state_block_hash = reference_header.seal(); 329 | let mut state_anchors: Vec = vec![]; 330 | 331 | // Loop backwards until we reach the execution block beacon root 332 | loop { 333 | let timestamp = self 334 | .get_eip_4788_timestamp( 335 | U256::from(execution_anchor.id().as_timestamp().unwrap()), 336 | current_state_block_hash, 337 | ) 338 | .await?; 339 | // Prefetch the beacon roots contract call for timestamp 340 | let state = self.retrieve_state(timestamp, current_state_block_hash).await?; 341 | let parent_beacon_root = get_beacon_root_from_state(&state, timestamp); 342 | 343 | state_anchors.insert(0, BeaconStateAnchor::new(state, current_anchor.take().unwrap())); 344 | 345 | // Check if we've reached the execution block beacon root 346 | if timestamp == U256::from(execution_anchor.id().as_timestamp().unwrap()) { 347 | assert!( 348 | parent_beacon_root == execution_anchor.beacon_root(), 349 | "failed to validate final beacon anchor" 350 | ); 351 | break; 352 | } 353 | 354 | current_state_block_hash = self 355 | .beacon_anchor_builder 356 | .client 357 | .get_execution_payload_block_hash(parent_beacon_root.to_string()) 358 | .await?; 359 | 360 | // Update the current anchor with the new beacon root 361 | let _ = current_anchor.replace( 362 | self.beacon_anchor_builder 363 | .build_beacon_anchor( 364 | parent_beacon_root, 365 | BeaconAnchorId::Timestamp(timestamp.to()), 366 | BeaconBlockField::StateRoot, 367 | ) 368 | .await?, 369 | ); 370 | } 371 | 372 | Ok(Anchor::ChainedEip4788(ChainedBeaconAnchor::new(execution_anchor, state_anchors))) 373 | } 374 | } 375 | 376 | fn verify_merkle_root( 377 | block_hash: B256, 378 | proof: &[B256], 379 | generalized_index: usize, 380 | beacon_root: B256, 381 | ) -> bool { 382 | rebuild_merkle_root(block_hash, generalized_index, proof) == beacon_root 383 | } 384 | -------------------------------------------------------------------------------- /crates/host-executor/src/beacon/client.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::B256; 2 | use ethereum_consensus::{phase0::SignedBeaconBlockHeader, Fork}; 3 | use reqwest::Client as ReqwestClient; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::value::RawValue; 6 | use url::Url; 7 | 8 | use crate::BeaconError; 9 | 10 | use super::SignedBeaconBlock; 11 | 12 | /// A client used for connecting and querying a beacon node. 13 | #[derive(Debug, Clone)] 14 | pub struct BeaconClient { 15 | rpc_url: Url, 16 | client: ReqwestClient, 17 | } 18 | 19 | /// The raw response returned by the Beacon Node APIs. 20 | #[derive(Debug, Serialize, Deserialize)] 21 | struct BeaconResponse { 22 | data: T, 23 | } 24 | 25 | /// The raw response returned by the Beacon Node APIs. 26 | #[derive(Debug, Serialize, Deserialize)] 27 | struct BeaconRawResponse<'a> { 28 | pub version: Fork, 29 | pub execution_optimistic: bool, 30 | pub finalized: bool, 31 | #[serde(borrow)] 32 | data: &'a RawValue, 33 | } 34 | 35 | /// The response returned by the `get_block_header` API. 36 | #[derive(Debug, Serialize, Deserialize)] 37 | struct BlockHeaderResponse { 38 | pub root: B256, 39 | pub canonical: bool, 40 | pub header: SignedBeaconBlockHeader, 41 | } 42 | 43 | impl<'de> serde::Deserialize<'de> for SignedBeaconBlock { 44 | fn deserialize(deserializer: D) -> Result 45 | where 46 | D: serde::Deserializer<'de>, 47 | { 48 | let BeaconRawResponse { version, data, .. } = BeaconRawResponse::deserialize(deserializer)?; 49 | let data = match version { 50 | Fork::Phase0 => serde_json::from_str(data.get()).map(SignedBeaconBlock::Phase0), 51 | Fork::Altair => serde_json::from_str(data.get()).map(SignedBeaconBlock::Altair), 52 | Fork::Bellatrix => serde_json::from_str(data.get()).map(SignedBeaconBlock::Bellatrix), 53 | Fork::Capella => serde_json::from_str(data.get()).map(SignedBeaconBlock::Capella), 54 | Fork::Deneb => serde_json::from_str(data.get()).map(SignedBeaconBlock::Deneb), 55 | Fork::Electra => serde_json::from_str(data.get()).map(SignedBeaconBlock::Electra), 56 | } 57 | .map_err(serde::de::Error::custom)?; 58 | 59 | Ok(data) 60 | } 61 | } 62 | 63 | impl BeaconClient { 64 | pub fn new(rpc_url: Url) -> Self { 65 | Self { rpc_url, client: ReqwestClient::new() } 66 | } 67 | 68 | /// Gets the block at the given `beacon_id`. 69 | pub async fn get_block(&self, beacon_id: String) -> Result { 70 | let endpoint = format!("{}eth/v2/beacon/blocks/{}", self.rpc_url, beacon_id); 71 | 72 | let response = self.client.get(&endpoint).send().await?; 73 | let block = response.error_for_status()?.json::().await?; 74 | 75 | Ok(block) 76 | } 77 | 78 | /// Gets the block header at the given given parent root. 79 | pub async fn get_header_from_parent_root( 80 | &self, 81 | parent_root: String, 82 | ) -> Result<(B256, SignedBeaconBlockHeader), BeaconError> { 83 | let endpoint = format!("{}eth/v1/beacon/headers", self.rpc_url); 84 | 85 | let response = 86 | self.client.get(&endpoint).query(&[("parent_root", parent_root)]).send().await?; 87 | let response = 88 | response.error_for_status()?.json::>>().await?; 89 | 90 | Ok((response.data[0].root, response.data[0].header.clone())) 91 | } 92 | 93 | /// Retrieves the execution bock hash at the given `beacon_id`. 94 | pub async fn get_execution_payload_block_hash( 95 | &self, 96 | beacon_id: String, 97 | ) -> Result { 98 | let block = self.get_block(beacon_id).await?; 99 | let block_hash = match block { 100 | SignedBeaconBlock::Phase0(_) => None, 101 | SignedBeaconBlock::Altair(_) => None, 102 | SignedBeaconBlock::Bellatrix(b) => Some(b.message.body.execution_payload.block_hash), 103 | SignedBeaconBlock::Capella(b) => Some(b.message.body.execution_payload.block_hash), 104 | SignedBeaconBlock::Deneb(b) => Some(b.message.body.execution_payload.block_hash), 105 | SignedBeaconBlock::Electra(b) => Some(b.message.body.execution_payload.block_hash), 106 | }; 107 | 108 | block_hash.ok_or_else(|| BeaconError::ExecutionPayloadMissing).map(|h| B256::from_slice(&h)) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /crates/host-executor/src/beacon/mod.rs: -------------------------------------------------------------------------------- 1 | mod signed_beacon_block; 2 | pub(crate) use signed_beacon_block::mainnet::SignedBeaconBlock; 3 | 4 | mod client; 5 | pub use client::BeaconClient; 6 | -------------------------------------------------------------------------------- /crates/host-executor/src/beacon/signed_beacon_block.rs: -------------------------------------------------------------------------------- 1 | use ethereum_consensus::{altair, bellatrix, capella, deneb, electra, phase0, ssz::prelude::*}; 2 | 3 | #[derive(Debug, Clone, PartialEq, Eq, Serializable, HashTreeRoot, serde::Serialize)] 4 | #[ssz(transparent)] 5 | #[serde(untagged)] 6 | pub enum SignedBeaconBlock< 7 | const MAX_PROPOSER_SLASHINGS: usize, 8 | const MAX_VALIDATORS_PER_COMMITTEE: usize, 9 | const MAX_ATTESTER_SLASHINGS: usize, 10 | const MAX_ATTESTATIONS: usize, 11 | const MAX_DEPOSITS: usize, 12 | const MAX_VOLUNTARY_EXITS: usize, 13 | const SYNC_COMMITTEE_SIZE: usize, 14 | const BYTES_PER_LOGS_BLOOM: usize, 15 | const MAX_EXTRA_DATA_BYTES: usize, 16 | const MAX_BYTES_PER_TRANSACTION: usize, 17 | const MAX_TRANSACTIONS_PER_PAYLOAD: usize, 18 | const MAX_WITHDRAWALS_PER_PAYLOAD: usize, 19 | const MAX_BLS_TO_EXECUTION_CHANGES: usize, 20 | const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize, 21 | const MAX_VALIDATORS_PER_SLOT: usize, 22 | const MAX_COMMITTEES_PER_SLOT: usize, 23 | const MAX_ATTESTER_SLASHINGS_ELECTRA: usize, 24 | const MAX_ATTESTATIONS_ELECTRA: usize, 25 | const MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: usize, 26 | const MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: usize, 27 | const MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: usize, 28 | > { 29 | Phase0( 30 | phase0::SignedBeaconBlock< 31 | MAX_PROPOSER_SLASHINGS, 32 | MAX_VALIDATORS_PER_COMMITTEE, 33 | MAX_ATTESTER_SLASHINGS, 34 | MAX_ATTESTATIONS, 35 | MAX_DEPOSITS, 36 | MAX_VOLUNTARY_EXITS, 37 | >, 38 | ), 39 | Altair( 40 | altair::SignedBeaconBlock< 41 | MAX_PROPOSER_SLASHINGS, 42 | MAX_VALIDATORS_PER_COMMITTEE, 43 | MAX_ATTESTER_SLASHINGS, 44 | MAX_ATTESTATIONS, 45 | MAX_DEPOSITS, 46 | MAX_VOLUNTARY_EXITS, 47 | SYNC_COMMITTEE_SIZE, 48 | >, 49 | ), 50 | Bellatrix( 51 | bellatrix::SignedBeaconBlock< 52 | MAX_PROPOSER_SLASHINGS, 53 | MAX_VALIDATORS_PER_COMMITTEE, 54 | MAX_ATTESTER_SLASHINGS, 55 | MAX_ATTESTATIONS, 56 | MAX_DEPOSITS, 57 | MAX_VOLUNTARY_EXITS, 58 | SYNC_COMMITTEE_SIZE, 59 | BYTES_PER_LOGS_BLOOM, 60 | MAX_EXTRA_DATA_BYTES, 61 | MAX_BYTES_PER_TRANSACTION, 62 | MAX_TRANSACTIONS_PER_PAYLOAD, 63 | >, 64 | ), 65 | Capella( 66 | capella::SignedBeaconBlock< 67 | MAX_PROPOSER_SLASHINGS, 68 | MAX_VALIDATORS_PER_COMMITTEE, 69 | MAX_ATTESTER_SLASHINGS, 70 | MAX_ATTESTATIONS, 71 | MAX_DEPOSITS, 72 | MAX_VOLUNTARY_EXITS, 73 | SYNC_COMMITTEE_SIZE, 74 | BYTES_PER_LOGS_BLOOM, 75 | MAX_EXTRA_DATA_BYTES, 76 | MAX_BYTES_PER_TRANSACTION, 77 | MAX_TRANSACTIONS_PER_PAYLOAD, 78 | MAX_WITHDRAWALS_PER_PAYLOAD, 79 | MAX_BLS_TO_EXECUTION_CHANGES, 80 | >, 81 | ), 82 | Deneb( 83 | deneb::SignedBeaconBlock< 84 | MAX_PROPOSER_SLASHINGS, 85 | MAX_VALIDATORS_PER_COMMITTEE, 86 | MAX_ATTESTER_SLASHINGS, 87 | MAX_ATTESTATIONS, 88 | MAX_DEPOSITS, 89 | MAX_VOLUNTARY_EXITS, 90 | SYNC_COMMITTEE_SIZE, 91 | BYTES_PER_LOGS_BLOOM, 92 | MAX_EXTRA_DATA_BYTES, 93 | MAX_BYTES_PER_TRANSACTION, 94 | MAX_TRANSACTIONS_PER_PAYLOAD, 95 | MAX_WITHDRAWALS_PER_PAYLOAD, 96 | MAX_BLS_TO_EXECUTION_CHANGES, 97 | MAX_BLOB_COMMITMENTS_PER_BLOCK, 98 | >, 99 | ), 100 | Electra( 101 | electra::SignedBeaconBlock< 102 | MAX_PROPOSER_SLASHINGS, 103 | MAX_VALIDATORS_PER_SLOT, 104 | MAX_COMMITTEES_PER_SLOT, 105 | MAX_ATTESTER_SLASHINGS_ELECTRA, 106 | MAX_ATTESTATIONS_ELECTRA, 107 | MAX_DEPOSITS, 108 | MAX_VOLUNTARY_EXITS, 109 | SYNC_COMMITTEE_SIZE, 110 | BYTES_PER_LOGS_BLOOM, 111 | MAX_EXTRA_DATA_BYTES, 112 | MAX_BYTES_PER_TRANSACTION, 113 | MAX_TRANSACTIONS_PER_PAYLOAD, 114 | MAX_WITHDRAWALS_PER_PAYLOAD, 115 | MAX_BLS_TO_EXECUTION_CHANGES, 116 | MAX_BLOB_COMMITMENTS_PER_BLOCK, 117 | MAX_DEPOSIT_REQUESTS_PER_PAYLOAD, 118 | MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, 119 | MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, 120 | >, 121 | ), 122 | } 123 | 124 | pub(crate) mod mainnet { 125 | use ethereum_consensus::{ 126 | altair::mainnet::SYNC_COMMITTEE_SIZE, 127 | bellatrix::mainnet::{ 128 | BYTES_PER_LOGS_BLOOM, MAX_BYTES_PER_TRANSACTION, MAX_EXTRA_DATA_BYTES, 129 | MAX_TRANSACTIONS_PER_PAYLOAD, 130 | }, 131 | capella::mainnet::{MAX_BLS_TO_EXECUTION_CHANGES, MAX_WITHDRAWALS_PER_PAYLOAD}, 132 | deneb::mainnet::MAX_BLOB_COMMITMENTS_PER_BLOCK, 133 | phase0::mainnet::{ 134 | MAX_ATTESTATIONS, MAX_ATTESTER_SLASHINGS, MAX_COMMITTEES_PER_SLOT, MAX_DEPOSITS, 135 | MAX_PROPOSER_SLASHINGS, MAX_VALIDATORS_PER_COMMITTEE, MAX_VOLUNTARY_EXITS, 136 | }, 137 | }; 138 | 139 | const MAX_ATTESTER_SLASHINGS_ELECTRA: usize = 1; 140 | const MAX_ATTESTATIONS_ELECTRA: usize = 8; 141 | 142 | const MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: usize = 8192; 143 | const MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: usize = 16; 144 | const MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: usize = 2; 145 | const MAX_COMMITTEES_PER_SLOT_USIZE: usize = MAX_COMMITTEES_PER_SLOT as usize; 146 | 147 | const MAX_VALIDATORS_PER_SLOT: usize = 148 | MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT_USIZE; 149 | 150 | pub(crate) type SignedBeaconBlock = super::SignedBeaconBlock< 151 | MAX_PROPOSER_SLASHINGS, 152 | MAX_VALIDATORS_PER_COMMITTEE, 153 | MAX_ATTESTER_SLASHINGS, 154 | MAX_ATTESTATIONS, 155 | MAX_DEPOSITS, 156 | MAX_VOLUNTARY_EXITS, 157 | SYNC_COMMITTEE_SIZE, 158 | BYTES_PER_LOGS_BLOOM, 159 | MAX_EXTRA_DATA_BYTES, 160 | MAX_BYTES_PER_TRANSACTION, 161 | MAX_TRANSACTIONS_PER_PAYLOAD, 162 | MAX_WITHDRAWALS_PER_PAYLOAD, 163 | MAX_BLS_TO_EXECUTION_CHANGES, 164 | MAX_BLOB_COMMITMENTS_PER_BLOCK, 165 | MAX_VALIDATORS_PER_SLOT, 166 | MAX_COMMITTEES_PER_SLOT_USIZE, 167 | MAX_ATTESTER_SLASHINGS_ELECTRA, 168 | MAX_ATTESTATIONS_ELECTRA, 169 | MAX_DEPOSIT_REQUESTS_PER_PAYLOAD, 170 | MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, 171 | MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, 172 | >; 173 | } 174 | -------------------------------------------------------------------------------- /crates/host-executor/src/errors.rs: -------------------------------------------------------------------------------- 1 | use alloy_eips::{eip2718::Eip2718Error, BlockId}; 2 | use alloy_transport::TransportError; 3 | use rsp_mpt::FromProofError; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum HostError { 8 | #[error("Transport error: {0}")] 9 | TransportError(#[from] TransportError), 10 | #[error("Decoding error: {0}")] 11 | DecodingError(#[from] Eip2718Error), 12 | #[error("Trie from proof conversion error: {0}")] 13 | TrieFromProofError(#[from] FromProofError), 14 | #[error("Merkleization error: {0}")] 15 | MerkleizationError(#[from] ethereum_consensus::ssz::prelude::MerkleizationError), 16 | #[error("Beacon error: {0}")] 17 | BeaconError(#[from] BeaconError), 18 | #[error("Failed to convert the header for block {0}")] 19 | HeaderConversionError(u64), 20 | #[error("The block {0} don't exists")] 21 | BlockNotFoundError(BlockId), 22 | #[error("The parent beacon block root is missing in the header")] 23 | ParentBeaconBlockRootMissing, 24 | } 25 | 26 | #[derive(Error, Debug)] 27 | pub enum BeaconError { 28 | #[error("Reqwest error: {0}")] 29 | Reqwest(#[from] reqwest::Error), 30 | #[error("Serde error: {0}")] 31 | Serde(#[from] serde_json::Error), 32 | #[error("Execution payload missing")] 33 | ExecutionPayloadMissing, 34 | } 35 | -------------------------------------------------------------------------------- /crates/host-executor/src/events.rs: -------------------------------------------------------------------------------- 1 | use alloy_consensus::{Header, ReceiptEnvelope}; 2 | use alloy_eips::{eip2718::Eip2718Error, Decodable2718, Encodable2718}; 3 | use alloy_provider::{network::AnyNetwork, Provider}; 4 | use alloy_rpc_types::{AnyReceiptEnvelope, Log as RpcLog}; 5 | 6 | use crate::HostError; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct LogsPrefetcher + Clone> { 10 | provider: P, 11 | prefetch: bool, 12 | } 13 | 14 | impl + Clone> LogsPrefetcher

{ 15 | /// Creates a new [`HostExecutor`] with a specific [`Provider`] and [`BlockNumberOrTag`]. 16 | pub fn new(provider: P) -> Self { 17 | Self { provider, prefetch: false } 18 | } 19 | 20 | /// Trigger receipts prefetching. 21 | pub fn trigger_prefetch(&mut self) { 22 | self.prefetch = true; 23 | } 24 | 25 | /// Prefetch receipts for inclusion in [`sp1_cc_client_executor::EVMStateSketch`]. 26 | pub async fn prefetch_receipts( 27 | &self, 28 | header: &Header, 29 | ) -> Result, HostError> { 30 | if !self.prefetch { 31 | return Ok(vec![]); 32 | } 33 | 34 | self.provider 35 | .get_block_receipts(header.number.into()) 36 | .await? 37 | .unwrap_or_default() 38 | .into_iter() 39 | .map(|r| convert_receipt_envelope(r.inner.inner)) 40 | .collect::>() 41 | .map_err(Into::into) 42 | } 43 | } 44 | 45 | fn convert_receipt_envelope( 46 | any_receipt_envelope: AnyReceiptEnvelope, 47 | ) -> Result { 48 | let any_receipt_envelope = AnyReceiptEnvelope { 49 | inner: any_receipt_envelope.inner.map_logs(|l| l.inner), 50 | r#type: any_receipt_envelope.r#type, 51 | }; 52 | 53 | let mut buf = vec![]; 54 | 55 | any_receipt_envelope.encode_2718(&mut buf); 56 | 57 | ReceiptEnvelope::decode_2718(&mut buf.as_slice()) 58 | } 59 | -------------------------------------------------------------------------------- /crates/host-executor/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use rsp_primitives::genesis::Genesis; 2 | 3 | mod anchor_builder; 4 | pub use anchor_builder::{ 5 | AnchorBuilder, BeaconAnchorBuilder, BeaconAnchorKind, ChainedBeaconAnchorBuilder, 6 | ConsensusBeaconAnchor, Eip4788BeaconAnchor, HeaderAnchorBuilder, 7 | }; 8 | 9 | mod beacon; 10 | pub use beacon::BeaconClient; 11 | 12 | mod errors; 13 | pub use errors::{BeaconError, HostError}; 14 | 15 | mod events; 16 | pub use events::LogsPrefetcher; 17 | 18 | mod sketch; 19 | pub use sketch::EvmSketch; 20 | 21 | mod sketch_builder; 22 | pub use sketch_builder::EvmSketchBuilder; 23 | 24 | #[cfg(test)] 25 | mod test; 26 | -------------------------------------------------------------------------------- /crates/host-executor/src/sketch.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeSet, sync::Arc}; 2 | 3 | use alloy_consensus::ReceiptEnvelope; 4 | use alloy_eips::{eip2718::Eip2718Error, Decodable2718, Encodable2718}; 5 | use alloy_evm::Evm; 6 | use alloy_primitives::{Address, Bytes, B256, U256}; 7 | use alloy_provider::{network::AnyNetwork, Provider}; 8 | use alloy_rpc_types::{AnyReceiptEnvelope, Filter, Log as RpcLog}; 9 | use alloy_sol_types::SolCall; 10 | use eyre::eyre; 11 | use reth_chainspec::ChainSpec; 12 | use revm::{context::result::ExecutionResult, database::CacheDB}; 13 | use rsp_mpt::EthereumState; 14 | use rsp_primitives::{account_proof::eip1186_proof_to_account_proof, genesis::Genesis}; 15 | use rsp_rpc_db::RpcDb; 16 | use sp1_cc_client_executor::{io::EvmSketchInput, new_evm, Anchor, ContractInput}; 17 | 18 | use crate::{EvmSketchBuilder, HostError}; 19 | 20 | /// ['EvmSketch'] is used to prefetch all the data required to execute a block and query logs in the 21 | /// zkVM. 22 | #[derive(Debug)] 23 | pub struct EvmSketch

{ 24 | /// The genesis block specification. 25 | pub genesis: Genesis, 26 | /// The anchor to execute our view functions on. 27 | pub anchor: Anchor, 28 | /// The [`RpcDb`] used to back the EVM. 29 | pub rpc_db: RpcDb, 30 | /// The receipts used to retrieve event logs. 31 | pub receipts: Option>, 32 | /// The provider used to fetch data. 33 | pub provider: P, 34 | } 35 | 36 | impl EvmSketch<()> { 37 | pub fn builder() -> EvmSketchBuilder<(), ()> { 38 | EvmSketchBuilder::default() 39 | } 40 | } 41 | 42 | impl

EvmSketch

43 | where 44 | P: Provider + Clone, 45 | { 46 | /// Executes a smart contract call. 47 | /// 48 | /// The accessed accounts and storages are recorded, and included in a [`EvmSketchInput`] 49 | /// when [`Self::finalize`] is called. 50 | pub async fn call( 51 | &self, 52 | contract_address: Address, 53 | caller_address: Address, 54 | calldata: C, 55 | ) -> eyre::Result { 56 | let cache_db = CacheDB::new(&self.rpc_db); 57 | let chain_spec = Arc::new(ChainSpec::try_from(&self.genesis)?); 58 | let mut evm = new_evm(cache_db, self.anchor.header(), U256::ZERO, chain_spec); 59 | let input = ContractInput::new_call(contract_address, caller_address, calldata); 60 | let output = evm.transact(&input)?; 61 | 62 | let output_bytes = match output.result { 63 | ExecutionResult::Success { output, .. } => Ok(output.data().clone()), 64 | ExecutionResult::Revert { output, .. } => Ok(output), 65 | ExecutionResult::Halt { reason, .. } => Err(eyre!("Execution halted: {reason:?}")), 66 | }?; 67 | 68 | Ok(C::abi_decode_returns(&output_bytes)?) 69 | } 70 | 71 | /// Executes a smart contract call, using the provided [`ContractInput`]. 72 | pub async fn call_raw(&self, input: &ContractInput) -> eyre::Result { 73 | let cache_db = CacheDB::new(&self.rpc_db); 74 | let chain_spec = Arc::new(ChainSpec::try_from(&self.genesis)?); 75 | let mut evm = new_evm(cache_db, self.anchor.header(), U256::ZERO, chain_spec); 76 | let output = evm.transact(input)?; 77 | 78 | let output_bytes = match output.result { 79 | ExecutionResult::Success { output, .. } => Ok(output.data().clone()), 80 | ExecutionResult::Revert { output, .. } => Ok(output), 81 | ExecutionResult::Halt { reason, .. } => Err(eyre!("Execution halted: {reason:?}")), 82 | }?; 83 | 84 | Ok(output_bytes) 85 | } 86 | 87 | /// Executes a smart contract creation. 88 | pub async fn create(&self, caller_address: Address, calldata: Bytes) -> eyre::Result { 89 | let cache_db = CacheDB::new(&self.rpc_db); 90 | let chain_spec = Arc::new(ChainSpec::try_from(&self.genesis)?); 91 | let mut evm = new_evm(cache_db, self.anchor.header(), U256::ZERO, chain_spec); 92 | let input = ContractInput::new_create(caller_address, calldata); 93 | let output = evm.transact(&input)?; 94 | 95 | let output_bytes = match output.result { 96 | ExecutionResult::Success { output, .. } => Ok(output.data().clone()), 97 | ExecutionResult::Revert { output, .. } => Ok(output), 98 | ExecutionResult::Halt { reason, .. } => Err(eyre!("Execution halted: {reason:?}")), 99 | }?; 100 | 101 | Ok(output_bytes.clone()) 102 | } 103 | 104 | /// Prefetch the logs matching the provided `filter`, allowing them to be retrieved in the 105 | /// client using [`get_logs`]. 106 | /// 107 | /// [`get_logs`]: sp1_cc_client_executor::ClientExecutor::get_logs 108 | pub async fn get_logs(&mut self, filter: &Filter) -> Result, HostError> { 109 | let logs = self.provider.get_logs(filter).await?; 110 | 111 | if !logs.is_empty() && self.receipts.is_none() { 112 | let receipts = self 113 | .provider 114 | .get_block_receipts(self.anchor.header().number.into()) 115 | .await? 116 | .unwrap_or_default() 117 | .into_iter() 118 | .map(|r| convert_receipt_envelope(r.inner.inner)) 119 | .collect::>()?; 120 | 121 | self.receipts = Some(receipts); 122 | } 123 | 124 | Ok(logs) 125 | } 126 | 127 | /// Returns the cumulative [`EvmSketchInput`] after executing some smart contracts. 128 | pub async fn finalize(self) -> Result { 129 | let block_number = self.anchor.header().number; 130 | 131 | // For every account touched, fetch the storage proofs for all the slots touched. 132 | let state_requests = self.rpc_db.get_state_requests(); 133 | tracing::info!("fetching storage proofs"); 134 | let mut storage_proofs = Vec::new(); 135 | 136 | for (address, used_keys) in state_requests.iter() { 137 | let keys = used_keys 138 | .iter() 139 | .map(|key| B256::from(*key)) 140 | .collect::>() 141 | .into_iter() 142 | .collect::>(); 143 | 144 | let storage_proof = 145 | self.provider.get_proof(*address, keys).block_id(block_number.into()).await?; 146 | storage_proofs.push(eip1186_proof_to_account_proof(storage_proof)); 147 | } 148 | 149 | let storage_proofs_by_address = 150 | storage_proofs.iter().map(|item| (item.address, item.clone())).collect(); 151 | let state = EthereumState::from_proofs( 152 | self.anchor.header().state_root, 153 | &storage_proofs_by_address, 154 | )?; 155 | 156 | // Fetch the parent headers needed to constrain the BLOCKHASH opcode. 157 | let oldest_ancestor = *self.rpc_db.oldest_ancestor.read().unwrap(); 158 | let mut ancestor_headers = vec![]; 159 | tracing::info!("fetching {} ancestor headers", block_number - oldest_ancestor); 160 | for height in (oldest_ancestor..=(block_number - 1)).rev() { 161 | let block = self.provider.get_block_by_number(height.into()).full().await?.unwrap(); 162 | ancestor_headers.push( 163 | block 164 | .inner 165 | .header 166 | .inner 167 | .clone() 168 | .try_into_header() 169 | .map_err(|h| HostError::HeaderConversionError(h.number))?, 170 | ); 171 | } 172 | 173 | Ok(EvmSketchInput { 174 | anchor: self.anchor, 175 | genesis: self.genesis, 176 | ancestor_headers, 177 | state, 178 | state_requests, 179 | bytecodes: self.rpc_db.get_bytecodes(), 180 | receipts: self.receipts, 181 | }) 182 | } 183 | } 184 | 185 | fn convert_receipt_envelope( 186 | any_receipt_envelope: AnyReceiptEnvelope, 187 | ) -> Result { 188 | let any_receipt_envelope = AnyReceiptEnvelope { 189 | inner: any_receipt_envelope.inner.map_logs(|l| l.inner), 190 | r#type: any_receipt_envelope.r#type, 191 | }; 192 | 193 | let mut buf = vec![]; 194 | 195 | any_receipt_envelope.encode_2718(&mut buf); 196 | 197 | ReceiptEnvelope::decode_2718(&mut buf.as_slice()) 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use sp1_cc_client_executor::io::EvmSketchInput; 203 | 204 | use crate::EvmSketch; 205 | 206 | // Function that requires T to be `Sync` 207 | fn assert_sync() {} 208 | 209 | #[test] 210 | fn test_sync() { 211 | assert_sync::>(); 212 | assert_sync::(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /crates/host-executor/src/sketch_builder.rs: -------------------------------------------------------------------------------- 1 | use alloy_eips::BlockId; 2 | use alloy_provider::{network::AnyNetwork, Provider, RootProvider}; 3 | use rsp_primitives::genesis::Genesis; 4 | use rsp_rpc_db::RpcDb; 5 | use url::Url; 6 | 7 | use crate::{ 8 | anchor_builder::{ 9 | AnchorBuilder, BeaconAnchorBuilder, ChainedBeaconAnchorBuilder, HeaderAnchorBuilder, 10 | }, 11 | ConsensusBeaconAnchor, Eip4788BeaconAnchor, EvmSketch, HostError, 12 | }; 13 | 14 | /// A builder for [`EvmSketch`]. 15 | #[derive(Debug)] 16 | pub struct EvmSketchBuilder { 17 | block: BlockId, 18 | genesis: Genesis, 19 | provider: P, 20 | anchor_builder: A, 21 | } 22 | 23 | impl EvmSketchBuilder { 24 | /// Sets the block on which the contract will be called. 25 | pub fn at_block>(mut self, block: B) -> Self { 26 | self.block = block.into(); 27 | self 28 | } 29 | /// Sets the chain on which the contract will be called. 30 | pub fn with_genesis(mut self, genesis: Genesis) -> Self { 31 | self.genesis = genesis; 32 | self 33 | } 34 | } 35 | 36 | impl EvmSketchBuilder<(), ()> { 37 | /// Sets the Ethereum HTTP RPC endpoint that will be used. 38 | pub fn el_rpc_url( 39 | self, 40 | rpc_url: Url, 41 | ) -> EvmSketchBuilder, HeaderAnchorBuilder>> 42 | { 43 | let provider = RootProvider::new_http(rpc_url); 44 | EvmSketchBuilder { 45 | block: self.block, 46 | genesis: self.genesis, 47 | provider: provider.clone(), 48 | anchor_builder: HeaderAnchorBuilder::new(provider), 49 | } 50 | } 51 | } 52 | 53 | impl

EvmSketchBuilder> 54 | where 55 | P: Provider, 56 | { 57 | /// Sets the Beacon HTTP RPC endpoint that will be used. 58 | pub fn cl_rpc_url( 59 | self, 60 | rpc_url: Url, 61 | ) -> EvmSketchBuilder> { 62 | EvmSketchBuilder { 63 | block: self.block, 64 | genesis: self.genesis, 65 | provider: self.provider, 66 | anchor_builder: BeaconAnchorBuilder::new(self.anchor_builder, rpc_url), 67 | } 68 | } 69 | } 70 | 71 | impl

EvmSketchBuilder> 72 | where 73 | P: Provider, 74 | { 75 | /// Sets the Beacon HTTP RPC endpoint that will be used. 76 | pub fn at_reference_block>( 77 | self, 78 | block_id: B, 79 | ) -> EvmSketchBuilder> { 80 | EvmSketchBuilder { 81 | block: self.block, 82 | genesis: self.genesis, 83 | provider: self.provider, 84 | anchor_builder: ChainedBeaconAnchorBuilder::new(self.anchor_builder, block_id.into()), 85 | } 86 | } 87 | 88 | /// Configures the builder to generate an [`Anchor`] containing the slot number associated to 89 | /// the beacon block root. 90 | /// 91 | /// This is useful for verification methods that have direct access to the state of the beacon 92 | /// chain, such as systems using beacon light clients. 93 | /// 94 | /// [`Anchor`]: sp1_cc_client_executor::Anchor 95 | pub fn consensus(self) -> EvmSketchBuilder> { 96 | EvmSketchBuilder { 97 | block: self.block, 98 | genesis: self.genesis, 99 | provider: self.provider, 100 | anchor_builder: self.anchor_builder.into_consensus(), 101 | } 102 | } 103 | } 104 | 105 | impl EvmSketchBuilder 106 | where 107 | P: Provider + Clone, 108 | A: AnchorBuilder, 109 | { 110 | /// Builds an [`EvmSketch`]. 111 | pub async fn build(self) -> Result, HostError> { 112 | let anchor = self.anchor_builder.build(self.block).await?; 113 | let block_number = anchor.header().number; 114 | 115 | let sketch = EvmSketch { 116 | genesis: self.genesis, 117 | anchor, 118 | rpc_db: RpcDb::new(self.provider.clone(), block_number), 119 | receipts: None, 120 | provider: self.provider, 121 | }; 122 | 123 | Ok(sketch) 124 | } 125 | } 126 | 127 | impl Default for EvmSketchBuilder<(), ()> { 128 | fn default() -> Self { 129 | Self { 130 | block: BlockId::default(), 131 | genesis: Genesis::Mainnet, 132 | provider: (), 133 | anchor_builder: (), 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /crates/host-executor/src/test.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::{address, Address}; 2 | use alloy_rpc_types::BlockNumberOrTag; 3 | use alloy_sol_macro::sol; 4 | use alloy_sol_types::SolCall; 5 | use revm_primitives::{hex, Bytes}; 6 | use rsp_primitives::genesis::Genesis; 7 | use sp1_cc_client_executor::{ClientExecutor, ContractInput, ContractPublicValues}; 8 | use url::Url; 9 | use ERC20Basic::nameCall; 10 | use IOracleHelper::getRatesCall; 11 | 12 | use crate::EvmSketch; 13 | 14 | sol! { 15 | /// Simplified interface of the ERC20Basic interface. 16 | interface ERC20Basic { 17 | function name() public constant returns (string memory); 18 | } 19 | } 20 | 21 | sol! { 22 | /// Simplified interface of the IUniswapV3PoolState interface. 23 | interface IUniswapV3PoolState { 24 | function slot0( 25 | ) external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked); 26 | } 27 | } 28 | 29 | sol! { 30 | /// Interface to the multiplexer contract. It gets the exchange rates of many tokens, including 31 | /// apxEth, ankrEth, and pufEth. 32 | interface IOracleHelper { 33 | function getRates(address[] memory collaterals) external view returns (uint256[] memory); 34 | } 35 | } 36 | 37 | /// Multiplexer collateral addresses 38 | const COLLATERALS: [Address; 12] = [ 39 | address!("E95A203B1a91a908F9B9CE46459d101078c2c3cb"), 40 | address!("9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6"), 41 | address!("Be9895146f7AF43049ca1c1AE358B0541Ea49704"), 42 | address!("7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"), 43 | address!("A35b1B31Ce002FBF2058D22F30f95D405200A15b"), 44 | address!("D9A442856C234a39a81a089C06451EBAa4306a72"), 45 | address!("ae78736Cd615f374D3085123A210448E74Fc6393"), 46 | address!("A1290d69c65A6Fe4DF752f95823fae25cB99e5A7"), 47 | address!("ac3E018457B222d93114458476f3E3416Abbe38F"), 48 | address!("9D39A5DE30e57443BfF2A8307A4256c8797A3497"), 49 | address!("f951E335afb289353dc249e82926178EaC7DEd78"), 50 | address!("Cd5fE23C85820F7B72D0926FC9b05b43E359b7ee"), 51 | ]; 52 | 53 | sol! { 54 | /// Part of the SimpleStaking interface 55 | interface SimpleStaking { 56 | function getStake(address addr) public view returns (uint256); 57 | function update(address addr, uint256 weight) public; 58 | function verifySigned(bytes32[] memory messageHashes, bytes[] memory signatures) public view returns (uint256); 59 | } 60 | } 61 | 62 | #[tokio::test(flavor = "multi_thread")] 63 | async fn test_multiplexer() -> eyre::Result<()> { 64 | let get_rates_call = getRatesCall { collaterals: COLLATERALS.to_vec() }; 65 | 66 | let public_values = test_e2e( 67 | address!("0A8c00EcFA0816F4f09289ac52Fcb88eA5337526"), 68 | Address::default(), 69 | get_rates_call, 70 | ) 71 | .await?; 72 | 73 | let rates = getRatesCall::abi_decode_returns(&public_values.contractOutput)?; 74 | 75 | println!("rates: {:?}", rates); 76 | 77 | Ok(()) 78 | } 79 | 80 | #[tokio::test(flavor = "multi_thread")] 81 | async fn test_uniswap() -> eyre::Result<()> { 82 | let slot0_call = IUniswapV3PoolState::slot0Call {}; 83 | 84 | let public_values = test_e2e( 85 | address!("1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801"), 86 | Address::default(), 87 | slot0_call, 88 | ) 89 | .await?; 90 | 91 | let _price_x96_bytes = 92 | IUniswapV3PoolState::slot0Call::abi_decode_returns(&public_values.contractOutput)? 93 | .sqrtPriceX96; 94 | 95 | Ok(()) 96 | } 97 | 98 | /// This test goes to the Wrapped Ether contract, and gets the name of the token. 99 | /// This should always be "Wrapped Ether". 100 | #[tokio::test(flavor = "multi_thread")] 101 | async fn test_wrapped_eth() -> eyre::Result<()> { 102 | let name_call = nameCall {}; 103 | 104 | let public_values = test_e2e( 105 | address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), 106 | Address::default(), 107 | name_call, 108 | ) 109 | .await?; 110 | 111 | let name = nameCall::abi_decode_returns(&public_values.contractOutput)?; 112 | assert_eq!(name, String::from("Wrapped Ether")); 113 | 114 | Ok(()) 115 | } 116 | 117 | /// This tests contract creation transactions. 118 | #[tokio::test(flavor = "multi_thread")] 119 | async fn test_contract_creation() -> eyre::Result<()> { 120 | // Load environment variables. 121 | dotenv::dotenv().ok(); 122 | 123 | let bytecode = "0x6080604052348015600e575f5ffd5b50415f5260205ff3fe"; 124 | 125 | // Use `ETH_SEPOLIA_RPC_URL` to get all of the necessary state for the smart contract call. 126 | let rpc_url = std::env::var("ETH_SEPOLIA_RPC_URL") 127 | .unwrap_or_else(|_| panic!("Missing ETH_SEPOLIA_RPC_URL in env")); 128 | 129 | let sketch = EvmSketch::builder() 130 | .at_block(BlockNumberOrTag::Safe) // Get a recent blob to get the hash from. 131 | .with_genesis(Genesis::Sepolia) 132 | .el_rpc_url(Url::parse(&rpc_url)?) 133 | .build() 134 | .await?; 135 | 136 | // Keep track of the block hash. Later, validate the client's execution against this. 137 | let bytes = hex::decode(bytecode).expect("Decoding failed"); 138 | println!("Checking coinbase"); 139 | let _check_coinbase = sketch.create(Address::default(), Bytes::from(bytes)).await?; 140 | Ok(()) 141 | } 142 | 143 | /// Emulates the entire workflow of executing a smart contract call, without using SP1. 144 | /// 145 | /// First, executes the smart contract call with the given [`ContractInput`] in the host executor. 146 | /// After getting the [`EVMStateSketch`] from the host executor, executes the same smart contract 147 | /// call in the client executor. 148 | async fn test_e2e( 149 | contract_address: Address, 150 | caller_address: Address, 151 | calldata: C, 152 | ) -> eyre::Result { 153 | // Load environment variables. 154 | dotenv::dotenv().ok(); 155 | 156 | // Prepare the host executor. 157 | // 158 | // Use `RPC_URL` to get all of the necessary state for the smart contract call. 159 | let rpc_url = std::env::var("ETH_RPC_URL").unwrap_or_else(|_| panic!("Missing RPC_URL")); 160 | let sketch = EvmSketch::builder() 161 | .at_block(BlockNumberOrTag::Latest) // Which block transactions are executed on. 162 | .with_genesis(Genesis::Sepolia) 163 | .el_rpc_url(Url::parse(&rpc_url)?) 164 | .build() 165 | .await?; 166 | 167 | let _contract_output = sketch.call(contract_address, caller_address, calldata.clone()).await?; 168 | 169 | // Now that we've executed all of the calls, get the `EVMStateSketch` from the host executor. 170 | let state_sketch = sketch.finalize().await?; 171 | 172 | let client_executor = ClientExecutor::new(&state_sketch)?; 173 | let contract_input = ContractInput::new_call(contract_address, caller_address, calldata); 174 | 175 | let public_values = client_executor.execute(contract_input)?; 176 | 177 | Ok(public_values) 178 | } 179 | -------------------------------------------------------------------------------- /crates/host-executor/tests/anchor.rs: -------------------------------------------------------------------------------- 1 | use alloy_provider::{network::AnyNetwork, RootProvider}; 2 | use revm_primitives::{b256, uint}; 3 | use sp1_cc_host_executor::{ 4 | AnchorBuilder, BeaconAnchorBuilder, ChainedBeaconAnchorBuilder, HeaderAnchorBuilder, 5 | }; 6 | 7 | #[tokio::test] 8 | async fn test_deneb_beacon_anchor() { 9 | dotenv::dotenv().ok(); 10 | 11 | let eth_rpc_url = 12 | std::env::var("ETH_RPC_URL").unwrap_or_else(|_| panic!("Missing ETH_RPC_URL")); 13 | let beacon_rpc_url = 14 | std::env::var("BEACON_RPC_URL").unwrap_or_else(|_| panic!("Missing BEACON_RPC_URL")); 15 | let provider = RootProvider::::new_http(eth_rpc_url.parse().unwrap()); 16 | 17 | let beacon_anchor_builder = BeaconAnchorBuilder::new( 18 | HeaderAnchorBuilder::new(provider), 19 | beacon_rpc_url.parse().unwrap(), 20 | ); 21 | 22 | let anchor = beacon_anchor_builder.build(22300000).await.unwrap(); 23 | let resolved = anchor.resolve(); 24 | 25 | assert_eq!(resolved.id, uint!(1745028935_U256)); // Timestamp 26 | 27 | assert_eq!( 28 | resolved.hash, 29 | b256!("0xc35d26c08f8e7065e874263f6025b625bca6ed4d3af97da932e5c9be74491ac8") 30 | ) 31 | } 32 | 33 | #[tokio::test] 34 | async fn test_electra_beacon_anchor() { 35 | dotenv::dotenv().ok(); 36 | 37 | let eth_rpc_url = 38 | std::env::var("ETH_RPC_URL").unwrap_or_else(|_| panic!("Missing ETH_RPC_URL")); 39 | let beacon_rpc_url = 40 | std::env::var("BEACON_RPC_URL").unwrap_or_else(|_| panic!("Missing BEACON_RPC_URL")); 41 | let provider = RootProvider::::new_http(eth_rpc_url.parse().unwrap()); 42 | 43 | let beacon_anchor_builder = BeaconAnchorBuilder::new( 44 | HeaderAnchorBuilder::new(provider), 45 | beacon_rpc_url.parse().unwrap(), 46 | ); 47 | 48 | let anchor = beacon_anchor_builder.build(22500000).await.unwrap(); 49 | let resolved = anchor.resolve(); 50 | 51 | assert_eq!(resolved.id, uint!(1747451519_U256)); // Timestamp 52 | 53 | assert_eq!( 54 | resolved.hash, 55 | b256!("0xd1fa05159386e8ee0ef3a158a4e37a0a807de7d7e1e2d016f364cec3efcb88f9") 56 | ) 57 | } 58 | 59 | #[tokio::test] 60 | async fn test_consensus_beacon_anchor() { 61 | dotenv::dotenv().ok(); 62 | 63 | let eth_rpc_url = 64 | std::env::var("ETH_RPC_URL").unwrap_or_else(|_| panic!("Missing ETH_RPC_URL")); 65 | let beacon_rpc_url = 66 | std::env::var("BEACON_RPC_URL").unwrap_or_else(|_| panic!("Missing BEACON_RPC_URL")); 67 | let provider = RootProvider::::new_http(eth_rpc_url.parse().unwrap()); 68 | 69 | let beacon_anchor_builder = BeaconAnchorBuilder::new( 70 | HeaderAnchorBuilder::new(provider), 71 | beacon_rpc_url.parse().unwrap(), 72 | ) 73 | .into_consensus(); 74 | 75 | let anchor = beacon_anchor_builder.build(22500000).await.unwrap(); 76 | let resolved = anchor.resolve(); 77 | 78 | assert_eq!(resolved.id, uint!(11718957_U256)); // Slot 79 | 80 | assert_eq!( 81 | resolved.hash, 82 | b256!("0xd1fa05159386e8ee0ef3a158a4e37a0a807de7d7e1e2d016f364cec3efcb88f9") 83 | ) 84 | } 85 | 86 | #[tokio::test] 87 | async fn test_deneb_chained_beacon_anchor() { 88 | dotenv::dotenv().ok(); 89 | 90 | let eth_rpc_url = 91 | std::env::var("ETH_RPC_URL").unwrap_or_else(|_| panic!("Missing ETH_RPC_URL")); 92 | let beacon_rpc_url = 93 | std::env::var("BEACON_RPC_URL").unwrap_or_else(|_| panic!("Missing BEACON_RPC_URL")); 94 | let provider = RootProvider::::new_http(eth_rpc_url.parse().unwrap()); 95 | 96 | let chained_beacon_anchor_builder = ChainedBeaconAnchorBuilder::new( 97 | BeaconAnchorBuilder::new( 98 | HeaderAnchorBuilder::new(provider), 99 | beacon_rpc_url.parse().unwrap(), 100 | ), 101 | 22350000.into(), 102 | ); 103 | 104 | let anchor = chained_beacon_anchor_builder.build(22300000).await.unwrap(); 105 | let resolved = anchor.resolve(); 106 | 107 | assert_eq!( 108 | resolved.hash, 109 | b256!("0x4315c94f7adbe9ad88608b111ddc5ba2240f087248415b51d172e3e89229ddb7") 110 | ) 111 | } 112 | 113 | #[tokio::test] 114 | async fn test_electra_chained_beacon_anchor() { 115 | dotenv::dotenv().ok(); 116 | 117 | let eth_rpc_url = 118 | std::env::var("ETH_RPC_URL").unwrap_or_else(|_| panic!("Missing ETH_RPC_URL")); 119 | let beacon_rpc_url = 120 | std::env::var("BEACON_RPC_URL").unwrap_or_else(|_| panic!("Missing BEACON_RPC_URL")); 121 | let provider = RootProvider::::new_http(eth_rpc_url.parse().unwrap()); 122 | 123 | let chained_beacon_anchor_builder = ChainedBeaconAnchorBuilder::new( 124 | BeaconAnchorBuilder::new( 125 | HeaderAnchorBuilder::new(provider), 126 | beacon_rpc_url.parse().unwrap(), 127 | ), 128 | 22510000.into(), 129 | ); 130 | 131 | let anchor = chained_beacon_anchor_builder.build(22450000).await.unwrap(); 132 | let resolved = anchor.resolve(); 133 | 134 | assert_eq!( 135 | resolved.hash, 136 | b256!("0x62a94f3faf03493b691de25dda6ecd9150af709a817ebe9e3c6b654aa4b54f81") 137 | ) 138 | } 139 | -------------------------------------------------------------------------------- /examples/events/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "events-client" 3 | description = "" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # workspace 8 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 9 | 10 | # alloy 11 | alloy-primitives.workspace = true 12 | alloy-rpc-types.workspace = true 13 | alloy-sol-types.workspace = true 14 | 15 | # sp1 16 | sp1-zkvm.workspace = true 17 | -------------------------------------------------------------------------------- /examples/events/client/src/lib.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::{address, Address}; 2 | use alloy_sol_types::sol; 3 | 4 | pub const WETH: Address = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); 5 | 6 | sol! { 7 | interface IERC20 { 8 | event Transfer(address indexed from, address indexed to, uint256 value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/events/client/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | sp1_zkvm::entrypoint!(main); 3 | 4 | use alloy_rpc_types::Filter; 5 | use alloy_sol_types::SolEvent; 6 | use events_client::{IERC20, WETH}; 7 | use sp1_cc_client_executor::{io::EvmSketchInput, ClientExecutor}; 8 | 9 | pub fn main() { 10 | let state_sketch = sp1_zkvm::io::read::(); 11 | 12 | // Initialize the client executor with the state sketch. 13 | // This step also validates all of the storage against the provided state root. 14 | let executor = ClientExecutor::new(&state_sketch).unwrap(); 15 | let filter = Filter::new().address(WETH).event(IERC20::Transfer::SIGNATURE); 16 | let logs = executor.get_logs::(filter).unwrap(); 17 | 18 | println!("WETH transfers: {}", logs.len()) 19 | } 20 | -------------------------------------------------------------------------------- /examples/events/host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "events" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # workspace 8 | sp1-cc-host-executor = { path = "../../../crates/host-executor" } 9 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 10 | events-client = { path = "../client" } 11 | 12 | # misc 13 | clap = { version = "4.0", features = ["derive"] } 14 | dotenv.workspace = true 15 | eyre.workspace = true 16 | tokio.workspace = true 17 | url.workspace = true 18 | 19 | # alloy 20 | alloy.workspace = true 21 | alloy-sol-macro.workspace = true 22 | alloy-sol-types.workspace = true 23 | 24 | # sp1 25 | sp1-sdk.workspace = true 26 | 27 | 28 | [build-dependencies] 29 | sp1-build.workspace = true 30 | -------------------------------------------------------------------------------- /examples/events/host/build.rs: -------------------------------------------------------------------------------- 1 | use sp1_build::build_program; 2 | 3 | fn main() { 4 | build_program("../client"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/events/host/src/main.rs: -------------------------------------------------------------------------------- 1 | use alloy::{eips::BlockNumberOrTag, rpc::types::Filter}; 2 | use alloy_sol_types::SolEvent; 3 | use clap::Parser; 4 | use events_client::{IERC20, WETH}; 5 | use sp1_cc_host_executor::EvmSketch; 6 | use sp1_sdk::{include_elf, utils, ProverClient, SP1Stdin}; 7 | use url::Url; 8 | 9 | /// The ELF we want to execute inside the zkVM. 10 | const ELF: &[u8] = include_elf!("events-client"); 11 | 12 | /// The arguments for the command. 13 | #[derive(Parser, Debug)] 14 | #[clap(author, version, about, long_about = None)] 15 | struct Args { 16 | #[clap(long)] 17 | prove: bool, 18 | } 19 | 20 | #[tokio::main] 21 | async fn main() -> eyre::Result<()> { 22 | dotenv::dotenv().ok(); 23 | 24 | // Setup logging. 25 | utils::setup_logger(); 26 | 27 | // Parse the command line arguments. 28 | let args = Args::parse(); 29 | 30 | let block_number = BlockNumberOrTag::Number(22417000); 31 | 32 | let rpc_url = 33 | std::env::var("ETH_RPC_URL").unwrap_or_else(|_| panic!("Missing ETH_RPC_URL in env")); 34 | let mut sketch = EvmSketch::builder() 35 | .at_block(block_number) // Get a recent blob to get the hash from. 36 | .el_rpc_url(Url::parse(&rpc_url)?) 37 | .build() 38 | .await?; 39 | 40 | // Create a `ProverClient`. 41 | let client = ProverClient::from_env(); 42 | let mut stdin = SP1Stdin::new(); 43 | 44 | let filter = Filter::new() 45 | .address(WETH) 46 | .at_block_hash(sketch.anchor.header().hash_slow()) 47 | .event(IERC20::Transfer::SIGNATURE); 48 | 49 | let _ = sketch.get_logs(&filter).await.unwrap(); 50 | 51 | let input = sketch.finalize().await?; 52 | 53 | stdin.write(&input); 54 | 55 | // Execute the program using the `ProverClient.execute` method, without generating a proof. 56 | let (_, report) = client.execute(ELF, &stdin).run().unwrap(); 57 | println!("executed program with {} cycles", report.total_instruction_count()); 58 | 59 | // If the prove flag is not set, we return here. 60 | if !args.prove { 61 | return Ok(()); 62 | } 63 | 64 | // Generate the proof for the given program and input. 65 | let (pk, vk) = client.setup(ELF); 66 | let proof = client.prove(&pk, &stdin).plonk().run().unwrap(); 67 | println!("generated proof"); 68 | 69 | // Verify proof and public values. 70 | client.verify(&proof, &vk).expect("verification failed"); 71 | println!("successfully generated and verified proof for the program!"); 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /examples/example-deploy/host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | version = "0.1.0" 3 | name = "example-deploy" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # workspace 8 | sp1-cc-host-executor = { path = "../../../crates/host-executor" } 9 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 10 | 11 | alloy-primitives.workspace = true 12 | alloy-sol-types.workspace = true 13 | alloy-rpc-types.workspace = true 14 | alloy-sol-macro.workspace = true 15 | alloy-provider.workspace = true 16 | alloy.workspace = true 17 | dotenv.workspace = true 18 | 19 | # misc: 20 | url.workspace = true 21 | tokio.workspace = true 22 | eyre.workspace = true 23 | bincode.workspace = true 24 | -------------------------------------------------------------------------------- /examples/example-deploy/host/src/main.rs: -------------------------------------------------------------------------------- 1 | //! The following contact will be ephemerally deployed to retrieve a coinbase from a block. 2 | //! This trick can be used to retrieve whatever on chain logic without needing to deploy a contract. 3 | //! Just write the information that you want to retrieve on solidity in the constructor and return 4 | //! it. 5 | 6 | use alloy::hex; 7 | use alloy_primitives::{Address, Bytes}; 8 | use alloy_rpc_types::BlockNumberOrTag; 9 | use alloy_sol_types::SolValue; 10 | use sp1_cc_host_executor::{EvmSketch, Genesis}; 11 | use url::Url; 12 | 13 | /// The following bytecode corresponds to the following solidity contract: 14 | /// ```solidity 15 | /// /** 16 | /// * Contract that returns a coinbase 17 | /// */ 18 | /// contract CoinbaseScrapper { 19 | /// /** 20 | /// * Returns the blobHash on index 0 21 | /// */ 22 | /// constructor() { 23 | /// assembly { 24 | /// mstore(0, coinbase()) 25 | /// return(0, 0x20) 26 | /// } 27 | /// } 28 | /// } 29 | /// ``` 30 | const BYTECODE: &str = "0x6080604052348015600e575f5ffd5b50415f5260205ff3fe"; 31 | 32 | #[tokio::main] 33 | async fn main() -> eyre::Result<()> { 34 | dotenv::dotenv().ok(); 35 | 36 | // Use `ETH_SEPOLIA_RPC_URL` to get all of the necessary state for the smart contract call. 37 | let rpc_url = std::env::var("ETH_SEPOLIA_RPC_URL") 38 | .unwrap_or_else(|_| panic!("Missing ETH_SEPOLIA_RPC_URL in env")); 39 | let sketch = EvmSketch::builder() 40 | .at_block(BlockNumberOrTag::Safe) // Get a recent blob to get the hash from. 41 | .with_genesis(Genesis::Sepolia) 42 | .el_rpc_url(Url::parse(&rpc_url)?) 43 | .build() 44 | .await?; 45 | 46 | // Keep track of the block hash. Later, validate the client's execution against this. 47 | let bytes = hex::decode(BYTECODE).expect("Decoding failed"); 48 | println!("Checking coinbase"); 49 | let check_coinbase = sketch.create(Address::default(), Bytes::from(bytes)).await?; 50 | 51 | let decoded_address: Address = Address::abi_decode(&check_coinbase)?; 52 | 53 | println!("Coinbase address: {:?}", decoded_address); 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /examples/multiplexer/ZkOracleHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Helper contract to fetch collateral asset prices 5 | 6 | interface IsfrxETH { 7 | function pricePerShare() external view returns (uint256); 8 | } 9 | 10 | interface IrETH { 11 | function getExchangeRate() external view returns (uint256); 12 | } 13 | 14 | interface IWStETH { 15 | function stEthPerToken() external view returns (uint256); 16 | } 17 | 18 | interface IcbETH { 19 | function exchangeRate() external view returns (uint256); 20 | } 21 | 22 | interface IankrETH { 23 | function sharesToBonds(uint256) external view returns (uint256); 24 | } 25 | 26 | interface IswETH { 27 | function swETHToETHRate() external view returns (uint256); 28 | } 29 | 30 | interface IethxOracle { 31 | function exchangeRate() external view returns (uint256 reportingBlockNumber, uint256 totalETHBalance, uint256 totalETHXSupply); 32 | } 33 | 34 | interface IApxETH { 35 | function assetsPerShare() external view returns (uint256); 36 | } 37 | 38 | interface IPufETH { 39 | function previewRedeem(uint256 amount) external view returns (uint256); 40 | } 41 | 42 | interface IRsETH { 43 | function rsETHPrice() external view returns (uint256); 44 | } 45 | 46 | interface ISUSDe { 47 | function previewRedeem(uint256 amount) external view returns (uint256); 48 | } 49 | 50 | interface IWeETH { 51 | function getRate() external view returns (uint256); 52 | } 53 | 54 | contract ZkOracleHelper { 55 | 56 | address public constant ankrETH = 0xE95A203B1a91a908F9B9CE46459d101078c2c3cb; 57 | address public constant apxEth = 0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6; 58 | address public constant cbETH = 0xBe9895146f7AF43049ca1c1AE358B0541Ea49704; 59 | 60 | address public constant ethx = 0xA35b1B31Ce002FBF2058D22F30f95D405200A15b; 61 | address public constant ethxOracle = 0xF64bAe65f6f2a5277571143A24FaaFDFC0C2a737; 62 | 63 | address public constant ezEth = 0xbf5495Efe5DB9ce00f80364C8B423567e58d2110; 64 | address public constant pufEth = 0xD9A442856C234a39a81a089C06451EBAa4306a72; 65 | 66 | address public constant rETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; 67 | 68 | address public constant rsEth = 0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7; 69 | address public constant rsEthOracle = 0x349A73444b1a310BAe67ef67973022020d70020d; 70 | 71 | address public constant sfrxETH = 0xac3E018457B222d93114458476f3E3416Abbe38F; 72 | address public constant sUSDe = 0x9D39A5DE30e57443BfF2A8307A4256c8797A3497; 73 | 74 | address public constant swETH = 0xf951E335afb289353dc249e82926178EaC7DEd78; 75 | address public constant weEth = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; 76 | address public constant wstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; 77 | 78 | 79 | function getRate(address collateral) public view returns (uint256) { 80 | if (collateral == ankrETH) return IankrETH(ankrETH).sharesToBonds(1e18); 81 | if (collateral == apxEth) return IApxETH(apxEth).assetsPerShare(); 82 | if (collateral == wstETH) return IWStETH(wstETH).stEthPerToken(); 83 | if (collateral == cbETH) return IcbETH(cbETH).exchangeRate(); 84 | if (collateral == ethx) { 85 | (, uint256 totalETHBalance, uint256 totalETHXSupply) = IethxOracle(ethxOracle).exchangeRate(); 86 | return (totalETHBalance * 1e18) / totalETHXSupply; 87 | } 88 | if (collateral == pufEth) return IPufETH(pufEth).previewRedeem(1e18); 89 | if (collateral == rETH) return IrETH(rETH).getExchangeRate(); 90 | if (collateral == rsEth) return IRsETH(rsEthOracle).rsETHPrice(); 91 | if (collateral == sfrxETH) return IsfrxETH(sfrxETH).pricePerShare(); 92 | if (collateral == sUSDe) return ISUSDe(sUSDe).previewRedeem(1e18); 93 | if (collateral == swETH) return IswETH(swETH).swETHToETHRate(); 94 | if (collateral == weEth) return IWeETH(weEth).getRate(); 95 | else revert(); 96 | } 97 | 98 | function getRates(address[] memory collaterals) external view returns (uint256[] memory) { 99 | uint256[] memory rates = new uint256[](collaterals.length); 100 | for (uint256 i = 0; i < collaterals.length; i++) { 101 | rates[i] = getRate(collaterals[i]); 102 | } 103 | return rates; 104 | } 105 | } -------------------------------------------------------------------------------- /examples/multiplexer/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multiplexer-client" 3 | description = "" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # workspace 8 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 9 | 10 | # alloy 11 | alloy-primitives = { workspace = true, features = ["serde"] } 12 | alloy-sol-types.workspace = true 13 | alloy-sol-macro.workspace = true 14 | 15 | # sp1 16 | sp1-zkvm.workspace = true 17 | 18 | # misc 19 | bincode = "1.3.3" 20 | -------------------------------------------------------------------------------- /examples/multiplexer/client/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | sp1_zkvm::entrypoint!(main); 3 | 4 | use alloy_primitives::{address, Address}; 5 | use alloy_sol_macro::sol; 6 | use alloy_sol_types::SolValue; 7 | use sp1_cc_client_executor::{io::EvmSketchInput, ClientExecutor, ContractInput}; 8 | 9 | sol! { 10 | /// Interface to the multiplexer contract. It gets the prices of many tokens, including 11 | /// apxEth, ankrEth, pufEth, and more. 12 | interface IOracleHelper { 13 | function getRates(address[] memory collaterals) external view returns (uint256[] memory); 14 | } 15 | } 16 | 17 | /// Address of the multiplexer contract on Ethereum Mainnet. 18 | const CONTRACT: Address = address!("0A8c00EcFA0816F4f09289ac52Fcb88eA5337526"); 19 | 20 | /// Inputs to the contract call. 21 | const COLLATERALS: [Address; 12] = [ 22 | address!("E95A203B1a91a908F9B9CE46459d101078c2c3cb"), 23 | address!("9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6"), 24 | address!("Be9895146f7AF43049ca1c1AE358B0541Ea49704"), 25 | address!("7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"), 26 | address!("A35b1B31Ce002FBF2058D22F30f95D405200A15b"), 27 | address!("D9A442856C234a39a81a089C06451EBAa4306a72"), 28 | address!("ae78736Cd615f374D3085123A210448E74Fc6393"), 29 | address!("A1290d69c65A6Fe4DF752f95823fae25cB99e5A7"), 30 | address!("ac3E018457B222d93114458476f3E3416Abbe38F"), 31 | address!("9D39A5DE30e57443BfF2A8307A4256c8797A3497"), 32 | address!("f951E335afb289353dc249e82926178EaC7DEd78"), 33 | address!("Cd5fE23C85820F7B72D0926FC9b05b43E359b7ee"), 34 | ]; 35 | 36 | pub fn main() { 37 | // Read the state sketch from stdin. Use this during the execution in order to 38 | // access Ethereum state. 39 | let state_sketch_bytes = sp1_zkvm::io::read::>(); 40 | let state_sketch = bincode::deserialize::(&state_sketch_bytes).unwrap(); 41 | 42 | // Initialize the client executor with the state sketch. 43 | // This step also validates all of the storage against the provided state root. 44 | let executor = ClientExecutor::new(&state_sketch).unwrap(); 45 | 46 | // Execute the getRates call using the client executor. 47 | let calldata = IOracleHelper::getRatesCall { collaterals: COLLATERALS.to_vec() }; 48 | let call = ContractInput::new_call(CONTRACT, Address::default(), calldata); 49 | let public_vals = executor.execute(call).unwrap(); 50 | 51 | // Commit the abi-encoded output. 52 | sp1_zkvm::io::commit_slice(&public_vals.abi_encode()); 53 | } 54 | -------------------------------------------------------------------------------- /examples/multiplexer/host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | version = "0.1.0" 3 | name = "multiplexer" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # workspace 8 | sp1-cc-host-executor = { path = "../../../crates/host-executor" } 9 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 10 | 11 | alloy-primitives.workspace = true 12 | alloy-sol-types.workspace = true 13 | alloy-rpc-types.workspace = true 14 | alloy-sol-macro.workspace = true 15 | alloy-provider.workspace = true 16 | 17 | # misc: 18 | url.workspace = true 19 | tokio.workspace = true 20 | eyre.workspace = true 21 | bincode.workspace = true 22 | dotenv.workspace = true 23 | 24 | # sp1 25 | sp1-sdk.workspace = true 26 | 27 | [build-dependencies] 28 | sp1-build.workspace = true 29 | 30 | [features] 31 | default = [] 32 | cuda = ["sp1-sdk/cuda"] 33 | -------------------------------------------------------------------------------- /examples/multiplexer/host/build.rs: -------------------------------------------------------------------------------- 1 | use sp1_build::{build_program_with_args, BuildArgs}; 2 | 3 | fn main() { 4 | build_program_with_args( 5 | &format!("../{}", "client"), 6 | BuildArgs { ignore_rust_version: true, ..Default::default() }, 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /examples/multiplexer/host/src/main.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::{address, Address}; 2 | use alloy_rpc_types::BlockNumberOrTag; 3 | use alloy_sol_macro::sol; 4 | use alloy_sol_types::{SolCall, SolValue}; 5 | use sp1_cc_client_executor::ContractPublicValues; 6 | use sp1_cc_host_executor::EvmSketch; 7 | use sp1_sdk::{include_elf, utils, ProverClient, SP1Stdin}; 8 | use url::Url; 9 | use IOracleHelper::getRatesCall; 10 | 11 | sol! { 12 | /// Interface to the multiplexer contract. It gets the exchange rates of many tokens, including 13 | /// apxEth, ankrEth, and pufEth. 14 | interface IOracleHelper { 15 | function getRates(address[] memory collaterals) external view returns (uint256[] memory); 16 | } 17 | } 18 | 19 | /// Address of the multiplexer contract on Ethereum Mainnet. 20 | const CONTRACT: Address = address!("0A8c00EcFA0816F4f09289ac52Fcb88eA5337526"); 21 | 22 | /// Inputs to the contract call. 23 | const COLLATERALS: [Address; 12] = [ 24 | address!("E95A203B1a91a908F9B9CE46459d101078c2c3cb"), 25 | address!("9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6"), 26 | address!("Be9895146f7AF43049ca1c1AE358B0541Ea49704"), 27 | address!("7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"), 28 | address!("A35b1B31Ce002FBF2058D22F30f95D405200A15b"), 29 | address!("D9A442856C234a39a81a089C06451EBAa4306a72"), 30 | address!("ae78736Cd615f374D3085123A210448E74Fc6393"), 31 | address!("A1290d69c65A6Fe4DF752f95823fae25cB99e5A7"), 32 | address!("ac3E018457B222d93114458476f3E3416Abbe38F"), 33 | address!("9D39A5DE30e57443BfF2A8307A4256c8797A3497"), 34 | address!("f951E335afb289353dc249e82926178EaC7DEd78"), 35 | address!("Cd5fE23C85820F7B72D0926FC9b05b43E359b7ee"), 36 | ]; 37 | 38 | /// The ELF we want to execute inside the zkVM. 39 | const ELF: &[u8] = include_elf!("multiplexer-client"); 40 | 41 | #[tokio::main] 42 | async fn main() -> eyre::Result<()> { 43 | dotenv::dotenv().ok(); 44 | 45 | // Setup logging. 46 | utils::setup_logger(); 47 | 48 | // Prepare the host executor. 49 | // 50 | // Use `ETH_RPC_URL` to get all of the necessary state for the smart contract call. 51 | let rpc_url = 52 | std::env::var("ETH_RPC_URL").unwrap_or_else(|_| panic!("Missing ETH_RPC_URL in env")); 53 | let sketch = EvmSketch::builder() 54 | .at_block(BlockNumberOrTag::Safe) 55 | .el_rpc_url(Url::parse(&rpc_url)?) 56 | .build() 57 | .await?; 58 | 59 | // Keep track of the block hash. Later, the client's execution will be validated against this. 60 | let block_hash = sketch.anchor.resolve().hash; 61 | 62 | // Describes the call to the getRates function. 63 | let call = IOracleHelper::getRatesCall { collaterals: COLLATERALS.to_vec() }; 64 | 65 | // Call getRates from the host executor. 66 | let _encoded_rates = sketch.call(CONTRACT, Address::default(), call).await?; 67 | 68 | // Now that we've executed all of the calls, get the `EVMStateSketch` from the host executor. 69 | let input = sketch.finalize().await?; 70 | 71 | // Feed the sketch into the client. 72 | let input_bytes = bincode::serialize(&input)?; 73 | let mut stdin = SP1Stdin::new(); 74 | stdin.write(&input_bytes); 75 | 76 | // Create a `ProverClient`. 77 | let client = ProverClient::from_env(); 78 | 79 | // Execute the program using the `ProverClient.execute` method, without generating a proof. 80 | let (_, report) = client.execute(ELF, &stdin).run().unwrap(); 81 | println!("executed program with {} cycles", report.total_instruction_count()); 82 | 83 | // Generate the proof for the given program and input. 84 | let (pk, vk) = client.setup(ELF); 85 | let proof = client.prove(&pk, &stdin).run().unwrap(); 86 | println!("generated proof"); 87 | 88 | // Read the public values, and deserialize them. 89 | let public_vals = ContractPublicValues::abi_decode(proof.public_values.as_slice())?; 90 | 91 | // Read the block hash, and verify that it's the same as the one inputted. 92 | assert_eq!(public_vals.anchorHash, block_hash); 93 | 94 | // Print the fetched rates. 95 | let rates = getRatesCall::abi_decode_returns(&public_vals.contractOutput)?; 96 | println!("Got these rates: \n{:?}", rates); 97 | 98 | // Verify proof and public values. 99 | client.verify(&proof, &vk).expect("verification failed"); 100 | println!("successfully generated and verified proof for the program!"); 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /examples/uniswap/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uniswap-client" 3 | description = "" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # workspace 8 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 9 | 10 | # alloy 11 | alloy-primitives = { workspace = true, features = ["serde"] } 12 | alloy-sol-types.workspace = true 13 | alloy-sol-macro.workspace = true 14 | 15 | # sp1 16 | sp1-zkvm.workspace = true 17 | 18 | # misc 19 | bincode = "1.3.3" 20 | -------------------------------------------------------------------------------- /examples/uniswap/client/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | sp1_zkvm::entrypoint!(main); 3 | 4 | use alloy_primitives::{address, Address}; 5 | use alloy_sol_macro::sol; 6 | use alloy_sol_types::SolValue; 7 | use sp1_cc_client_executor::{io::EvmSketchInput, ClientExecutor, ContractInput, Genesis}; 8 | sol! { 9 | /// Simplified interface of the IUniswapV3PoolState interface. 10 | interface IUniswapV3PoolState { 11 | function slot0( 12 | ) external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked); 13 | } 14 | } 15 | 16 | /// Address of Uniswap V3 pool. 17 | const MAINNET_POOL_CONTRACT: Address = address!("1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801"); 18 | const SEPOLIA_POOL_CONTRACT: Address = address!("3289680dD4d6C10bb19b899729cda5eEF58AEfF1"); 19 | 20 | pub fn main() { 21 | // Read the state sketch from stdin. Use this during the execution in order to 22 | // access Ethereum state. 23 | let state_sketch_bytes = sp1_zkvm::io::read::>(); 24 | let state_sketch = bincode::deserialize::(&state_sketch_bytes).unwrap(); 25 | 26 | let pool_contract = match state_sketch.genesis { 27 | Genesis::Mainnet => MAINNET_POOL_CONTRACT, 28 | Genesis::Sepolia => SEPOLIA_POOL_CONTRACT, 29 | _ => unimplemented!(), 30 | }; 31 | 32 | // Initialize the client executor with the state sketch. 33 | // This step also validates all of the storage against the provided state root. 34 | let executor = ClientExecutor::new(&state_sketch).unwrap(); 35 | 36 | // Execute the slot0 call using the client executor. 37 | let slot0_call = IUniswapV3PoolState::slot0Call {}; 38 | let call = ContractInput::new_call(pool_contract, Address::default(), slot0_call); 39 | let public_vals = executor.execute(call).unwrap(); 40 | 41 | // Commit the abi-encoded output. 42 | sp1_zkvm::io::commit_slice(&public_vals.abi_encode()); 43 | } 44 | -------------------------------------------------------------------------------- /examples/uniswap/contracts/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /examples/uniswap/contracts/README.md: -------------------------------------------------------------------------------- 1 | # Uniswap sample verification contract 2 | 3 | This contract demonstrates how to deserialize a proof, public values, and vkey from a json file, and use them for on chain verification. 4 | 5 | Make sure you have [foundry](https://github.com/foundry-rs/foundry) installed. 6 | 7 | First, run the uniswap example with `cargo run --release --bin uniswap`. This serializes a proof, public values, and a vkey to [`plonk-fixture.json`](./src/fixtures/plonk-fixture.json). 8 | 9 | You can run the sample contract locally using `forge test -vvv`. This deserializes the relevant information from `plonk-fixture.json`, and verifies the proof using the SP1 verifier contract. -------------------------------------------------------------------------------- /examples/uniswap/contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | fs_permissions = [{ access = "read-write", path = "./" }] 6 | -------------------------------------------------------------------------------- /examples/uniswap/contracts/remappings.txt: -------------------------------------------------------------------------------- 1 | @sp1-contracts/=./lib/sp1-contracts/contracts/src/ -------------------------------------------------------------------------------- /examples/uniswap/contracts/src/UniswapCall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {ISP1Verifier} from "@sp1-contracts/ISP1Verifier.sol"; 5 | import {ContractPublicValues, ContractCall} from "@sp1-contracts/v4.0.0-rc.3/utils/ContractCall.sol"; 6 | 7 | /// @title SP1 UniswapCall. 8 | /// @notice This contract implements a simple example of verifying the proof of call to a smart 9 | /// contract. 10 | contract UniswapCall { 11 | using ContractCall for ContractPublicValues; 12 | 13 | /// @notice The address of the SP1 verifier contract. 14 | address public verifier; 15 | 16 | /// @notice The verification key for the uniswapCall program. 17 | bytes32 public uniswapCallProgramVKey; 18 | 19 | constructor(address _verifier, bytes32 _uniswapCallProgramVKey) { 20 | verifier = _verifier; 21 | uniswapCallProgramVKey = _uniswapCallProgramVKey; 22 | } 23 | 24 | /// @notice The entrypoint for verifying the proof of a uniswapCall number. 25 | /// @param _proofBytes The encoded proof. 26 | /// @param _publicValues The encoded public values. 27 | function verifyUniswapCallProof(bytes calldata _publicValues, bytes calldata _proofBytes) 28 | public 29 | view 30 | returns (uint160) 31 | { 32 | ISP1Verifier(verifier).verifyProof(uniswapCallProgramVKey, _publicValues, _proofBytes); 33 | ContractPublicValues memory publicValues = abi.decode(_publicValues, (ContractPublicValues)); 34 | publicValues.verify(); 35 | 36 | uint160 sqrtPriceX96 = abi.decode(publicValues.contractOutput, (uint160)); 37 | return sqrtPriceX96; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/uniswap/contracts/src/fixtures/plonk-fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "vkey": "0x00976b3c18f25b95bc6abc51747acd5c6a4a54470e9af5388e05ffcda0c664cd", 3 | "publicValues": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000013a54c04804cee837fd95195099e56fc5ed546c5982c751d137c4ec3dfed763c9bb491e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d42064fc4beb5f8aaf85f4617ae8b3b5b8bd80100000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000043850c7bd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000cdebb6337bcb5fd82c7f27affffffffffffffffffffffffffffffffffffffffffffffffffffffffffff165f0000000000000000000000000000000000000000000000000000000000000071000000000000000000000000000000000000000000000000000000000000012c000000000000000000000000000000000000000000000000000000000000012c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", 4 | "proof": "0x1b34fe111796212ededc6111f5ebc8b42c35e8be9561919653288b2028783a21c238167112cc9c3efa3ba040b2a357a5d9f9c7fce2146f74450d519176971bf238d5033202c0126857efcf4fb1bbad6caeb3479f0d7543e0a600ffdff922111c89aee05f1e07115ec5db1c09f3c25f2d81e74e021d5b75e29ab264dbd805f759770373301c82fc96e4e956896cb8223964cd320c07488e2015a4c19cda1f453afaf4407901519ca5f17151bef6718c657496cf43a96f549dd42d8e07ca6685d3ff2d2628027a509099846dba11b271a382652defe1efe585cfc6171361588462ea140c8c16f9430c9ec9baf7a64e904c3732aadd150faac6fec6e80d1e529706a0ad60aa072ea175ac6fb9745e5592f3e3b87ffe0af80cc3fb35f0e9636546e66a128d4a08c1e722e2beec33b2b579e336a8d32a2dc8fa2942f8c46e0997476b213c78e500670ba2429778a3254b256534a4041808dd07ee3b29f7a914c8870ce91890d724e0ac5d9204238cd86a47ad09617031361bfcb6b75ecb3608e1a79bd46c9c4c130ee9edc143ff6fd2e7319d8ec06a738a20b84e4fe8e9aad6a712149c9cd47901b3584463aba91c6c134ebfa4ebbde355ad83c2d5a77cefc80c73ad1efa3e5c2e66c0537d1a8d6126745328f90c4bc9fe984c29e435334bdda2320653f2a6b41288e406623d9af0fa8b1d68630121ba17e0c399ae3497caad5e4b320866e2ce236c4e7bef70ea9b6918281fc186ab45cfaa28c585102a982a8fc1ec02f652a51db10ddc68fff2d2dbdd38f4e4cd9b8db3db27251eae41655627c9fb1c53270d1df70b60d0a3e1934468cdef9152de88bfbd763b4f4a196f8851dcb09420df9624e989259d2b32c0dd5beb3aef3663c844a35e45ee8367ef7b47159ad872481b1e089dc5d719769450b13b834a932040c5e928d22458038aaa13caba3569d6b1112954bb43ddd0e614bc05bed91b13cfd800e5902642da9093c07a7157d2963410e01e2382e87db8f325b9241a8f22848ce5b11fa82e41a1e5de3070aa0200ca1c76dc199b4ea6594db37c68d7ebf063b80bff90dcc7cd7bc7ddea61167d360225dc60994d3d2fe35ed3e55a8dc5686e1e62b5f561ac67033a9ae73d21d47ec50a08c02355319516065e278a00f01cac6d6904d129cce3223e96f2095b0220ca1c1d2e49f3883c536d822c14a5bb562308c8dbf1df566ca81f7646355173f842" 5 | } -------------------------------------------------------------------------------- /examples/uniswap/contracts/test/UniswapCall.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.20; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {stdJson} from "forge-std/StdJson.sol"; 6 | import {SP1VerifierGateway} from "@sp1-contracts/SP1VerifierGateway.sol"; 7 | import {UniswapCall} from "../src/UniswapCall.sol"; 8 | 9 | struct SP1ProofFixtureJson { 10 | bytes proof; 11 | bytes publicValues; 12 | bytes32 vkey; 13 | } 14 | 15 | contract UniswapCallTest is Test { 16 | using stdJson for string; 17 | 18 | address verifier; 19 | UniswapCall public uniswapCall; 20 | 21 | function loadFixture() public view returns (SP1ProofFixtureJson memory) { 22 | string memory root = vm.projectRoot(); 23 | string memory path = string.concat(root, "/src/fixtures/plonk-fixture.json"); 24 | string memory json = vm.readFile(path); 25 | bytes memory jsonBytes = json.parseRaw("."); 26 | return abi.decode(jsonBytes, (SP1ProofFixtureJson)); 27 | } 28 | 29 | function setUp() public { 30 | SP1ProofFixtureJson memory fixture = loadFixture(); 31 | verifier = address(new SP1VerifierGateway(address(1))); 32 | uniswapCall = new UniswapCall(verifier, fixture.vkey); 33 | 34 | vm.roll(20600100); 35 | vm.setBlockhash(20600000, 0x4804cee837fd95195099e56fc5ed546c5982c751d137c4ec3dfed763c9bb491e); 36 | } 37 | 38 | function test_ValidUniswapCallProof() public { 39 | SP1ProofFixtureJson memory fixture = loadFixture(); 40 | 41 | vm.mockCall(verifier, abi.encodeWithSelector(SP1VerifierGateway.verifyProof.selector), abi.encode(true)); 42 | 43 | uint160 rate = uniswapCall.verifyUniswapCallProof(fixture.publicValues, fixture.proof); 44 | 45 | console.log(rate); 46 | } 47 | 48 | function test_Revert_InvalidUniswapCallProof() public { 49 | vm.expectRevert(); 50 | 51 | SP1ProofFixtureJson memory fixture = loadFixture(); 52 | 53 | // Create a fake proof. 54 | bytes memory fakeProof = new bytes(fixture.proof.length); 55 | 56 | uniswapCall.verifyUniswapCallProof(fixture.publicValues, fakeProof); 57 | } 58 | } -------------------------------------------------------------------------------- /examples/uniswap/host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | version = "0.1.0" 3 | name = "uniswap" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "uniswap-basic" 8 | path = "src/basic.rs" 9 | 10 | [[bin]] 11 | name = "uniswap-onchain-verify" 12 | path = "src/onchain_verify.rs" 13 | 14 | [dependencies] 15 | # workspace 16 | sp1-cc-host-executor = { path = "../../../crates/host-executor" } 17 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 18 | 19 | alloy-contract.workspace = true 20 | alloy-node-bindings.workspace = true 21 | alloy-primitives.workspace = true 22 | alloy-sol-types.workspace = true 23 | alloy-rpc-types.workspace = true 24 | alloy-sol-macro.workspace = true 25 | alloy-provider.workspace = true 26 | # Alloy host dependencies 27 | alloy.workspace = true 28 | 29 | # misc: 30 | url.workspace = true 31 | tokio.workspace = true 32 | eyre.workspace = true 33 | bincode.workspace = true 34 | serde.workspace = true 35 | serde_json.workspace = true 36 | dotenv.workspace = true 37 | clap = { version = "4.0", features = ["derive"] } 38 | 39 | # sp1 40 | sp1-sdk.workspace = true 41 | 42 | 43 | [build-dependencies] 44 | sp1-build.workspace = true 45 | 46 | 47 | [features] 48 | default = [] 49 | cuda = ["sp1-sdk/cuda"] 50 | 51 | -------------------------------------------------------------------------------- /examples/uniswap/host/build.rs: -------------------------------------------------------------------------------- 1 | use sp1_build::{build_program_with_args, BuildArgs}; 2 | 3 | fn main() { 4 | build_program_with_args( 5 | &format!("../{}", "client"), 6 | BuildArgs { ignore_rust_version: true, ..Default::default() }, 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /examples/uniswap/host/src/basic.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use alloy::hex; 4 | use alloy_primitives::{address, Address}; 5 | use alloy_rpc_types::BlockNumberOrTag; 6 | use alloy_sol_macro::sol; 7 | use alloy_sol_types::{SolCall, SolValue}; 8 | use clap::Parser; 9 | use serde::{Deserialize, Serialize}; 10 | use sp1_cc_client_executor::ContractPublicValues; 11 | use sp1_cc_host_executor::EvmSketch; 12 | use sp1_sdk::{include_elf, utils, HashableKey, ProverClient, SP1ProofWithPublicValues, SP1Stdin}; 13 | use url::Url; 14 | use IUniswapV3PoolState::slot0Call; 15 | 16 | sol! { 17 | /// Simplified interface of the IUniswapV3PoolState interface. 18 | interface IUniswapV3PoolState { 19 | function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked); 20 | } 21 | } 22 | 23 | /// Address of Uniswap V3 pool. 24 | const CONTRACT: Address = address!("1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801"); 25 | 26 | /// The ELF we want to execute inside the zkVM. 27 | const ELF: &[u8] = include_elf!("uniswap-client"); 28 | 29 | /// A fixture that can be used to test the verification of SP1 zkVM proofs inside Solidity. 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | #[serde(rename_all = "camelCase")] 32 | struct SP1CCProofFixture { 33 | vkey: String, 34 | public_values: String, 35 | proof: String, 36 | } 37 | 38 | /// The arguments for the command. 39 | #[derive(Parser, Debug)] 40 | #[clap(author, version, about, long_about = None)] 41 | struct Args { 42 | #[clap(long)] 43 | prove: bool, 44 | 45 | #[clap(long, env)] 46 | eth_rpc_url: Url, 47 | } 48 | 49 | /// Generate a `SP1CCProofFixture`, and save it as a json file. 50 | /// 51 | /// This is useful for verifying the proof of contract call execution on chain. 52 | fn save_fixture(vkey: String, proof: &SP1ProofWithPublicValues) { 53 | let fixture = SP1CCProofFixture { 54 | vkey, 55 | public_values: format!("0x{}", hex::encode(proof.public_values.as_slice())), 56 | proof: format!("0x{}", hex::encode(proof.bytes())), 57 | }; 58 | 59 | let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../contracts/src/fixtures"); 60 | std::fs::create_dir_all(&fixture_path).expect("failed to create fixture path"); 61 | std::fs::write( 62 | fixture_path.join("plonk-fixture.json".to_lowercase()), 63 | serde_json::to_string_pretty(&fixture).unwrap(), 64 | ) 65 | .expect("failed to write fixture"); 66 | } 67 | 68 | #[tokio::main] 69 | async fn main() -> eyre::Result<()> { 70 | dotenv::dotenv().ok(); 71 | 72 | // Setup logging. 73 | utils::setup_logger(); 74 | 75 | // Parse the command line arguments. 76 | let args = Args::parse(); 77 | 78 | // Which block transactions are executed on. 79 | let block_number = BlockNumberOrTag::Number(20600000); 80 | 81 | // Prepare the host executor. 82 | let sketch = 83 | EvmSketch::builder().at_block(block_number).el_rpc_url(args.eth_rpc_url).build().await?; 84 | 85 | // Keep track of the block hash. Later, validate the client's execution against this. 86 | let block_hash = sketch.anchor.resolve().hash; 87 | 88 | // Make the call to the slot0 function. 89 | let slot0_call = IUniswapV3PoolState::slot0Call {}; 90 | let _price_x96_bytes = sketch.call(CONTRACT, Address::default(), slot0_call).await?; 91 | 92 | // Now that we've executed all of the calls, get the `EVMStateSketch` from the host executor. 93 | let input = sketch.finalize().await?; 94 | 95 | // Feed the sketch into the client. 96 | let input_bytes = bincode::serialize(&input)?; 97 | let mut stdin = SP1Stdin::new(); 98 | stdin.write(&input_bytes); 99 | 100 | // Create a `ProverClient`. 101 | let client = ProverClient::from_env(); 102 | 103 | // Execute the program using the `ProverClient.execute` method, without generating a proof. 104 | let (_, report) = client.execute(ELF, &stdin).run().unwrap(); 105 | println!("executed program with {} cycles", report.total_instruction_count()); 106 | 107 | // If the prove flag is not set, we return here. 108 | if !args.prove { 109 | return Ok(()); 110 | } 111 | 112 | // Generate the proof for the given program and input. 113 | let (pk, vk) = client.setup(ELF); 114 | let proof = client.prove(&pk, &stdin).plonk().run().unwrap(); 115 | println!("generated proof"); 116 | 117 | // Read the public values, and deserialize them. 118 | let public_vals = ContractPublicValues::abi_decode(proof.public_values.as_slice())?; 119 | 120 | // Check that the provided block hash matches the one in the proof. 121 | assert_eq!(public_vals.anchorHash, block_hash); 122 | println!("verified block hash"); 123 | 124 | // Read the output, and then calculate the uniswap exchange rate. 125 | // 126 | // Note that this output is read from values commited to in the program using 127 | // `sp1_zkvm::io::commit`. 128 | let sqrt_price_x96 = slot0Call::abi_decode_returns(&public_vals.contractOutput)?.sqrtPriceX96; 129 | let sqrt_price = f64::from(sqrt_price_x96) / 2f64.powi(96); 130 | let price = sqrt_price * sqrt_price; 131 | println!("Proven exchange rate is: {}%", price); 132 | 133 | // Save the proof, public values, and vkey to a json file. 134 | save_fixture(vk.bytes32(), &proof); 135 | println!("saved proof to plonk-fixture.json"); 136 | 137 | // Verify proof and public values. 138 | client.verify(&proof, &vk).expect("verification failed"); 139 | println!("successfully generated and verified proof for the program!"); 140 | Ok(()) 141 | } 142 | -------------------------------------------------------------------------------- /examples/uniswap/host/src/onchain_verify.rs: -------------------------------------------------------------------------------- 1 | use alloy::network::AnyNetwork; 2 | use alloy_primitives::{address, Address}; 3 | use alloy_provider::RootProvider; 4 | use alloy_rpc_types::BlockNumberOrTag; 5 | use alloy_sol_macro::sol; 6 | use alloy_sol_types::SolValue; 7 | use clap::Parser; 8 | use serde::{Deserialize, Serialize}; 9 | use sp1_cc_client_executor::ContractPublicValues; 10 | use sp1_cc_host_executor::{EvmSketch, Genesis}; 11 | use sp1_sdk::{include_elf, utils, ProverClient, SP1Stdin}; 12 | use url::Url; 13 | 14 | /// Address of a Uniswap V3 pool. 15 | const POOL_CONTRACT: Address = address!("3289680dD4d6C10bb19b899729cda5eEF58AEfF1"); 16 | /// Address of the Uniswap verifier contract on Sepolia. 17 | const UNISWAP_CALL_CONTRACT: Address = address!("Bdff74efA64da02Ecb64d07EA5a2BD2A06993F2A"); 18 | 19 | /// The ELF we want to execute inside the zkVM. 20 | const ELF: &[u8] = include_elf!("uniswap-client"); 21 | 22 | sol!( 23 | #[sol(rpc)] 24 | "../contracts/src/UniswapCall.sol" 25 | ); 26 | 27 | sol! { 28 | /// Simplified interface of the IUniswapV3PoolState interface. 29 | interface IUniswapV3PoolState { 30 | function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked); 31 | } 32 | } 33 | 34 | /// A fixture that can be used to test the verification of SP1 zkVM proofs inside Solidity. 35 | #[derive(Debug, Clone, Serialize, Deserialize)] 36 | #[serde(rename_all = "camelCase")] 37 | struct SP1CCProofFixture { 38 | vkey: String, 39 | public_values: String, 40 | proof: String, 41 | } 42 | 43 | /// The arguments for the command. 44 | #[derive(Parser, Debug)] 45 | #[clap(author, version, about, long_about = None)] 46 | struct Args { 47 | #[clap(long)] 48 | execution_block: u64, 49 | 50 | #[clap(long)] 51 | reference_block: Option, 52 | 53 | #[clap(long, env)] 54 | eth_sepolia_rpc_url: Url, 55 | 56 | #[clap(long, env)] 57 | beacon_sepolia_rpc_url: Option, 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() -> eyre::Result<()> { 62 | dotenv::dotenv().ok(); 63 | 64 | // Setup logging. 65 | utils::setup_logger(); 66 | 67 | // Parse the command line arguments. 68 | let args = Args::parse(); 69 | 70 | // Which block transactions are executed on. 71 | let block_number = BlockNumberOrTag::Number(args.execution_block); 72 | 73 | let provider = RootProvider::::new_http(args.eth_sepolia_rpc_url.clone()); 74 | 75 | // Create a `ProverClient`. 76 | let client = ProverClient::from_env(); 77 | 78 | let (pk, _) = client.setup(ELF); 79 | 80 | let contract = UniswapCall::new(UNISWAP_CALL_CONTRACT, provider.clone()); 81 | 82 | // Prepare the sketch. 83 | let sketch_builder = EvmSketch::builder() 84 | .with_genesis(Genesis::Sepolia) 85 | .at_block(block_number) 86 | .el_rpc_url(args.eth_sepolia_rpc_url.clone()); 87 | 88 | let sketch = if let Some(beacon_sepolia_rpc_url) = args.beacon_sepolia_rpc_url { 89 | let sketch_builder = sketch_builder.cl_rpc_url(beacon_sepolia_rpc_url.clone()); 90 | 91 | if let Some(reference_block) = args.reference_block { 92 | let sketch_builder = sketch_builder.at_reference_block(reference_block); 93 | 94 | sketch_builder.build().await? 95 | } else { 96 | sketch_builder.build().await? 97 | } 98 | } else { 99 | sketch_builder.build().await? 100 | }; 101 | 102 | // Keep track of the block root. Later, validate the client's execution against this. 103 | let block_root = sketch.anchor.resolve().hash; 104 | 105 | // Make the call to the slot0 function. 106 | let slot0_call = IUniswapV3PoolState::slot0Call {}; 107 | let _price_x96_bytes = sketch.call(POOL_CONTRACT, Address::default(), slot0_call).await?; 108 | 109 | // Now that we've executed all of the calls, get the `EvmSketchInput` from the sketch. 110 | let input = sketch.finalize().await?; 111 | 112 | // Feed the sketch into the client. 113 | let input_bytes = bincode::serialize(&input)?; 114 | let mut stdin = SP1Stdin::new(); 115 | stdin.write(&input_bytes); 116 | 117 | // Execute the program using the `ProverClient.execute` method, without generating a proof. 118 | let (_, report) = client.execute(ELF, &stdin).run().unwrap(); 119 | println!("executed program with {} cycles", report.total_instruction_count()); 120 | 121 | // Generate the proof for the given program and input. 122 | let proof = client.prove(&pk, &stdin).groth16().run().unwrap(); 123 | println!("generated proof"); 124 | 125 | // Read the public values, and deserialize them. 126 | let public_vals = ContractPublicValues::abi_decode(proof.public_values.as_slice())?; 127 | 128 | // Check that the provided block root matches the one in the proof. 129 | assert_eq!(public_vals.anchorHash, block_root); 130 | println!("verified block root"); 131 | 132 | // Verify onchain. 133 | let sqrt_price_x96 = contract 134 | .verifyUniswapCallProof(proof.public_values.to_vec().into(), proof.bytes().into()) 135 | .call() 136 | .await 137 | .unwrap(); 138 | println!("verified proof onchain"); 139 | 140 | let sqrt_price = f64::from(sqrt_price_x96) / 2f64.powi(96); 141 | let price = sqrt_price * sqrt_price; 142 | println!("Proven exchange rate is: {}%", price); 143 | 144 | Ok(()) 145 | } 146 | -------------------------------------------------------------------------------- /examples/verify-quorum/SimpleStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 5 | 6 | /// @title SimpleStaking 7 | /// @notice This contract models a voting scheme, where each address has some stake. 8 | /// Eventually, when a vote is called, signatures are collected and the total stake 9 | /// corresponding to those signatures is returned. 10 | contract SimpleStaking { 11 | using ECDSA for bytes32; 12 | 13 | mapping(address => uint256) public stakeWeight; 14 | 15 | /// @notice Returns the total stake of an address. 16 | function getStake(address addr) public view returns (uint256) { 17 | return stakeWeight[addr]; 18 | } 19 | 20 | /// @notice Updates the stake of an address. 21 | function update(address addr, uint256 weight) public { 22 | stakeWeight[addr] = weight; 23 | } 24 | 25 | /// @notice Collects signatures over many messages, and returns the total stake corresponding 26 | /// to those signatures. 27 | /// 28 | /// Calling this function onchain could be expensive with a large 29 | /// number of signatures -- in that case, it would be better to prove its execution 30 | /// with SP1. 31 | function verifySigned( 32 | bytes32[] memory messageHashes, 33 | bytes[] memory signatures 34 | ) public view returns (uint256) { 35 | require( 36 | messageHashes.length == signatures.length, 37 | "Input arrays must have the same length" 38 | ); 39 | 40 | uint256 totalStake = 0; 41 | 42 | for (uint i = 0; i < messageHashes.length; i++) { 43 | address recoveredSigner = messageHashes[i].recover(signatures[i]); 44 | totalStake += stakeWeight[recoveredSigner]; 45 | } 46 | 47 | return totalStake; 48 | } 49 | } -------------------------------------------------------------------------------- /examples/verify-quorum/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "verify-quorum-client" 3 | description = "" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # workspace 8 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 9 | 10 | # alloy 11 | alloy-primitives = { workspace = true, features = ["serde"] } 12 | alloy-sol-types.workspace = true 13 | alloy-sol-macro.workspace = true 14 | 15 | # sp1 16 | sp1-zkvm.workspace = true 17 | 18 | # misc 19 | bincode = "1.3.3" 20 | -------------------------------------------------------------------------------- /examples/verify-quorum/client/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | sp1_zkvm::entrypoint!(main); 3 | 4 | use alloy_primitives::{address, Address, Bytes, B256}; 5 | use alloy_sol_macro::sol; 6 | use alloy_sol_types::SolValue; 7 | use sp1_cc_client_executor::{io::EvmSketchInput, ClientExecutor, ContractInput}; 8 | 9 | sol! { 10 | /// Part of the SimpleStaking interface 11 | interface SimpleStaking { 12 | function getStake(address addr) public view returns (uint256); 13 | function update(address addr, uint256 weight) public; 14 | function verifySigned(bytes32[] memory messageHashes, bytes[] memory signatures) public view returns (uint256); 15 | } 16 | } 17 | 18 | /// Address of the SimpleStaking contract on Ethereum Sepolia. 19 | const CONTRACT: Address = address!("C82bbB1719271318282fe332795935f39B89b5cf"); 20 | 21 | pub fn main() { 22 | // Read the state sketch from stdin. Use this during the execution in order to 23 | // access Ethereum state. 24 | let state_sketch_bytes = sp1_zkvm::io::read::>(); 25 | let state_sketch = bincode::deserialize::(&state_sketch_bytes).unwrap(); 26 | 27 | // Read messages and signatures from stdin. 28 | let messages = sp1_zkvm::io::read::>(); 29 | let signatures = sp1_zkvm::io::read::>(); 30 | 31 | // Initialize the client executor with the state sketch. 32 | // This step also validates all of the storage against the provided state root. 33 | let executor = ClientExecutor::new(&state_sketch).unwrap(); 34 | 35 | // Set up the call to `verifySigned`. 36 | let verify_signed_call = ContractInput::new_call( 37 | CONTRACT, 38 | Address::default(), 39 | SimpleStaking::verifySignedCall { messageHashes: messages, signatures }, 40 | ); 41 | 42 | // Execute the call. 43 | let public_vals = executor.execute(verify_signed_call).unwrap(); 44 | 45 | // Commit the result. 46 | sp1_zkvm::io::commit_slice(&public_vals.abi_encode()); 47 | } 48 | -------------------------------------------------------------------------------- /examples/verify-quorum/host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | version = "0.1.0" 3 | name = "verify-quorum" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # workspace 8 | sp1-cc-host-executor = { path = "../../../crates/host-executor" } 9 | sp1-cc-client-executor = { path = "../../../crates/client-executor" } 10 | 11 | alloy-primitives = { workspace = true, features = ["rand"] } 12 | alloy-sol-types.workspace = true 13 | alloy-rpc-types.workspace = true 14 | alloy-sol-macro.workspace = true 15 | alloy-provider.workspace = true 16 | 17 | # reth 18 | reth-primitives.workspace = true 19 | 20 | # misc: 21 | url.workspace = true 22 | tokio.workspace = true 23 | eyre.workspace = true 24 | bincode.workspace = true 25 | dotenv.workspace = true 26 | secp256k1 = { version = "0.29", features = ["recovery", "global-context", "rand"]} 27 | rand_core = "0.6.4" 28 | rand_chacha = "0.3.1" 29 | 30 | # sp1 31 | sp1-sdk.workspace = true 32 | 33 | [build-dependencies] 34 | sp1-build.workspace = true 35 | 36 | [features] 37 | default = [] 38 | cuda = ["sp1-sdk/cuda"] 39 | 40 | -------------------------------------------------------------------------------- /examples/verify-quorum/host/build.rs: -------------------------------------------------------------------------------- 1 | use sp1_build::{build_program_with_args, BuildArgs}; 2 | 3 | fn main() { 4 | build_program_with_args( 5 | &format!("../{}", "client"), 6 | BuildArgs { ignore_rust_version: true, ..Default::default() }, 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /examples/verify-quorum/host/src/main.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::{address, keccak256, Address, Bytes, B256}; 2 | use alloy_rpc_types::BlockNumberOrTag; 3 | use alloy_sol_macro::sol; 4 | use alloy_sol_types::{SolCall, SolValue}; 5 | use rand_chacha::ChaCha20Rng; 6 | use rand_core::{RngCore, SeedableRng}; 7 | use secp256k1::{generate_keypair, Message, PublicKey, SECP256K1}; 8 | use sp1_cc_client_executor::ContractPublicValues; 9 | use sp1_cc_host_executor::{EvmSketch, Genesis}; 10 | use sp1_sdk::{include_elf, utils, ProverClient, SP1Stdin}; 11 | use url::Url; 12 | use SimpleStaking::verifySignedCall; 13 | 14 | sol! { 15 | /// Part of the SimpleStaking interface 16 | interface SimpleStaking { 17 | function getStake(address addr) public view returns (uint256); 18 | function update(address addr, uint256 weight) public; 19 | function verifySigned(bytes32[] memory messageHashes, bytes[] memory signatures) public view returns (uint256); 20 | } 21 | } 22 | 23 | /// Address of the SimpleStaking contract on Ethereum Sepolia. 24 | const CONTRACT: Address = address!("C82bbB1719271318282fe332795935f39B89b5cf"); 25 | 26 | /// The ELF we want to execute inside the zkVM. 27 | const ELF: &[u8] = include_elf!("verify-quorum-client"); 28 | 29 | /// The number of stakers. 30 | const NUM_STAKERS: usize = 3; 31 | 32 | /// The seed for the RNG. 33 | /// 34 | /// Addresses corresponding to those generated by this seed were prepopulated with some stake on 35 | /// Ethereum Sepolia. Changing this seed will result in different addresses, and almost certainly 36 | /// 0 total stake. 37 | const SEED: u64 = 12; 38 | 39 | #[tokio::main] 40 | async fn main() -> eyre::Result<()> { 41 | dotenv::dotenv().ok(); 42 | 43 | // Setup logging. 44 | utils::setup_logger(); 45 | 46 | // The testing rng we use to generate messages and secret keys. 47 | // 48 | // Note: this is deterministic based on the `SEED`. 49 | let mut test_rng = ChaCha20Rng::seed_from_u64(SEED); 50 | 51 | // Prepare the host executor. 52 | // 53 | // Use `ETH_SEPOLIA_RPC_URL` to get all of the necessary state for the smart contract call. 54 | let rpc_url = std::env::var("ETH_SEPOLIA_RPC_URL") 55 | .unwrap_or_else(|_| panic!("Missing ETH_SEPOLIA_RPC_URL in env")); 56 | let sketch = EvmSketch::builder() 57 | .at_block(BlockNumberOrTag::Latest) 58 | .with_genesis(Genesis::Sepolia) 59 | .el_rpc_url(Url::parse(&rpc_url)?) 60 | .build() 61 | .await?; 62 | // Keep track of the block hash. Later, validate the client's execution against this. 63 | let block_hash = sketch.anchor.resolve().hash; 64 | 65 | // Generate messages and signatures, with random (but deterministic) signing keys. 66 | let mut addresses = Vec::with_capacity(NUM_STAKERS); 67 | let mut signatures = Vec::with_capacity(NUM_STAKERS); 68 | let mut messages = Vec::with_capacity(NUM_STAKERS); 69 | 70 | for _ in 0..NUM_STAKERS { 71 | // Generate a random signing key and message, and sign the message with the key. 72 | let (sk, pk) = generate_keypair(&mut test_rng); 73 | let mut message = B256::ZERO; 74 | test_rng.fill_bytes(&mut message.0); 75 | let message_hash = alloy_primitives::keccak256(message); 76 | let signature = SECP256K1.sign_ecdsa_recoverable(&Message::from_digest(*message_hash), &sk); 77 | 78 | // Manually serialize the signature to match the EVM-compatible format 79 | let (id, r_and_s) = signature.serialize_compact(); 80 | let mut signature_bytes = r_and_s.to_vec(); 81 | signature_bytes.push((id.to_i32() as u8) + 27); 82 | let signature_bytes = Bytes::from(signature_bytes); 83 | 84 | // For transparency, print out the address corresponding to the public key of the signing 85 | // key. 86 | let address = public_key_to_address(pk); 87 | println!( 88 | "address: {}\nsignature: {}\nmessage: {}\n", 89 | address, signature_bytes, message_hash 90 | ); 91 | 92 | messages.push(message_hash); 93 | signatures.push(signature_bytes); 94 | addresses.push(address); 95 | } 96 | 97 | // Set up the call to `verifySigned`. 98 | let verify_signed_call = SimpleStaking::verifySignedCall { 99 | messageHashes: messages.clone(), 100 | signatures: signatures.clone(), 101 | }; 102 | 103 | // The host executes the call to `verifySigned`. 104 | let total_stake = sketch.call(CONTRACT, Address::default(), verify_signed_call).await?; 105 | println!("total_stake: {}", total_stake); 106 | 107 | // Now that we've executed the call, get the `EVMStateSketch` from the host executor. 108 | let input = sketch.finalize().await?; 109 | 110 | // Feed the sketch into the client. 111 | let input_bytes = bincode::serialize(&input)?; 112 | let mut stdin = SP1Stdin::new(); 113 | stdin.write(&input_bytes); 114 | 115 | // Additionally write the messages and signatures to stdin. 116 | stdin.write(&messages); 117 | stdin.write(&signatures); 118 | 119 | // Create a `ProverClient`. 120 | let client = ProverClient::from_env(); 121 | 122 | // Execute the program using the `ProverClient.execute` method, without generating a proof. 123 | let (_, report) = client.execute(ELF, &stdin).run().unwrap(); 124 | println!("executed program with {} cycles", report.total_instruction_count()); 125 | 126 | // Generate the proof for the given program and input. 127 | let (pk, vk) = client.setup(ELF); 128 | let proof = client.prove(&pk, &stdin).run().unwrap(); 129 | println!("generated proof"); 130 | 131 | // Read the public values, and deserialize them. 132 | let public_vals = ContractPublicValues::abi_decode(proof.public_values.as_slice())?; 133 | 134 | // Check that the provided block hash matches the one in the proof. 135 | assert_eq!(public_vals.anchorHash, block_hash); 136 | println!("verified block hash"); 137 | 138 | // Read the output, and then calculate the total stake associated with valid signatures. 139 | // 140 | // Note that this output is read from values commited to in the program using 141 | // `sp1_zkvm::io::commit`. 142 | let client_total_stake = verifySignedCall::abi_decode_returns(&public_vals.contractOutput)?; 143 | assert_eq!(client_total_stake, total_stake); 144 | println!("verified total stake calculation"); 145 | 146 | // Verify proof and public values. 147 | client.verify(&proof, &vk).expect("verification failed"); 148 | println!("successfully generated and verified proof for the program!"); 149 | Ok(()) 150 | } 151 | 152 | // Can't use `public_key_to_address()` from `reth_primitives` because Reth depends on 153 | // `secp256k1` 0.30 while this crate is still on 0.29. 154 | pub fn public_key_to_address(public: PublicKey) -> Address { 155 | // Strip out the first byte because that should be the SECP256K1_TAG_PUBKEY_UNCOMPRESSED 156 | // tag returned by libsecp's uncompressed pubkey serialization. 157 | let hash = keccak256(&public.serialize_uncompressed()[1..]); 158 | Address::from_slice(&hash[12..]) 159 | } 160 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85" 3 | components = ["llvm-tools", "rustc-dev"] 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | imports_granularity = "Crate" 3 | use_small_heuristics = "Max" 4 | comment_width = 100 5 | wrap_comments = true 6 | binop_separator = "Back" 7 | trailing_comma = "Vertical" 8 | trailing_semicolon = false 9 | use_field_init_shorthand = true 10 | format_code_in_doc_comments = true 11 | doc_comment_code_block_width = 100 --------------------------------------------------------------------------------