Skip to content

Commit

Permalink
Bonsai relay (risc0#696)
Browse files Browse the repository at this point in the history
* add bonsai-ethereum-relay, bonsai-sdk-async and bonsai-rest-api-mock

* add solc and forge to main workflow

* fix copyright

* fix copyright

* fix e2e_test.rs license

* add solc action

* replace solc with svm

* put sdk-async behind a feature flag

* add svm action

* add conditional svm installation

* improve Bonsai Relay README

* fix svm action

* set default debug level to info
capossele authored Jul 15, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent c0a7770 commit a83a652
Showing 62 changed files with 15,986 additions and 9 deletions.
13 changes: 13 additions & 0 deletions .github/actions/svm/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: svm install
description: Install svm
runs:
using: composite
steps:
- if: runner.os == 'macOS' && runner.arch == 'ARM64'
run: |
pgrep -q oahd && echo Rosetta already installed || /usr/sbin/softwareupdate --install-rosetta --agree-to-license
shell: bash

- run: |
which solc && echo Solc already installed || { cargo install svm-rs && svm install 0.8.17 && svm use 0.8.17; }
shell: bash
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -70,7 +70,9 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: risc0/foundry-toolchain@2fe7e70b520f62368a0e3c464f997df07ede420f
- uses: ./.github/actions/sccache
- uses: ./.github/actions/svm
- run: cargo test -F $FEATURE -F profiler
- run: cargo test -F $FEATURE --tests -- --ignored
- run: cargo test -F $FEATURE --manifest-path examples/Cargo.toml
22 changes: 13 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[workspace]
resolver = "2"
members = [
"bonsai/ethereum-relay",
"bonsai/rest-api-mock",
"bonsai/sdk",
"risc0/bootstrap",
"risc0/bootstrap/poseidon",
@@ -30,16 +32,18 @@ homepage = "https://risczero.com/"
repository = "https://github.com/risc0/risc0/"

[workspace.dependencies]
bonsai-sdk = { version = "0.2.0", default-features = false, path = "bonsai/sdk" }
risc0-build = { version = "0.16.1", default-features = false, path = "risc0/build" }
risc0-build-kernel = { version = "0.16.1", default-features = false, path = "risc0/build_kernel" }
risc0-circuit-rv32im = { version = "0.16.1", default-features = false, path = "risc0/circuit/rv32im" }
bonsai-ethereum-relay = { version = "0.2.0", default-features = false, path = "bonsai/ethereum-relay" }
bonsai-rest-api-mock = { version = "0.2.0", default-features = false, path = "bonsai/rest-api-mock" }
bonsai-sdk = { version = "0.2.0", default-features = false, path = "bonsai/sdk" }
risc0-build = { version = "0.16.1", default-features = false, path = "risc0/build" }
risc0-build-kernel = { version = "0.16.1", default-features = false, path = "risc0/build_kernel" }
risc0-circuit-rv32im = { version = "0.16.1", default-features = false, path = "risc0/circuit/rv32im" }
risc0-circuit-rv32im-sys = { version = "0.16.1", default-features = false, path = "risc0/circuit/rv32im-sys" }
risc0-core = { version = "0.16.1", default-features = false, path = "risc0/core" }
risc0-sys = { version = "0.16.1", default-features = false, path = "risc0/sys" }
risc0-zkp = { version = "0.16.1", default-features = false, path = "risc0/zkp" }
risc0-zkvm = { version = "0.16.1", default-features = false, path = "risc0/zkvm" }
risc0-zkvm-platform = { version = "0.16.1", default-features = false, path = "risc0/zkvm/platform" }
risc0-core = { version = "0.16.1", default-features = false, path = "risc0/core" }
risc0-sys = { version = "0.16.1", default-features = false, path = "risc0/sys" }
risc0-zkp = { version = "0.16.1", default-features = false, path = "risc0/zkp" }
risc0-zkvm = { version = "0.16.1", default-features = false, path = "risc0/zkvm" }
risc0-zkvm-platform = { version = "0.16.1", default-features = false, path = "risc0/zkvm/platform" }

[profile.bench]
lto = true
52 changes: 52 additions & 0 deletions bonsai/ethereum-relay/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[package]
name = "bonsai-ethereum-relay"
description = "A relayer to integrate Ethereum with Bonsai."
version = "0.2.0"
edition = { workspace = true }
license = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }

[dependencies]
anyhow = "1.0"
async-trait = "0.1.58"
axum = { version = "0.6.1", features = ["macros", "headers"] }
bincode = "1.3.3"
bonsai-proxy-contract = { path = "src/proxy-contract" }
bonsai-rest-api-mock = { workspace = true }
bonsai-sdk = { workspace = true, features = ["async"] }
clap = { version = "4.0", features = ["derive", "env"] }
displaydoc = "0.2"
ethers = { version = "=2.0.2", features = ["rustls", "ws"] }
ethers-signers = { version = "=2.0.2", features = ["aws"] }
ethers-solc = { version = "=2.0.2" }
futures = "0.3"
hex = "0.4.3"
hyper = "0.14"
pin-project = "1"
reqwest = { version = "0.11.14", features = ["stream", "json", "gzip"] }
risc0-zkvm = { workspace = true, features = ["binfmt"] }
rusoto_core = { version = "0.48.0", default-features = false, features = ["rustls"] }
rusoto_kms = { version = "0.48.0", default-features = false }
semver = "1.0"
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = "1.0"
snafu = "0.7"
thiserror = "1.0.11"
tokio = { version = "1.19", features = ["full", "sync"] }
tokio-stream = "0.1.12"
tower-http = { version = "0.4", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
typed-builder = "0.12.0"
utoipa = { version = "3.0.0", features = ["axum_extras", "time", "uuid"] }
utoipa-swagger-ui = { version = "3.0.2", features = ["axum", "debug-embed"] }
validator = { version = "0.16.0", features = ["derive"] }

[dev-dependencies]
bincode = "1"
bytemuck = "1.13"
risc0-zkvm-methods = { path = "../../risc0/zkvm/methods", default-features = false }
time = "0.3.11"
uuid = { version = "1.3.1", features = ["v4", "serde"] }
wiremock = "0.5"
80 changes: 80 additions & 0 deletions bonsai/ethereum-relay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Bonsai Ethereum Relay
This repository provides the `bonsai-ethereum-relay`, a tool to integrate Ethereum with Bonsai. It is coupled with an Ethereum Smart Contract able to proxy the interaction from Ethereum to Bonsai and vice versa.

## Usage
```console
Usage: bonsai-ethereum-relay [OPTIONS] --contract-address <CONTRACT_ADDRESS> --eth-node-url <ETH_NODE_URL> --wallet-key-identifier <WALLET_KEY_IDENTIFIER>

Options:
-p, --port <PORT>
The port of the relay server API [default: 8080]
--publish-mode
Toggle to disable the relay server API
--contract-address <CONTRACT_ADDRESS>
Bonsai Relay contract address on Ethereum
--eth-node-url <ETH_NODE_URL>
Ethereum Node endpoint
--eth-chain-id <ETH_CHAIN_ID>
Ethereum chain ID [default: 5]
-w, --wallet-key-identifier <WALLET_KEY_IDENTIFIER>
Wallet Key Identifier. Can be a private key as a hex string, or an AWS KMS key identifier [env: WALLET_KEY_IDENTIFIER]
--use-kms
Toggle to use a KMS client
-h, --help
Print help
-V, --version
Print version
```

A typical flow works as follows:
1. Deploy a Bonsai Relay Smart Contract on Ethereum at a given address `0xB..`.
2. Start an instance of the relay tool configured with the option `--contract-address` defined as `0xB..`.
3. Delegate some off-chain computation for a given Smart Contract `A` to Bonsai by registering the `Image` or `ELF` (i.e., the compiled binary responsible for executing the given computation on the RISC Zero ZKVM) to Bonsai.
4. The corresponding `Image ID` and the Bonsai Relay Smart Contract `0xB..` can be used to construct and deploy the Smart Contract `A` to Ethereum.
5. Send a transaction to Smart Contract `A` to trigger a `Callback request` event that the Bonsai Relay will catch and forward to Bonsai.
6. Once Bonsai has generated a proof of execution, the Bonsai Relay will forward this proof along with the result of the computation to the Bonsai Relay Smart Contract.
7. This triggers a verification of the proof on-chain, and only upon successful verification, the result of the computation will be forwarded to the original requester Smart Contract `A`.

### Publish mode
As an alternative to trigger a `Callback request` from Ethereum as described by step 5, the request can be sent directly to the Bonsai Relay via an HTTP REST API. Then, the remaining steps will flow as above. The following example explains how to do that.

#### Example Usage
The following example assumes that the Bonsai Relay is up and running with the server API enabled,
and that the memory image of your `ELF` is already registered against Bonsai with a given `IMAGE_ID` as its identifier.

```rust
// initialize a relay client
let relay_client = Client::from_parts(
"http://localhost:8080".to_string(), // here goes the actual url of the Bonsai Relay
"BONSAI_API_KEY" // here goes the actual Bonsai API-Key
)
.expect("Failed to initialize the relay client");

// Initialize the input for the guest.
// In this example we are sending a slice of bytes,
// where the first 4 bytes represents the length
// of the slice (in little endian).
let mut input = vec![0; 36];
input[0] = 32;
input[35] = 100;

// Create a CallbackRequest for the your contract
// example: (tests/solidity/contracts/Counter.sol).
let image_id: [u8; 32] = bytemuck::cast(IMAGE_ID);
let request = CallbackRequest {
callback_contract: counter.address(),
// you can use the command `solc --hashes tests/solidity/contracts/Counter.sol`
// to get the value for your actual contract
function_selector: [0xff, 0x58, 0x5c, 0xaf],
gas_limit: 3000000,
image_id,
input,
};

// Send the callback request to the Bonsai Relay.
relay_client
.callback_request(request)
.await
.expect("Callback request failed");

```
33 changes: 33 additions & 0 deletions bonsai/ethereum-relay/src/api/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 axum::{http::Request, middleware::Next, response::Response};

use super::{Error, Result};

pub(crate) async fn authorize<B>(mut req: Request<B>, next: Next<B>) -> Result<Response> {
if let Some(auth_header) = req
.headers()
.get("x-api-key")
.and_then(|header| header.to_str().ok())
{
let owned_value = auth_header.to_owned();
// insert the current user into a request extension so the handler can
// extract it
req.extensions_mut().insert(owned_value);
Ok(next.run(req).await)
} else {
Err(Error::Unauthorized)
}
}
74 changes: 74 additions & 0 deletions bonsai/ethereum-relay/src/api/bincode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 anyhow::Context;
use async_trait::async_trait;
use axum::{
body::Bytes,
extract::FromRequest,
http::{Request, StatusCode},
response::{IntoResponse, Response},
};
use hyper::{body::HttpBody, header, HeaderMap};
use serde::de::DeserializeOwned;

/// Bincode extractor of the request body as a stream.
/// When used as an extractor, it can deserialize request bodies into some type
/// that implements [`serde::Deserialize`] using [`bincode`].
pub(crate) struct Bincode<T>(pub T);

#[async_trait]
impl<T, S, B> FromRequest<S, B> for Bincode<T>
where
T: DeserializeOwned,
B: HttpBody + Send + 'static,
B::Data: Send,
B::Error: std::error::Error + Send + Sync,
S: Send + Sync,
{
type Rejection = Response;

async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
if !octet_stream_content_type(req.headers()) {
return Err((
StatusCode::UNSUPPORTED_MEDIA_TYPE,
"Expected request with `Content-Type: application/octet-stream`",
)
.into_response());
}

let bytes = Bytes::from_request(req, state)
.await
.map_err(IntoResponse::into_response)?;

let result = bincode::deserialize(&bytes).context("failed to deserialize");

let value = result.map_err(|err: anyhow::Error| {
(
StatusCode::BAD_REQUEST,
format!("Failed to parse request body: {err}"),
)
.into_response()
})?;

Ok(Bincode(value))
}
}

fn octet_stream_content_type(headers: &HeaderMap) -> bool {
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
return false;
};
content_type == "application/octet-stream"
}
63 changes: 63 additions & 0 deletions bonsai/ethereum-relay/src/api/callback_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 axum::{extract::State, Extension};
use bonsai_proxy_contract::CallbackRequestFilter;
use bonsai_sdk::alpha_async::get_client_from_parts;

use super::{bincode::Bincode, state::ApiState, Error, Result};
use crate::{
downloader::{
event_processor::EventProcessor,
proxy_callback_proof_processor::ProxyCallbackProofRequestProcessor,
},
sdk::client::CallbackRequest,
storage::Storage,
};

/// Publish a CallbackRequest to the Relayer.
///
/// Return status 200 on success.
#[utoipa::path(
post,
path = "/v1/callbacks",
request_body = CallbackRequest,
responses(
(status = 200, description = "Callback request sent successfully"),
(status = 400, description = "Bad request error"),
(status = 500, description = "Internal server error"),
)
)]
pub(crate) async fn post_callback_request<S: Storage + Sync + Send + Clone>(
Extension(api_key): Extension<String>,
State(s): State<ApiState<S>>,
Bincode(request): Bincode<CallbackRequest>,
) -> Result<(), Error> {
let client = get_client_from_parts(s.bonsai_url, api_key).await?;
let proxy = ProxyCallbackProofRequestProcessor::new(client, s.storage, Some(s.notifier));
proxy.process_event(request.into()).await
}

impl From<CallbackRequest> for CallbackRequestFilter {
fn from(val: CallbackRequest) -> Self {
CallbackRequestFilter {
account: ethers::types::Address::default(),
image_id: val.image_id,
input: val.input.into(),
callback_contract: val.callback_contract,
function_selector: val.function_selector,
gas_limit: val.gas_limit,
}
}
}
Loading

0 comments on commit a83a652

Please sign in to comment.