Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create and use Steel Solidity library #129

Merged
merged 13 commits into from
Jun 4, 2024
34 changes: 34 additions & 0 deletions contracts/src/steel/Steel.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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;

/// @title Steel Library
/// @notice This library provides a collection of utilities to work with Steel commitments in Solidity.
library Steel {
/// @notice A Commitment struct representing a block number and its block hash.
struct Commitment {
uint256 blockNumber; // Block number at which the commitment was made.
bytes32 blockHash; // Hash of the block at the specified block number.
}

/// @notice Validates if the provided Commitment matches the block hash of the given block number.
/// @param commitment The Commitment struct to validate.
/// @return isValid True if the commitment's block hash matches the block hash of the block number, false otherwise.
function validateCommitment(Commitment memory commitment) internal view returns (bool isValid) {
return commitment.blockHash == blockhash(commitment.blockNumber);
}
}
21 changes: 9 additions & 12 deletions examples/erc20-counter/README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
# RISC Zero View Call Proofs ERC20-Counter Example

> ***WARNING***: This software is still experimental, we do not recommend it for production use.

This example implements a counter that increments based on off-chain view call proofs submitted to the [Counter] contract.
The contract interacts with ERC-20 tokens, using view call proofs to verify that an account holds at least 1 token before incrementing the counter.
This contract leverages RISC Zero as a [coprocessor] for generating and verifying these proofs.
This example implements a counter that increments based on off-chain RISC Zero [Steel] proofs submitted to the [Counter] contract.
The contract interacts with ERC-20 tokens, using [Steel] proofs to verify that an account holds at least 1 token before incrementing the counter.

## Overview

The [Counter] contract is designed to interact with the Ethereum blockchain, leveraging the power of RISC Zero view call proofs to perform a specific operation: incrementing a counter based on the token holdings of an account.
The [Counter] contract is designed to interact with the Ethereum blockchain, leveraging the power of RISC Zero [Steel] proofs to perform a specific operation: incrementing a counter based on the token holdings of an account.

### Contract Functionality

#### Increment Counter

The core functionality of the [Counter] contract is to increment an internal counter whenever a valid view call proof is submitted.
The core functionality of the [Counter] contract is to increment an internal counter whenever a valid proof was submitted.
This proof must demonstrate that a specified account holds at least one unit of a particular ERC-20 token.
The contract ensures that the counter is only incremented when the proof is verified and the condition of holding at least one token is met.

#### View Call Proof Submission
#### Steel Proof Submission

Users or entities can submit view call proofs to the [Counter] contract.
Users or entities can submit proofs to the [Counter] contract.
These proofs are generated off-chain using the RISC Zero zkVM.
The proof encapsulates the verification of an account's token balance without exposing the account's details or requiring direct on-chain queries.

#### Token Balance Verification

Upon receiving a view call proof, the [Counter] contract decodes and verifies it against the specified ERC-20 token contract.
This process involves validating the proof against the contract's state at a specific block height, ensuring the account in question indeed holds at least one token at the time of the proof's generation.
Upon receiving a [Steel] proof, the [Counter] contract decodes the proof and validates it against the contract's state at a certain block height.
This ensures that the account in question actually holds at least one token at the time the proof was generated.

#### Counter Management

Expand Down Expand Up @@ -80,7 +77,7 @@ When you're ready, follow the [deployment guide] to get your application running
[RISC Zero]: https://www.risczero.com/
[Sepolia]: https://www.alchemy.com/overviews/sepolia-testnet
[cargo-binstall]: https://github.com/cargo-bins/cargo-binstall#cargo-binaryinstall
[coprocessor]: https://www.risczero.com/news/a-guide-to-zk-coprocessors-for-scalability
[deployment guide]: ./deployment-guide.md
[install Rust]: https://doc.rust-lang.org/cargo/getting-started/installation.html
[Counter]: ./contracts/Counter.sol
[Steel]: https://www.risczero.com/blog/introducing-steel
79 changes: 39 additions & 40 deletions examples/erc20-counter/apps/src/bin/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
// to the Bonsai proving service and publish the received proofs directly
// to your deployed app contract.

use alloy_primitives::{address, Address};
use alloy_sol_types::{sol, SolCall, SolInterface};
use alloy_primitives::Address;
use alloy_sol_types::{sol, SolCall};
use anyhow::Result;
use apps::TxSender;
use clap::Parser;
Expand All @@ -27,23 +27,14 @@ use risc0_steel::{config::ETH_SEPOLIA_CHAIN_SPEC, ethereum::EthViewCallEnv, Cont
use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts, VerifierContext};
use tracing_subscriber::EnvFilter;

/// Address of the deployed contract to call the function on. Here: USDT contract on Sepolia
/// Must match the guest code.
const CONTRACT: Address = address!("299Da20a3e957c78d7634A55D88195224C9f9f6b");

sol! {
/// ERC-20 balance function signature.
interface IERC20 {
function balanceOf(address account) external view returns (uint);
}
}

// `ICounter` interface automatically generated via the alloy `sol!` macro.
sol! {
interface ICounter {
function increment(bytes calldata journal, bytes calldata seal);
}
}
sol!("../contracts/ICounter.sol");

/// Arguments of the publisher CLI.
#[derive(Parser, Debug)]
Expand All @@ -63,7 +54,11 @@ struct Args {

/// Counter's contract address on Ethereum
#[clap(long)]
contract: String,
contract: Address,

/// ERC20 contract address on Ethereum
#[clap(long)]
token: Address,

/// Account address to read the balance_of on Ethereum
#[clap(long)]
Expand All @@ -75,43 +70,36 @@ fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();

// parse the command line arguments
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,
)?;

// Create a view call environment from an RPC endpoint and a block number. If no block number is
// provided, the latest block is used. The `with_chain_spec` method is used to specify the
// chain configuration.
let mut env =
EthViewCallEnv::from_rpc(&args.rpc_url, None)?.with_chain_spec(&ETH_SEPOLIA_CHAIN_SPEC);
let number = env.header().number();
let mut contract = Contract::preflight(CONTRACT, &mut env);
// Create a view call environment from an RPC endpoint using the latest block
let mut env = EthViewCallEnv::from_rpc(&args.rpc_url, None)?;
env = env.with_chain_spec(&ETH_SEPOLIA_CHAIN_SPEC);

// Function to call
let account = args.account;
let call = IERC20::balanceOfCall { account };
// Prepare the function call
let call = IERC20::balanceOfCall {
account: args.account,
};

// Preflight the view call to construct the input that is required to execute the function in
// the guest. It also returns the result of the call.
// Preflight the call to execute the function in the guest.
let mut contract = Contract::preflight(args.token, &mut env);
let returns = contract.call_builder(&call).call()?;
let view_call_input = env.into_input()?;
println!(
"For block {} `{}` returns: {}",
number,
"For block {} calling `{}` on {} returns: {}",
env.header().number(),
IERC20::balanceOfCall::SIGNATURE,
args.token,
returns._0
);

println!("proving...");
let view_call_input = env.into_input()?;
let env = ExecutorEnv::builder()
.write(&view_call_input)?
.write(&account)?
.write(&args.token)?
.write(&args.account)?
.build()?;

let receipt = default_prover()
Expand All @@ -122,20 +110,31 @@ fn main() -> Result<()> {
&ProverOpts::groth16(),
)?
.receipt;
println!("proving...done");

// Create a new `TxSender`.
let tx_sender = TxSender::new(
args.chain_id,
&args.rpc_url,
&args.eth_wallet_private_key,
&args.contract.to_string(),
)?;

// Encode the groth16 seal with the selector
let seal = encode(receipt.inner.groth16()?.seal.clone())?;

// Encode the function call for `ICounter.increment(journal, seal)`.
let calldata = ICounter::ICounterCalls::increment(ICounter::incrementCall {
journal: receipt.journal.bytes.clone().into(),
let calldata = ICounter::incrementCall {
journalData: receipt.journal.bytes.into(),
seal: seal.into(),
})
}
.abi_encode();

// Send the calldata to Ethereum.
println!("sending tx...");
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(tx_sender.send(calldata))?;
println!("sending tx...done");

Ok(())
}
2 changes: 1 addition & 1 deletion examples/erc20-counter/apps/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// The following library provides utility functions to help with sending
// The following library provides utility functions to help with sending
// transactions to a deployed app contract on Ethereum.

use anyhow::Result;
Expand Down
61 changes: 35 additions & 26 deletions examples/erc20-counter/contracts/Counter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,47 +17,56 @@
pragma solidity ^0.8.20;

import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol";
import {Steel} from "risc0/steel/Steel.sol";
import {ICounter} from "./ICounter.sol";
import {ImageID} from "./ImageID.sol"; // auto-generated contract after running `cargo build`.

/// @title Counter.
/// @notice Implements a counter that increments based on off-chain view call proofs submitted to this contract.
/// @dev The contract interacts with ERC-20 tokens, using view call proofs to verify that an account holds at least 1 token
/// @title Counter
/// @notice Implements a counter that increments based on off-chain Steel proofs submitted to this contract.
/// @dev The contract interacts with ERC-20 tokens, using Steel proofs to verify that an account holds at least 1 token
/// before incrementing the counter. This contract leverages RISC0-zkVM for generating and verifying these proofs.
contract Counter {
struct BlockCommitment {
bytes32 blockHash;
uint256 blockNumber;
}
/// @notice RISC Zero verifier contract address.

IRiscZeroVerifier public immutable verifier;
contract Counter is ICounter {
/// @notice Image ID of the only zkVM binary to accept verification from.
bytes32 public constant imageId = ImageID.BALANCE_OF_ID;

/// @notice RISC Zero verifier contract address.
IRiscZeroVerifier public immutable verifier;

/// @notice Address of the ERC-20 token contract.
address public immutable tokenAddress;

/// @notice Counter to track the number of successful verifications.
uint256 public counter;

/// @notice Initialize the contract, binding it to a specified RISC Zero verifier.
constructor(IRiscZeroVerifier _verifier) {
/// @notice Journal that is committed to by the guest.
struct Journal {
Steel.Commitment commitment;
address tokenAddress;
}

/// @notice Initialize the contract, binding it to a specified RISC Zero verifier and ERC-20 token address.
constructor(IRiscZeroVerifier _verifier, address _tokenAddress) {
verifier = _verifier;
tokenAddress = _tokenAddress;
counter = 0;
}

/// @dev Increment the counter if the view call proof verifies that
/// the specified account holds at least 1 token.
///
/// The view call proof must be generated off-chain using RISC0-zkVM and submitted here.
/// This function performs the proof verification process.
function increment(bytes calldata journal, bytes calldata seal) public {
// Construct the expected journal data. Verify will fail if journal does not match.
BlockCommitment memory commitment = abi.decode(journal, (BlockCommitment));
require(blockhash(commitment.blockNumber) == commitment.blockHash);
verifier.verify(seal, imageId, sha256(journal));
counter = counter + 1;
/// @inheritdoc ICounter
function increment(bytes calldata journalData, bytes calldata seal) external {
// Decode and validate the journal data
Journal memory journal = abi.decode(journalData, (Journal));
require(journal.tokenAddress == tokenAddress, "Invalid token address");
require(Steel.validateCommitment(journal.commitment), "Invalid commitment");

// Verify the proof
bytes32 journalHash = sha256(journalData);
verifier.verify(seal, imageId, journalHash);

counter += 1;
}

/// @notice Returns the value of the counter.
function get() public view returns (uint256) {
/// @inheritdoc ICounter
function get() external view returns (uint256) {
return counter;
}
}
26 changes: 26 additions & 0 deletions examples/erc20-counter/contracts/ICounter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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;

interface ICounter {
/// @notice Increments the counter, if the Steel proof verifies that the specified account holds at least 1 token.
/// @dev The Steel proof must be generated off-chain using RISC0-zkVM and submitted here.
function increment(bytes calldata journalData, bytes calldata seal) external;

/// @notice Returns the value of the counter.
function get() external view returns (uint256);
}
Loading
Loading