diff --git a/.gitignore b/.gitignore index f4247e182..4479a774c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,10 @@ Cargo.lock methods/guest/Cargo.lock target/ + +# Ignores Foundry Generated files +## Generated ABI files in the /out dir +protocol-units/eth-settlement/contracts/out +protocol-units/eth-settlement/contracts/broadcast +protocol-units/eth-settlement/contracts/broadcast/*/31337/ +protocol-units/eth-settlement/contracts/broadcast/**/dry-run/ diff --git a/.gitmodules b/.gitmodules index 44319d7ed..d2c9efe43 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,9 @@ [submodule "vendors/ed25519-dalek"] path = movement-sdk/vendors/ed25519-dalek url = https://github.com/movemntdev/ed25519-dalek +[submodule "protocol-units/eth-settlement/contracts/lib/forge-std"] + path = protocol-units/eth-settlement/contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "protocol-units/eth-settlement/contracts/lib/openzeppelin-contracts"] + path = protocol-units/eth-settlement/contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/Cargo.toml b/Cargo.toml index 1133acce0..8e71fa4a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace.package] authors = ["Movement Labs"] edition = "2021" +version = "0.1.0" repository = "https://github.com/movemntdev/sdk" license = "Apache-2.0" @@ -8,22 +9,45 @@ license = "Apache-2.0" resolver = "2" members = [ "protocol-units/data-availability", - "protocol-units/zkfp/host", - "protocol-units/zkfp/methods", - "protocol-units/zkfp/methods/guest" + "protocol-units/eth-settlement/eth-adapter", + "protocol-units/eth-settlement/eth-publisher", + "protocol-units/eth-settlement/methods", + "protocol-units/eth-settlement/contracts", + "protocol-units/clients/eth", + "tests/integration/methods", + "tests/integration", + ] exclude = [ + "protocol-units/zkfp/host", + "protocol-units/zkfp/methods", + "protocol-units/zkfp/methods/guest", "shared-sequencer/movement-sequencer" ] [workspace.dependencies] anyhow = "1.0" +async-trait = { version = "0.1" } +alloy-primitives = { version = "0.6", default-features = false, features = ["rlp", "serde", "std"] } +alloy-sol-types = { version = "0.6" } +bincode = { version = "1.3" } +tokio = { version = "1", features = ["full"] } # To try (experimental) std support, add `features = [ "std" ]` to risc0-zkvm -risc0-zkvm = { version = "0.20.1", features = ["std"] } +tracing = "0.1.40" tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = "1.0" +serde_json = "1.0.68" +log = { version = "0.4" } tempfile = "3.2.0" +ethers = { version = "2.0" } +hex = "0.4.3" +bytemuck = { version = "1.14" } + +# Sovereign Labs dependencies +sov-rollup-interface = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", branch = "nightly", features = ["native"] } +sov-mock-da = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", branch = "nightly", features = ["native"] } +web3 = "0.19.0" # Move dependencies move-vm-runtime = { path = "protocol-units/zkfp/vendors/move/language/move-vm/runtime" } @@ -34,6 +58,20 @@ move-core-types = { path = "protocol-units/zkfp/vendors/move/language/move-core/ move-compiler = { path = "protocol-units/zkfp/vendors/move/language/move-compiler" } move-stdlib = { path = "protocol-units/zkfp/vendors/move/language/move-stdlib" } +# Risc0 dependencies +# NOTE: Using a git rev temporarily to get an unreleased version of risc0-build. +# Once the referenced commit is in a released version of risc0-build, this will go back to using a version. +bonsai-rest-api-mock = { version = "0.6.1", default-features = false } +bonsai-sdk = { version = "0.6.1", default-features = false } +risc0-build = { git = "https://github.com/risc0/risc0", rev = "7f731662", features = ["docker"] } +risc0-build-ethereum = { git = "https://github.com/risc0/risc0-ethereum", branch = "release-0.7" } +risc0-ethereum-relay= { git = "https://github.com/risc0/risc0-ethereum", branch = "release-0.7" } +risc0-ethereum-contracts = { git = "https://github.com/risc0/risc0-ethereum", branch = "release-0.7" } +risc0-zkvm = { version = "0.20.1", features = ["std"] } +risc0-zkp = { version = "0.20", default-features = false } + +eth-relay-test-methods = { path = "./tests/integration/methods" } + # Always optimize; building and running the guest takes much longer without optimization. [profile.dev] diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 000000000..5c5a9c37e --- /dev/null +++ b/foundry.toml @@ -0,0 +1,8 @@ +[profile.default] +src = "contracts" +out = "out" +libs = ["lib"] +test = "tests" +ffi = true + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/protocol-units/Cargo.toml b/protocol-units/Cargo.toml deleted file mode 100644 index adc9cf17c..000000000 --- a/protocol-units/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "protocol-units" -version = "0.1.0" -edition = "2021" -description = "Protocol Units for the Movement SDK" - -[[bin]] -name = "data-availability" -path = "data-availability/src/main.rs" - -[[bin]] -name = "host" -path = "zkfp/host/src/main.rs" - -[[bin]] -name = "methods" -path = "zkfp/methods/src/main.rs" - -[[bin]] -name = "guest" -path = "zkfp/guest/src/main.rs" - -[dependencies] -anyhow = "1.0" -risc0-zkvm = { version = "0.20.1", features = ["std"] } -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tempfile = "3.2.0" -serde = "1.0" - -methods = { path = "../methods" } - -# Move dependencies. -move-vm-runtime = { workspace = true } -move-vm-test-utils = { workspace = true } -move-binary-format = { workspace = true } -move-vm-types = { workspace = true } -move-core-types = { workspace = true } -move-compiler = { workspace = true } -move-stdlib = { workspace = true } \ No newline at end of file diff --git a/protocol-units/clients/eth/Cargo.toml b/protocol-units/clients/eth/Cargo.toml new file mode 100644 index 000000000..8d0113f73 --- /dev/null +++ b/protocol-units/clients/eth/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "eth-client" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +ethers = { workspace = true } +hex = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } + +risc0-ethereum-relay = { workspace = true } \ No newline at end of file diff --git a/protocol-units/clients/eth/src/lib.rs b/protocol-units/clients/eth/src/lib.rs new file mode 100644 index 000000000..535bf36bc --- /dev/null +++ b/protocol-units/clients/eth/src/lib.rs @@ -0,0 +1,119 @@ +use std::{str::FromStr, time::Duration}; +use anyhow::{anyhow, Context, Error, Result}; +use ethers::{ + core::k256::{ecdsa::SigningKey, SecretKey}, + middleware::SignerMiddleware, + prelude::*, + providers::{Provider, Ws}, +}; +use risc0_ethereum_relay::EthersClientConfig as Risc0EthersClientConfig; +use tracing::{debug, error}; + +/// We use this in combination with `EthersClientConfig` to +/// create the client. An example of this can be found in tests/integration/src/lib.rs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WalletKey(SecretKey); + +impl TryFrom for WalletKey { + type Error = Error; + fn try_from(value: String) -> Result { + let decoded = + hex::decode(value.trim_start_matches("0x")).context("Failed to decode private key.")?; + let key = + SecretKey::from_slice(&decoded).context("Failed to derive SecretKey instance.")?; + Ok(Self(key)) + } +} + +impl FromStr for WalletKey { + type Err = Error; + fn from_str(s: &str) -> Result { + s.to_string().try_into() + } +} + +impl From for WalletKey { + fn from(value: SecretKey) -> Self { + Self(value) + } +} + +impl From for SecretKey { + fn from(value: WalletKey) -> Self { + value.0 + } +} + +impl WalletKey { + pub fn get_key(&self) -> SecretKey { + self.0.clone() + } +} + +type EthersClientConfig = Risc0EthersClientConfig; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EthersClientConfig { + pub eth_node_url: String, + pub eth_chain_id: u64, + pub wallet_key_identifier: WalletKey, + pub retries: u64, + pub wait_time: Duration, +} + +impl EthersClientConfig { + pub fn new( + eth_node_url: String, + eth_chain_id: u64, + wallet_key_identifier: WalletKey, + retries: u64, + wait_time: Duration, + ) -> Self { + Self { + eth_node_url, + eth_chain_id, + wallet_key_identifier, + retries, + wait_time, + } + } + + pub async fn get_client(&self) -> Result, Wallet>> { + let provider = self.provider().await?; + let signer = self.get_signer()?; + let client = SignerMiddleware::new(provider, signer); + Ok(client) + } + + pub async fn provider(&self) -> Result> { + let provider = Provider::::connect_with_reconnects(self.eth_node_url.clone(), 60) + .await + .context("Failed to connect to Ethereum node.")?; + Ok(provider) + } + + pub fn get_signer(&self) -> Result> { + let signing_key = SigningKey::from(self.wallet_key_identifier.get_key()); + let signer = LocalWallet::from(signing_key).with_chain_id(self.eth_chain_id); + Ok(signer) + } + + pub async fn get_client_with_reconnects( + &self, + ) -> Result, Wallet>> { + for _ in 0..self.retries { + let client = self.get_client().await; + if client.is_ok() { + return client; + } else { + debug!( + "Failed to create client. Retrying in {:?} seconds.", + self.wait_time.as_secs() + ); + tokio::time::sleep(self.wait_time).await; + } + } + error!("Failed to create client."); + Err(anyhow!("Failed to create client.")) + } +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/README.md b/protocol-units/eth-settlement/README.md new file mode 100644 index 000000000..d0019ecd8 --- /dev/null +++ b/protocol-units/eth-settlement/README.md @@ -0,0 +1,33 @@ +# `m2` + +## `eth-settlement` +To run the tests, first install the following dependencies: +- foundry + +Start the Anvil local network: +```bash +anvil +``` + +Deploy the contracts in `contracts/eth-settlement`: +```bash +# from root of the project +cd contracts/eth-settlement +forge script script/DeploySettlement.s.sol --broadcast --rpc-url http://localhost:8545 --private-key +``` + +**Note**: Never use the private key above in production. + +Set your environment variables: +```bash +ETH_RPC_URL=http://localhost:8545 +ETH_CONTRACT_ADDRESS= +ETH_CONTRACT_ABI_PATH=../contracts/eth-settlement/out/Settlement.sol/Settlement.json +``` + +Run the tests: +```bash +# from root of the project +cd eth-settlement +cargo test test_eth_settlement_service_env +``` \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/Cargo.toml b/protocol-units/eth-settlement/contracts/Cargo.toml new file mode 100644 index 000000000..062aab9c5 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "eth-contracts" +version = "0.1.0" +edition = "2021" +description = "Movement Labs Contracts for Eth Settlement with Risc0 Groth16 Bonsai service" + +[lib] +path = "src/lib.rs" + +[dependencies] +ethers = { workspace = true } \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/ImageID.sol b/protocol-units/eth-settlement/contracts/ImageID.sol new file mode 100644 index 000000000..ee1f90aa7 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/ImageID.sol @@ -0,0 +1,23 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// This file is automatically generated + +pragma solidity ^0.8.20; + +library ImageID { + bytes32 public constant IS_EVEN_ID = bytes32(0x04d6ce2d1c7ffa390e73114c5213f76aada67c8301d88b7698a48fd7543a444b); +} diff --git a/protocol-units/eth-settlement/contracts/README.md b/protocol-units/eth-settlement/contracts/README.md new file mode 100644 index 000000000..9265b4558 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/protocol-units/eth-settlement/contracts/cache/solidity-files-cache.json b/protocol-units/eth-settlement/contracts/cache/solidity-files-cache.json new file mode 100644 index 000000000..9933c3363 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/cache/solidity-files-cache.json @@ -0,0 +1 @@ +{"_format":"ethers-rs-sol-cache-3","paths":{"artifacts":"out","build_infos":"out/build-info","sources":"src","tests":"test","scripts":"script","libraries":["lib"]},"files":{"lib/forge-std/lib/ds-test/src/test.sol":{"lastModificationDate":1710184601246,"contentHash":"9febff9d09f18af5306669dc276c4c43","sourceName":"lib/forge-std/lib/ds-test/src/test.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.5.0","artifacts":{"DSTest":{"0.8.24+commit.e11b9ed9.Linux.gcc":"test.sol/DSTest.json"}}},"lib/forge-std/src/Base.sol":{"lastModificationDate":1710184601246,"contentHash":"ee13c050b1914464f1d3f90cde90204b","sourceName":"lib/forge-std/src/Base.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/StdStorage.sol","lib/forge-std/src/Vm.sol"],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"CommonBase":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Base.sol/CommonBase.json"},"ScriptBase":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Base.sol/ScriptBase.json"},"TestBase":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Base.sol/TestBase.json"}}},"lib/forge-std/src/Script.sol":{"lastModificationDate":1710184601246,"contentHash":"ba325c778a7da8a21c2136aa32763c14","sourceName":"lib/forge-std/src/Script.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/Base.sol","lib/forge-std/src/StdChains.sol","lib/forge-std/src/StdCheats.sol","lib/forge-std/src/StdJson.sol","lib/forge-std/src/StdMath.sol","lib/forge-std/src/StdStorage.sol","lib/forge-std/src/StdStyle.sol","lib/forge-std/src/StdUtils.sol","lib/forge-std/src/Vm.sol","lib/forge-std/src/console.sol","lib/forge-std/src/console2.sol","lib/forge-std/src/interfaces/IMulticall3.sol","lib/forge-std/src/mocks/MockERC20.sol","lib/forge-std/src/mocks/MockERC721.sol","lib/forge-std/src/safeconsole.sol"],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"Script":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Script.sol/Script.json"}}},"lib/forge-std/src/StdAssertions.sol":{"lastModificationDate":1710184601246,"contentHash":"6cc2858240bcd443debbbf075490e325","sourceName":"lib/forge-std/src/StdAssertions.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/lib/ds-test/src/test.sol","lib/forge-std/src/StdMath.sol"],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"StdAssertions":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdAssertions.sol/StdAssertions.json"}}},"lib/forge-std/src/StdChains.sol":{"lastModificationDate":1710184601250,"contentHash":"f7ec55b5333e3d0687535fa29695e9c6","sourceName":"lib/forge-std/src/StdChains.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/Vm.sol"],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"StdChains":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdChains.sol/StdChains.json"}}},"lib/forge-std/src/StdCheats.sol":{"lastModificationDate":1710184601250,"contentHash":"7922ae0087a21ee3cdb97137be18c06c","sourceName":"lib/forge-std/src/StdCheats.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/StdStorage.sol","lib/forge-std/src/Vm.sol","lib/forge-std/src/console2.sol"],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"StdCheats":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdCheats.sol/StdCheats.json"},"StdCheatsSafe":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdCheats.sol/StdCheatsSafe.json"}}},"lib/forge-std/src/StdError.sol":{"lastModificationDate":1710184601250,"contentHash":"64c896e1276a291776e5ea5aecb3870a","sourceName":"lib/forge-std/src/StdError.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"stdError":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdError.sol/stdError.json"}}},"lib/forge-std/src/StdInvariant.sol":{"lastModificationDate":1710184601250,"contentHash":"0a580d6fac69e9d4b6504f747f3c0c24","sourceName":"lib/forge-std/src/StdInvariant.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"StdInvariant":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdInvariant.sol/StdInvariant.json"}}},"lib/forge-std/src/StdJson.sol":{"lastModificationDate":1710184601250,"contentHash":"a341308627b7eeab7589534daad58186","sourceName":"lib/forge-std/src/StdJson.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/Vm.sol"],"versionRequirement":">=0.6.0, <0.9.0","artifacts":{"stdJson":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdJson.sol/stdJson.json"}}},"lib/forge-std/src/StdMath.sol":{"lastModificationDate":1710184601250,"contentHash":"9da8f453eba6bb98f3d75bc6822bfb29","sourceName":"lib/forge-std/src/StdMath.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"stdMath":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdMath.sol/stdMath.json"}}},"lib/forge-std/src/StdStorage.sol":{"lastModificationDate":1710184601250,"contentHash":"abd6f3379e6e2a7979b18abc21aea0c1","sourceName":"lib/forge-std/src/StdStorage.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/Vm.sol"],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"stdStorage":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdStorage.sol/stdStorage.json"},"stdStorageSafe":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdStorage.sol/stdStorageSafe.json"}}},"lib/forge-std/src/StdStyle.sol":{"lastModificationDate":1710184601250,"contentHash":"6281165a12aa639705c691fccefd855e","sourceName":"lib/forge-std/src/StdStyle.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/Vm.sol"],"versionRequirement":">=0.4.22, <0.9.0","artifacts":{"StdStyle":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdStyle.sol/StdStyle.json"}}},"lib/forge-std/src/StdUtils.sol":{"lastModificationDate":1710184601250,"contentHash":"2ace460f60242ec59c9310db966aee97","sourceName":"lib/forge-std/src/StdUtils.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/Vm.sol","lib/forge-std/src/interfaces/IMulticall3.sol","lib/forge-std/src/mocks/MockERC20.sol","lib/forge-std/src/mocks/MockERC721.sol"],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"StdUtils":{"0.8.24+commit.e11b9ed9.Linux.gcc":"StdUtils.sol/StdUtils.json"}}},"lib/forge-std/src/Test.sol":{"lastModificationDate":1710184601250,"contentHash":"15866901137b5670eabf31362523bd28","sourceName":"lib/forge-std/src/Test.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/lib/ds-test/src/test.sol","lib/forge-std/src/Base.sol","lib/forge-std/src/StdAssertions.sol","lib/forge-std/src/StdChains.sol","lib/forge-std/src/StdCheats.sol","lib/forge-std/src/StdError.sol","lib/forge-std/src/StdInvariant.sol","lib/forge-std/src/StdJson.sol","lib/forge-std/src/StdMath.sol","lib/forge-std/src/StdStorage.sol","lib/forge-std/src/StdStyle.sol","lib/forge-std/src/StdUtils.sol","lib/forge-std/src/Vm.sol","lib/forge-std/src/console.sol","lib/forge-std/src/console2.sol","lib/forge-std/src/interfaces/IMulticall3.sol","lib/forge-std/src/mocks/MockERC20.sol","lib/forge-std/src/mocks/MockERC721.sol","lib/forge-std/src/safeconsole.sol"],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"Test":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Test.sol/Test.json"}}},"lib/forge-std/src/Vm.sol":{"lastModificationDate":1710184601250,"contentHash":"c791ff15c3c9d600bd396c7ae4ee836c","sourceName":"lib/forge-std/src/Vm.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"Vm":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Vm.sol/Vm.json"},"VmSafe":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Vm.sol/VmSafe.json"}}},"lib/forge-std/src/console.sol":{"lastModificationDate":1710184601250,"contentHash":"100b8a33b917da1147740d7ab8b0ded3","sourceName":"lib/forge-std/src/console.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.4.22, <0.9.0","artifacts":{"console":{"0.8.24+commit.e11b9ed9.Linux.gcc":"console.sol/console.json"}}},"lib/forge-std/src/console2.sol":{"lastModificationDate":1710184601250,"contentHash":"491ca717c1915995e78cc361485a3067","sourceName":"lib/forge-std/src/console2.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.4.22, <0.9.0","artifacts":{"console2":{"0.8.24+commit.e11b9ed9.Linux.gcc":"console2.sol/console2.json"}}},"lib/forge-std/src/interfaces/IMulticall3.sol":{"lastModificationDate":1710184601254,"contentHash":"7b131ca1ca32ef6378b7b9ad5488b901","sourceName":"lib/forge-std/src/interfaces/IMulticall3.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"IMulticall3":{"0.8.24+commit.e11b9ed9.Linux.gcc":"IMulticall3.sol/IMulticall3.json"}}},"lib/forge-std/src/mocks/MockERC20.sol":{"lastModificationDate":1710184601254,"contentHash":"9ec848e74c56e40afdd01ac0c873dc3b","sourceName":"lib/forge-std/src/mocks/MockERC20.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"MockERC20":{"0.8.24+commit.e11b9ed9.Linux.gcc":"MockERC20.sol/MockERC20.json"}}},"lib/forge-std/src/mocks/MockERC721.sol":{"lastModificationDate":1710184601254,"contentHash":"b6729ea581dcd9b4238eee9ca4d3bd89","sourceName":"lib/forge-std/src/mocks/MockERC721.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"IERC721TokenReceiver":{"0.8.24+commit.e11b9ed9.Linux.gcc":"MockERC721.sol/IERC721TokenReceiver.json"},"MockERC721":{"0.8.24+commit.e11b9ed9.Linux.gcc":"MockERC721.sol/MockERC721.json"}}},"lib/forge-std/src/safeconsole.sol":{"lastModificationDate":1710184601254,"contentHash":"ac3b1bf5a444db5db3656021830258a8","sourceName":"lib/forge-std/src/safeconsole.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.6.2, <0.9.0","artifacts":{"safeconsole":{"0.8.24+commit.e11b9ed9.Linux.gcc":"safeconsole.sol/safeconsole.json"}}},"lib/openzeppelin-contracts/contracts/utils/math/SafeCast.sol":{"lastModificationDate":1710189618514,"contentHash":"d72cdfaacd4b1b8e090b57f4b7200ddc","sourceName":"lib/openzeppelin-contracts/contracts/utils/math/SafeCast.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":"^0.8.20","artifacts":{"SafeCast":{"0.8.24+commit.e11b9ed9.Linux.gcc":"SafeCast.sol/SafeCast.json"}}},"script/DeploySettlement.s.sol":{"lastModificationDate":1710184601322,"contentHash":"e6c700191d1c2db9f56e0c10f6658390","sourceName":"script/DeploySettlement.s.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/src/Base.sol","lib/forge-std/src/Script.sol","lib/forge-std/src/StdChains.sol","lib/forge-std/src/StdCheats.sol","lib/forge-std/src/StdJson.sol","lib/forge-std/src/StdMath.sol","lib/forge-std/src/StdStorage.sol","lib/forge-std/src/StdStyle.sol","lib/forge-std/src/StdUtils.sol","lib/forge-std/src/Vm.sol","lib/forge-std/src/console.sol","lib/forge-std/src/console2.sol","lib/forge-std/src/interfaces/IMulticall3.sol","lib/forge-std/src/mocks/MockERC20.sol","lib/forge-std/src/mocks/MockERC721.sol","lib/forge-std/src/safeconsole.sol","lib/openzeppelin-contracts/contracts/utils/math/SafeCast.sol","src/ControlID.sol","src/EvenNumber.sol","src/Groth16Verifier.sol","src/IRiscZeroVerifier.sol","src/ImageID.sol","src/Settlement.sol"],"versionRequirement":"^0.8.20","artifacts":{"DeploySettlement":{"0.8.24+commit.e11b9ed9.Linux.gcc":"DeploySettlement.s.sol/DeploySettlement.json"}}},"src/ControlID.sol":{"lastModificationDate":1710184601322,"contentHash":"9aa449c2b9d9bb77e7b195a3e14044bd","sourceName":"src/ControlID.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":"^0.8.9","artifacts":{"ControlID":{"0.8.24+commit.e11b9ed9.Linux.gcc":"ControlID.sol/ControlID.json"}}},"src/Counter.sol":{"lastModificationDate":1710265219680,"contentHash":"82a2eeb4cd786f838af9d2869e16bde9","sourceName":"src/Counter.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["src/IBonsaiRelay.sol"],"versionRequirement":"^0.8.17","artifacts":{"Counter":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Counter.sol/Counter.json"}}},"src/EvenNumber.sol":{"lastModificationDate":1710184601322,"contentHash":"f1def6e0c0ef041b83516b9f17d1c5af","sourceName":"src/EvenNumber.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["src/IRiscZeroVerifier.sol","src/ImageID.sol"],"versionRequirement":"^0.8.20","artifacts":{"EvenNumber":{"0.8.24+commit.e11b9ed9.Linux.gcc":"EvenNumber.sol/EvenNumber.json"}}},"src/Groth16Verifier.sol":{"lastModificationDate":1710184601322,"contentHash":"ec6c58755b517b222f23303b057b37da","sourceName":"src/Groth16Verifier.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":">=0.7.0, <0.9.0","artifacts":{"Groth16Verifier":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Groth16Verifier.sol/Groth16Verifier.json"}}},"src/IBonsaiRelay.sol":{"lastModificationDate":1710265203913,"contentHash":"b91d71c3229d1b318a146a79fe9b418d","sourceName":"src/IBonsaiRelay.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":"^0.8.17","artifacts":{"IBonsaiRelay":{"0.8.24+commit.e11b9ed9.Linux.gcc":"IBonsaiRelay.sol/IBonsaiRelay.json"}}},"src/IRiscZeroVerifier.sol":{"lastModificationDate":1710184601322,"contentHash":"b826d2ccadd9d82f334ae1fb458eb020","sourceName":"src/IRiscZeroVerifier.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":"^0.8.9","artifacts":{"IRiscZeroVerifier":{"0.8.24+commit.e11b9ed9.Linux.gcc":"IRiscZeroVerifier.sol/IRiscZeroVerifier.json"},"OutputLib":{"0.8.24+commit.e11b9ed9.Linux.gcc":"IRiscZeroVerifier.sol/OutputLib.json"},"ReceiptClaimLib":{"0.8.24+commit.e11b9ed9.Linux.gcc":"IRiscZeroVerifier.sol/ReceiptClaimLib.json"}}},"src/ImageID.sol":{"lastModificationDate":1710263466372,"contentHash":"b930470b51ada7436fa0f91fc7b1d429","sourceName":"src/ImageID.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":"^0.8.20","artifacts":{"ImageID":{"0.8.24+commit.e11b9ed9.Linux.gcc":"ImageID.sol/ImageID.json"}}},"src/Settlement.sol":{"lastModificationDate":1710184601322,"contentHash":"65b7a5950fb52edd67def8a9e6f25cba","sourceName":"src/Settlement.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/openzeppelin-contracts/contracts/utils/math/SafeCast.sol","src/Groth16Verifier.sol","src/IRiscZeroVerifier.sol"],"versionRequirement":"^0.8.0","artifacts":{"Settlement":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Settlement.sol/Settlement.json"}}},"test/Elf.sol":{"lastModificationDate":1710263466380,"contentHash":"7f8f4b8eb9a553762376c5ff3ee7a16e","sourceName":"test/Elf.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":"^0.8.20","artifacts":{"Elf":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Elf.sol/Elf.json"}}},"test/Settlement.t.sol":{"lastModificationDate":1710184601322,"contentHash":"28c3df212386b5b508204c98f091ea4a","sourceName":"test/Settlement.t.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":["lib/forge-std/lib/ds-test/src/test.sol","lib/forge-std/src/Base.sol","lib/forge-std/src/StdAssertions.sol","lib/forge-std/src/StdChains.sol","lib/forge-std/src/StdCheats.sol","lib/forge-std/src/StdError.sol","lib/forge-std/src/StdInvariant.sol","lib/forge-std/src/StdJson.sol","lib/forge-std/src/StdMath.sol","lib/forge-std/src/StdStorage.sol","lib/forge-std/src/StdStyle.sol","lib/forge-std/src/StdUtils.sol","lib/forge-std/src/Test.sol","lib/forge-std/src/Vm.sol","lib/forge-std/src/console.sol","lib/forge-std/src/console2.sol","lib/forge-std/src/interfaces/IMulticall3.sol","lib/forge-std/src/mocks/MockERC20.sol","lib/forge-std/src/mocks/MockERC721.sol","lib/forge-std/src/safeconsole.sol","lib/openzeppelin-contracts/contracts/utils/math/SafeCast.sol","src/ControlID.sol","src/Groth16Verifier.sol","src/IRiscZeroVerifier.sol","src/Settlement.sol","test/TestReceipt.sol"],"versionRequirement":"^0.8.0","artifacts":{"SettlementTest":{"0.8.24+commit.e11b9ed9.Linux.gcc":"Settlement.t.sol/SettlementTest.json"}}},"test/TestReceipt.sol":{"lastModificationDate":1710184601322,"contentHash":"b95742b4a0bb6ee688f65b8c551088a8","sourceName":"test/TestReceipt.sol","solcConfig":{"settings":{"optimizer":{"enabled":true,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"paris","viaIR":false,"libraries":{}}},"imports":[],"versionRequirement":"^0.8.13","artifacts":{"TestReceipt":{"0.8.24+commit.e11b9ed9.Linux.gcc":"TestReceipt.sol/TestReceipt.json"}}}}} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/foundry.toml b/protocol-units/eth-settlement/contracts/foundry.toml new file mode 100644 index 000000000..25b918f9c --- /dev/null +++ b/protocol-units/eth-settlement/contracts/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/protocol-units/eth-settlement/contracts/lib/forge-std b/protocol-units/eth-settlement/contracts/lib/forge-std new file mode 160000 index 000000000..ae570fec0 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce diff --git a/protocol-units/eth-settlement/contracts/lib/openzeppelin-contracts b/protocol-units/eth-settlement/contracts/lib/openzeppelin-contracts new file mode 160000 index 000000000..dbb6104ce --- /dev/null +++ b/protocol-units/eth-settlement/contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/protocol-units/eth-settlement/contracts/remappings.txt b/protocol-units/eth-settlement/contracts/remappings.txt new file mode 100644 index 000000000..c9c74332c --- /dev/null +++ b/protocol-units/eth-settlement/contracts/remappings.txt @@ -0,0 +1 @@ +openzeppelin/=./lib/openzeppelin-contracts/ \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/script/DeploySettlement.s.sol b/protocol-units/eth-settlement/contracts/script/DeploySettlement.s.sol new file mode 100644 index 000000000..f5dfc69cc --- /dev/null +++ b/protocol-units/eth-settlement/contracts/script/DeploySettlement.s.sol @@ -0,0 +1,46 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.20; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {Settlement} from "../src/Settlement.sol"; +import {ControlID} from "../src/ControlID.sol"; + +import {EvenNumber} from "../src/EvenNumber.sol"; + +/// @notice Deployment script for the RISC Zero starter project. +/// @dev Use the following environment variable to control the deployment: +/// * ETH_WALLET_PRIVATE_KEY private key of the wallet to be used for deployment. +/// +/// See the Foundry documentation for more information about Solidity scripts. +/// https://book.getfoundry.sh/tutorials/solidity-scripting +contract DeploySettlement is Script { + function run() external { + uint256 deployerKey = uint256(vm.envBytes32("ETH_WALLET_PRIVATE_KEY")); + + vm.startBroadcast(deployerKey); + + Settlement settlement = new Settlement(ControlID.CONTROL_ID_0, ControlID.CONTROL_ID_1); + console2.log("Deployed Settlement", address(settlement)); + + EvenNumber evenNumber = new EvenNumber(settlement); + console2.log("Deployed EvenNumber to", address(evenNumber)); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/src/ControlID.sol b/protocol-units/eth-settlement/contracts/src/ControlID.sol new file mode 100644 index 000000000..c706f4962 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/ControlID.sol @@ -0,0 +1,25 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// This file is automatically generated by: +// cargo xtask bootstrap-groth16 + +pragma solidity ^0.8.9; + +library ControlID { + uint256 public constant CONTROL_ID_0 = 0x447d7e12291364db4bc5421164880129; + uint256 public constant CONTROL_ID_1 = 0x12c49ad247d28a32147e13615c6c81f9; +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/src/Counter.sol b/protocol-units/eth-settlement/contracts/src/Counter.sol new file mode 100644 index 000000000..de97168dd --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/Counter.sol @@ -0,0 +1,32 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {IBonsaiRelay} from "./IBonsaiRelay.sol"; + +contract Counter { + uint256 public value = 0; + + // Submit request + function request_callback(bytes32 image_id, bytes calldata input, uint64 gas_limit, IBonsaiRelay relay) public { + relay.requestCallback(image_id, input, address(this), this.callback.selector, gas_limit); + } + + // Callback function expected to be invoked by the Bonsai Relay contract + function callback(uint256 increment) public { + value = value + increment; + } +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/src/EvenNumber.sol b/protocol-units/eth-settlement/contracts/src/EvenNumber.sol new file mode 100644 index 000000000..4f6fa0d69 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/EvenNumber.sol @@ -0,0 +1,38 @@ +pragma solidity ^0.8.20; + +import {IRiscZeroVerifier} from "./IRiscZeroVerifier.sol"; +import {ImageID} from "./ImageID.sol"; // auto-generated contract after running `cargo build`. + +/// @title A starter application using RISC Zero. +/// @notice This basic application holds a number, guaranteed to be even. +/// @dev This contract demonstrates one pattern for offloading the computation of an expensive +/// or difficult to implement function to a RISC Zero guest running on Bonsai. +contract EvenNumber { + /// @notice RISC Zero verifier contract address. + IRiscZeroVerifier public immutable verifier; + /// @notice Image ID of the only zkVM binary to accept verification from. + bytes32 public constant imageId = ImageID.IS_EVEN_ID; + + /// @notice A number that is guaranteed, by the RISC Zero zkVM, to be even. + /// It can be set by calling the `set` function. + uint256 public number; + + /// @notice Initialize the contract, binding it to a specified RISC Zero verifier. + constructor(IRiscZeroVerifier _verifier) { + verifier = _verifier; + number = 0; + } + + /// @notice Set the even number stored on the contract. Requires a RISC Zero proof that the number is even. + function set(uint256 x, bytes32 postStateDigest, bytes calldata seal) public { + // Construct the expected journal data. Verify will fail if journal does not match. + bytes memory journal = abi.encode(x); + require(verifier.verify(seal, imageId, postStateDigest, sha256(journal))); + number = x; + } + + /// @notice Returns the number stored. + function get() public view returns (uint256) { + return number; + } +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/src/Groth16Verifier.sol b/protocol-units/eth-settlement/contracts/src/Groth16Verifier.sol new file mode 100644 index 000000000..1cba43089 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/Groth16Verifier.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright 2021 0KIMS association. + + This file is generated with [snarkJS](https://github.com/iden3/snarkjs). + + snarkJS is a free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + snarkJS is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with snarkJS. If not, see . +*/ + +pragma solidity >=0.7.0 <0.9.0; + +contract Groth16Verifier { + // Scalar field size + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + // Base field size + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + // Verification Key data + uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; + uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; + uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; + uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; + uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; + uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; + uint256 constant deltax1 = 18518940221910320856687047018635785128750837022059566906616608708313475199865; + uint256 constant deltax2 = 9492326610711013918333865133991413442330971822743127449106067493230447878125; + uint256 constant deltay1 = 19483644759748826533215810634368877792922012485854314246298395665859158607201; + uint256 constant deltay2 = 21375251776817431660251933179512026180139877181625068362970095925425149918084; + + uint256 constant IC0x = 5283414572476013565779278723585415063371186194506872223482170607932178811733; + uint256 constant IC0y = 18704069070102836155408936676819275373965966640372164023392964533091458933020; + + uint256 constant IC1x = 4204832149120840018317309580010992142700029278901617154852760187580780425598; + uint256 constant IC1y = 12454324579480242399557363837918019584959512625719173397955145140913291575910; + + uint256 constant IC2x = 14956117485756386823219519866025248834283088288522682527835557402788427995664; + uint256 constant IC2y = 6968527870554016879785099818512699922114301060378071349626144898778340839382; + + uint256 constant IC3x = 6512168907754184210144919576616764035747139382744482291187821746087116094329; + uint256 constant IC3y = 17156131719875889332084290091263207055049222677188492681713268727972722760739; + + uint256 constant IC4x = 5195346330747727606774560791771406703229046454464300598774280139349802276261; + uint256 constant IC4y = 16279160127031959334335024858510026085227931356896384961436876214395869945425; + + // Memory data + uint16 constant pVk = 0; + uint16 constant pPairing = 128; + + uint16 constant pLastMem = 896; + + function verifyProof( + uint256[2] calldata _pA, + uint256[2][2] calldata _pB, + uint256[2] calldata _pC, + uint256[4] calldata _pubSignals + ) public view returns (bool) { + assembly { + function checkField(v) { + if iszero(lt(v, q)) { + mstore(0, 0) + return(0, 0x20) + } + } + + // G1 function to multiply a G1 value(x,y) to value in an address + function g1_mulAccC(pR, x, y, s) { + let success + let mIn := mload(0x40) + mstore(mIn, x) + mstore(add(mIn, 32), y) + mstore(add(mIn, 64), s) + + success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + + mstore(add(mIn, 64), mload(pR)) + mstore(add(mIn, 96), mload(add(pR, 32))) + + success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + } + + function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk { + let _pPairing := add(pMem, pPairing) + let _pVk := add(pMem, pVk) + + mstore(_pVk, IC0x) + mstore(add(_pVk, 32), IC0y) + + // Compute the linear combination vk_x + + g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0))) + + g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32))) + + g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64))) + + g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96))) + + // -A + mstore(_pPairing, calldataload(pA)) + mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) + + // B + mstore(add(_pPairing, 64), calldataload(pB)) + mstore(add(_pPairing, 96), calldataload(add(pB, 32))) + mstore(add(_pPairing, 128), calldataload(add(pB, 64))) + mstore(add(_pPairing, 160), calldataload(add(pB, 96))) + + // alpha1 + mstore(add(_pPairing, 192), alphax) + mstore(add(_pPairing, 224), alphay) + + // beta2 + mstore(add(_pPairing, 256), betax1) + mstore(add(_pPairing, 288), betax2) + mstore(add(_pPairing, 320), betay1) + mstore(add(_pPairing, 352), betay2) + + // vk_x + mstore(add(_pPairing, 384), mload(add(pMem, pVk))) + mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) + + // gamma2 + mstore(add(_pPairing, 448), gammax1) + mstore(add(_pPairing, 480), gammax2) + mstore(add(_pPairing, 512), gammay1) + mstore(add(_pPairing, 544), gammay2) + + // C + mstore(add(_pPairing, 576), calldataload(pC)) + mstore(add(_pPairing, 608), calldataload(add(pC, 32))) + + // delta2 + mstore(add(_pPairing, 640), deltax1) + mstore(add(_pPairing, 672), deltax2) + mstore(add(_pPairing, 704), deltay1) + mstore(add(_pPairing, 736), deltay2) + + let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) + + isOk := and(success, mload(_pPairing)) + } + + let pMem := mload(0x40) + mstore(0x40, add(pMem, pLastMem)) + + // Validate that all evaluations ∈ F + + checkField(calldataload(add(_pubSignals, 0))) + + checkField(calldataload(add(_pubSignals, 32))) + + checkField(calldataload(add(_pubSignals, 64))) + + checkField(calldataload(add(_pubSignals, 96))) + + checkField(calldataload(add(_pubSignals, 128))) + + // Validate all evaluations + let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) + + mstore(0, isValid) + return(0, 0x20) + } + } +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/src/IBonsaiRelay.sol b/protocol-units/eth-settlement/contracts/src/IBonsaiRelay.sol new file mode 100644 index 000000000..23eb30a18 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/IBonsaiRelay.sol @@ -0,0 +1,83 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.17; + +/// @notice Data required to authorize a callback to be sent through the relay. +struct CallbackAuthorization { + /// @notice SNARK proof acting as the cryptographic seal over the execution results. + bytes seal; + /// @notice Digest of the zkVM SystemState after execution. + /// @dev The relay does not additionally check any property of this digest, but needs the + /// digest in order to reconstruct the ReceiptClaim hash to which the proof is linked. + bytes32 postStateDigest; +} + +/// @notice Callback data, provided by the Relay service. +struct Callback { + CallbackAuthorization auth; + /// @notice address of the contract to receive the callback. + address callbackContract; + /// @notice payload containing the callback function selector, journal bytes, and image ID. + /// @dev payload is destructured and checked against the authorization data to ensure that + /// the journal is a valid execution result of the zkVM guest defined by the image ID. + /// The payload is then used directly as the calldata for the callback. + bytes payload; + /// @notice maximum amount of gas the callback function may use. + uint64 gasLimit; +} + +/// @notice The interface for the Bonsai relay contract +interface IBonsaiRelay { + /// @notice Event emitted upon receiving a callback request through requestCallback. + event CallbackRequest( + address account, + bytes32 imageId, + bytes input, + address callbackContract, + bytes4 functionSelector, + uint64 gasLimit + ); + + /// @notice Submit request to receive a callback. + /// @dev This function will usually be called be the Bonsai user's application contract, and + /// will log an event that the Bonsai Relay will detect and respond to. + function requestCallback( + bytes32 imageId, + bytes calldata input, + address callbackContract, + bytes4 functionSelector, + uint64 gasLimit + ) external; + + /// @notice Determines if the given authorization is valid for the image ID and journal. + /// @dev A (imageId, journal) pair should be valid, and the respective callback authorized, if + /// and only if the journal is the result of the correct execution of the zkVM guest. + function callbackIsAuthorized(bytes32 imageId, bytes calldata journal, CallbackAuthorization calldata auth) + external + view + returns (bool); + + /// @notice Submit a batch of callbacks, authorized by an attached SNARK proof. + /// @dev This function is usually called by the Bonsai Relay. Note that this function does not + /// revert when one of the inner callbacks reverts. + /// @return invocationResults a list of booleans indicated if the calldata succeeded or failed. + function invokeCallbacks(Callback[] calldata callbacks) external returns (bool[] memory invocationResults); + + /// @notice Submit a single callback, authorized by an attached SNARK proof. + /// @dev This function is usually called by the Bonsai Relay. This function reverts if the callback fails. + function invokeCallback(Callback calldata callback) external; +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/src/IRiscZeroVerifier.sol b/protocol-units/eth-settlement/contracts/src/IRiscZeroVerifier.sol new file mode 100644 index 000000000..607d2d38b --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/IRiscZeroVerifier.sol @@ -0,0 +1,155 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.9; + +/// @notice Public claims about a zkVM guest execution, such as the journal committed to by the guest. +/// @dev Also includes important information such as the exit code and the starting and ending system +/// state (i.e. the state of memory). `ReceiptClaim` is a "Merkle-ized struct" supporting +/// partial openings of the underlying fields from a hash commitment to the full structure. +struct ReceiptClaim { + /// @notice Digest of the SystemState just before execution has begun. + bytes32 preStateDigest; + /// @notice Digest of the SystemState just after execution has completed. + bytes32 postStateDigest; + /// @notice The exit code for the execution. + ExitCode exitCode; + /// @notice A digest of the input to the guest. + /// @dev This field is currently unused and must be set to the zero digest. + bytes32 input; + /// @notice Digest of the Output of the guest, including the journal + /// and assumptions set during execution. + bytes32 output; +} + +library ReceiptClaimLib { + bytes32 constant TAG_DIGEST = sha256("risc0.ReceiptClaim"); + + function digest(ReceiptClaim memory claim) internal pure returns (bytes32) { + return sha256( + abi.encodePacked( + TAG_DIGEST, + // down + claim.input, + claim.preStateDigest, + claim.postStateDigest, + claim.output, + // data + uint32(claim.exitCode.system) << 24, + uint32(claim.exitCode.user) << 24, + // down.length + uint16(4) << 8 + ) + ); + } +} + +/// @notice Exit condition indicated by the zkVM at the end of the guest execution. +/// @dev Exit codes have a "system" part and a "user" part. Semantically, the system part is set to +/// indicate the type of exit (e.g. halt, pause, or system split) and is directly controlled by the +/// zkVM. The user part is an exit code, similar to exit codes used in Linux, chosen by the guest +/// program to indicate additional information (e.g. 0 to indicate success or 1 to indicate an +/// error). +struct ExitCode { + SystemExitCode system; + uint8 user; +} + +/// @notice Exit condition indicated by the zkVM at the end of the execution covered by this proof. +/// @dev +/// `Halted` indicates normal termination of a program with an interior exit code returned from the +/// guest program. A halted program cannot be resumed. +/// +/// `Paused` indicates the execution ended in a paused state with an interior exit code set by the +/// guest program. A paused program can be resumed such that execution picks up where it left +/// of, with the same memory state. +/// +/// `SystemSplit` indicates the execution ended on a host-initiated system split. System split is +/// mechanism by which the host can temporarily stop execution of the execution ended in a system +/// split has no output and no conclusions can be drawn about whether the program will eventually +/// halt. System split is used in continuations to split execution into individually provable segments. +enum SystemExitCode { + Halted, + Paused, + SystemSplit +} + +/// @notice Output field in the `ReceiptClaim`, committing to a claimed journal and assumptions list. +struct Output { + /// @notice Digest of the journal committed to by the guest execution. + bytes32 journalDigest; + /// @notice Digest of the ordered list of `ReceiptClaim` digests corresponding to the + /// calls to `env::verify` and `env::verify_integrity`. + /// @dev Verifying the integrity of a `Receipt` corresponding to a `ReceiptClaim` with a + /// non-empty assumptions list does not guarantee unconditionally any of the claims over the + /// guest execution (i.e. if the assumptions list is non-empty, then the journal digest cannot + /// be trusted to correspond to a genuine execution). The claims can be checked by additional + /// verifying a `Receipt` for every digest in the assumptions list. + bytes32 assumptionsDigest; +} + +library OutputLib { + bytes32 constant TAG_DIGEST = sha256("risc0.Output"); + + function digest(Output memory output) internal pure returns (bytes32) { + return sha256( + abi.encodePacked( + TAG_DIGEST, + // down + output.journalDigest, + output.assumptionsDigest, + // down.length + uint16(2) << 8 + ) + ); + } +} + +/// @notice A receipt attesting to the execution of a guest program. +/// @dev A receipt contains two parts: a seal and a claim. The seal is a zero-knowledge proof +/// attesting to knowledge of a zkVM execution resulting the claim. The claim is a set of public +/// outputs for the execution. Crucially, the claim includes the journal and the image ID. The +/// image ID identifies the program that was executed, and the journal is the public data written +/// by the program. +struct Receipt { + bytes seal; + ReceiptClaim claim; +} + +/// @notice Verifier interface for RISC Zero receipts of execution. +interface IRiscZeroVerifier { + /// @notice Verify that the given seal is a valid RISC Zero proof of execution with the + /// given image ID, post-state digest, and journal digest. + /// @dev This method additionally ensures that the input hash is all-zeros (i.e. no + /// committed input), the exit code is (Halted, 0), and there are no assumptions (i.e. the + /// receipt is unconditional). + /// @param seal The encoded cryptographic proof (i.e. SNARK). + /// @param imageId The identifier for the guest program. + /// @param postStateDigest A hash of the final memory state. Required to run the verifier, but + /// otherwise can be left unconstrained for most use cases. + /// @param journalDigest The SHA-256 digest of the journal bytes. + /// @return true if the receipt passes the verification checks. The return code must be checked. + function verify(bytes calldata seal, bytes32 imageId, bytes32 postStateDigest, bytes32 journalDigest) + external + view + returns (bool); + + /// @notice Verify that the given receipt is a valid RISC Zero receipt, ensuring the `seal` is + /// valid a cryptographic proof of the execution with the given `claim`. + /// @param receipt The receipt to be verified. + /// @return true if the receipt passes the verification checks. The return code must be checked. + function verifyIntegrity(Receipt calldata receipt) external view returns (bool); +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/src/ImageID.sol b/protocol-units/eth-settlement/contracts/src/ImageID.sol new file mode 100644 index 000000000..ee1f90aa7 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/ImageID.sol @@ -0,0 +1,23 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// This file is automatically generated + +pragma solidity ^0.8.20; + +library ImageID { + bytes32 public constant IS_EVEN_ID = bytes32(0x04d6ce2d1c7ffa390e73114c5213f76aada67c8301d88b7698a48fd7543a444b); +} diff --git a/protocol-units/eth-settlement/contracts/src/Settlement.sol b/protocol-units/eth-settlement/contracts/src/Settlement.sol new file mode 100644 index 000000000..fa7a1239e --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/Settlement.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Output, OutputLib, Receipt, ReceiptClaim, ReceiptClaimLib, IRiscZeroVerifier, SystemExitCode, ExitCode} from "./IRiscZeroVerifier.sol"; +import {Groth16Verifier} from "./Groth16Verifier.sol"; +import {SafeCast} from "openzeppelin/contracts/utils/math/SafeCast.sol"; + +/// @notice A Groth16 seal over the claimed receipt claim. +struct Seal { + uint256[2] a; + uint256[2][2] b; + uint256[2] c; +} + +// A toy settlement contract to post and retrieve proofs of settlements +contract Settlement is IRiscZeroVerifier, Groth16Verifier { + using ReceiptClaimLib for ReceiptClaim; + using OutputLib for Output; + using SafeCast for uint256; + + struct Proof { + bytes proofData; + address signer; // Address of the signer who posted the proof + } + + /// @notice Control ID hash for the identity_p254 predicate decomposed by `splitDigest`. + /// @dev This value controls what set of recursion programs, and therefore what version of the + /// zkVM circuit, will be accepted by this contract. Each instance of this verifier contract + /// will accept a single release of the RISC Zero circuits. + /// + /// New releases of RISC Zero's zkVM require updating these values. These values can be + /// obtained by running `cargo run --bin bonsai-ethereum-contracts -F control-id` + uint256 public immutable CONTROL_ID_0; + uint256 public immutable CONTROL_ID_1; + + // Mapping from block height to array of Proofs + mapping(uint64 => Proof[]) public proofsByHeight; + + // Mapping to keep track of allowed signers + mapping(address => bool) public isSigner; + + event ProofAdded(uint64 indexed blockHeight, bytes proofData, address indexed signer); + event SignerAdded(address indexed signer); + event SignerRemoved(address indexed signer); + + // Modifier to restrict function calls to allowed signers + modifier onlySigner() { + require(isSigner[msg.sender], "Caller is not an allowed signer"); + _; + } + + constructor(uint256 control_id_0, uint256 control_id_1) { + // Initialize the contract with the deployer as an allowed signer + isSigner[msg.sender] = true; + CONTROL_ID_0 = control_id_0; + CONTROL_ID_1 = control_id_1; + } + + // Function to add a signer + function addSigner(address _signer) external { // todo: change back to only signer + isSigner[_signer] = true; + emit SignerAdded(_signer); + } + + // Function to remove a signer + function removeSigner(address _signer) external { // todo: change back to only signe + isSigner[_signer] = false; + emit SignerRemoved(_signer); + } + + // Function to post a settlement + function settle(uint64 blockHeight, bytes calldata proofData) external { // todo: change back to only signer + proofsByHeight[blockHeight].push(Proof(proofData, msg.sender)); + emit ProofAdded(blockHeight, proofData, msg.sender); + } + + // Function to get settlements by block height + function getProofsAtHeight(uint64 blockHeight) external view returns (bytes[] memory) { + Proof[] memory proofs = proofsByHeight[blockHeight]; + bytes[] memory proofData = new bytes[](proofs.length); + for (uint i = 0; i < proofs.length; i++) { + proofData[i] = proofs[i].proofData; + } + return proofData; + } + + /// @notice splits a digest into two 128-bit words to use as public signal inputs. + /// @dev RISC Zero's Circom verifier circuit takes each of two hash digests in two 128-bit + /// chunks. These values can be derived from the digest by splitting the digest in half and + /// then reversing the bytes of each. + function splitDigest(bytes32 digest) internal pure returns (uint256, uint256) { + uint256 reversed = reverseByteOrderUint256(uint256(digest)); + return (uint256(uint128(uint256(reversed))), uint256(reversed >> 128)); + } + + /// @inheritdoc IRiscZeroVerifier + function verify(bytes calldata seal, bytes32 imageId, bytes32 postStateDigest, bytes32 journalDigest) + public + view + returns (bool) + { + Receipt memory receipt = Receipt( + seal, + ReceiptClaim( + imageId, + postStateDigest, + ExitCode(SystemExitCode.Halted, 0), + bytes32(0), + Output(journalDigest, bytes32(0)).digest() + ) + ); + return verifyIntegrity(receipt); + } + + function verifyIntegrity(Receipt memory receipt) public view returns (bool) { + (uint256 claim0, uint256 claim1) = splitDigest(receipt.claim.digest()); + Seal memory seal = abi.decode(receipt.seal, (Seal)); + return this.verifyProof(seal.a, seal.b, seal.c, [CONTROL_ID_0, CONTROL_ID_1, claim0, claim1]); + } + + /// @notice reverse the byte order of the uint256 value. + /// @dev Soldity uses a big-endian ABI encoding. Reversing the byte order before encoding + /// ensure that the encoded value will be little-endian. + /// Written by k06a. https://ethereum.stackexchange.com/a/83627 + function reverseByteOrderUint256(uint256 input) public pure returns (uint256 v) { + v = input; + + // swap bytes + v = ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) + | ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + + // swap 2-byte long pairs + v = ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) + | ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + + // swap 4-byte long pairs + v = ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) + | ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + + // swap 8-byte long pairs + v = ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) + | ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + + // swap 16-byte long pairs + v = (v >> 128) | (v << 128); + } +} diff --git a/protocol-units/eth-settlement/contracts/src/lib.rs b/protocol-units/eth-settlement/contracts/src/lib.rs new file mode 100644 index 000000000..4efb2a068 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/src/lib.rs @@ -0,0 +1,4 @@ +use ethers::prelude::*; + +abigen!(Settlement, "$CARGO_MANIFEST_DIR/out/Settlement.sol/Settlement.json"); +abigen!(Counter, "$CARGO_MANIFEST_DIR/out/Counter.sol/Counter.json"); \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/test/Elf.sol b/protocol-units/eth-settlement/contracts/test/Elf.sol new file mode 100644 index 000000000..ee505c499 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/test/Elf.sol @@ -0,0 +1,24 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// This file is automatically generated + +pragma solidity ^0.8.20; + +library Elf { + string public constant IS_EVEN_PATH = + "/home/movses/mvmt/sdk/target/.rust-analyzer/riscv-guest/riscv32im-risc0-zkvm-elf/release/is-even"; +} diff --git a/protocol-units/eth-settlement/contracts/test/Settlement.t.sol b/protocol-units/eth-settlement/contracts/test/Settlement.t.sol new file mode 100644 index 000000000..28467cfa3 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/test/Settlement.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; + +import "ds-test/test.sol"; +import "../src/Settlement.sol"; +import "forge-std/Vm.sol"; +import { + IRiscZeroVerifier, + Output, + OutputLib, + Receipt as RiscZeroReceipt, + ReceiptClaim, + ReceiptClaimLib, + ExitCode, + SystemExitCode +} from "../src/IRiscZeroVerifier.sol"; +import {TestReceipt} from "./TestReceipt.sol"; +import {ControlID} from "../src/ControlID.sol"; + +contract SettlementTest is DSTest { + using OutputLib for Output; + using ReceiptClaimLib for ReceiptClaim; + + Vm vm = Vm(HEVM_ADDRESS); + Settlement settlement; + address signer1 = address(0x1); + address signer2 = address(0x2); + bytes exampleProofData = "exampleProof"; + + RiscZeroReceipt internal TEST_RECEIPT = RiscZeroReceipt( + TestReceipt.SEAL, + ReceiptClaim( + TestReceipt.IMAGE_ID, + TestReceipt.POST_DIGEST, + ExitCode(SystemExitCode.Halted, 0), + bytes32(0x0000000000000000000000000000000000000000000000000000000000000000), + Output(sha256(TestReceipt.JOURNAL), bytes32(0)).digest() + ) + ); + + function setUp() public { + settlement = new Settlement(ControlID.CONTROL_ID_0, ControlID.CONTROL_ID_1); + settlement.addSigner(signer1); + } + + function testAddSigner() public { + assertTrue(settlement.isSigner(signer1), "signer1 should be a signer after addition"); + } + + function testRemoveSigner() public { + settlement.removeSigner(signer1); + assertTrue(!settlement.isSigner(signer1), "signer1 should not be a signer after removal"); + } + + function testVerifyKnownGoodReceipt() external view { + require(settlement.verifyIntegrity(TEST_RECEIPT), "verification failed"); + } + + // A no-so-thorough test to make sure changing the bits causes a failure. + function testVerifyMangledReceipts() external view { + RiscZeroReceipt memory mangled = TEST_RECEIPT; + + mangled.seal[0] ^= bytes1(uint8(1)); + require(!settlement.verifyIntegrity(mangled), "verification passed on mangled seal value"); + mangled = TEST_RECEIPT; + + mangled.claim.preStateDigest ^= bytes32(uint256(1)); + require(!settlement.verifyIntegrity(mangled), "verification passed on mangled preStateDigest value"); + mangled = TEST_RECEIPT; + + mangled.claim.postStateDigest ^= bytes32(uint256(1)); + require(!settlement.verifyIntegrity(mangled), "verification passed on mangled postStateDigest value"); + mangled = TEST_RECEIPT; + + mangled.claim.exitCode = ExitCode(SystemExitCode.SystemSplit, 0); + require(!settlement.verifyIntegrity(mangled), "verification passed on mangled exitCode value"); + mangled = TEST_RECEIPT; + + mangled.claim.input ^= bytes32(uint256(1)); + require(!settlement.verifyIntegrity(mangled), "verification passed on mangled input value"); + mangled = TEST_RECEIPT; + + mangled.claim.output ^= bytes32(uint256(1)); + require(!settlement.verifyIntegrity(mangled), "verification passed on mangled input value"); + mangled = TEST_RECEIPT; + + // Just a quick sanity check + require(settlement.verifyIntegrity(mangled), "verification failed"); + } + + // function testFailSettleNotSigner() public { + // vm.prank(signer2); + // settlement.settle(1, exampleProofData); + // } + + function testSettleAndRetrieve() public { + vm.prank(signer1); + settlement.settle(1, exampleProofData); + + bytes[] memory proofs = settlement.getProofsAtHeight(1); + assertEq(proofs.length, 1, "There should be one proof for block height 1"); + assertEq(string(proofs[0]), string(exampleProofData), "The proofData should match exampleProofData"); + } + + // Removed testGetSettlement and testFailGetLeadSettlementNoSettlements as they do not apply anymore +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/test/TestReceipt.sol b/protocol-units/eth-settlement/contracts/test/TestReceipt.sol new file mode 100644 index 000000000..2d6d1e473 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/test/TestReceipt.sol @@ -0,0 +1,28 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// This file is automatically generated by: +// cargo xtask bootstrap-groth16 + +pragma solidity ^0.8.13; + +library TestReceipt { + bytes public constant SEAL = + hex"0b79052f45ac3c4873607632575e2f8777ee1776e702eb09c419487a80228fd51ae4c3bec6344ddcc9c31f180ea571326102d5f640e2f2a17dec9f608007e63802f4c00951d50a256afc2fb6829d3679748b5ea57931bf6921ab07a7492ca3751fddf7493b473b6f99c3c036004f9f1c0d985fb00cbd6d64d4ddb5cabafd49fc29c7ac262d12fc190522384f0b77b3139fcc51516eefc088a6bc49093b28029827d7612bff120f2880eab4b7bb1aa1cdaa563193827f01119f8e57cef6daf6a516d8c0445ea5f5514fdbe9e3a1e2b6d4cc4ac7983982bbc8cffee7a9177032a32382aadee5f7017ab25b2a7c82b355d46e7c9cbe13587d552cebd14a816b448e"; + bytes32 public constant POST_DIGEST = bytes32(0xb8e6dde26b99d84dd257eef56a4f9adc1b03a0bb88ca5a3ad39a91e367f5c3f7); + bytes public constant JOURNAL = hex""; + bytes32 public constant IMAGE_ID = bytes32(0x35e14f92e1c7989a467611bb39d2cf7f46dc4488404f8e8179960cc7be13f8a8); +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/contracts/workflows/test.yml b/protocol-units/eth-settlement/contracts/workflows/test.yml new file mode 100644 index 000000000..9282e8294 --- /dev/null +++ b/protocol-units/eth-settlement/contracts/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: workflow_dispatch + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/protocol-units/eth-settlement/eth-adapter/Cargo.toml b/protocol-units/eth-settlement/eth-adapter/Cargo.toml new file mode 100644 index 000000000..1c3f0eabb --- /dev/null +++ b/protocol-units/eth-settlement/eth-adapter/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "eth-adapter" +version = "0.1.0" +edition = "2021" + +[dependencies] +sov-rollup-interface = { workspace = true } +sov-mock-da = { workspace = true } +tokio = { workspace = true } +async-trait = { workspace = true } +web3 = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +hex = { workspace = true } +serde_json = { workspace = true } +# ethers-rs = { workspace = true } \ No newline at end of file diff --git a/protocol-units/eth-settlement/eth-adapter/src/lib.rs b/protocol-units/eth-settlement/eth-adapter/src/lib.rs new file mode 100644 index 000000000..a68848025 --- /dev/null +++ b/protocol-units/eth-settlement/eth-adapter/src/lib.rs @@ -0,0 +1,413 @@ +use ethabi::{Bytes, Token}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sov_rollup_interface::da::BlockHeaderTrait; +use sov_rollup_interface::{da::DaSpec, services::da::DaService}; +use std::fmt; +use std::sync::Arc; +use std::{any, fs}; +use tokio::sync::RwLock; +use web3::signing::Key; +use web3::transports::Http; +use web3::{ + contract::tokens::Tokenizable, + contract::{Contract, Options}, + ethabi, + types::{Address, Bytes as Web3Bytes, H256, U256}, + Web3, +}; + +#[derive(Debug, Clone)] +pub struct EthSettlementService> { + pub da_service: T, + pub web3_client: Arc>>, + pub contract_address: Address, + pub contract: Contract, +} + +impl> EthSettlementService { + /// Attempts to create a new `EthSettlementService` with the provided RPC URL and contract address. + pub fn try_new( + da_service: T, + rpc_url: &str, + contract_address: &str, + contract_path: &str, + ) -> Result { + let http = Http::new(rpc_url)?; + let web3_client = Web3::new(http); + let contract_address = contract_address + .parse() + .map_err(|_| web3::Error::Decoder("Failed to parse contract address".into()))?; + + let file_contents = fs::read_to_string(contract_path)?; + + // Parse the string into a JSON object + let json: Value = serde_json::from_str(&file_contents)?; + + // Extract the ABI part of the JSON + let abi = json["abi"].clone(); + + let contract = Contract::from_json( + web3_client.eth(), + contract_address, + // read the bytes from the contract path + abi.to_string().as_bytes(), + ) + .map_err(|e| web3::Error::Decoder(format!("Failed to create contract instance: {}", e)))?; + + Ok(Self { + da_service, + web3_client: Arc::new(RwLock::new(web3_client)), + contract_address, + contract, + }) + } + + /// Attempts to create a new `EthSettlementService` using an RPC URL from the environment. + pub fn try_env(da_service: T) -> Result { + let rpc_url = std::env::var("ETH_RPC_URL").map_err(|_| { + web3::Error::Transport(web3::error::TransportError::Message(String::from( + "ETH_RPC_URL environment variable not set", + ))) + })?; + + let contract_address = std::env::var("ETH_CONTRACT_ADDRESS").map_err(|_| { + web3::Error::Transport(web3::error::TransportError::Message(String::from( + "ETH_CONTRACT_ADDRESS environment variable not set", + ))) + })?; + + let contract_path = std::env::var("ETH_CONTRACT_ABI_PATH").map_err(|_| { + web3::Error::Transport(web3::error::TransportError::Message(String::from( + "ETH_CONTRACT_ABI_PATH environment variable not set", + ))) + })?; + + Self::try_new( + da_service, + &rpc_url, + &contract_address, + contract_path.as_str(), + ) + } +} + +#[async_trait::async_trait] +impl> DaService for EthSettlementService { + type Error = T::Error; + type FilteredBlock = T::FilteredBlock; + type Spec = T::Spec; + type Verifier = T::Verifier; + type HeaderStream = T::HeaderStream; + type TransactionId = T::TransactionId; + + async fn get_block_at(&self, height: u64) -> Result { + self.da_service.get_block_at(height).await + } + + async fn get_last_finalized_block_header( + &self, + ) -> Result<::BlockHeader, Self::Error> { + self.da_service.get_last_finalized_block_header().await + } + + async fn subscribe_finalized_header(&self) -> Result { + self.da_service.subscribe_finalized_header().await + } + + async fn get_head_block_header( + &self, + ) -> Result<::BlockHeader, Self::Error> { + self.da_service.get_head_block_header().await + } + + fn extract_relevant_blobs( + &self, + block: &Self::FilteredBlock, + ) -> Vec<::BlobTransaction> { + self.da_service.extract_relevant_blobs(block) + } + + async fn get_extraction_proof( + &self, + block: &Self::FilteredBlock, + blobs: &[::BlobTransaction], + ) -> ( + ::InclusionMultiProof, + ::CompletenessProof, + ) { + self.da_service.get_extraction_proof(block, blobs).await + } + + async fn send_transaction(&self, blob: &[u8]) -> Result { + self.da_service.send_transaction(blob).await + } + + // Sends an aggregated ZK proof to the Ethereum blockchain + async fn send_aggregated_zk_proof( + &self, + aggregated_proof_data: &[u8], + ) -> Result { + // todo: this is too naive, but for now we just use the last finalized block height + let height = self.get_last_finalized_block_header().await?.height(); + + let web3 = self.web3_client.read().await; + let accounts = web3.eth().accounts().await?; + let from = accounts[0]; // Ensure this account is unlocked and has enough balance. + + let receipt: Receipt = serde_json::from_slice(aggregated_proof_data)?; + let tx_hash = self + .contract + .call("verifyIntegrity", receipt, from, Options::default()) + .await?; + + // @TODO: Why u64 returned here, what does sov-lab adapter do with this val? + + Ok(height) + } + + async fn get_aggregated_proofs_at(&self, height: u64) -> Result>, Self::Error> { + let block_height = U256::from(height); + + // The proofs wont be coming from the on chain contract but our shared sequencer. + // Mock this. + let proofs: Vec> = self + .contract + .query( + "getProofsAtHeight", + block_height, + None, + Options::default(), + None, + ) + .await?; + + Ok(proofs) + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +pub struct Receipt { + pub seal: Bytes, + pub claim: ReceiptClaim, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ReceiptClaim { + pub pre_state_digest: [u8; 32], + pub post_state_digest: [u8; 32], + pub exit_code: ExitCode, + pub input: [u8; 32], + pub output: [u8; 32], +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ExitCode { + pub system_exit_code: SystemExitCode, + pub user_exit_code: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] +pub enum SystemExitCode { + Halted, + Paused, + SystemSplit, +} + +impl fmt::Debug for Receipt { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Receipt") + .field("seal", &hex::encode(&self.seal)) + .field("claim", &self.claim) + .finish() + } +} + +impl Tokenizable for Receipt { + fn into_token(self) -> Token { + Token::Tuple(vec![ + Token::Bytes(self.seal), + Token::Tuple(vec![ + Token::FixedBytes(self.claim.pre_state_digest.to_vec()), + Token::FixedBytes(self.claim.post_state_digest.to_vec()), + self.claim.exit_code.into_token(), + Token::FixedBytes(self.claim.input.to_vec()), + Token::FixedBytes(self.claim.output.to_vec()), + ]), + ]) + } + + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::Tuple(tokens) => { + let seal = match &tokens[0] { + Token::Bytes(bytes) => bytes, + _ => { + return Err(web3::contract::Error::InvalidOutputType( + "Expected Bytes".to_string(), + )) + } + }; + + let claim = match &tokens[1] { + Token::Tuple(tokens) => { + let pre_state_digest = match &tokens[0] { + Token::FixedBytes(bytes) => bytes, + _ => { + return Err(web3::contract::Error::InvalidOutputType( + "Expected FixedBytes".to_string(), + )) + } + }; + + let post_state_digest = match &tokens[1] { + Token::FixedBytes(bytes) => bytes, + _ => { + return Err(web3::contract::Error::InvalidOutputType( + "Expected FixedBytes".to_string(), + )) + } + }; + + let exit_code = match &tokens[2] { + token => ExitCode::from_token(token.clone())?, + }; + + let input = match &tokens[3] { + Token::FixedBytes(bytes) => bytes, + _ => { + return Err(web3::contract::Error::InvalidOutputType( + "Expected FixedBytes".to_string(), + )) + } + }; + + let output = match &tokens[4] { + Token::FixedBytes(bytes) => bytes, + _ => { + return Err(web3::contract::Error::InvalidOutputType( + "Expected FixedBytes".to_string(), + )) + } + }; + + ReceiptClaim { + pre_state_digest: pre_state_digest.as_slice().try_into().unwrap(), + post_state_digest: post_state_digest.as_slice().try_into().unwrap(), + exit_code, + input: input.as_slice().try_into().unwrap(), + output: output.as_slice().try_into().unwrap(), + } + } + _ => { + return Err(web3::contract::Error::InvalidOutputType( + "Expected Tuple".to_string(), + )) + } + }; + + Ok(Receipt { + seal: seal.to_vec(), + claim, + }) + } + _ => Err(web3::contract::Error::InvalidOutputType( + "Expected Tuple".to_string(), + )), + } + } +} + +impl Tokenizable for ExitCode { + fn into_token(self) -> Token { + Token::Tuple(vec![ + self.system_exit_code.into_token(), + Token::Uint(self.user_exit_code.into()), + ]) + } + + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::Tuple(tokens) => { + let system_exit_code = match &tokens[0] { + token => SystemExitCode::from_token(token.clone())?, + }; + + let user_exit_code = match tokens[1] { + Token::Uint(uint) => uint.as_u64(), + _ => { + return Err(web3::contract::Error::InvalidOutputType( + "Expected Uint".to_string(), + )) + } + }; + + Ok(ExitCode { + system_exit_code, + user_exit_code, + }) + } + _ => Err(web3::contract::Error::InvalidOutputType( + "Expected Tuple".to_string(), + )), + } + } +} + +impl Tokenizable for SystemExitCode { + fn into_token(self) -> Token { + match self { + SystemExitCode::Halted => Token::Uint(U256::from(0)), + SystemExitCode::Paused => Token::Uint(U256::from(1)), + Self::SystemSplit => Token::Uint(U256::from(2)), + } + } + + fn from_token(token: Token) -> Result + where + Self: Sized, + { + match token { + Token::Uint(uint) => match uint.as_u64() { + 0 => Ok(SystemExitCode::Halted), + 1 => Ok(SystemExitCode::Paused), + 2 => Ok(SystemExitCode::SystemSplit), + _ => Err(web3::contract::Error::InvalidOutputType( + "Invalid SystemExitCode".to_string(), + )), + }, + _ => Err(web3::contract::Error::InvalidOutputType( + "Expected Uint".to_string(), + )), + } + } +} + +#[cfg(test)] +pub mod test { + + use super::*; + use sov_mock_da::{MockAddress, MockDaService}; + + #[tokio::test] + async fn test_eth_settlement_service_env() -> Result<(), anyhow::Error> { + let da_service = MockDaService::new( + // 32 &[u8] bytes + MockAddress::new([0; 32]), + ); + + let da = EthSettlementService::try_env(da_service)?; + + da.send_aggregated_zk_proof(&[0; 32]).await?; + let proofs = da.get_aggregated_proofs_at(0).await?; + assert_eq!(proofs[0], [0; 32]); + + Ok(()) + } +} diff --git a/protocol-units/eth-settlement/eth-publisher/Cargo.toml b/protocol-units/eth-settlement/eth-publisher/Cargo.toml new file mode 100644 index 000000000..5d37f6b07 --- /dev/null +++ b/protocol-units/eth-settlement/eth-publisher/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "eth-publisher" +version = "0.1.0" +edition = { workspace = true } +description = "The publisher service for Eth Settlement" + +[dependencies] +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true } +anyhow = { workspace = true } +bincode = { workspace = true } +bonsai-sdk = { workspace = true } +bytemuck = { workspace = true } +clap = { version = "4.0", features = ["derive", "env"] } +env_logger = { version = "0.10" } +ethers = { workspace = true } +hex = { workspace = true } +log = { workspace = true } +risc0-ethereum-contracts = { workspace = true } +risc0-zkvm = { workspace = true } +serde = { workspace = true } +tokio = { version = "1.35", features = ["full"] } +web3 = { workspace = true } + +methods = { path = "../methods" } \ No newline at end of file diff --git a/protocol-units/eth-settlement/eth-publisher/src/bin/publisher.rs b/protocol-units/eth-settlement/eth-publisher/src/bin/publisher.rs new file mode 100644 index 000000000..e61e2a180 --- /dev/null +++ b/protocol-units/eth-settlement/eth-publisher/src/bin/publisher.rs @@ -0,0 +1,76 @@ +use alloy_primitives::U256; +use alloy_sol_types::{sol, SolInterface, SolValue}; +use anyhow::{Context, Result}; +use eth_publisher::{BonsaiProver, TxSender}; +use clap::Parser; +use methods::IS_EVEN_ELF; + +// `IEvenNumber` interface automatically generated via the alloy `sol!` macro. +sol! { + interface IEvenNumber { + function set(uint256 x, bytes32 post_state_digest, bytes calldata seal); + } +} + +/// Arguments of the publisher CLI. +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Ethereum chain ID + #[clap(long)] + chain_id: u64, + + /// Ethereum Node endpoint. + #[clap(long, env)] + eth_wallet_private_key: String, + + /// Ethereum Node endpoint. + #[clap(long)] + rpc_url: String, + + /// Application's contract address on Ethereum + #[clap(long)] + contract: String, + + /// The input to provide to the guest binary + #[clap(short, long)] + input: U256, +} + +fn main() -> Result<()> { + env_logger::init(); + let args = Args::parse(); + + // Create a new `TxSender`. + let tx_sender = TxSender::new( + args.chain_id, + &args.rpc_url, + &args.eth_wallet_private_key, + &args.contract, + )?; + + // ABI encode the input for the guest binary, to match what the `is_even` guest + // code expects. + let input = args.input.abi_encode(); + + // Send an off-chain proof request to the Bonsai proving service. + let (journal, post_state_digest, seal) = BonsaiProver::prove(IS_EVEN_ELF, &input)?; + + // Decode the journal. Must match what was written in the guest with + // `env::commit_slice`. + let x = U256::abi_decode(&journal, true).context("decoding journal data")?; + + // Encode the function call for `IEvenNumber.set(x)`. + let calldata = IEvenNumber::IEvenNumberCalls::set(IEvenNumber::setCall { + x, + post_state_digest, + seal, + }) + .abi_encode(); + + // Send the calldata to Ethereum. + let runtime = tokio::runtime::Runtime::new()?; + runtime.block_on(tx_sender.send(calldata))?; + + Ok(()) +} diff --git a/protocol-units/eth-settlement/eth-publisher/src/lib.rs b/protocol-units/eth-settlement/eth-publisher/src/lib.rs new file mode 100644 index 000000000..604e69038 --- /dev/null +++ b/protocol-units/eth-settlement/eth-publisher/src/lib.rs @@ -0,0 +1,138 @@ +use std::time::Duration; + +use alloy_primitives::FixedBytes; +use anyhow::{Context, Result}; +use bonsai_sdk::alpha as bonsai_sdk; +use ethers::prelude::*; +use risc0_ethereum_contracts::groth16::Seal; +use risc0_zkvm::{compute_image_id, Receipt}; + +/// Wrapper of a `SignerMiddleware` client to send transactions to the given +/// contract's `Address`. +pub struct TxSender { + chain_id: u64, + client: SignerMiddleware, Wallet>, + contract: Address, +} + +impl TxSender { + /// Creates a new `TxSender`. + pub fn new(chain_id: u64, rpc_url: &str, private_key: &str, contract: &str) -> Result { + let provider = Provider::::try_from(rpc_url)?; + let wallet: LocalWallet = private_key.parse::()?.with_chain_id(chain_id); + let client = SignerMiddleware::new(provider.clone(), wallet.clone()); + let contract = contract.parse::
()?; + + Ok(TxSender { + chain_id, + client, + contract, + }) + } + + /// Send a transaction with the given calldata. + pub async fn send(&self, calldata: Vec) -> Result> { + let tx = TransactionRequest::new() + .chain_id(self.chain_id) + .to(self.contract) + .from(self.client.address()) + .data(calldata); + + log::info!("Transaction request: {:?}", &tx); + + let tx = self.client.send_transaction(tx, None).await?.await?; + + log::info!("Transaction receipt: {:?}", &tx); + + Ok(tx) + } +} + +/// An implementation of a Prover that runs on Bonsai. +pub struct BonsaiProver {} +impl BonsaiProver { + /// Generates a snark proof as a triplet (`Vec`, `FixedBytes<32>`, + /// `Vec) for the given elf and input. + pub fn prove(elf: &[u8], input: &[u8]) -> Result<(Vec, FixedBytes<32>, Vec)> { + let client = bonsai_sdk::Client::from_env(risc0_zkvm::VERSION)?; + + // Compute the image_id, then upload the ELF with the image_id as its key. + let image_id = compute_image_id(elf)?; + let image_id_hex = image_id.to_string(); + client.upload_img(&image_id_hex, elf.to_vec())?; + log::info!("Image ID: 0x{}", image_id_hex); + + // Prepare input data and upload it. + let input_id = client.upload_input(input.to_vec())?; + + // Start a session running the prover. + let session = client.create_session(image_id_hex, input_id, vec![])?; + log::info!("Created session: {}", session.uuid); + let _receipt = loop { + let res = session.status(&client)?; + if res.status == "RUNNING" { + log::info!( + "Current status: {} - state: {} - continue polling...", + res.status, + res.state.unwrap_or_default() + ); + std::thread::sleep(Duration::from_secs(15)); + continue; + } + if res.status == "SUCCEEDED" { + // Download the receipt, containing the output. + let receipt_url = res + .receipt_url + .context("API error, missing receipt on completed session")?; + + let receipt_buf = client.download(&receipt_url)?; + let receipt: Receipt = bincode::deserialize(&receipt_buf)?; + + break receipt; + } + + panic!( + "Workflow exited: {} - | err: {}", + res.status, + res.error_msg.unwrap_or_default() + ); + }; + + // Fetch the snark. + let snark_session = client.create_snark(session.uuid)?; + log::info!("Created snark session: {}", snark_session.uuid); + let snark_receipt = loop { + let res = snark_session.status(&client)?; + match res.status.as_str() { + "RUNNING" => { + log::info!("Current status: {} - continue polling...", res.status,); + std::thread::sleep(Duration::from_secs(15)); + continue; + } + "SUCCEEDED" => { + break res.output.context("No snark generated :(")?; + } + _ => { + panic!( + "Workflow exited: {} err: {}", + res.status, + res.error_msg.unwrap_or_default() + ); + } + } + }; + + let snark = snark_receipt.snark; + log::debug!("Snark proof!: {snark:?}"); + + let seal = Seal::abi_encode(snark).context("Read seal")?; + let post_state_digest: FixedBytes<32> = snark_receipt + .post_state_digest + .as_slice() + .try_into() + .context("Read post_state_digest")?; + let journal = snark_receipt.journal; + + Ok((journal, post_state_digest, seal)) + } +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/methods/Cargo.toml b/protocol-units/eth-settlement/methods/Cargo.toml new file mode 100644 index 000000000..709032328 --- /dev/null +++ b/protocol-units/eth-settlement/methods/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "methods" +version = "0.1.0" +edition = "2021" + +[package.metadata.risc0] +methods = ["guest"] + +[build-dependencies] +hex = { workspace = true } +risc0-build = { workspace = true } +risc0-build-ethereum = { workspace = true } +risc0-zkp = { workspace = true } + +[dev-dependencies] +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true } +risc0-zkvm = { workspace = true, features = ["client"] } \ No newline at end of file diff --git a/protocol-units/eth-settlement/methods/build.rs b/protocol-units/eth-settlement/methods/build.rs new file mode 100644 index 000000000..e6933dab4 --- /dev/null +++ b/protocol-units/eth-settlement/methods/build.rs @@ -0,0 +1,46 @@ +// Copyright 2023 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::HashMap, env}; + +use risc0_build::{embed_methods_with_options, DockerOptions, GuestOptions}; +use risc0_build_ethereum::generate_solidity_files; + +// Paths where the generated Solidity files will be written. +const SOLIDITY_IMAGE_ID_PATH: &str = "../contracts/src/ImageID.sol"; +const SOLIDITY_ELF_PATH: &str = "../contracts/test/Elf.sol"; + +fn main() { + // Builds can be made deterministic, and thereby reproducible, by using Docker to build the + // guest. Check the RISC0_USE_DOCKER variable and use Docker to build the guest if set. + let use_docker = env::var("RISC0_USE_DOCKER").ok().map(|_| DockerOptions { + root_dir: Some("../".into()), + }); + + // Generate Rust source files for the methods crate. + let guests = embed_methods_with_options(HashMap::from([( + "guests", + GuestOptions { + features: Vec::new(), + use_docker, + }, + )])); + + // Generate Solidity source files for use with Forge. + let solidity_opts = risc0_build_ethereum::Options::default() + .with_image_id_sol_path(SOLIDITY_IMAGE_ID_PATH) + .with_elf_sol_path(SOLIDITY_ELF_PATH); + + generate_solidity_files(guests.as_slice(), &solidity_opts).unwrap(); +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/methods/guest/Cargo.toml b/protocol-units/eth-settlement/methods/guest/Cargo.toml new file mode 100644 index 000000000..c97faea39 --- /dev/null +++ b/protocol-units/eth-settlement/methods/guest/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "guests" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "is-even" +path = "src/bin/is_even.rs" + +[workspace] + +[dependencies] +alloy-primitives = { version = "0.6", default-features = false, features = ["rlp", "serde", "std"] } +alloy-sol-types = { version = "0.6" } +risc0-zkvm = { version = "0.20.1", default-features = false, features = ['std'] } + +[profile.release] +lto = "thin" \ No newline at end of file diff --git a/protocol-units/eth-settlement/methods/guest/src/bin/is_even.rs b/protocol-units/eth-settlement/methods/guest/src/bin/is_even.rs new file mode 100644 index 000000000..46261f4ca --- /dev/null +++ b/protocol-units/eth-settlement/methods/guest/src/bin/is_even.rs @@ -0,0 +1,35 @@ +// Copyright 2023 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Read; + +use alloy_primitives::U256; +use alloy_sol_types::SolValue; +use risc0_zkvm::guest::env; + +fn main() { + // Read the input data for this application. + let mut input_bytes = Vec::::new(); + env::stdin().read_to_end(&mut input_bytes).unwrap(); + // Decode and parse the input + let number = ::abi_decode(&input_bytes, true).unwrap(); + + // Run the computation. + // In this case, asserting that the provided number is even. + assert!(number.bit(0) == false, "number is not even"); + + // Commit the journal that will be received by the application contract. + // Journal is encoded using Solidity ABI for easy decoding in the app contract. + env::commit_slice(number.abi_encode().as_slice()); +} \ No newline at end of file diff --git a/protocol-units/eth-settlement/methods/src/lib.rs b/protocol-units/eth-settlement/methods/src/lib.rs new file mode 100644 index 000000000..e0c8e2194 --- /dev/null +++ b/protocol-units/eth-settlement/methods/src/lib.rs @@ -0,0 +1,53 @@ +// Copyright 2023 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Generated crate containing the image ID and ELF binary of the build guest. +include!(concat!(env!("OUT_DIR"), "/methods.rs")); + +#[cfg(test)] +mod tests { + use alloy_primitives::U256; + use alloy_sol_types::SolValue; + use risc0_zkvm::{default_executor, ExecutorEnv}; + + #[test] + fn proves_even_number() { + let even_number = U256::from(1304); + + let env = ExecutorEnv::builder() + .write_slice(&even_number.abi_encode()) + .build() + .unwrap(); + + // NOTE: Use the executor to run tests without proving. + let session_info = default_executor().execute(env, super::IS_EVEN_ELF).unwrap(); + + let x = U256::abi_decode(&session_info.journal.bytes, true).unwrap(); + assert_eq!(x, even_number); + } + + #[test] + #[should_panic(expected = "number is not even")] + fn rejects_odd_number() { + let odd_number = U256::from(75); + + let env = ExecutorEnv::builder() + .write_slice(&odd_number.abi_encode()) + .build() + .unwrap(); + + // NOTE: Use the executor to run tests without proving. + default_executor().execute(env, super::IS_EVEN_ELF).unwrap(); + } +} \ No newline at end of file diff --git a/protocol-units/zkfp/eth-zk/Cargo.toml b/protocol-units/zkfp/eth-zk/Cargo.toml new file mode 100644 index 000000000..71414b9c0 --- /dev/null +++ b/protocol-units/zkfp/eth-zk/Cargo.toml @@ -0,0 +1,25 @@ +[package] +resolver = "2" +members = [ + "eth-settlement" +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Liam Monninger "] +homepage = "https://www.movementlabs.xyz" +publish = false +repository = "https://github.com/movemntdev/m2" +rust-version = "1.70" + +[workspace.dependencies] +async-trait = { version = "0.1" } +anyhow = { version = "1" } # For flexible error handling +sov-rollup-interface = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", branch = "nightly", features = ["native"] } +sov-mock-da = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", branch = "nightly", features = ["native"] } +tokio = { version = "1", features = ["full"] } +web3 = "0.19.0" +serde_json = "1.0.68" +# ethers-rs = "0.2.3" diff --git a/protocol-units/zkfp/eth-zk/README.md b/protocol-units/zkfp/eth-zk/README.md new file mode 100644 index 000000000..d0019ecd8 --- /dev/null +++ b/protocol-units/zkfp/eth-zk/README.md @@ -0,0 +1,33 @@ +# `m2` + +## `eth-settlement` +To run the tests, first install the following dependencies: +- foundry + +Start the Anvil local network: +```bash +anvil +``` + +Deploy the contracts in `contracts/eth-settlement`: +```bash +# from root of the project +cd contracts/eth-settlement +forge script script/DeploySettlement.s.sol --broadcast --rpc-url http://localhost:8545 --private-key +``` + +**Note**: Never use the private key above in production. + +Set your environment variables: +```bash +ETH_RPC_URL=http://localhost:8545 +ETH_CONTRACT_ADDRESS= +ETH_CONTRACT_ABI_PATH=../contracts/eth-settlement/out/Settlement.sol/Settlement.json +``` + +Run the tests: +```bash +# from root of the project +cd eth-settlement +cargo test test_eth_settlement_service_env +``` \ No newline at end of file diff --git a/protocol-units/zkfp/eth-zk/eth-settlement/Cargo.toml b/protocol-units/zkfp/eth-zk/eth-settlement/Cargo.toml new file mode 100644 index 000000000..c52ec33f8 --- /dev/null +++ b/protocol-units/zkfp/eth-zk/eth-settlement/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sui-block-executor" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sov-rollup-interface = { workspace = true } +sov-mock-da = { workspace = true } +tokio = { workspace = true } +async-trait = { workspace = true } +web3 = { workspace = true } +anyhow = { workspace = true } +serde_json = { workspace = true } +# ethers-rs = { workspace = true } \ No newline at end of file diff --git a/protocol-units/zkfp/eth-zk/eth-settlement/src/lib.rs b/protocol-units/zkfp/eth-zk/eth-settlement/src/lib.rs new file mode 100644 index 000000000..15b389f8f --- /dev/null +++ b/protocol-units/zkfp/eth-zk/eth-settlement/src/lib.rs @@ -0,0 +1,209 @@ +use sov_rollup_interface::da::BlockHeaderTrait; +use sov_rollup_interface::{ + services::da::DaService, + da::DaSpec, +}; +use web3::ethabi::Token; +use web3::{ + contract::{Contract, Options}, + ethabi, + types::{Address, Bytes, U256, H256}, + Web3, +}; +use web3::transports::Http; +use web3::signing::Key; +use std::sync::Arc; +use tokio::sync::RwLock; +use std::{any, fs}; +use serde_json::Value; + +#[derive(Debug, Clone)] +pub struct EthSettlementService> { + pub da_service: T, + pub web3_client: Arc>>, + pub contract_address: Address, + pub contract: Contract, +} + +impl> EthSettlementService { + + /// Attempts to create a new `EthSettlementService` with the provided RPC URL and contract address. + pub fn try_new( + da_service: T, + rpc_url: &str, + contract_address: &str, + contract_path : &str + ) -> Result { + + let http = Http::new(rpc_url)?; + let web3_client = Web3::new(http); + let contract_address = contract_address.parse().map_err(|_| web3::Error::Decoder("Failed to parse contract address".into()))?; + + let file_contents = fs::read_to_string(contract_path)?; + + // Parse the string into a JSON object + let json: Value = serde_json::from_str(&file_contents)?; + + // Extract the ABI part of the JSON + let abi = json["abi"].clone(); + + let contract = Contract::from_json( + web3_client.eth(), + contract_address, + // read the bytes from the contract path + abi.to_string().as_bytes() + ).map_err( + |e| web3::Error::Decoder(format!("Failed to create contract instance: {}", e)) + )?; + + Ok(Self { + da_service, + web3_client: Arc::new(RwLock::new(web3_client)), + contract_address, + contract, + }) + } + + /// Attempts to create a new `EthSettlementService` using an RPC URL from the environment. + pub fn try_env(da_service: T) -> Result { + + let rpc_url = std::env::var("ETH_RPC_URL").map_err(|_| web3::Error::Transport( + web3::error::TransportError::Message( + String::from("ETH_RPC_URL environment variable not set") + ) + ))?; + + let contract_address = std::env::var("ETH_CONTRACT_ADDRESS").map_err(|_| web3::Error::Transport( + web3::error::TransportError::Message( + String::from("ETH_CONTRACT_ADDRESS environment variable not set") + ) + ))?; + + let contract_path = std::env::var("ETH_CONTRACT_ABI_PATH").map_err(|_| web3::Error::Transport( + web3::error::TransportError::Message( + String::from("ETH_CONTRACT_ABI_PATH environment variable not set") + ) + ))?; + + Self::try_new( + da_service, &rpc_url, + &contract_address, contract_path.as_str() + ) + } + + +} + + +#[async_trait::async_trait] +impl> DaService for EthSettlementService { + + type Error = T::Error; + type FilteredBlock = T::FilteredBlock; + type Spec = T::Spec; + type Verifier = T::Verifier; + type HeaderStream = T::HeaderStream; + type TransactionId = T::TransactionId; + + async fn get_block_at(&self, height: u64) -> Result { + self.da_service.get_block_at(height).await + } + + async fn get_last_finalized_block_header( + &self, + ) -> Result<::BlockHeader, Self::Error> { + self.da_service.get_last_finalized_block_header().await + } + + async fn subscribe_finalized_header(&self) -> Result { + self.da_service.subscribe_finalized_header().await + } + + async fn get_head_block_header( + &self, + ) -> Result<::BlockHeader, Self::Error> { + self.da_service.get_head_block_header().await + } + + fn extract_relevant_blobs( + &self, + block: &Self::FilteredBlock, + ) -> Vec<::BlobTransaction> { + self.da_service.extract_relevant_blobs(block) + } + + async fn get_extraction_proof( + &self, + block: &Self::FilteredBlock, + blobs: &[::BlobTransaction], + ) -> ( + ::InclusionMultiProof, + ::CompletenessProof, + ) { + self.da_service.get_extraction_proof(block, blobs).await + } + + + async fn send_transaction(&self, blob: &[u8]) -> Result { + self.da_service.send_transaction(blob).await + } + + // Sends an aggregated ZK proof to the Ethereum blockchain + async fn send_aggregated_zk_proof(&self, aggregated_proof_data: &[u8]) -> Result { + + // todo: this is too naive, but for now we just use the last finalized block height + let height = self.get_last_finalized_block_header().await?.height(); + + let web3 = self.web3_client.read().await; + let accounts = web3.eth().accounts().await?; + let from = accounts[0]; // Ensure this account is unlocked and has enough balance. + + let proof_data = Bytes(aggregated_proof_data.to_vec()); + + let tx_hash = self.contract + .call("settle", (height, proof_data), from, Options::default()) + .await?; + + + Ok(height) + } + + + async fn get_aggregated_proofs_at(&self, height: u64) -> Result>, Self::Error> { + + let block_height = U256::from(height); + + let proofs: Vec> = self.contract + .query("getProofsAtHeight", block_height, None, Options::default(), None) + .await?; + + Ok(proofs) + + } + +} + +#[cfg(test)] +pub mod test { + + use super::*; + use sov_mock_da::{MockDaService, MockAddress}; + + #[tokio::test] + async fn test_eth_settlement_service_env()-> Result<(), anyhow::Error> { + + let da_service = MockDaService::new( + // 32 &[u8] bytes + MockAddress::new([0; 32]), + ); + + let da = EthSettlementService::try_env(da_service)?; + + da.send_aggregated_zk_proof(&[0; 32]).await?; + let proofs = da.get_aggregated_proofs_at(0).await?; + assert_eq!(proofs[0], [0; 32]); + + Ok(()) + } + +} \ No newline at end of file diff --git a/protocol-units/zkfp/move-zk/vendors/move/.vscode/launch.json b/protocol-units/zkfp/move-zk/vendors/move/.vscode/launch.json new file mode 100644 index 000000000..4b2fa3280 --- /dev/null +++ b/protocol-units/zkfp/move-zk/vendors/move/.vscode/launch.json @@ -0,0 +1,40 @@ +// This file defines launch configurations for Visual Studio Code. +// +// When VS Code is used to open the directory containing this repository (which contains this launch +// configuration at its root), it reads the values in this file and presents the user with launch +// options in its "Run and Debug" view. +// +// The launch configurations below allow the VS Code user to, for example: +// * Launch a new VS Code window that has the extension in this repository installed and enabled. +// * Launch a new VS Code window to run the extension's tests. +// +// When viewing this file in VS Code, you may use IntelliSense to learn about possible attributes, +// or hover to view descriptions of existing attributes. For more information, visit: +// https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch with Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/language/move-analyzer/editors/code" + ], + "preLaunchTask": "Pretest" + }, + { + "name": "VS Code Tokenizer Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/language/move-analyzer/editors/code/tests", + "--extensionDevelopmentPath=${workspaceFolder}/language/move-analyzer/editors/code", + "--extensionTestsPath=${workspaceFolder}/language/move-analyzer/editors/code/out/tests/index.js" + ], + "outFiles": ["${workspaceFolder}/language/move-analyzer/editors/code/out/tests/**/*.js"], + "preLaunchTask": "Pretest" + } + ] +} diff --git a/protocol-units/zkfp/move-zk/vendors/move/.vscode/tasks.json b/protocol-units/zkfp/move-zk/vendors/move/.vscode/tasks.json new file mode 100644 index 000000000..74c9b19b3 --- /dev/null +++ b/protocol-units/zkfp/move-zk/vendors/move/.vscode/tasks.json @@ -0,0 +1,23 @@ +// This file define "tasks" that automate parts of a programmer's workflow when +// developing the Visual Studio Code extension in this repository. See +// https://go.microsoft.com/fwlink/?LinkId=733558 for documentation. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Pretest", + "group": "build", + "isBackground": false, + "type": "npm", + "script": "pretest", + "path": "language/move-analyzer/editors/code/", + "problemMatcher": { + "base": "$tsc", + "fileLocation": [ + "relative", + "${workspaceFolder}/language/move-analyzer/editors/code/" + ] + } + } + ] +} diff --git a/shared-sequencer/.gitignore b/shared-sequencer/.gitignore index f2e972dd8..8e4c7616e 100644 --- a/shared-sequencer/.gitignore +++ b/shared-sequencer/.gitignore @@ -4,3 +4,6 @@ # These are backup files generated by rustfmt **/*.rs.bk + +# We don't want to include all the foundry library files in the repo +/protocol-units/contracts/eth-settlement/lib/* diff --git a/shared-sequencer/movement-sequencer/Cargo.toml b/shared-sequencer/movement-sequencer/Cargo.toml index cc3b2d4a5..7b4633e31 100644 --- a/shared-sequencer/movement-sequencer/Cargo.toml +++ b/shared-sequencer/movement-sequencer/Cargo.toml @@ -29,6 +29,7 @@ serde_json = "1.0.113" # https://github.com/serde-rs/json/releases serde_with = { version = "3.6.1", features = ["hex"] } tokio = { version = "1.36.0", features = ["fs", "rt-multi-thread"] } tonic = { version = "0.11.0", features = ["gzip"] } +tempfile = "3.2.0" futures = "0.3.30" # new deps diff --git a/shared-sequencer/movement-sequencer/src/block/mod.rs b/shared-sequencer/movement-sequencer/src/block/mod.rs index 7673b1aa8..5ff1a3ba3 100644 --- a/shared-sequencer/movement-sequencer/src/block/mod.rs +++ b/shared-sequencer/movement-sequencer/src/block/mod.rs @@ -21,16 +21,33 @@ use serde_with::serde_as; #[serde_as] #[derive(Serialize, Deserialize, Clone, Derivative, Default)] -#[derivative(Debug, PartialEq, Eq)] +#[derivative(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Transaction { + // the id of the rollup chain that the transaction is for #[serde_as(as = "Hex0xBytes")] pub consumer_id : Vec, + // the data of the transaction #[serde_as(as = "Hex0xBytes")] pub data : Vec, } impl Transaction { + pub fn new(consumer_id: Vec, data: Vec) -> Self { + Self { + consumer_id, + data, + } + } + + /// Creates a transaction for testing + pub fn test() -> Self { + Self { + consumer_id: vec![0, 1, 2, 3, 4], + data: vec![5, 6, 7, 8, 9], + } + } + #[must_use] pub fn id(&self) -> ids::Id { let data_and_consumer_id = [self.data.clone(), self.consumer_id.clone()].concat(); @@ -72,6 +89,28 @@ pub struct Block { impl Block { + + /// Creates a transaction for testing + pub fn test() -> Self { + let consumer_id = vec![0, 1, 2, 3, 4]; + let data = vec![5, 6, 7, 8, 9]; + let transactions = vec![Transaction::new(consumer_id, data)]; + let parent_id = ids::Id::empty(); + let height = 0; + let timestamp = Utc::now().timestamp() as u64; + let status = choices::status::Status::default(); + Self { + parent_id, + height, + timestamp, + transactions, + status, + bytes: vec![], + id: ids::Id::empty(), + state: state::State::default(), + } + } + /// Can fail if the block can't be serialized to JSON. /// # Errors /// Will fail if the block can't be serialized to JSON. diff --git a/shared-sequencer/movement-sequencer/src/mempool/service.rs b/shared-sequencer/movement-sequencer/src/mempool/mempool.rs similarity index 100% rename from shared-sequencer/movement-sequencer/src/mempool/service.rs rename to shared-sequencer/movement-sequencer/src/mempool/mempool.rs diff --git a/shared-sequencer/movement-sequencer/src/mempool/mod.rs b/shared-sequencer/movement-sequencer/src/mempool/mod.rs index 0db5258e3..42aac63f2 100644 --- a/shared-sequencer/movement-sequencer/src/mempool/mod.rs +++ b/shared-sequencer/movement-sequencer/src/mempool/mod.rs @@ -1,29 +1,84 @@ +use serde::{Deserialize, Serialize}; + use crate::block::{ Transaction, Block }; +use std::cmp::Ordering; pub mod rocksdb; #[tonic::async_trait] pub trait MempoolTransactionOperations { - /// Checks whether a transaction exists in the mempool. - async fn has_transaction(&self, transaction_id : avalanche_types::ids::Id) -> Result; + // todo: move mempool_transaction methods into separate trait + + /// Checks whether a mempool transaction exists in the mempool. + async fn has_mempool_transaction(&self, transaction_id : avalanche_types::ids::Id) -> Result; + + /// Adds a mempool transaction to the mempool. + async fn add_mempool_transaction(&self, tx: MempoolTransaction) -> Result<(), anyhow::Error>; + + /// Removes a mempool transaction from the mempool. + async fn remove_mempool_transaction(&self, transaction_id: avalanche_types::ids::Id) -> Result<(), anyhow::Error>; + + /// Pops mempool transaction from the mempool. + async fn pop_mempool_transaction(&self) -> Result, anyhow::Error>; + + /// Gets a mempool transaction from the mempool. + async fn get_mempool_transaction(&self, transaction_id: avalanche_types::ids::Id) -> Result, anyhow::Error>; + + /// Pops the next n mempool transactions from the mempool. + async fn pop_mempool_transactions(&self, n: usize) -> Result, anyhow::Error> { + let mut mempool_transactions = Vec::new(); + for _ in 0..n { + if let Some(mempool_transaction) = self.pop_mempool_transaction().await? { + mempool_transactions.push(mempool_transaction); + } else { + break; + } + } + Ok(mempool_transactions) + } + + /// Checks whether the mempool has the transaction. + async fn has_transaction(&self, transaction_id: avalanche_types::ids::Id) -> Result { + self.has_mempool_transaction(transaction_id).await + } /// Adds a transaction to the mempool. - async fn add_transaction(&self, tx: Transaction) -> Result<(), anyhow::Error>; + async fn add_transaction(&self, tx: Transaction) -> Result<(), anyhow::Error> { + + if self.has_transaction(tx.id()).await? { + return Ok(()); + } + + let mempool_transaction = MempoolTransaction::slot_now(tx); + self.add_mempool_transaction(mempool_transaction).await + + } /// Removes a transaction from the mempool. - async fn remove_transaction(&self, transaction_id: avalanche_types::ids::Id) -> Result<(), anyhow::Error>; + async fn remove_transaction(&self, transaction_id: avalanche_types::ids::Id) -> Result<(), anyhow::Error> { + self.remove_mempool_transaction(transaction_id).await + } /// Pops transaction from the mempool. - async fn pop_transaction(&self, transaction_id: avalanche_types::ids::Id) -> Result; - + async fn pop_transaction(&self) -> Result, anyhow::Error> { + let mempool_transaction = self.pop_mempool_transaction().await?; + Ok(mempool_transaction.map(|mempool_transaction| mempool_transaction.transaction)) + } + /// Gets a transaction from the mempool. - async fn get_transaction(&self, transaction_id: avalanche_types::ids::Id) -> Result; + async fn get_transaction(&self, transaction_id: avalanche_types::ids::Id) -> Result, anyhow::Error> { + let mempool_transaction = self.get_mempool_transaction(transaction_id).await?; + Ok(mempool_transaction.map(|mempool_transaction| mempool_transaction.transaction)) + } - /// Provides well-ordered transaction iterable - async fn iter(&self) -> Result, anyhow::Error>; + /// Pops the next n transactions from the mempool. + async fn pop_transactions(&self, n: usize) -> Result, anyhow::Error> { + let mempool_transactions = self.pop_mempool_transactions(n).await?; + Ok(mempool_transactions.into_iter().map(|mempool_transaction| mempool_transaction.transaction).collect()) + } } @@ -38,15 +93,91 @@ pub trait MempoolBlockOperations { /// Removes a block from the mempool. async fn remove_block(&self, block_id: avalanche_types::ids::Id) -> Result<(), anyhow::Error>; - - /// Pops block from the mempool. - async fn pop_block(&self, block_id: avalanche_types::ids::Id) -> Result; /// Gets a block from the mempool. - async fn get_block(&self, block_id: avalanche_types::ids::Id) -> Result; + async fn get_block(&self, block_id: avalanche_types::ids::Id) -> Result, anyhow::Error>; + +} +/// Wraps a transaction with a timestamp for help ordering. +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct MempoolTransaction { + pub transaction: Transaction, + pub timestamp: u64, + pub slot_seconds : u64 } +impl PartialOrd for MempoolTransaction { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Ordered first by slot_seconds, then by transaction. +/// This allows us to use a BTreeSet to order transactions by slot_seconds, and then by transaction and pop them off in order. +impl Ord for MempoolTransaction { + fn cmp(&self, other: &Self) -> Ordering { + // First, compare by slot_seconds + match self.slot_seconds.cmp(&other.slot_seconds) { + Ordering::Equal => {} + non_equal => return non_equal, + } + // If slot_seconds are equal, then compare by transaction + self.transaction.cmp(&other.transaction) + } +} + +impl MempoolTransaction { + + const SLOT_SECONDS : u64 = 2; + + /// Creates a test MempoolTransaction. + pub fn test() -> Self { + Self { + transaction: Transaction::test(), + timestamp: 0, + slot_seconds: Self::SLOT_SECONDS + } + } + + pub fn at_time(transaction: Transaction, timestamp: u64) -> Self { + let floor = ( + timestamp / Self::SLOT_SECONDS + ) * Self::SLOT_SECONDS; + Self { + transaction, + timestamp : floor, + slot_seconds: Self::SLOT_SECONDS + } + } + + pub fn new(transaction: Transaction, timestamp: u64, slot_seconds: u64) -> Self { + Self { + transaction, + timestamp, + slot_seconds + } + } + + /// Creates a new MempoolTransaction with the current timestamp floored to the nearest slot. + /// todo: probably want to move this out to a factory. + pub fn slot_now(transaction : Transaction) -> MempoolTransaction { + + let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + + Self::at_time(transaction, timestamp) + + } + + pub fn id(&self) -> avalanche_types::ids::Id { + self.transaction.id() + } + + +} + + +/// Combines RocksdbMempool with InMemoryMempool. #[derive(Debug, Clone)] pub struct Mempool { diff --git a/shared-sequencer/movement-sequencer/src/mempool/rocksdb.rs b/shared-sequencer/movement-sequencer/src/mempool/rocksdb.rs index e0403bbce..288ab9a84 100644 --- a/shared-sequencer/movement-sequencer/src/mempool/rocksdb.rs +++ b/shared-sequencer/movement-sequencer/src/mempool/rocksdb.rs @@ -1,6 +1,7 @@ use super::{ MempoolBlockOperations, - MempoolTransactionOperations + MempoolTransactionOperations, + MempoolTransaction }; use rocksdb::{DB, Options, ColumnFamilyDescriptor}; use std::sync::Arc; @@ -10,69 +11,119 @@ use tokio::sync::RwLock; use avalanche_types::ids::Id; use futures::stream::Stream; use crate::block::{Transaction, Block}; +use anyhow::Error; #[derive(Debug, Clone)] pub struct RocksdbMempool { - db: Arc> + db: Arc>, } - impl RocksdbMempool { - pub fn new(path: &str) -> Self { + pub fn try_new(path: &str) -> Result { let mut options = Options::default(); options.create_if_missing(true); options.create_missing_column_families(true); - let transaction_cf = ColumnFamilyDescriptor::new("transactions", Options::default()); - let block_cf = ColumnFamilyDescriptor::new("blocks", Options::default()); + let mempool_transactions_cf = ColumnFamilyDescriptor::new("mempool_transactions", Options::default()); + let transaction_truths_cf = ColumnFamilyDescriptor::new("transaction_truths", Options::default()); + let blocks_cf = ColumnFamilyDescriptor::new("blocks", Options::default()); + let transaction_lookups_cf = ColumnFamilyDescriptor::new("transaction_lookups", Options::default()); - let db = DB::open_cf_descriptors(&options, path, vec![transaction_cf, block_cf]) - .expect("Failed to open database with column families"); + let db = DB::open_cf_descriptors( + &options, + path, + vec![mempool_transactions_cf, transaction_truths_cf, blocks_cf, transaction_lookups_cf] + ).map_err(|e| Error::new(e))?; - RocksdbMempool { - db: Arc::new(RwLock::new(db)), - } + Ok(RocksdbMempool { + db: Arc::new(RwLock::new(db)) + }) + } + + pub fn construct_mempool_transaction_key(transaction: &MempoolTransaction) -> String { + + // pad to 32 characters + let slot_seconds_str = format!("{:032}", transaction.timestamp); + + // Assuming transaction.transaction.id() returns a hex string of length 32 + let transaction_id_hex = transaction.transaction.id(); // This should be a String of hex characters + + // Concatenate the two parts to form a 48-character hex string key + let key = format!("{}:{}", slot_seconds_str, transaction_id_hex); + + key + } + + + + + /// Helper function to retrieve the key for mempool transaction from the lookup table. + async fn get_mempool_transaction_key(&self, transaction_id: &Id) -> Result>, Error> { + let db = self.db.read().await; + let cf_handle = db.cf_handle("transaction_lookups").ok_or_else(|| Error::msg("CF handle not found"))?; + db.get_cf(&cf_handle, transaction_id.to_vec()).map_err(|e| Error::new(e)) } + } #[tonic::async_trait] impl MempoolTransactionOperations for RocksdbMempool { - async fn has_transaction(&self, transaction_id: Id) -> Result { - let db = self.db.read().await; - let cf_handle = db.cf_handle("transactions").expect("CF handle not found"); - Ok(db.get_cf(&cf_handle, transaction_id.to_vec())?.is_some()) + async fn has_mempool_transaction(&self, transaction_id: Id) -> Result { + let key = self.get_mempool_transaction_key(&transaction_id).await?; + match key { + Some(k) => { + let db = self.db.read().await; + let cf_handle = db.cf_handle("mempool_transactions").ok_or_else(|| Error::msg("CF handle not found"))?; + Ok(db.get_cf(&cf_handle, k)?.is_some()) + }, + None => Ok(false), + } } - async fn add_transaction(&self, tx: Transaction) -> Result<(), anyhow::Error> { + async fn add_mempool_transaction(&self, tx: MempoolTransaction) -> Result<(), Error> { let serialized_tx = serde_json::to_vec(&tx)?; let db = self.db.write().await; - let cf_handle = db.cf_handle("transactions").expect("CF handle not found"); - db.put_cf(&cf_handle, tx.id().to_vec(), &serialized_tx)?; + let mempool_transactions_cf_handle = db.cf_handle("mempool_transactions").ok_or_else(|| Error::msg("CF handle not found"))?; + let transaction_lookups_cf_handle = db.cf_handle("transaction_lookups").ok_or_else(|| Error::msg("CF handle not found"))?; + + let key = Self::construct_mempool_transaction_key(&tx); + db.put_cf(&mempool_transactions_cf_handle, &key, &serialized_tx)?; + db.put_cf(&transaction_lookups_cf_handle, tx.transaction.id().to_vec(), &key)?; + Ok(()) } - async fn remove_transaction(&self, tx_id: Id) -> Result<(), anyhow::Error> { - let db = self.db.write().await; - let cf_handle = db.cf_handle("transactions").expect("CF handle not found"); - db.delete_cf(&cf_handle, tx_id.to_vec())?; - Ok(()) - } + async fn remove_mempool_transaction(&self, transaction_id: Id) -> Result<(), Error> { + let key = self.get_mempool_transaction_key(&transaction_id).await?; - async fn pop_transaction(&self, tx_id: Id) -> Result { - let db = self.db.write().await; - let cf_handle = db.cf_handle("transactions").expect("CF handle not found"); - let serialized_tx = db.get_cf(&cf_handle, tx_id.to_vec())?.expect("Transaction not found"); - let tx: Transaction = serde_json::from_slice(&serialized_tx)?; - db.delete_cf(&cf_handle, tx_id.to_vec())?; - Ok(tx) + match key { + Some(k) => { + let db = self.db.write().await; + let cf_handle = db.cf_handle("mempool_transactions").ok_or_else(|| Error::msg("CF handle not found"))?; + db.delete_cf(&cf_handle, k)?; + let lookups_cf_handle = db.cf_handle("transaction_lookups").ok_or_else(|| Error::msg("CF handle not found"))?; + db.delete_cf(&lookups_cf_handle, transaction_id.to_vec())?; + }, + None => (), + } + Ok(()) } - async fn get_transaction(&self, tx_id: Id) -> Result { + // Updated method signatures and implementations go here + async fn get_mempool_transaction(&self, transaction_id: Id) -> Result, Error> { + let key = match self.get_mempool_transaction_key(&transaction_id).await? { + Some(k) => k, + None => return Ok(None), // If no key found in lookup, return None + }; let db = self.db.read().await; - let cf_handle = db.cf_handle("transactions").expect("CF handle not found"); - let serialized_tx = db.get_cf(&cf_handle, tx_id.to_vec())?.expect("Transaction not found"); - let tx: Transaction = serde_json::from_slice(&serialized_tx)?; - Ok(tx) + let cf_handle = db.cf_handle("mempool_transactions").ok_or_else(|| Error::msg("CF handle not found"))?; + match db.get_cf(&cf_handle, &key)? { + Some(serialized_tx) => { + let tx: MempoolTransaction = serde_json::from_slice(&serialized_tx)?; + Ok(Some(tx)) + }, + None => Ok(None), + } } async fn iter(&self) { @@ -84,42 +135,131 @@ impl MempoolTransactionOperations for RocksdbMempool { #[tonic::async_trait] impl MempoolBlockOperations for RocksdbMempool { - async fn has_block(&self, block_id: Id) -> Result { + async fn has_block(&self, block_id: Id) -> Result { let db = self.db.read().await; - let cf_handle = db.cf_handle("blocks").expect("CF handle not found"); + let cf_handle = db.cf_handle("blocks").ok_or_else(|| Error::msg("CF handle not found"))?; Ok(db.get_cf(&cf_handle, block_id.to_vec())?.is_some()) } - async fn add_block(&self, block: Block) -> Result<(), anyhow::Error> { + async fn add_block(&self, block: Block) -> Result<(), Error> { let serialized_block = serde_json::to_vec(&block)?; let db = self.db.write().await; - let cf_handle = db.cf_handle("blocks").expect("CF handle not found"); + let cf_handle = db.cf_handle("blocks").ok_or_else(|| Error::msg("CF handle not found"))?; db.put_cf(&cf_handle, block.id().to_vec(), &serialized_block)?; Ok(()) } - async fn remove_block(&self, block_id: Id) -> Result<(), anyhow::Error> { + async fn remove_block(&self, block_id: Id) -> Result<(), Error> { let db = self.db.write().await; - let cf_handle = db.cf_handle("blocks").expect("CF handle not found"); + let cf_handle = db.cf_handle("blocks").ok_or_else(|| Error::msg("CF handle not found"))?; db.delete_cf(&cf_handle, block_id.to_vec())?; Ok(()) } - async fn pop_block(&self, block_id: Id) -> Result { - let db = self.db.write().await; - let cf_handle = db.cf_handle("blocks").expect("CF handle not found"); - let serialized_block = db.get_cf(&cf_handle, block_id.to_vec())?.expect("Block not found"); - let block: Block = serde_json::from_slice(&serialized_block)?; - db.delete_cf(&cf_handle, block_id.to_vec())?; - Ok(block) + async fn get_block(&self, block_id: Id) -> Result, Error> { + let db = self.db.read().await; + let cf_handle = db.cf_handle("blocks").ok_or_else(|| Error::msg("CF handle not found"))?; + let serialized_block = db.get_cf(&cf_handle, block_id.to_vec())?; + match serialized_block { + Some(serialized_block) => { + let block: Block = serde_json::from_slice(&serialized_block)?; + Ok(Some(block)) + }, + None => Ok(None), + } } + +} - async fn get_block(&self, block_id: Id) -> Result { - let db = self.db.read().await; - let cf_handle = db.cf_handle("blocks").expect("CF handle not found"); - let serialized_block = db.get_cf(&cf_handle, block_id.to_vec())?.expect("Block not found"); - let block: Block = serde_json::from_slice(&serialized_block)?; - Ok(block) + +pub mod test { + + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_rocksdb_mempool_basic_operations() -> Result<(), Error>{ + let temp_dir = tempdir().unwrap(); + let path = temp_dir.path().to_str().unwrap(); + let mempool = RocksdbMempool::try_new(path)?; + + let tx = MempoolTransaction::test(); + let tx_id = tx.id(); + mempool.add_mempool_transaction(tx.clone()).await?; + assert!(mempool.has_mempool_transaction(tx_id.clone()).await?); + let tx2 = mempool.get_mempool_transaction(tx_id.clone()).await?; + assert_eq!(Some(tx), tx2); + mempool.remove_mempool_transaction(tx_id.clone()).await?; + assert!(!mempool.has_mempool_transaction(tx_id.clone()).await?); + + let block = Block::test(); + let block_id = block.id(); + mempool.add_block(block.clone()).await?; + assert!(mempool.has_block(block_id.clone()).await?); + let block2 = mempool.get_block(block_id.clone()).await?; + assert_eq!(Some(block), block2); + mempool.remove_block(block_id.clone()).await?; + assert!(!mempool.has_block(block_id.clone()).await?); + + Ok(()) } - + + #[tokio::test] + async fn test_rocksdb_transaction_operations() -> Result<(), Error> { + let temp_dir = tempdir().unwrap(); + let path = temp_dir.path().to_str().unwrap(); + let mempool = RocksdbMempool::try_new(path)?; + + let tx = Transaction::test(); + let tx_id = tx.id(); + mempool.add_transaction(tx.clone()).await?; + assert!(mempool.has_transaction(tx_id.clone()).await?); + let tx2 = mempool.get_transaction(tx_id.clone()).await?; + assert_eq!(Some(tx), tx2); + mempool.remove_transaction(tx_id.clone()).await?; + assert!(!mempool.has_transaction(tx_id.clone()).await?); + + Ok(()) + } + + #[tokio::test] + async fn test_transaction_slot_based_ordering() -> Result<(), Error> { + let temp_dir = tempdir().unwrap(); + let path = temp_dir.path().to_str().unwrap(); + let mempool = RocksdbMempool::try_new(path)?; + + let tx1 = MempoolTransaction::at_time( + Transaction::new( + vec![1], + vec![1] + ), + 2, + ); + let tx2 = MempoolTransaction::at_time( + Transaction::new( + vec![2], + vec![2] + ), + 64, + ); + let tx3 = MempoolTransaction::at_time( + Transaction::new( + vec![3], + vec![3] + ), + 128 + ); + + mempool.add_mempool_transaction(tx2.clone()).await?; + mempool.add_mempool_transaction(tx1.clone()).await?; + mempool.add_mempool_transaction(tx3.clone()).await?; + + let txs = mempool.pop_mempool_transactions(3).await?; + assert_eq!(txs[0], tx1); + assert_eq!(txs[1], tx2); + assert_eq!(txs[2], tx3); + + Ok(()) + } + } \ No newline at end of file diff --git a/shared-sequencer/movement-sequencer/src/state/mod.rs b/shared-sequencer/movement-sequencer/src/state/mod.rs index 283390492..d796ca146 100644 --- a/shared-sequencer/movement-sequencer/src/state/mod.rs +++ b/shared-sequencer/movement-sequencer/src/state/mod.rs @@ -196,7 +196,7 @@ async fn test_state() { ids::Id::empty(), 0, random_manager::u64(), - random_manager::secure_bytes(10).unwrap(), + vec![], choices::status::Status::Accepted, ) .unwrap(); @@ -206,7 +206,7 @@ async fn test_state() { genesis_blk.id(), 1, genesis_blk.timestamp() + 1, - random_manager::secure_bytes(10).unwrap(), + vec![ ], choices::status::Status::Accepted, ) .unwrap(); diff --git a/shared-sequencer/movement-sequencer/src/vm/mod.rs b/shared-sequencer/movement-sequencer/src/vm/mod.rs index 723f4e342..fa5ede2cf 100644 --- a/shared-sequencer/movement-sequencer/src/vm/mod.rs +++ b/shared-sequencer/movement-sequencer/src/vm/mod.rs @@ -4,7 +4,7 @@ use std::{ collections::{HashMap, VecDeque}, io::{self, Error, ErrorKind}, sync::Arc, - time::Duration, + time::Duration, vec, }; use crate::{ diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml new file mode 100644 index 000000000..6d8deb348 --- /dev/null +++ b/tests/integration/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "integration-tests" +version = "0.1.0" +edition = "2021" + +[lib] +name = "integration_tests" +path = "src/lib.rs" + +[[test]] +name = "publisher_tests" +path = "publisher_tests.rs" + +[dependencies] +anyhow = { workspace = true } +bytemuck = { workspace = true } +ethers = { workspace = true } +tokio = { version = "1.0", features = ["full"] } + +# Workspace dependencies +eth-client = { path = "../../protocol-units/clients/eth" } +eth-adapter = { path = "../../protocol-units/eth-settlement/eth-adapter" } +eth-publisher = { path = "../../protocol-units/eth-settlement/eth-publisher" } +eth-contracts = { path = "../../protocol-units/eth-settlement/contracts" } +eth-relay-test-methods = { path = "./methods"} + +# Risc0 dependencies +bonsai-rest-api-mock = { workspace = true } +bonsai-sdk = { workspace = true, features = ["async"] } +risc0-zkvm = { workspace = true } +risc0-ethereum-relay = { workspace = true } +risc0-ethereum-contracts = { workspace = true } + +[features] +# Define any optional features +default = [] \ No newline at end of file diff --git a/tests/integration/eth_adapter.rs b/tests/integration/eth_adapter.rs new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/methods/Cargo.toml b/tests/integration/methods/Cargo.toml new file mode 100644 index 000000000..e194c3523 --- /dev/null +++ b/tests/integration/methods/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "eth-relay-test-methods" +version = { workspace = true } +edition = "2021" +publish = false + +[build-dependencies] +risc0-build = { workspace = true } + +[package.metadata.risc0] +methods = ["guest"] \ No newline at end of file diff --git a/tests/integration/methods/build.rs b/tests/integration/methods/build.rs new file mode 100644 index 000000000..f23f0a527 --- /dev/null +++ b/tests/integration/methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} \ No newline at end of file diff --git a/tests/integration/methods/guest/Cargo.toml b/tests/integration/methods/guest/Cargo.toml new file mode 100644 index 000000000..c4214a193 --- /dev/null +++ b/tests/integration/methods/guest/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "eth-relay-test-guests" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "0.20", default-features = false } \ No newline at end of file diff --git a/tests/integration/methods/guest/src/bin/slice_io.rs b/tests/integration/methods/guest/src/bin/slice_io.rs new file mode 100644 index 000000000..0c5dacae1 --- /dev/null +++ b/tests/integration/methods/guest/src/bin/slice_io.rs @@ -0,0 +1,31 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] +#![no_main] + +extern crate alloc; +use alloc::vec; + +use risc0_zkvm::guest::{env, env::Read}; + +risc0_zkvm::entry!(main); + +fn main() { + let mut len: u32 = 0; + env::stdin().read_slice(core::slice::from_mut(&mut len)); + let mut slice = vec![0u8; len as usize]; + env::stdin().read_slice(&mut slice); + env::commit_slice(&slice); +} \ No newline at end of file diff --git a/tests/integration/methods/src/lib.rs b/tests/integration/methods/src/lib.rs new file mode 100644 index 000000000..cd788e84c --- /dev/null +++ b/tests/integration/methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); \ No newline at end of file diff --git a/tests/integration/publisher_tests.rs b/tests/integration/publisher_tests.rs new file mode 100644 index 000000000..fa47ba05b --- /dev/null +++ b/tests/integration/publisher_tests.rs @@ -0,0 +1,88 @@ +use std::sync::Arc; +use bonsai_sdk::alpha_async::upload_img; +use eth_contracts::{Counter, Settlement}; +use ethers::prelude::*; +use ethers::types::{Bytes, H256 as ethers_H256, U256}; +use integration_tests::{get_anvil, get_bonsai_key, get_bonsai_url, get_ethers_client_config, get_bonsai_client}; +use risc0_ethereum_contracts::{BonsaiRelay, BonsaiTestRelay}; +use risc0_ethereum_relay::Relayer; +use eth_relay_test_methods::{SLICE_IO_ELF, SLICE_IO_ID}; +use tokio::time::{sleep, Duration}; + +async fn test_publisher() { + let anvil = get_anvil(); + + let ethers_client_config = get_ethers_client_config(anvil.as_ref()) + .await + .expect("Failed to get ethers client config"); + let ethers_client = Arc::new( + ethers_client_config + .get_client() + .await + .expect("Failed to get ethers client"), + ); + + let relay_contract = match risc0_zkvm::is_dev_mode() { + true => BonsaiTestRelay::deploy(ethers_client.clone(), ethers_client.signer().chain_id()) + .expect("unable to deploy the BonsaiTestRelay contract") + .send() + .await + .expect("unable to send the BonsaiTestRelay contract") + .address(), + false => { + let control_id_0 = U256::from_str_radix("0x447d7e12291364db4bc5421164880129", 16) + .expect("unable to parse control_id_0"); + let control_id_1 = U256::from_str_radix("0x12c49ad247d28a32147e13615c6c81f9", 16) + .expect("unable to parse control_id_1"); + + let verifier = Settlement::deploy(ethers_client.clone(), (control_id_0, control_id_1)) + .expect("unable to deploy the Settlement contract") + .send() + .await + .expect("unable to send the Settlement contract") + .address(); + + BonsaiRelay::deploy(ethers_client.clone(), verifier) + .expect("unable to deploy the BonsaiRelay contract") + .send() + .await + .expect("unable to send the BonsaiRelay contract") + .address() + } + }; + + let counter = Counter::deploy(ethers_client.clone(), ()) + .expect("unable to deploy the Counter contract") + .send() + .await + .expect("unable to send the Counter contract") + .address(); + + // run the bonsai relayer + let relayer = Relayer { + rest_api: false, + dev_mode: risc0_zkvm::is_dev_mode(), + rest_api_port: "8080".to_string(), + bonsai_api_key: get_bonsai_url(), + bonsai_api_url: get_bonsai_key(), + relay_contract_address: relay_contract, + }; + + dbg!("starting relayer"); + tokio::spawn(relayer.run(ethers_client_config.clone())); + + // Wait for relayer to start + sleep(Duration::from_secs(2)).await; + + // register elf + let bonsai_client = get_bonsai_client(get_api_key()).await; + // crfeate the memoryImg, upload it and return theimageId + let image_id_bytes: [u8; 32] = bytemuck::cast(SLICE_IO_ID); + let image_id = hex::encode(image_id_bytes); + upload_img( + bonsai_client.clone(), + image_id.clone(), + SLICE_IO_ELF.to_vec()) + .await + .expect("Failed to upload elf"); +} diff --git a/tests/integration/src/lib.rs b/tests/integration/src/lib.rs new file mode 100644 index 000000000..34616441e --- /dev/null +++ b/tests/integration/src/lib.rs @@ -0,0 +1,98 @@ +use anyhow::{Context, Result}; +use eth_client::{EthersClientConfig, WalletKey}; +use bonsai_sdk::{ + alpha::Client as BonsaiClient, + alpha_async::{get_client_from_parts}, +}; +use ethers::{ + providers::{Middleware, Provider, Ws}, + utils::AnvilInstance, +}; +use std::{sync::Arc, time::Duration}; + +const POLL_INTERVAL: Duration = Duration::from_secs(1); +const WAIT_DURATION: Duration = Duration::from_secs(5); +const MAX_RETRIES: u64 = 7 * 24 * 60 * 60 / WAIT_DURATION.as_secs(); // 1 week +const BONSAI_API_URI: &str = "http://localhost:8081"; + +/// Returns an empty Anvil builder. The default port is 8545. The mnemonic is +/// chosen randomly. +pub fn get_anvil() -> Option { + match std::env::var("ETHEREUM_HOST") { + Ok(_) => None, + _ => Some(ethers::utils::Anvil::new().spawn()), + } +} + +/// Returns a wallet key identifier defined by the env variable +/// `WALLET_KEY_IDENTIFIER` or from the given optional `anvil` instance. +pub fn get_wallet_key_identifier(anvil: Option<&AnvilInstance>) -> Result { + match std::env::var("WALLET_KEY_IDENTIFIER") { + Ok(wallet_key_identifier) => wallet_key_identifier.try_into(), + _ => { + let anvil = anvil.context("Anvil not instantiated.")?; + Ok(anvil.keys()[0].clone().into()) + } + } +} + +/// Returns an Ethereum Client Configuration struct. +pub async fn get_ethers_client_config(anvil: Option<&AnvilInstance>) -> Result { + let provider = get_ws_provider(anvil).await.unwrap(); + let eth_node_url = get_ws_provider_endpoint(anvil).await.unwrap(); + let eth_chain_id = provider.get_chainid().await.unwrap().as_u64(); + let wallet_key_identifier = get_wallet_key_identifier(anvil).unwrap(); + let ethers_client_config = EthersClientConfig::new( + eth_node_url, + eth_chain_id, + wallet_key_identifier, + MAX_RETRIES, + WAIT_DURATION, + ); + Ok(ethers_client_config) +} + +/// Returns an abstract provider for interacting with the Ethereum JSON RPC API +/// over Websockets. +pub async fn get_ws_provider(anvil: Option<&AnvilInstance>) -> Result> { + let endpoint = get_ws_provider_endpoint(anvil).await?; + Ok(Provider::::connect(&endpoint) + .await + .context("could not connect to {endpoint}")? + .interval(POLL_INTERVAL)) +} + +/// Returns the Websocket endpoint for the Ethereum JSON RPC API. +pub async fn get_ws_provider_endpoint(anvil: Option<&AnvilInstance>) -> Result { + let endpoint = match std::env::var("ETHEREUM_HOST") { + Ok(ethereum_host) => format!("ws://{ethereum_host}"), + _ => anvil.context("Anvil not instantiated.")?.ws_endpoint(), + }; + Ok(endpoint) +} + +pub async fn get_bonsai_client(api_key: String) -> BonsaiClient { + let bonsai_api_endpoint = get_bonsai_url(); + get_client_from_parts(bonsai_api_endpoint, api_key, risc0_zkvm::VERSION) + .await + .unwrap() +} + +pub fn get_bonsai_url() -> String { + let endpoint = match std::env::var("BONSAI_API_URL") { + Ok(endpoint) => endpoint, + Err(_) => BONSAI_API_URI.to_string(), + }; + + endpoint + .is_empty() + .then(|| BONSAI_API_URI.to_string()) + .unwrap_or(endpoint) +} + +pub fn get_bonsai_key() -> String { + match std::env::var("BONSAI_API_KEY") { + Ok(api_key) => api_key, + _ => "test_key".to_string(), + } +}