Cleanup cross zone transfer (#52)

* builder apis

* drop unused nonce import

* zone witness in cross zone transfer scenario

* add cms to the ledger when proving

* Reliable risc0 recursion (#53)

* introduce risc0_images and risc0_images_police

* static instead of const for elfs

* gen images in a loop to resolve id changes due to recursion

* r0_proofs reference each other through risc0_images when recursing

* update ledger to use risc0_images

* remove debug panics

---------

Co-authored-by: Giacomo Pasini <g.pasini98@gmail.com>
This commit is contained in:
davidrusu 2025-03-03 12:09:35 +01:00 committed by GitHub
parent dc2fd35894
commit 2622908bcd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 315 additions and 122 deletions

View File

@ -7,7 +7,9 @@ members = [
"risc0_proofs",
"bundle_risc0_proof",
"tx_risc0_proof",
"ledger_validity_proof"
"ledger_validity_proof",
"risc0_images",
"risc0_images_police",
]
# Always optimize; building and running the risc0_proofs takes much longer without optimization.

View File

@ -1,5 +1,5 @@
[package]
name = "nomos_cl_bundle_risc0_proof"
name = "nomos_mantle_bundle_risc0_proof"
version = "0.1.0"
edition = "2021"

View File

@ -10,7 +10,7 @@ risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] }
cl = { path = "../../cl" }
ledger_proof_statements = { path = "../../ledger_proof_statements" }
nomos_cl_tx_risc0_proof = { path = "../../tx_risc0_proof" }
risc0_images = { path = "../../risc0_images" }
[patch.crates-io]

View File

@ -5,7 +5,11 @@ fn main() {
let bundle_private: BundleWitness = env::read();
for tx in &bundle_private.txs {
env::verify(nomos_cl_tx_risc0_proof::TX_ID, &serde::to_vec(&tx).unwrap()).unwrap();
env::verify(
risc0_images::nomos_mantle_tx_risc0_proof::TX_ID,
&serde::to_vec(&tx).unwrap(),
)
.unwrap();
}
env::commit(&bundle_private.commit());

View File

@ -86,6 +86,67 @@ pub struct OutputWitness {
}
impl OutputWitness {
pub fn new(
value: u64,
unit: Unit,
nf_pk: NullifierCommitment,
zone_id: ZoneId,
rng: impl RngCore,
) -> Self {
Self {
state: [0; 32],
value,
unit,
nonce: Nonce::random(rng),
zone_id,
nf_pk,
}
}
pub fn reissue(input: InputWitness, rng: impl RngCore) -> Self {
Self::new(
input.value,
input.unit_witness.unit(),
input.nf_sk.commit(),
input.zone_id,
rng,
)
}
pub fn spend_with_change(
input: InputWitness,
amount: u64,
to_pk: NullifierCommitment,
to_zone: ZoneId,
mut rng: impl RngCore,
) -> (OutputWitness, OutputWitness) {
assert!(input.value > amount);
let transfer = OutputWitness::reissue(input, &mut rng)
.set_value(amount)
.set_nf_pk(to_pk)
.set_zone(to_zone);
let change = OutputWitness::reissue(input, &mut rng).set_value(input.value - amount);
(transfer, change)
}
pub fn set_value(mut self, value: u64) -> Self {
self.value = value;
self
}
pub fn set_nf_pk(mut self, nf_pk: NullifierCommitment) -> Self {
self.nf_pk = nf_pk;
self
}
pub fn set_zone(mut self, zone_id: ZoneId) -> Self {
self.zone_id = zone_id;
self
}
pub fn note_commitment(&self) -> NoteCommitment {
NoteCommitment::commit(
self.state,

View File

@ -53,7 +53,7 @@ pub struct Tx {
pub updates: Vec<LedgerUpdate>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct TxWitness {
pub inputs: Vec<InputWitness>,
pub outputs: Vec<(OutputWitness, Vec<u8>)>,
@ -107,6 +107,17 @@ impl LedgerUpdateWitness {
}
impl TxWitness {
pub fn add_input(mut self, input: InputWitness, input_cm_proof: (MMR, MMRProof)) -> Self {
self.inputs.push(input);
self.frontier_paths.push(input_cm_proof);
self
}
pub fn add_output(mut self, output: OutputWitness, data: Vec<u8>) -> Self {
self.outputs.push((output, data));
self
}
pub fn compute_updates(&self, inputs: &[InputDerivedFields]) -> Vec<LedgerUpdateWitness> {
let mut updates = BTreeMap::new();
assert_eq!(self.inputs.len(), self.frontier_paths.len());

View File

@ -57,8 +57,9 @@ impl LedgerState {
self.nullifiers.root()
}
pub fn add_commitment(&mut self, cm: &NoteCommitment) -> MMRProof {
self.commitments.push(&cm.0)
pub fn add_commitment(&mut self, cm: &NoteCommitment) -> (MMR, MMRProof) {
let proof = self.commitments.push(&cm.0);
(self.commitments.clone(), proof)
}
pub fn add_nullifiers(&mut self, nfs: Vec<Nullifier>) -> BatchUpdateProof {

19
emmarin/cl/gen_risc0_images.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
# We generate in a *loop* because some risc0 proofs are recursive, so if a child
# proof's id changes, then the parent proof will also change, but we don't see the
# parent's id change until the next run.
cargo run --bin gen_risc0_images > risc0_images/src/lib.rs.new
while ! cmp -s risc0_images/src/lib.rs.new risc0_images/src/lib.rs
do
mv risc0_images/src/lib.rs.new risc0_images/src/lib.rs
cargo run --bin gen_risc0_images > risc0_images/src/lib.rs.new
echo "-------- FINISHED UPDATE ITERATION --------"
done
rm risc0_images/src/lib.rs.new
cargo test -p risc0_images_police

View File

@ -6,10 +6,7 @@ edition = "2021"
[dependencies]
cl = { path = "../cl" }
ledger_proof_statements = { path = "../ledger_proof_statements" }
nomos_mantle_risc0_proofs = { path = "../risc0_proofs" }
nomos_cl_bundle_risc0_proof = { path = "../bundle_risc0_proof" }
nomos_cl_tx_risc0_proof = { path = "../tx_risc0_proof" }
ledger_validity_proof = { path = "../ledger_validity_proof" }
risc0_images = { path = "../risc0_images" }
risc0-zkvm = { version = "1.0", features = ["prove", "metal"] }
risc0-groth16 = { version = "1.0" }
rand = "0.8.5"

View File

@ -23,7 +23,11 @@ impl ProvedBundle {
let opts = risc0_zkvm::ProverOpts::succinct();
let prove_info = prover
.prove_with_opts(env, nomos_cl_bundle_risc0_proof::BUNDLE_ELF, &opts)
.prove_with_opts(
env,
risc0_images::nomos_mantle_bundle_risc0_proof::BUNDLE_ELF,
&opts,
)
.unwrap();
println!(
@ -46,7 +50,7 @@ impl ProvedBundle {
pub fn verify(&self) -> bool {
self.risc0_receipt
.verify(nomos_cl_bundle_risc0_proof::BUNDLE_ID)
.verify(risc0_images::nomos_mantle_bundle_risc0_proof::BUNDLE_ID)
.is_ok()
}
}

View File

@ -11,7 +11,7 @@ pub struct ProvedLedgerTransition {
}
impl ProvedLedgerTransition {
pub fn prove(mut ledger: LedgerState, zone_id: ZoneId, bundles: Vec<ProvedBundle>) -> Self {
pub fn prove(ledger: &mut LedgerState, zone_id: ZoneId, bundles: Vec<ProvedBundle>) -> Self {
let mut env = risc0_zkvm::ExecutorEnv::builder();
let mut w_bundles = Vec::new();
let mut nullifiers = Vec::new();
@ -53,6 +53,16 @@ impl ProvedLedgerTransition {
nf_proofs: ledger.add_nullifiers(nullifiers),
};
for bundle in &witness.bundles {
for update in &bundle.bundle.updates {
if update.zone_id == zone_id {
for cm in &update.outputs {
ledger.add_commitment(cm);
}
}
}
}
witness.write(&mut env);
let env = env.build().unwrap();
@ -65,7 +75,7 @@ impl ProvedLedgerTransition {
// This struct contains the receipt along with statistics about execution of the guest
let opts = risc0_zkvm::ProverOpts::succinct();
let prove_info = prover
.prove_with_opts(env, ledger_validity_proof::LEDGER_ELF, &opts)
.prove_with_opts(env, risc0_images::ledger_validity_proof::LEDGER_ELF, &opts)
.unwrap();
println!(
@ -89,7 +99,7 @@ impl ProvedLedgerTransition {
pub fn verify(&self) -> bool {
self.risc0_receipt
.verify(ledger_validity_proof::LEDGER_ID)
.verify(risc0_images::ledger_validity_proof::LEDGER_ID)
.is_ok()
}
}

View File

@ -31,7 +31,7 @@ impl StfProof {
}
pub fn nop_stf() -> [u8; 32] {
risc0_stf(nomos_mantle_risc0_proofs::STF_NOP_ID)
risc0_stf(risc0_images::nomos_mantle_risc0_proofs::STF_NOP_ID)
}
pub fn prove_nop(public: StfPublic) -> Self {
@ -47,7 +47,11 @@ impl StfProof {
let opts = risc0_zkvm::ProverOpts::succinct();
let prove_info = prover
.prove_with_opts(env, nomos_mantle_risc0_proofs::STF_NOP_ELF, &opts)
.prove_with_opts(
env,
risc0_images::nomos_mantle_risc0_proofs::STF_NOP_ELF,
&opts,
)
.unwrap();
println!(
@ -60,7 +64,7 @@ impl StfProof {
let receipt = prove_info.receipt;
Self {
risc0_id: nomos_mantle_risc0_proofs::STF_NOP_ID,
risc0_id: risc0_images::nomos_mantle_risc0_proofs::STF_NOP_ID,
public,
risc0_receipt: receipt,
}

View File

@ -36,7 +36,11 @@ impl ProvedTx {
// This struct contains the receipt along with statistics about execution of the guest
let opts = risc0_zkvm::ProverOpts::succinct();
let prove_info = prover
.prove_with_opts(env, nomos_cl_tx_risc0_proof::TX_ELF, &opts)
.prove_with_opts(
env,
risc0_images::nomos_mantle_tx_risc0_proof::TX_ELF,
&opts,
)
.map_err(|_| Error::Risc0ProofFailed)?;
println!(
@ -57,7 +61,7 @@ impl ProvedTx {
pub fn verify(&self) -> bool {
self.risc0_receipt
.verify(nomos_cl_tx_risc0_proof::TX_ID)
.verify(risc0_images::nomos_mantle_tx_risc0_proof::TX_ID)
.is_ok()
}
}

View File

@ -1,8 +1,8 @@
use cl::{
crust::{
balance::{UnitWitness, NOP_COVENANT},
BundleWitness, InputWitness, Nonce, Nullifier, NullifierCommitment, NullifierSecret,
OutputWitness, TxWitness,
BundleWitness, InputWitness, NoteCommitment, Nullifier, NullifierCommitment,
NullifierSecret, OutputWitness, TxWitness,
},
ds::mmr::{MMRProof, MMR},
mantle::{
@ -16,9 +16,12 @@ use ledger::{
update::ProvedBatchUpdate,
};
use ledger_proof_statements::stf::StfPublic;
use rand::Rng;
use rand::{Rng, RngCore};
use rand_core::CryptoRngCore;
const ZONE_A: ZoneId = [0u8; 32];
const ZONE_B: ZoneId = [1u8; 32];
fn nmo() -> UnitWitness {
UnitWitness {
spending_covenant: NOP_COVENANT,
@ -49,42 +52,19 @@ fn cross_transfer_transition(
to: User,
amount: u64,
to_zone: ZoneId,
mut ledger_in: LedgerState,
mut ledger_out: LedgerState,
ledger_in: &mut LedgerState,
ledger_out: &mut LedgerState,
) -> (ProvedLedgerTransition, ProvedLedgerTransition) {
assert!(amount <= input.value);
println!("nfs in zone_a: {}", ledger_in.nullifiers.len());
println!("nfs in zone_b: {}", ledger_out.nullifiers.len());
let mut rng = rand::thread_rng();
let change = input.value - amount;
let transfer = OutputWitness {
state: Default::default(),
value: amount,
unit: nmo().unit(),
nonce: Nonce::random(&mut rng),
zone_id: to_zone,
nf_pk: to.pk(),
};
let change = OutputWitness {
state: Default::default(),
value: change,
unit: nmo().unit(),
nonce: Nonce::random(&mut rng),
zone_id: input.zone_id,
nf_pk: input.nf_sk.commit(), // return change to sender
};
let (transfer, change) =
OutputWitness::spend_with_change(input, amount, to.pk(), to_zone, &mut rng);
// Construct the tx consuming the input and producing the two outputs.
let tx_witness = TxWitness {
inputs: vec![input],
outputs: vec![(transfer, vec![]), (change, vec![])],
data: Default::default(),
mints: vec![],
burns: vec![],
frontier_paths: vec![input_proof],
};
let tx_witness = TxWitness::default()
.add_input(input, input_proof)
.add_output(transfer, vec![])
.add_output(change, vec![]);
let proved_tx = ProvedTx::prove(
tx_witness.clone(),
@ -102,95 +82,78 @@ fn cross_transfer_transition(
println!("proving ledger A transition");
let ledger_in_transition =
ProvedLedgerTransition::prove(ledger_in.clone(), input.zone_id, vec![bundle.clone()]);
ProvedLedgerTransition::prove(ledger_in, input.zone_id, vec![bundle.clone()]);
println!("proving ledger B transition");
let ledger_out_transition =
ProvedLedgerTransition::prove(ledger_out.clone(), to_zone, vec![bundle]);
ledger_in.add_commitment(&change.note_commitment());
ledger_in.add_nullifiers(vec![input.nullifier()]);
ledger_out.add_commitment(&transfer.note_commitment());
assert_eq!(
ledger_in_transition.public().ledger,
ledger_in.to_witness().commit()
);
assert_eq!(
ledger_out_transition.public().ledger,
ledger_out.to_witness().commit()
);
let ledger_out_transition = ProvedLedgerTransition::prove(ledger_out, to_zone, vec![bundle]);
(ledger_in_transition, ledger_out_transition)
}
struct ZoneWitness {
ledger: LedgerState,
}
impl ZoneWitness {
fn new() -> Self {
let ledger = LedgerState::default();
Self { ledger }
}
fn state(&self) -> ZoneState {
ZoneState {
stf: StfProof::nop_stf(),
zone_data: [0; 32],
ledger: self.ledger.to_witness().commit(),
}
}
fn fill_nfs(&mut self, amount: usize, mut rng: impl RngCore) {
self.ledger.add_nullifiers(
std::iter::repeat_with(|| Nullifier(rng.gen()))
.take(amount)
.collect(),
);
}
fn add_commitment(&mut self, cm: &NoteCommitment) -> (MMR, MMRProof) {
self.ledger.add_commitment(cm)
}
}
#[test]
fn zone_update_cross() {
let mut rng = rand::thread_rng();
let zone_a_id = [0; 32];
let zone_b_id = [1; 32];
// alice is sending 8 NMO to bob.
// Alice is sending 8 NMO to bob.
let alice = User::random(&mut rng);
let bob = User::random(&mut rng);
// Alice has an unspent note worth 10 NMO
let utxo = OutputWitness {
state: Default::default(),
value: 10,
unit: nmo().unit(),
nonce: Nonce::random(&mut rng),
zone_id: zone_a_id,
nf_pk: alice.pk(),
};
let utxo = OutputWitness::new(10, nmo().unit(), alice.pk(), ZONE_A, &mut rng);
let alice_input = InputWitness::from_output(utxo, alice.sk(), nmo());
let mut ledger_a = LedgerState::default();
ledger_a.add_nullifiers(
std::iter::repeat_with(|| Nullifier(rng.gen()))
.take(2_usize.pow(10))
.collect(),
);
let alice_cm_path = ledger_a.add_commitment(&utxo.note_commitment());
let alice_cm_proof = (ledger_a.commitments.clone(), alice_cm_path);
let mut zone_a = ZoneWitness::new();
zone_a.fill_nfs(2_usize.pow(10), &mut rng);
let alice_cm_proof = zone_a.add_commitment(&utxo.note_commitment());
let ledger_b = LedgerState::default();
let mut zone_b = ZoneWitness::new();
let zone_a_old = ZoneState {
stf: StfProof::nop_stf(),
zone_data: [0; 32],
ledger: ledger_a.to_witness().commit(),
};
let (zone_a_old, zone_b_old) = (zone_a.state(), zone_b.state());
let zone_b_old = ZoneState {
stf: StfProof::nop_stf(),
zone_data: [0; 32],
ledger: ledger_b.to_witness().commit(),
};
let (ledger_a_transition, ledger_b_transition) = cross_transfer_transition(
let (ledger_proof_a, ledger_proof_b) = cross_transfer_transition(
alice_input,
alice_cm_proof,
bob,
8,
zone_b_id,
ledger_a,
ledger_b,
ZONE_B,
&mut zone_a.ledger,
&mut zone_b.ledger,
);
let zone_a_new = ZoneState {
ledger: ledger_a_transition.public().ledger,
..zone_a_old
};
let zone_b_new = ZoneState {
ledger: ledger_b_transition.public().ledger,
..zone_b_old
};
let (zone_a_new, zone_b_new) = (zone_a.state(), zone_b.state());
let stf_proof_a = StfProof::prove_nop(StfPublic {
old: zone_a_old,
@ -217,7 +180,7 @@ fn zone_update_cross() {
let proved_batch = ProvedBatchUpdate {
batch,
ledger_proofs: vec![ledger_a_transition, ledger_b_transition],
ledger_proofs: vec![ledger_proof_a, ledger_proof_b],
stf_proofs: vec![stf_proof_a, stf_proof_b],
};

View File

@ -10,7 +10,7 @@ risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] }
cl = { path = "../../cl" }
ledger_proof_statements = { path = "../../ledger_proof_statements" }
nomos_cl_bundle_risc0_proof = { path = "../../bundle_risc0_proof" }
risc0_images = { path = "../../risc0_images" }
[patch.crates-io]
# add RISC Zero accelerator support for all downstream usages of the following crates.

View File

@ -23,7 +23,7 @@ fn main() {
} in bundles
{
env::verify(
nomos_cl_bundle_risc0_proof::BUNDLE_ID,
risc0_images::nomos_mantle_bundle_risc0_proof::BUNDLE_ID,
&serde::to_vec(&bundle).unwrap(),
)
.unwrap();

View File

@ -0,0 +1,7 @@
[package]
name = "risc0_images"
version = "0.1.0"
edition = "2021"
[dependencies]
binary_macros = "1.0.0"

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
[package]
name = "risc0_images_police"
version = "0.1.0"
edition = "2021"
[dependencies]
sha2 = "0.10"
base64 = "0.22"
risc0_images = { path = "../risc0_images" }
nomos_mantle_risc0_proofs = { path = "../risc0_proofs" }
nomos_mantle_bundle_risc0_proof = { path = "../bundle_risc0_proof" }
nomos_mantle_tx_risc0_proof = { path = "../tx_risc0_proof" }
ledger_validity_proof = { path = "../ledger_validity_proof" }

View File

@ -0,0 +1,25 @@
use base64::prelude::*;
macro_rules! gen_risc0_image {
($module:ident, $id:ident, $elf:ident) => {
println!("pub mod {} {{", stringify!($module));
println!(
" pub const {}: [u32; 8] = {:?};",
stringify!($id),
$module::$id
);
println!(
" pub static {}: &[u8] = binary_macros::base64!({:?});",
stringify!($elf),
BASE64_STANDARD.encode(&$module::$elf)
);
println!("}}");
};
}
fn main() {
gen_risc0_image!(nomos_mantle_risc0_proofs, STF_NOP_ID, STF_NOP_ELF);
gen_risc0_image!(nomos_mantle_bundle_risc0_proof, BUNDLE_ID, BUNDLE_ELF);
gen_risc0_image!(nomos_mantle_tx_risc0_proof, TX_ID, TX_ELF);
gen_risc0_image!(ledger_validity_proof, LEDGER_ID, LEDGER_ELF);
}

View File

@ -0,0 +1,51 @@
#[cfg(test)]
mod tests {
fn hash(x: impl AsRef<[u8]>) -> [u8; 32] {
use sha2::{Digest, Sha256};
Sha256::digest(x).into()
}
#[test]
fn ensure_images_are_correct() {
assert_eq!(
risc0_images::nomos_mantle_risc0_proofs::STF_NOP_ID,
nomos_mantle_risc0_proofs::STF_NOP_ID,
"STF_NOP_ID"
);
assert_eq!(
hash(risc0_images::nomos_mantle_risc0_proofs::STF_NOP_ELF),
hash(nomos_mantle_risc0_proofs::STF_NOP_ELF),
"STF_NOP_ELF"
);
assert_eq!(
risc0_images::nomos_mantle_bundle_risc0_proof::BUNDLE_ID,
nomos_mantle_bundle_risc0_proof::BUNDLE_ID,
"BUNDLE_ID"
);
assert_eq!(
hash(risc0_images::nomos_mantle_bundle_risc0_proof::BUNDLE_ELF),
hash(nomos_mantle_bundle_risc0_proof::BUNDLE_ELF),
"BUNDLE_ELF"
);
assert_eq!(
risc0_images::nomos_mantle_tx_risc0_proof::TX_ID,
nomos_mantle_tx_risc0_proof::TX_ID,
"TX_ID"
);
assert_eq!(
hash(risc0_images::nomos_mantle_tx_risc0_proof::TX_ELF),
hash(nomos_mantle_tx_risc0_proof::TX_ELF),
"TX_ELF"
);
assert_eq!(
risc0_images::ledger_validity_proof::LEDGER_ID,
ledger_validity_proof::LEDGER_ID,
"LEDGER_ID"
);
assert_eq!(
hash(risc0_images::ledger_validity_proof::LEDGER_ELF),
hash(ledger_validity_proof::LEDGER_ELF),
"LEDGER_ELF"
);
}
}

View File

@ -1,5 +1,5 @@
[package]
name = "nomos_cl_tx_risc0_proof"
name = "nomos_mantle_tx_risc0_proof"
version = "0.1.0"
edition = "2021"