Skip to content

Commit 56bf29b

Browse files
authored
feat: add Schnorr signature-based integrity verification to native adapter (#94)
* chore: add k256 as dep * feat: update native host to use schnorr signatures for integrity * fix: update programs to use updated API * fix: avoid unwraps in verify fn * ci: extract toolchain version correctly
1 parent 4086265 commit 56bf29b

File tree

14 files changed

+180
-80
lines changed

14 files changed

+180
-80
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Extract Rust toolchain version
2+
description: "Extracts the Rust toolchain version from rust-toolchain.toml"
3+
inputs: {}
4+
outputs:
5+
rust-version:
6+
description: "The Rust toolchain version from rust-toolchain.toml"
7+
value: ${{ steps.extract.outputs.rust-version }}
8+
9+
runs:
10+
using: "composite"
11+
steps:
12+
- name: Extract Rust toolchain version
13+
id: extract
14+
shell: bash
15+
run: |
16+
VERSION="$(grep '^channel = ' rust-toolchain.toml | sed 's/channel = "\(.*\)"/\1/')"
17+
echo "rust-version=$VERSION" >> "$GITHUB_OUTPUT"

.github/workflows/lint.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ env:
1212
permissions: {}
1313

1414
jobs:
15+
extract-rust-version:
16+
name: Extract Rust toolchain version
17+
runs-on: ubuntu-latest
18+
outputs:
19+
rust-version: ${{ steps.extract.outputs.rust-version }}
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
23+
with:
24+
persist-credentials: false
25+
- name: Extract Rust toolchain version
26+
id: extract
27+
uses: ./.github/actions/extract-rust-version # zizmor: ignore[unpinned-uses]
28+
1529
clippy:
1630
name: Run clippy on crates
1731
runs-on: ubuntu-latest
@@ -48,6 +62,7 @@ jobs:
4862
crate-checks:
4963
name: Check that crates compile on their own
5064
runs-on: ubuntu-latest
65+
needs: extract-rust-version
5166
timeout-minutes: 90 # cold run takes a lot of time as each crate is compiled separately
5267
steps:
5368
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
@@ -61,7 +76,7 @@ jobs:
6176

6277
- uses: dtolnay/rust-toolchain@a02741459ec5e501b9843ed30b535ca0a0376ae4 # nightly
6378
with:
64-
toolchain: nightly-2024-07-27
79+
toolchain: ${{ needs.extract-rust-version.outputs.rust-version }}
6580
- uses: taiki-e/install-action@1b0e852a3465a29cd0b17876f2de42a88d145e91 # cargo-hack
6681
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
6782
with:
@@ -80,6 +95,7 @@ jobs:
8095
fmt:
8196
name: Check code formatting
8297
runs-on: ubuntu-latest
98+
needs: extract-rust-version
8399
timeout-minutes: 30
84100
steps:
85101
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
@@ -88,7 +104,7 @@ jobs:
88104
- uses: dtolnay/rust-toolchain@a02741459ec5e501b9843ed30b535ca0a0376ae4 # nightly
89105
with:
90106
components: rustfmt
91-
toolchain: nightly-2024-07-27
107+
toolchain: ${{ needs.extract-rust-version.outputs.rust-version }}
92108
- run: cargo fmt --all --check
93109

94110
codespell:

.github/workflows/prover.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,28 @@ on: pull_request
55
permissions: {}
66

77
jobs:
8+
extract-rust-version:
9+
name: Extract Rust toolchain version
10+
runs-on: ubuntu-latest
11+
outputs:
12+
rust-version: ${{ steps.extract.outputs.rust-version }}
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
16+
with:
17+
persist-credentials: false
18+
- name: Extract Rust toolchain version
19+
id: extract
20+
uses: ./.github/actions/extract-rust-version # zizmor: ignore[unpinned-uses]
21+
822
eval_perf:
923
permissions:
1024
# Needed to install the toolchain.
1125
contents: write
1226
# Needed to post the performance report comments.
1327
pull-requests: write
1428
runs-on: ubuntu-latest
29+
needs: extract-rust-version
1530

1631
steps:
1732
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
@@ -21,7 +36,7 @@ jobs:
2136
- name: Set up Rust
2237
uses: dtolnay/rust-toolchain@a02741459ec5e501b9843ed30b535ca0a0376ae4 # nightly
2338
with:
24-
toolchain: nightly-2024-07-27
39+
toolchain: ${{ needs.extract-rust-version.outputs.rust-version }}
2540

2641
- name: Cleanup space
2742
uses: ./.github/actions/cleanup # zizmor: ignore[unpinned-uses]

.github/workflows/unit.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,24 @@ concurrency:
1818
permissions: {}
1919

2020
jobs:
21+
extract-rust-version:
22+
name: Extract Rust toolchain version
23+
runs-on: ubuntu-latest
24+
outputs:
25+
rust-version: ${{ steps.extract.outputs.rust-version }}
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
29+
with:
30+
persist-credentials: false
31+
- name: Extract Rust toolchain version
32+
id: extract
33+
uses: ./.github/actions/extract-rust-version # zizmor: ignore[unpinned-uses]
34+
2135
test:
2236
name: Run unit tests and generate report
2337
runs-on: ubuntu-latest
38+
needs: extract-rust-version
2439
timeout-minutes: 60 # better fail-safe than the default 360 in github actions
2540
steps:
2641
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
@@ -35,7 +50,7 @@ jobs:
3550
- uses: dtolnay/rust-toolchain@a02741459ec5e501b9843ed30b535ca0a0376ae4 # nightly
3651
with:
3752
components: llvm-tools-preview
38-
toolchain: nightly-2024-07-27
53+
toolchain: ${{ needs.extract-rust-version.outputs.rust-version }}
3954
- name: Install latest nextest release
4055
uses: taiki-e/install-action@9903ab6feadaec33945de535fe9d181b91802a55 # v2
4156
with:
@@ -66,6 +81,7 @@ jobs:
6681
doc:
6782
name: Run doc tests
6883
runs-on: ubuntu-latest
84+
needs: extract-rust-version
6985
env:
7086
RUST_BACKTRACE: 1
7187
timeout-minutes: 60
@@ -81,7 +97,7 @@ jobs:
8197

8298
- uses: dtolnay/rust-toolchain@a02741459ec5e501b9843ed30b535ca0a0376ae4 # nightly
8399
with:
84-
toolchain: nightly-2024-07-27
100+
toolchain: ${{ needs.extract-rust-version.outputs.rust-version }}
85101
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
86102
with:
87103
cache-on-failure: true

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ bincode = "1.3"
4545
borsh = { version = "1.5.0", features = ["derive"] }
4646
cfg-if = "1.0.0"
4747
hex = { version = "0.4", features = ["serde"] }
48+
k256 = { version = "0.13.4", features = ["schnorr"] }
49+
rand_core = "0.6"
4850
serde = { version = "1.0", features = ["derive"] }
4951
sha2 = "0.10"
5052
thiserror = "1.0"

adapters/native/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ async-trait.workspace = true
1919
bincode.workspace = true
2020
borsh.workspace = true
2121
hex.workspace = true
22+
k256.workspace = true
23+
rand_core.workspace = true
2224
serde.workspace = true

adapters/native/src/host.rs

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
use std::{env, fmt, sync::Arc};
22

33
use async_trait::async_trait;
4+
use k256::schnorr::{
5+
signature::{Signer, Verifier},
6+
Signature, SigningKey,
7+
};
8+
use rand_core::OsRng;
49
use zkaleido::{
510
ExecutionSummary, Proof, ProofMetadata, ProofReceipt, ProofReceiptWithMetadata, ProofType,
611
PublicValues, VerifyingKey, VerifyingKeyCommitment, ZkVm, ZkVmError, ZkVmExecutor, ZkVmHost,
@@ -12,11 +17,12 @@ use crate::{env::NativeMachine, input::NativeMachineInputBuilder, proof::NativeP
1217

1318
type ProcessProofFn = dyn Fn(&NativeMachine) -> ZkVmResult<()> + Send + Sync;
1419

15-
/// A native host that holds a reference to a proof-processing function (`process_proof`).
20+
/// A native host that holds a reference to a proof-processing function and a Schnorr signing key.
1621
///
1722
/// This struct can be cloned cheaply (due to the internal [`Arc`]), and used by various
18-
/// parts of the application to execute native proofs or validations without
19-
/// requiring a real cryptographic backend.
23+
/// parts of the application to execute native proofs with Schnorr signature-based verification.
24+
/// The signing key is used to sign public values during proof generation and verify them during
25+
/// verification.
2026
#[derive(Clone)]
2127
pub struct NativeHost {
2228
/// A function wrapped in [`Arc`] and [`Box`] that processes proofs for a
@@ -25,15 +31,57 @@ pub struct NativeHost {
2531
/// By storing the function in a dynamic pointer (`Box<dyn ...>`) inside an
2632
/// [`Arc`], multiple host instances or threads can share the same proof
2733
/// logic without needing to replicate code or data.
28-
pub process_proof: Arc<Box<ProcessProofFn>>,
34+
process_fn: Arc<Box<ProcessProofFn>>,
35+
36+
/// The Schnorr signing key used for signing public values during proof generation
37+
/// and verifying signatures during proof verification.
38+
schnorr_key: SigningKey,
39+
}
40+
41+
impl NativeHost {
42+
/// Creates a new [`NativeHost`] with the given proof processing function.
43+
///
44+
/// Generates a fresh Schnorr signing key pair to sign and verify proof outputs,
45+
/// providing authenticity guarantees for native execution.
46+
///
47+
/// This method accepts infallible functions that return `()`. For functions that
48+
/// may fail and return `ZkVmResult<()>`, use [`new_fallible`](Self::new_fallible) instead.
49+
pub fn new<F>(process_fn: F) -> Self
50+
where
51+
F: Fn(&NativeMachine) + Send + Sync + 'static,
52+
{
53+
let schnorr_key = SigningKey::random(&mut OsRng);
54+
Self {
55+
process_fn: Arc::new(Box::new(move |zkvm: &NativeMachine| -> ZkVmResult<()> {
56+
process_fn(zkvm);
57+
Ok(())
58+
})),
59+
schnorr_key,
60+
}
61+
}
62+
63+
/// Creates a new [`NativeHost`] with a fallible proof processing function.
64+
///
65+
/// Use this method when your processing function may fail and returns `ZkVmResult<()>`.
66+
/// For infallible functions that return `()`, use [`new`](Self::new) instead.
67+
pub fn new_fallible<F>(process_fn: F) -> Self
68+
where
69+
F: Fn(&NativeMachine) -> ZkVmResult<()> + Send + Sync + 'static,
70+
{
71+
let schnorr_key = SigningKey::random(&mut OsRng);
72+
Self {
73+
process_fn: Arc::new(Box::new(process_fn)),
74+
schnorr_key,
75+
}
76+
}
2977
}
3078

3179
impl ZkVmHost for NativeHost {}
3280

3381
impl ZkVmExecutor for NativeHost {
3482
type Input<'a> = NativeMachineInputBuilder;
3583
fn execute<'a>(&self, native_machine: NativeMachine) -> ZkVmResult<ExecutionSummary> {
36-
(self.process_proof)(&native_machine)?;
84+
(self.process_fn)(&native_machine)?;
3785
let output = native_machine.state.borrow().output.clone();
3886
let public_values = PublicValues::new(output);
3987
// There is no straightforward equivalent of cycles and gas for native execution
@@ -58,7 +106,9 @@ impl ZkVmProver for NativeHost {
58106
) -> ZkVmResult<NativeProofReceipt> {
59107
let execution_result = self.execute(native_machine)?;
60108
let public_values = execution_result.into_public_values();
61-
let proof = Proof::default();
109+
// Sign the public values using the Schnorr signing key
110+
let signature = self.schnorr_key.sign(public_values.as_bytes());
111+
let proof = Proof::new(signature.to_bytes().to_vec());
62112
let receipt = ProofReceipt::new(proof, public_values);
63113

64114
let version: &str = env!("CARGO_PKG_VERSION");
@@ -72,14 +122,30 @@ impl ZkVmProver for NativeHost {
72122
impl ZkVmTypedVerifier for NativeHost {
73123
type ZkVmProofReceipt = NativeProofReceipt;
74124

75-
fn verify_inner(&self, _proof: &NativeProofReceipt) -> ZkVmResult<()> {
125+
fn verify_inner(&self, proof: &NativeProofReceipt) -> ZkVmResult<()> {
126+
let receipt: ProofReceiptWithMetadata = proof
127+
.clone()
128+
.try_into()
129+
.map_err(ZkVmError::InvalidProofReceipt)?;
130+
let signature = Signature::try_from(receipt.receipt().proof().as_bytes())
131+
.map_err(|e| ZkVmError::ProofVerificationError(format!("invalid signature: {e}")))?;
132+
// Verify the Schnorr signature over the public values
133+
self.schnorr_key
134+
.verifying_key()
135+
.verify(receipt.receipt().public_values().as_bytes(), &signature)
136+
.map_err(|e| {
137+
ZkVmError::ProofVerificationError(format!("signature verification failed: {e}"))
138+
})?;
139+
76140
Ok(())
77141
}
78142
}
79143

80144
impl ZkVmVkProvider for NativeHost {
81145
fn vk(&self) -> VerifyingKey {
82-
VerifyingKey::default()
146+
// Return the Schnorr public key (verifying key) as the verifying key
147+
let schnorr_public_key = self.schnorr_key.verifying_key().to_bytes().to_vec();
148+
VerifyingKey::new(schnorr_public_key)
83149
}
84150

85151
fn vk_commitment(&self) -> VerifyingKeyCommitment {

adapters/native/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
//! [`ZkVmHost`](zkaleido::ZkVmHost), and [`ZkVmInputBuilder`](zkaleido::ZkVmInputBuilder) traits,
55
//! allowing you to run a ZkVM-like environment without generating actual zero-knowledge proofs.
66
//!
7-
//! In native mode, the proof statements are executed directly in Rust, resulting in
8-
//! an **empty proof** but still producing the **expected public parameters**. This approach
9-
//! bypasses the usual process of ELF generation used by the ZkVM, making it useful for:
7+
//! In native mode, the proof statements are executed directly in Rust, producing the
8+
//! **expected public values**. When proving, the adapter generates a **Schnorr signature**
9+
//! over the public values instead of a zero-knowledge proof. This approach
10+
//! bypasses the usual process of ELF generation and ZKP generation used by the ZkVM, making it
11+
//! useful for:
1012
//!
1113
//! - **Local testing**: Quickly verify logic and expected outputs without the overhead of full
1214
//! proof generation.

examples/fibonacci/src/program.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,22 @@ impl ZkVmProgram for FibProgram {
3333

3434
#[cfg(test)]
3535
pub mod tests {
36-
use std::sync::Arc;
37-
3836
use zkaleido::ZkVmProgram;
39-
use zkaleido_native_adapter::{NativeHost, NativeMachine};
37+
use zkaleido_native_adapter::NativeHost;
4038

4139
use crate::{process_fibonacci, program::FibProgram};
4240

4341
pub fn get_native_host() -> NativeHost {
44-
NativeHost {
45-
process_proof: Arc::new(Box::new(move |zkvm: &NativeMachine| {
46-
process_fibonacci(zkvm);
47-
Ok(())
48-
})),
49-
}
42+
NativeHost::new(process_fibonacci)
5043
}
5144

5245
#[test]
5346
fn test_native() {
5447
let input = 5;
5548
let host = get_native_host();
56-
let receipt = FibProgram::prove(&input, &host).unwrap().receipt().clone();
57-
let output = FibProgram::process_output::<NativeHost>(receipt.public_values()).unwrap();
49+
let receipt = FibProgram::prove(&input, &host).unwrap();
50+
let output =
51+
FibProgram::process_output::<NativeHost>(receipt.receipt().public_values()).unwrap();
5852
assert_eq!(output, 5);
5953
}
6054
}

0 commit comments

Comments
 (0)