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: transaction outcome proof verification with composition #25

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions near-zk-light-client/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"rust-analyzer.linkedProjects": [
"./methods/guest/Cargo.toml",
"./outcome_methods/guest/Cargo.toml",
"./host/Cargo.toml"
]
}
2 changes: 1 addition & 1 deletion near-zk-light-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["host", "methods", "types"]
members = ["host", "methods", "outcome_methods", "types"]

# Always optimize; building and running the guest takes much longer without optimization.
[profile.dev]
Expand Down
7 changes: 6 additions & 1 deletion near-zk-light-client/host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ edition = "2021"

[dependencies]
methods = { path = "../methods" }
risc0-zkvm = { version = "0.20.1" }
outcome_methods = { path = "../outcome_methods" }
risc0-zkvm = { version = "0.21.0" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
serde = "1.0"
near-zk-types = { path = "../types" }
serde_json = "1.0"
borsh = "1.3.0"
anyhow = "1.0"
near-jsonrpc-client = "0.8.0"
near-primitives = "0.20.1"
near-jsonrpc-primitives = "0.20.1"
tokio = { version = "1.37.0", features = ["full"] }
158 changes: 140 additions & 18 deletions near-zk-light-client/host/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use borsh::{BorshDeserialize, BorshSerialize};
use methods::{LIGHT_CLIENT_ELF, LIGHT_CLIENT_ID};
use near_jsonrpc_client::{methods as rpc_methods, JsonRpcClient};
use near_jsonrpc_primitives::types::chunks::ChunkReference;
use near_primitives::types::{BlockId, BlockReference, Finality};
use near_zk_types::{
LightClientBlockLiteView, LightClientBlockView, PrevBlockContext, ValidatorStakeView,
BlockCommitData, LightClientBlockLiteView, LightClientBlockView, PrevBlockContext,
ValidatorStakeView,
};
use outcome_methods::{OUTCOME_ELF, OUTCOME_ID};
use risc0_zkvm::{default_prover, ExecutorEnv, Receipt};
use serde::{Deserialize, Serialize};

Expand All @@ -24,50 +30,166 @@ struct TestCase {
params: Params,
}

fn main() -> anyhow::Result<()> {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run`
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.init();
// let client = JsonRpcClient::connect("https://rpc.mainnet.near.org");
let client = JsonRpcClient::connect("https://archival-rpc.mainnet.near.org");

// NOTE: These test vectors come from https://github.com/austinabell/near-light-client-tests
// and are generated using mainnet data pulled from RPC.
let contents = include_str!("../../test-vectors/mainnet-80000000-81000000.json");
// Get the current block (just to get current height, maybe a better way to do this?)
let cur_block = client
.call(rpc_methods::block::RpcBlockRequest {
block_reference: BlockReference::Finality(Finality::Final),
})
.await?;

let test_cases: Vec<TestCase> = serde_json::from_str(&contents)?;
let initial_block_params = &test_cases[0].params;
let mut prev_context: PrevBlockContext = PrevBlockContext::Block {
prev_block: initial_block_params.previous_block.clone(),
current_bps: initial_block_params.current_bps.clone(),
// Get protocol config to retrieve epoch length.
let protocol_config = client
.call(
rpc_methods::EXPERIMENTAL_protocol_config::RpcProtocolConfigRequest {
block_reference: BlockReference::Finality(Finality::Final),
},
)
.await?;

// Pick a start height from 2 epochs back, with some buffer for the latency of requests.
let start_height = cur_block.header.height - (protocol_config.epoch_length * 3) - 7;

// Get a block from a previous epoch (really just need the hash, but this is what the rpc has).
let start_block = client
.call(rpc_methods::block::RpcBlockRequest {
block_reference: BlockReference::BlockId(BlockId::Height(start_height)),
})
.await?;

let light_client_block = client
.call(
rpc_methods::next_light_client_block::RpcLightClientNextBlockRequest {
last_block_hash: start_block.header.hash,
},
)
.await?
.expect("No valid light client block at this epoch");

let current_bps = round_trip_borsh(
light_client_block
.next_bps
.expect("light client block from previous epoch should have next_bps"),
)?;

let mut prev_context = PrevBlockContext::Block {
prev_block: LightClientBlockLiteView {
inner_lite: round_trip_borsh(light_client_block.inner_lite)?,
prev_block_hash: round_trip_borsh(light_client_block.prev_block_hash)?,
inner_rest_hash: round_trip_borsh(light_client_block.inner_rest_hash)?,
},
current_bps,
};
let mut prev_proof: Option<Receipt> = None;

for test_case in test_cases {
println!("Test description: {}", test_case.description);
let test_case = test_case.params;
let borsh_buffer = borsh::to_vec(&(&LIGHT_CLIENT_ID, &prev_context, &test_case.new_block))?;
// Obtain the default prover.
let prover = default_prover();

// Verify a few recent light client blocks.
let mut prev_proof: Option<Receipt> = None;
let mut last_block_hash = light_client_block.prev_block_hash;
for _ in 0..2 {
let block = client
.call(
rpc_methods::next_light_client_block::RpcLightClientNextBlockRequest {
last_block_hash,
},
)
.await?
.expect("should retrieve light client blocks for past epochs");
println!(
"proving block at height {}, epoch {}",
block.inner_lite.height, block.inner_lite.epoch_id
);
let mut builder = ExecutorEnv::builder();
if let Some(ref receipt) = prev_proof {
// Verifying a proof recursively requires adding the previous proof as an assumption.
builder.add_assumption(receipt.clone());
}
let env = builder.write_slice(&borsh_buffer).build()?;

// Obtain the default prover.
let prover = default_prover();
let borsh_buffer = borsh::to_vec(&(&LIGHT_CLIENT_ID, &prev_context, &block))?;
let env = builder.write_slice(&borsh_buffer).build()?;

// Produce a receipt by proving the specified ELF binary.
let receipt = prover.prove(env, LIGHT_CLIENT_ELF)?;

receipt.verify(LIGHT_CLIENT_ID)?;

let block_commit_data: BlockCommitData = borsh::from_slice(&receipt.journal.bytes)?;

// Update the previous context to verify off the last proof.
prev_context = PrevBlockContext::Proof {
journal: receipt.journal.bytes.clone(),
};
prev_proof = Some(receipt);
last_block_hash =
near_primitives::hash::CryptoHash(block_commit_data.new_block_lite.hash().0);
}

// Retrieve some transactions in the latest block to prove.
let chunk_id = cur_block.chunks.first().unwrap().chunk_hash;
let chunk = client
.call(rpc_methods::chunk::RpcChunkRequest {
chunk_reference: ChunkReference::ChunkHash { chunk_id },
})
.await?;

let prev_proof = prev_proof.unwrap();

for transaction in chunk.transactions.into_iter().take(3) {
println!("proving {:?}", transaction);
// Get execution proof outcome.
let rpc_outcome = client
.call(
rpc_methods::light_client_proof::RpcLightClientExecutionProofRequest {
id: near_primitives::types::TransactionOrReceiptId::Transaction {
transaction_hash: transaction.hash,
sender_id: transaction.signer_id,
},
light_client_head: last_block_hash,
},
)
.await?;

let outcome_proof = near_zk_types::RpcLightClientExecutionProofResponse {
outcome_proof: round_trip_borsh(rpc_outcome.outcome_proof)?,
outcome_root_proof: round_trip_borsh(rpc_outcome.outcome_root_proof)?,
block_header_lite: round_trip_borsh(rpc_outcome.block_header_lite)?,
block_proof: round_trip_borsh(rpc_outcome.block_proof)?,
};

// Verify proof, composing with light client proof
let mut builder = ExecutorEnv::builder();

// Add light client proof as assumption.
builder.add_assumption(prev_proof.clone());

let borsh_buffer = borsh::to_vec(&(&prev_proof.journal.bytes, &outcome_proof))?;
let env = builder.write_slice(&borsh_buffer).build()?;

// Produce a receipt by proving the specified ELF binary.
let receipt = prover.prove(env, OUTCOME_ELF)?;

receipt.verify(OUTCOME_ID)?;
}

Ok(())
}

// Conversions simply because near primitives types had bloat that could not be compiled in the
// zkvm. Just round trip serializing for dev expedience, not necessary.
// TODO get rid of this
fn round_trip_borsh<R>(origin: impl BorshSerialize) -> anyhow::Result<R>
where
R: BorshDeserialize,
{
let serialized = borsh::to_vec(&origin)?;
Ok(borsh::from_slice(&serialized)?)
}
2 changes: 1 addition & 1 deletion near-zk-light-client/methods/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"

[build-dependencies]
risc0-build = { version = "0.20.1" }
risc0-build = { version = "0.21.0" }

[package.metadata.risc0]
methods = ["guest"]
2 changes: 1 addition & 1 deletion near-zk-light-client/methods/guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2021"
[workspace]

[dependencies]
risc0-zkvm = { version = "0.20.1", default-features = false, features = ["std"] }
risc0-zkvm = { version = "0.21.0", default-features = false, features = ["std"] }
near-zk-types = { path = "../../types" }
borsh = "1.3"
sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.6-risczero.0" }
Expand Down
51 changes: 21 additions & 30 deletions near-zk-light-client/methods/guest/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
#![no_main]

use near_zk_types::{
ApprovalInner, CryptoHash, LightClientBlockLiteView, LightClientBlockView, PrevBlockContext,
ValidatorStakeView,
ApprovalInner, BlockCommitData, CryptoHash, LightClientBlockLiteView, LightClientBlockView,
PrevBlockContext, ValidatorStakeView,
};
use risc0_zkvm::guest::env;
use sha2::{Digest, Sha256};

risc0_zkvm::guest::entry!(main);

type CommitData = (
[u32; 8],
CryptoHash,
LightClientBlockLiteView,
Vec<ValidatorStakeView>,
);

fn main() {
let mut reader = env::stdin();
let (guest_id, prev_block_context, new_block): (
Expand All @@ -27,10 +16,14 @@ fn main() {
let (first_block_hash, last_known_block, current_bps) = match prev_block_context {
PrevBlockContext::Proof { journal } => {
env::verify(guest_id, &journal).expect("Failed to verify recursive journal");
let (prev_id, hash, last_known_block, current_bps): CommitData =
borsh::from_slice(&journal).expect("Invalid journal format");
assert_eq!(guest_id, prev_id, "Guest program IDs do not match");
(hash, last_known_block, current_bps)
let BlockCommitData {
block_guest_id,
first_block_hash,
new_block_lite,
block_producers,
} = borsh::from_slice(&journal).expect("Invalid journal format");
assert_eq!(guest_id, block_guest_id, "Guest program IDs do not match");
(first_block_hash, new_block_lite, block_producers)
}
PrevBlockContext::Block {
prev_block,
Expand Down Expand Up @@ -137,21 +130,19 @@ fn main() {
"Next block producers hash doesn't match"
);


// NOTE: this has the assumption that only one block per epoch will be validated. If it's
// necessary to validate multiple blocks per epoch, this should be changed.
// Update block producers to be committed.
block_producers = next_bps;
}

borsh::to_writer(
&mut env::journal(),
&(
// Note: guest_id shouldn't be needed if only verifying one block. Handling optional
// values in practice would unnecessarily complicate things.
// TODO double check not having guest id be optional is correct.
&guest_id,
&first_block_hash,
&new_block_lite,
&block_producers,
),
)
.unwrap();
let commit_data = BlockCommitData {
block_guest_id: guest_id,
first_block_hash,
new_block_lite,
block_producers,
};

borsh::to_writer(&mut env::journal(), &commit_data).unwrap();
}
10 changes: 10 additions & 0 deletions near-zk-light-client/outcome_methods/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "outcome_methods"
version = "0.1.0"
edition = "2021"

[build-dependencies]
risc0-build = { version = "0.21.0" }

[package.metadata.risc0]
methods = ["guest"]
3 changes: 3 additions & 0 deletions near-zk-light-client/outcome_methods/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
risc0_build::embed_methods();
}
17 changes: 17 additions & 0 deletions near-zk-light-client/outcome_methods/guest/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "outcome"
version = "0.1.0"
edition = "2021"

[workspace]

[dependencies]
risc0-zkvm = { version = "0.21.0", default-features = false, features = ["std"] }
near-zk-types = { path = "../../types" }
borsh = "1.3"
sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.6-risczero.0" }
methods = { path = "../../methods" }

[patch.crates-io]
crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.2-risc0" }
ed25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.0-risczero.1" }
Loading