Integrate zone withdrawal with CL (#17)

* aat: integrate withdraw with CL

* aat: withdrawal passes!

* aat: cleanup withdrawals a bit

* aat: move Ptx{Input|Output|Private to cl::partial_tx

* aat: zone_state zone transition validation coded w.r.t. metadata

* aat: rename meta to in_meta in zone transition validation
This commit is contained in:
davidrusu 2024-08-07 23:37:21 +04:00 committed by GitHub
parent 5547b739c5
commit a320c20d25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 651 additions and 305 deletions

View File

@ -6,7 +6,6 @@ edition = "2021"
[dependencies] [dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
cl = { path = "../../cl/cl" } cl = { path = "../../cl/cl" }
goas_proof_statements = { path = "../proof_statements" } ledger_proof_statements = { path = "../../cl/ledger_proof_statements" }
proof_statements = { path = "../../cl/proof_statements" }
once_cell = "1" once_cell = "1"
sha2 = "0.10" sha2 = "0.10"

View File

@ -0,0 +1,36 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Event {
Spend(Spend),
}
impl Event {
pub fn to_bytes(&self) -> Vec<u8> {
// TODO: add variant tag to byte encoding
match self {
Event::Spend(spend) => spend.to_bytes().to_vec(),
}
}
}
/// An event that authorizes spending zone funds
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Spend {
pub amount: u64,
/// The public key of the recipient
pub to: cl::NullifierCommitment,
/// The nullifier of note that is being spent, this is to avoid using the spend event to
/// for multiple notes
pub fund_nf: cl::Nullifier,
}
impl Spend {
pub fn to_bytes(&self) -> [u8; 72] {
let mut bytes = [0; 72];
bytes[0..8].copy_from_slice(&self.amount.to_le_bytes());
bytes[8..40].copy_from_slice(self.to.as_bytes());
bytes[40..72].copy_from_slice(self.fund_nf.as_bytes());
bytes
}
}

View File

@ -1,6 +1,7 @@
pub mod events;
use cl::{ use cl::{
balance::Unit, balance::Unit,
crypto,
input::InputWitness, input::InputWitness,
nullifier::{Nullifier, NullifierCommitment}, nullifier::{Nullifier, NullifierCommitment},
output::OutputWitness, output::OutputWitness,
@ -17,14 +18,14 @@ pub const MAX_EVENTS: usize = 1 << 8;
// state of the zone // state of the zone
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub struct StateCommitment([u8; 32]); pub struct StateCommitment(pub [u8; 32]);
pub type AccountId = u32; pub type AccountId = u32;
// PLACEHOLDER: this is probably going to be NMO? // PLACEHOLDER: this is probably going to be NMO?
pub static ZONE_CL_FUNDS_UNIT: Lazy<Unit> = Lazy::new(|| crypto::hash_to_curve(b"NMO")); pub static ZONE_CL_FUNDS_UNIT: Lazy<Unit> = Lazy::new(|| cl::note::unit_point("NMO"));
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZoneMetadata { pub struct ZoneMetadata {
pub zone_vk: [u8; 32], pub zone_vk: [u8; 32],
pub funds_vk: [u8; 32], pub funds_vk: [u8; 32],
@ -41,11 +42,11 @@ impl ZoneMetadata {
} }
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StateWitness { pub struct StateWitness {
pub balances: BTreeMap<u32, u32>, pub balances: BTreeMap<AccountId, u64>,
pub included_txs: Vec<Input>, pub included_txs: Vec<Input>,
pub output_events: Vec<Event>, pub output_events: Vec<events::Event>,
pub zone_metadata: ZoneMetadata, pub zone_metadata: ZoneMetadata,
} }
@ -67,34 +68,61 @@ impl StateWitness {
StateCommitment(root) StateCommitment(root)
} }
fn events_root(&self) -> [u8; 32] { pub fn withdraw(mut self, w: Withdraw) -> Self {
let event_bytes = Vec::from_iter( self.included_txs.push(Input::Withdraw(w));
self.output_events
.iter() let Withdraw {
.map(Event::to_bytes) from,
.map(Vec::from_iter), amount,
); to,
fund_nf,
} = w;
let from_balance = self.balances.entry(from).or_insert(0);
*from_balance = from_balance
.checked_sub(amount)
.expect("insufficient funds in account");
let spend_auth = events::Spend {
amount,
to,
fund_nf,
};
self.output_events.push(events::Event::Spend(spend_auth));
self
}
pub fn events_root(&self) -> [u8; 32] {
let event_bytes = Vec::from_iter(self.output_events.iter().map(events::Event::to_bytes));
let event_merkle_leaves = cl::merkle::padded_leaves(&event_bytes); let event_merkle_leaves = cl::merkle::padded_leaves(&event_bytes);
cl::merkle::root::<MAX_EVENTS>(event_merkle_leaves) cl::merkle::root::<MAX_EVENTS>(event_merkle_leaves)
} }
fn included_txs_root(&self) -> [u8; 32] { pub fn included_txs_root(&self) -> [u8; 32] {
// this is a placeholder // this is a placeholder
let tx_bytes = [vec![0u8; 32]]; let tx_bytes = [vec![0u8; 32]];
let tx_merkle_leaves = cl::merkle::padded_leaves(&tx_bytes); let tx_merkle_leaves = cl::merkle::padded_leaves(&tx_bytes);
cl::merkle::root::<MAX_TXS>(tx_merkle_leaves) cl::merkle::root::<MAX_TXS>(tx_merkle_leaves)
} }
fn balances_root(&self) -> [u8; 32] { pub fn balances_root(&self) -> [u8; 32] {
let balance_bytes = Vec::from_iter(self.balances.iter().map(|(k, v)| { let balance_bytes = Vec::from_iter(self.balances.iter().map(|(owner, balance)| {
let mut bytes = [0; 8]; let mut bytes: Vec<u8> = vec![];
bytes.copy_from_slice(&k.to_le_bytes()); bytes.extend(owner.to_le_bytes());
bytes[8..].copy_from_slice(&v.to_le_bytes()); bytes.extend(balance.to_le_bytes());
bytes.to_vec() bytes
})); }));
let balance_merkle_leaves = cl::merkle::padded_leaves(&balance_bytes); let balance_merkle_leaves = cl::merkle::padded_leaves(&balance_bytes);
cl::merkle::root::<MAX_BALANCES>(balance_merkle_leaves) cl::merkle::root::<MAX_BALANCES>(balance_merkle_leaves)
} }
pub fn event_merkle_path(&self, event: events::Event) -> Vec<cl::merkle::PathNode> {
let idx = self.output_events.iter().position(|e| e == &event).unwrap();
let event_bytes = Vec::from_iter(self.output_events.iter().map(events::Event::to_bytes));
let event_merkle_leaves = cl::merkle::padded_leaves(&event_bytes);
cl::merkle::path::<MAX_EVENTS>(event_merkle_leaves, idx)
}
} }
impl From<StateCommitment> for [u8; 32] { impl From<StateCommitment> for [u8; 32] {
@ -103,27 +131,35 @@ impl From<StateCommitment> for [u8; 32] {
} }
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Withdraw { pub struct Withdraw {
pub from: AccountId, pub from: AccountId,
pub amount: AccountId, pub amount: u64,
pub to: NullifierCommitment, pub to: NullifierCommitment,
pub nf: Nullifier, pub fund_nf: Nullifier,
} }
impl Withdraw { impl Withdraw {
pub fn to_event(&self) -> events::Spend {
events::Spend {
amount: self.amount,
to: self.to,
fund_nf: self.fund_nf,
}
}
pub fn to_bytes(&self) -> [u8; 72] { pub fn to_bytes(&self) -> [u8; 72] {
let mut bytes = [0; 72]; let mut bytes = [0; 72];
bytes[0..4].copy_from_slice(&self.from.to_le_bytes()); bytes[0..4].copy_from_slice(&self.from.to_le_bytes());
bytes[4..8].copy_from_slice(&self.amount.to_le_bytes()); bytes[4..8].copy_from_slice(&self.amount.to_le_bytes());
bytes[8..40].copy_from_slice(self.to.as_bytes()); bytes[8..40].copy_from_slice(self.to.as_bytes());
bytes[40..72].copy_from_slice(self.nf.as_bytes()); bytes[40..72].copy_from_slice(self.fund_nf.as_bytes());
bytes bytes
} }
} }
/// A deposit of funds into the zone /// A deposit of funds into the zone
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deposit { pub struct Deposit {
/// The note that is used to deposit funds into the zone /// The note that is used to deposit funds into the zone
pub deposit: InputWitness, pub deposit: InputWitness,
@ -137,21 +173,8 @@ pub struct Deposit {
pub zone_funds_out: OutputWitness, pub zone_funds_out: OutputWitness,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Input { pub enum Input {
Withdraw(Withdraw), Withdraw(Withdraw),
Deposit(Deposit), Deposit(Deposit),
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Event {
Spend(goas_proof_statements::zone_funds::Spend),
}
impl Event {
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Event::Spend(spend) => spend.to_bytes().to_vec(),
}
}
}

View File

@ -16,4 +16,8 @@ common = { path = "../common" }
tempfile = "3" tempfile = "3"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
rand = "0.8.5" rand = "0.8.5"
rand_core = "0.6.0"
cl = { path = "../../cl/cl" } cl = { path = "../../cl/cl" }
ledger = { path = "../../cl/ledger" }
ledger_proof_statements = { path = "../../cl/ledger_proof_statements" }
goas_proof_statements = { path = "../proof_statements" }

View File

@ -4,10 +4,9 @@
/// This workaround manually calls into docker after creating a directory with the required permissions. /// This workaround manually calls into docker after creating a directory with the required permissions.
/// In addition, splitting the process in different stages highlights better the different work that /// In addition, splitting the process in different stages highlights better the different work that
/// needs to be done which could be split across different actors. /// needs to be done which could be split across different actors.
use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use risc0_zkvm::{get_prover_server, ProverOpts, Receipt}; use risc0_zkvm::{get_prover_server, ProverOpts, Receipt};
use std::path::PathBuf;
const WORK_DIR_ENV: &str = "RISC0_WORK_DIR"; const WORK_DIR_ENV: &str = "RISC0_WORK_DIR";
@ -47,8 +46,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
risc0_groth16::docker::stark_to_snark(&converted.get_seal_bytes())?; risc0_groth16::docker::stark_to_snark(&converted.get_seal_bytes())?;
std::fs::create_dir_all(&args.output_dir)?; std::fs::create_dir_all(&args.output_dir)?;
std::fs::copy(work_dir_path.join("proof.json"), args.output_dir.join("proof.json"))?; std::fs::copy(
std::fs::copy(work_dir_path.join("public.json"), args.output_dir.join("public.json"))?; work_dir_path.join("proof.json"),
args.output_dir.join("proof.json"),
)?;
std::fs::copy(
work_dir_path.join("public.json"),
args.output_dir.join("public.json"),
)?;
Ok(()) Ok(())
} }

View File

@ -0,0 +1,73 @@
use common::{events::Event, Input, StateWitness};
use goas_proof_statements::{zone_funds::SpendFundsPrivate, zone_state::ZoneStatePrivate};
pub fn prove_zone_stf(
state: StateWitness,
inputs: Vec<Input>,
zone_in: cl::PartialTxInputWitness,
zone_out: cl::PartialTxOutputWitness,
) -> ledger::DeathProof {
let private_inputs = ZoneStatePrivate {
state,
inputs,
zone_in,
zone_out,
};
let env = risc0_zkvm::ExecutorEnv::builder()
.write(&private_inputs)
.unwrap()
.build()
.unwrap();
let prover = risc0_zkvm::default_prover();
use std::time::Instant;
let start_t = Instant::now();
let opts = risc0_zkvm::ProverOpts::succinct();
let prove_info = prover
.prove_with_opts(env, goas_risc0_proofs::ZONE_STATE_ELF, &opts)
.unwrap();
println!("STARK 'zone_stf' prover time: {:.2?}", start_t.elapsed());
let receipt = prove_info.receipt;
ledger::DeathProof::from_risc0(goas_risc0_proofs::ZONE_STATE_ID, receipt)
}
pub fn prove_zone_fund_withdraw(
in_zone_funds: cl::PartialTxInputWitness,
zone_note: cl::PartialTxOutputWitness,
out_zone_funds: cl::PartialTxOutputWitness,
spent_note: cl::PartialTxOutputWitness,
out_zone_state: &StateWitness,
withdraw: common::Withdraw,
) -> ledger::DeathProof {
let spend_event = withdraw.to_event();
let private_inputs = SpendFundsPrivate {
in_zone_funds,
zone_note,
out_zone_funds,
spent_note,
spend_event,
spend_event_state_path: out_zone_state.event_merkle_path(Event::Spend(spend_event)),
balances_root: out_zone_state.balances_root(),
txs_root: out_zone_state.included_txs_root(),
};
let env = risc0_zkvm::ExecutorEnv::builder()
.write(&private_inputs)
.unwrap()
.build()
.unwrap();
let prover = risc0_zkvm::default_prover();
use std::time::Instant;
let start_t = Instant::now();
let opts = risc0_zkvm::ProverOpts::succinct();
let prove_info = prover
.prove_with_opts(env, goas_risc0_proofs::SPEND_ZONE_FUNDS_ELF, &opts)
.unwrap();
println!("STARK 'zone_fund' prover time: {:.2?}", start_t.elapsed());
let receipt = prove_info.receipt;
ledger::DeathProof::from_risc0(goas_risc0_proofs::SPEND_ZONE_FUNDS_ID, receipt)
}

View File

@ -0,0 +1,157 @@
use std::collections::BTreeMap;
use cl::{NoteWitness, NullifierNonce, NullifierSecret};
use common::{events::Event, Input, StateWitness, ZoneMetadata, ZONE_CL_FUNDS_UNIT};
use ledger::death_constraint::DeathProof;
use rand_core::CryptoRngCore;
fn zone_state_death_constraint() -> [u8; 32] {
ledger::death_constraint::risc0_id_to_cl_death_constraint(goas_risc0_proofs::ZONE_STATE_ID)
}
fn zone_fund_death_constraint() -> [u8; 32] {
ledger::death_constraint::risc0_id_to_cl_death_constraint(
goas_risc0_proofs::SPEND_ZONE_FUNDS_ID,
)
}
fn zone_fund_utxo(
value: u64,
zone_meta: ZoneMetadata,
mut rng: impl CryptoRngCore,
) -> cl::OutputWitness {
cl::OutputWitness::public(
cl::NoteWitness {
value,
unit: *common::ZONE_CL_FUNDS_UNIT,
death_constraint: zone_meta.funds_vk,
state: zone_meta.id(),
},
NullifierNonce::random(&mut rng),
)
}
fn zone_state_utxo(zone: &StateWitness, mut rng: impl CryptoRngCore) -> cl::OutputWitness {
cl::OutputWitness::public(
cl::NoteWitness {
value: 1,
unit: zone.zone_metadata.unit,
death_constraint: zone.zone_metadata.zone_vk,
state: zone.commit().0,
},
NullifierNonce::random(&mut rng),
)
}
#[test]
fn test_withdrawal() {
let mut rng = rand::thread_rng();
let alice = 42;
let alice_sk = NullifierSecret::random(&mut rng);
let init_state = StateWitness {
balances: BTreeMap::from_iter([(alice, 100)]),
included_txs: vec![],
output_events: vec![],
zone_metadata: ZoneMetadata {
zone_vk: zone_state_death_constraint(),
funds_vk: zone_fund_death_constraint(),
unit: cl::note::unit_point("ZONE_STATE"),
},
};
let zone_fund_in =
cl::InputWitness::public(zone_fund_utxo(35240, init_state.zone_metadata, &mut rng));
let zone_state_in = cl::InputWitness::public(zone_state_utxo(&init_state, &mut rng));
let withdraw = common::Withdraw {
from: alice,
amount: 78,
to: alice_sk.commit(),
fund_nf: zone_fund_in.nullifier(),
};
let end_state = init_state.clone().withdraw(withdraw);
let zone_state_out = cl::OutputWitness::public(
cl::NoteWitness {
state: end_state.commit().0,
..zone_state_in.note
},
zone_state_in.evolved_nonce(),
);
let zone_fund_out = cl::OutputWitness::public(
cl::NoteWitness {
value: zone_fund_in.note.value - withdraw.amount,
..zone_fund_in.note
},
zone_fund_in.evolved_nonce(),
);
let alice_withdrawal = cl::OutputWitness::random(
NoteWitness::stateless(
withdraw.amount,
*ZONE_CL_FUNDS_UNIT,
DeathProof::nop_constraint(),
),
alice_sk.commit(),
&mut rng,
);
let withdraw_ptx = cl::PartialTxWitness {
inputs: vec![zone_state_in, zone_fund_in],
outputs: vec![zone_state_out, zone_fund_out, alice_withdrawal],
};
let death_proofs = BTreeMap::from_iter([
(
zone_state_in.nullifier(),
executor::prove_zone_stf(
init_state.clone(),
vec![Input::Withdraw(withdraw)],
withdraw_ptx.input_witness(0), // input state note (input #0)
withdraw_ptx.output_witness(0), // output state note (output #0)
),
),
(
zone_fund_in.nullifier(),
executor::prove_zone_fund_withdraw(
withdraw_ptx.input_witness(1), // input fund note (input #1)
withdraw_ptx.output_witness(0), // output state note (output #0)
withdraw_ptx.output_witness(1), // output state note (output #0)
withdraw_ptx.output_witness(2), // output state note (output #0)
&end_state,
withdraw,
),
),
]);
let note_commitments = vec![
zone_state_in.note_commitment(),
zone_fund_in.note_commitment(),
];
let withdraw_proof =
ledger::partial_tx::ProvedPartialTx::prove(&withdraw_ptx, death_proofs, &note_commitments)
.expect("withdraw proof failed");
assert!(withdraw_proof.verify());
assert_eq!(withdraw_proof.outputs[0].output, zone_state_out.commit());
assert_eq!(
zone_state_out.note.state,
StateWitness {
balances: BTreeMap::from_iter([(alice, 22)]),
included_txs: vec![Input::Withdraw(withdraw)],
output_events: vec![Event::Spend(common::events::Spend {
amount: 78,
to: alice_sk.commit(),
fund_nf: zone_fund_in.nullifier()
})],
zone_metadata: init_state.zone_metadata
}
.commit()
.0
)
}

View File

@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
common = { path = "../common" }
cl = { path = "../../cl/cl" } cl = { path = "../../cl/cl" }
proof_statements = { path = "../../cl/proof_statements" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View File

@ -1 +1,2 @@
pub mod zone_funds; pub mod zone_funds;
pub mod zone_state;

View File

@ -0,0 +1,24 @@
/// The User Note encodes the logic of the atomic asset transfer
///
/// The scenario is as follows:
/// The user, let's call her Alice has 100 NMO in Zone A and she wants to move it to
/// Zone B. She wants to arrange this transfer so that both the withdrawal from Zone
/// A and the deposit to Zone B occur atomically.
///
/// The Alice will create a partial tx that looks like this:
///
/// [fee note] -> [user note]
///
/// The User Note will encode the logic that orchestrates the withdrawal from zone A
/// and deposit to zone B.
///
/// The User Notes death constraint requires the following statements to be satisfied
/// in order for the fee to be captured.
///
/// 1. w_tx = withdraw(amt=100 NMO, from=Alice) tx was included in Zone A.
/// 2. d_tx = deposit(amt=100 NMO, to=Alice) tx was included in Zone B.
/// 3. w_tx is included in Zone A iff d_tx is included in Zone B
///
/// Details:
/// - the withdrawal in zone A must not be a general withdrawal tx, it must be bound to the user note.
/// i.e. the user_note must be present in the ptx for the withdrawal to be valid in Zone A.

View File

@ -1,39 +1,17 @@
use proof_statements::{ptx::PartialTxInputPrivate, ptx::PartialTxOutputPrivate};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// An event that authorizes spending zone funds
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Spend {
pub amount: u64,
/// The public key of the recipient
pub to: cl::NullifierCommitment,
/// The nullifier of note that is being spent, this is to avoid using the spend event to
/// for multiple notes
pub nf: cl::Nullifier,
}
impl Spend {
pub fn to_bytes(&self) -> [u8; 72] {
let mut bytes = [0; 72];
bytes[0..8].copy_from_slice(&self.amount.to_le_bytes());
bytes[8..40].copy_from_slice(self.to.as_bytes());
bytes[40..72].copy_from_slice(self.nf.as_bytes());
bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpendFundsPrivate { pub struct SpendFundsPrivate {
/// The note we're spending /// The note we're spending
pub in_zone_funds: PartialTxInputPrivate, pub in_zone_funds: cl::PartialTxInputWitness,
/// The zone note that is authorizing the spend /// The zone note that is authorizing the spend
pub zone_note: PartialTxOutputPrivate, pub zone_note: cl::PartialTxOutputWitness,
/// The note that is being created to send the change back to the zone /// The note that is being created to send the change back to the zone
pub out_zone_funds: PartialTxOutputPrivate, pub out_zone_funds: cl::PartialTxOutputWitness,
/// The spent funds note /// The spent funds note
pub spent_note: PartialTxOutputPrivate, pub spent_note: cl::PartialTxOutputWitness,
/// The event emitted by the zone that authorizes the spend /// The event emitted by the zone that authorizes the spend
pub spend_event: Spend, pub spend_event: common::events::Spend,
/// Path to the zone output events root /// Path to the zone output events root
pub spend_event_state_path: Vec<cl::merkle::PathNode>, pub spend_event_state_path: Vec<cl::merkle::PathNode>,
/// Merkle root of txs included in the zone /// Merkle root of txs included in the zone

View File

@ -0,0 +1,10 @@
use common::{Input, StateWitness};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZoneStatePrivate {
pub state: StateWitness,
pub inputs: Vec<Input>,
pub zone_in: cl::PartialTxInputWitness,
pub zone_out: cl::PartialTxOutputWitness,
}

View File

@ -10,7 +10,7 @@ risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
cl = { path = "../../../cl/cl" } cl = { path = "../../../cl/cl" }
goas_proof_statements = { path = "../../proof_statements" } goas_proof_statements = { path = "../../proof_statements" }
proof_statements = { path = "../../../cl/proof_statements" } ledger_proof_statements = { path = "../../../cl/ledger_proof_statements" }
sha2 = "0.10" sha2 = "0.10"

View File

@ -2,9 +2,9 @@
/// ///
/// Our goal: prove the zone authorized spending of funds /// Our goal: prove the zone authorized spending of funds
use cl::merkle; use cl::merkle;
use cl::nullifier::{Nullifier, NullifierSecret}; use cl::partial_tx::PtxRoot;
use goas_proof_statements::zone_funds::SpendFundsPrivate; use goas_proof_statements::zone_funds::SpendFundsPrivate;
use proof_statements::death_constraint::DeathConstraintPublic; use ledger_proof_statements::death_constraint::DeathConstraintPublic;
use risc0_zkvm::guest::env; use risc0_zkvm::guest::env;
fn main() { fn main() {
@ -19,11 +19,12 @@ fn main() {
balances_root, balances_root,
} = env::read(); } = env::read();
let ptx_root = in_zone_funds.ptx_root(); let input_root = in_zone_funds.input_root();
let nf = Nullifier::new(in_zone_funds.input.nf_sk, in_zone_funds.input.nonce); let output_root = out_zone_funds.output_root();
// check the zone funds note is the one in the spend event
assert_eq!(nf, spend_event.nf); assert_eq!(output_root, zone_note.output_root());
assert_eq!(ptx_root, zone_note.ptx_root()); assert_eq!(output_root, spent_note.output_root());
assert_eq!(output_root, out_zone_funds.output_root());
// ** Assert the spent event was an output of the correct zone stf ** // ** Assert the spent event was an output of the correct zone stf **
// The zone state field is a merkle tree over: // The zone state field is a merkle tree over:
@ -45,8 +46,6 @@ fn main() {
zone_note.output.note.state zone_note.output.note.state
); );
assert_eq!(ptx_root, out_zone_funds.ptx_root());
// Check we return the rest of the funds back to the zone // Check we return the rest of the funds back to the zone
let change = in_zone_funds let change = in_zone_funds
.input .input
@ -57,39 +56,41 @@ fn main() {
assert_eq!(out_zone_funds.output.note.value, change); assert_eq!(out_zone_funds.output.note.value, change);
// zone funds output should have the same death constraints as the zone funds input // zone funds output should have the same death constraints as the zone funds input
assert_eq!( assert_eq!(
out_zone_funds.output.note.death_constraint, in_zone_funds.input.note.death_constraint,
in_zone_funds.input.note.death_constraint out_zone_funds.output.note.death_constraint
); );
assert_eq!( assert_eq!(
out_zone_funds.output.note.unit, in_zone_funds.input.note.unit,
in_zone_funds.input.note.unit out_zone_funds.output.note.unit
); );
// zone funds nullifier, nonce and value blinding should be public so that everybody can spend it // ensure zone fund sk's, blindings and nonces are propagated correctly.
assert_eq!( assert_eq!(
out_zone_funds.output.nf_pk, in_zone_funds.input.nf_sk.commit(),
NullifierSecret::from_bytes([0; 16]).commit() out_zone_funds.output.nf_pk
); );
assert_eq!( assert_eq!(
out_zone_funds.output.balance_blinding, in_zone_funds.input.balance_blinding,
in_zone_funds.input.balance_blinding out_zone_funds.output.balance_blinding
); );
assert_eq!( assert_eq!(
in_zone_funds.input.evolved_nonce(),
out_zone_funds.output.nonce, out_zone_funds.output.nonce,
in_zone_funds.input.evolved_nonce()
); );
// the state is propagated // the state is propagated
assert_eq!( assert_eq!(
in_zone_funds.input.note.state,
out_zone_funds.output.note.state, out_zone_funds.output.note.state,
in_zone_funds.input.note.state
); );
assert_eq!(ptx_root, spent_note.ptx_root());
// check the correct amount of funds is being spent // check the correct amount of funds is being spent
assert_eq!(spent_note.output.note.value, spend_event.amount); assert_eq!(spent_note.output.note.value, spend_event.amount);
assert_eq!(spent_note.output.note.unit, in_zone_funds.input.note.unit); assert_eq!(spent_note.output.note.unit, in_zone_funds.input.note.unit);
// check the correct recipient is being paid // check the correct recipient is being paid
assert_eq!(spent_note.output.nf_pk, spend_event.to); assert_eq!(spent_note.output.nf_pk, spend_event.to);
let nf = in_zone_funds.input.nullifier();
assert_eq!(nf, spend_event.fund_nf); // ensure this event was meant for this note.
let ptx_root = PtxRoot(merkle::node(input_root, output_root));
env::commit(&DeathConstraintPublic { ptx_root, nf }); env::commit(&DeathConstraintPublic { ptx_root, nf });
} }

View File

@ -15,7 +15,7 @@ bincode = "1"
common = { path = "../../common" } common = { path = "../../common" }
cl = { path = "../../../cl/cl" } cl = { path = "../../../cl/cl" }
goas_proof_statements = { path = "../../proof_statements" } goas_proof_statements = { path = "../../proof_statements" }
proof_statements = { path = "../../../cl/proof_statements" } ledger_proof_statements = { path = "../../../cl/ledger_proof_statements" }
sha2 = "0.10" sha2 = "0.10"
[patch.crates-io] [patch.crates-io]

View File

@ -1,5 +1,4 @@
use cl::{ use cl::{
input::InputWitness,
merkle, merkle,
nullifier::{Nullifier, NullifierSecret}, nullifier::{Nullifier, NullifierSecret},
partial_tx::{MAX_INPUTS, MAX_OUTPUTS}, partial_tx::{MAX_INPUTS, MAX_OUTPUTS},
@ -7,37 +6,10 @@ use cl::{
}; };
use common::*; use common::*;
use goas_proof_statements::zone_funds::Spend; use goas_proof_statements::zone_state::ZoneStatePrivate;
use proof_statements::{ use ledger_proof_statements::death_constraint::DeathConstraintPublic;
death_constraint::DeathConstraintPublic,
ptx::{PartialTxInputPrivate, PartialTxOutputPrivate},
};
use risc0_zkvm::guest::env; use risc0_zkvm::guest::env;
fn withdraw(mut state: StateWitness, withdraw: Withdraw) -> StateWitness {
state.included_txs.push(Input::Withdraw(withdraw));
let Withdraw {
from,
amount,
to,
nf,
} = withdraw;
let from_balance = state.balances.entry(from).or_insert(0);
*from_balance = from
.checked_sub(amount)
.expect("insufficient funds in account");
let spend_auth = Spend {
amount: amount.into(),
to,
nf,
};
state.output_events.push(Event::Spend(spend_auth));
state
}
fn deposit( fn deposit(
mut state: StateWitness, mut state: StateWitness,
deposit: Deposit, deposit: Deposit,
@ -114,7 +86,7 @@ fn deposit(
assert_eq!(nullifier, pub_inputs.nf); assert_eq!(nullifier, pub_inputs.nf);
// 6) We're now ready to do the deposit! // 6) We're now ready to do the deposit!
let amount = deposit.note.value as u32; let amount = deposit.note.value;
let to = AccountId::from_be_bytes(<[u8; 4]>::try_from(&deposit.note.state[0..4]).unwrap()); let to = AccountId::from_be_bytes(<[u8; 4]>::try_from(&deposit.note.state[0..4]).unwrap());
let to_balance = state.balances.entry(to).or_insert(0); let to_balance = state.balances.entry(to).or_insert(0);
@ -125,59 +97,68 @@ fn deposit(
state state
} }
fn validate_zone_input( fn validate_zone_transition(
input: &PartialTxInputPrivate, in_note: cl::PartialTxInputWitness,
state: &StateWitness, out_note: cl::PartialTxOutputWitness,
) -> (PtxRoot, Nullifier) { in_meta: ZoneMetadata,
let ptx_root = input.ptx_root(); in_state_cm: StateCommitment,
let nf = Nullifier::new(input.input.nf_sk, input.input.nonce); out_state: StateWitness,
) {
// Ensure input/output notes are committing to the expected states.
assert_eq!(in_note.input.note.state, in_state_cm.0);
assert_eq!(out_note.output.note.state, out_state.commit().0);
assert_eq!(input.input.note.state, <[u8; 32]>::from(state.commit())); // zone metadata is propagated
// should not be possible to create one but let's put this check here just in case assert_eq!(out_state.zone_metadata.id(), in_meta.id());
debug_assert_eq!(
input.input.note.death_constraint, // ensure units match metadata
state.zone_metadata.zone_vk assert_eq!(in_note.input.note.unit, in_meta.unit);
assert_eq!(out_note.output.note.unit, in_meta.unit);
// ensure constraints match metadata
assert_eq!(in_note.input.note.death_constraint, in_meta.zone_vk);
assert_eq!(out_note.output.note.death_constraint, in_meta.zone_vk);
// nullifier secret is propagated
assert_eq!(in_note.input.nf_sk.commit(), out_note.output.nf_pk);
// balance blinding is propagated
assert_eq!(
in_note.input.balance_blinding,
out_note.output.balance_blinding
); );
(ptx_root, nf)
}
fn validate_zone_output(
ptx: PtxRoot,
input: InputWitness,
output: PartialTxOutputPrivate,
state: &StateWitness,
) {
assert_eq!(ptx, output.ptx_root()); // the ptx root is the same as in the input
let output = output.output;
assert_eq!(output.note.state, <[u8; 32]>::from(state.commit())); // the state in the output is as calculated by this function
assert_eq!(output.note.death_constraint, state.zone_metadata.zone_vk); // the death constraint is the correct one
assert_eq!(output.nf_pk, NullifierSecret::from_bytes([0; 16]).commit()); // the nullifier secret is public
assert_eq!(output.balance_blinding, input.balance_blinding); // the balance blinding is the same as in the input
assert_eq!(output.note.unit, state.zone_metadata.unit); // the balance unit is the same as in the input
// the nonce is correctly evolved // the nonce is correctly evolved
assert_eq!(output.nonce, input.evolved_nonce()); assert_eq!(in_note.input.evolved_nonce(), out_note.output.nonce);
} }
fn main() { fn main() {
let zone_in: PartialTxInputPrivate = env::read(); let ZoneStatePrivate {
let mut state: StateWitness = env::read(); mut state,
let zone_out: PartialTxOutputPrivate = env::read(); inputs,
zone_in,
zone_out,
} = env::read();
let (ptx_root, nf) = validate_zone_input(&zone_in, &state); let pub_inputs = DeathConstraintPublic {
ptx_root: PtxRoot(cl::merkle::node(
zone_in.input_root(),
zone_out.output_root(),
)),
nf: zone_in.input.nullifier(),
};
let pub_inputs = DeathConstraintPublic { ptx_root, nf }; let in_meta = state.zone_metadata;
let in_state_cm = state.commit();
let inputs: Vec<Input> = env::read();
for input in inputs { for input in inputs {
match input { state = match input {
Input::Withdraw(input) => state = withdraw(state, input), Input::Withdraw(input) => state.withdraw(input),
Input::Deposit(input) => state = deposit(state, input, pub_inputs), Input::Deposit(input) => deposit(state, input, pub_inputs),
} }
} }
validate_zone_output(ptx_root, zone_in.input, zone_out, &state); validate_zone_transition(zone_in, zone_out, in_meta, in_state_cm, state);
env::commit(&pub_inputs); env::commit(&pub_inputs);
} }

View File

@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ "cl", "ledger", "proof_statements", "risc0_proofs"] members = [ "cl", "ledger", "ledger_proof_statements", "risc0_proofs"]
# Always optimize; building and running the risc0_proofs takes much longer without optimization. # Always optimize; building and running the risc0_proofs takes much longer without optimization.
[profile.dev] [profile.dev]

View File

@ -40,6 +40,10 @@ impl BalanceWitness {
Self(blinding) Self(blinding)
} }
pub fn unblinded() -> Self {
Self::new(Scalar::ZERO)
}
pub fn random(mut rng: impl CryptoRngCore) -> Self { pub fn random(mut rng: impl CryptoRngCore) -> Self {
Self::new(Scalar::random(&mut rng)) Self::new(Scalar::random(&mut rng))
} }
@ -60,8 +64,8 @@ pub fn balance(value: u64, unit: Unit, blinding: Scalar) -> Unit {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::note::unit_point;
#[test] #[test]
fn test_pederson_blinding_point_pre_compute() { fn test_pederson_blinding_point_pre_compute() {
@ -77,30 +81,34 @@ mod test {
#[test] #[test]
fn test_balance_zero_unitless() { fn test_balance_zero_unitless() {
// Zero is the same across all units // Zero is the same across all units
let (nmo, eth) = (unit_point("NMO"), unit_point("ETH"));
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let b = BalanceWitness::random(&mut rng); let b = BalanceWitness::random(&mut rng);
assert_eq!( assert_eq!(
b.commit(&NoteWitness::basic(0, "NMO")), b.commit(&NoteWitness::basic(0, nmo)),
b.commit(&NoteWitness::basic(0, "ETH")), b.commit(&NoteWitness::basic(0, eth)),
); );
} }
#[test] #[test]
fn test_balance_blinding() { fn test_balance_blinding() {
// balances are blinded // balances are blinded
let nmo = unit_point("NMO");
let r_a = Scalar::from(12u32); let r_a = Scalar::from(12u32);
let r_b = Scalar::from(8u32); let r_b = Scalar::from(8u32);
let bal_a = BalanceWitness::new(r_a); let bal_a = BalanceWitness::new(r_a);
let bal_b = BalanceWitness::new(r_b); let bal_b = BalanceWitness::new(r_b);
let note = NoteWitness::basic(10, "NMO"); let note = NoteWitness::basic(10, nmo);
let a = bal_a.commit(&note); let a = bal_a.commit(&note);
let b = bal_b.commit(&note); let b = bal_b.commit(&note);
assert_ne!(a, b); assert_ne!(a, b);
let diff_note = NoteWitness::basic(0, "NMO"); let diff_note = NoteWitness::basic(0, nmo);
assert_eq!( assert_eq!(
a.0 - b.0, a.0 - b.0,
BalanceWitness::new(r_a - r_b).commit(&diff_note).0 BalanceWitness::new(r_a - r_b).commit(&diff_note).0
@ -110,24 +118,28 @@ mod test {
#[test] #[test]
fn test_balance_units() { fn test_balance_units() {
// Unit's differentiate between values. // Unit's differentiate between values.
let (nmo, eth) = (unit_point("NMO"), unit_point("ETH"));
let b = BalanceWitness::new(Scalar::from(1337u32)); let b = BalanceWitness::new(Scalar::from(1337u32));
let nmo = NoteWitness::basic(10, "NMO"); let nmo = NoteWitness::basic(10, nmo);
let eth = NoteWitness::basic(10, "ETH"); let eth = NoteWitness::basic(10, eth);
assert_ne!(b.commit(&nmo), b.commit(&eth)); assert_ne!(b.commit(&nmo), b.commit(&eth));
} }
#[test] #[test]
fn test_balance_homomorphism() { fn test_balance_homomorphism() {
let nmo = unit_point("NMO");
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let b1 = BalanceWitness::random(&mut rng); let b1 = BalanceWitness::random(&mut rng);
let b2 = BalanceWitness::random(&mut rng); let b2 = BalanceWitness::random(&mut rng);
let b_zero = BalanceWitness::new(Scalar::ZERO); let b_zero = BalanceWitness::new(Scalar::ZERO);
let ten = NoteWitness::basic(10, "NMO"); let ten = NoteWitness::basic(10, nmo);
let eight = NoteWitness::basic(8, "NMO"); let eight = NoteWitness::basic(8, nmo);
let two = NoteWitness::basic(2, "NMO"); let two = NoteWitness::basic(2, nmo);
let zero = NoteWitness::basic(0, "NMO"); let zero = NoteWitness::basic(0, nmo);
// Values of same unit are homomorphic // Values of same unit are homomorphic
assert_eq!( assert_eq!(

View File

@ -29,7 +29,10 @@ impl Bundle {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
input::InputWitness, note::NoteWitness, nullifier::NullifierSecret, output::OutputWitness, input::InputWitness,
note::{unit_point, NoteWitness},
nullifier::NullifierSecret,
output::OutputWitness,
partial_tx::PartialTxWitness, partial_tx::PartialTxWitness,
}; };
@ -38,21 +41,22 @@ mod test {
#[test] #[test]
fn test_bundle_balance() { fn test_bundle_balance() {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let (nmo, eth, crv) = (unit_point("NMO"), unit_point("ETH"), unit_point("CRV"));
let nf_a = NullifierSecret::random(&mut rng); let nf_a = NullifierSecret::random(&mut rng);
let nf_b = NullifierSecret::random(&mut rng); let nf_b = NullifierSecret::random(&mut rng);
let nf_c = NullifierSecret::random(&mut rng); let nf_c = NullifierSecret::random(&mut rng);
let nmo_10_utxo = let nmo_10_utxo =
OutputWitness::random(NoteWitness::basic(10, "NMO"), nf_a.commit(), &mut rng); OutputWitness::random(NoteWitness::basic(10, nmo), nf_a.commit(), &mut rng);
let nmo_10_in = InputWitness::random(nmo_10_utxo, nf_a, &mut rng); let nmo_10_in = InputWitness::random(nmo_10_utxo, nf_a, &mut rng);
let eth_23_utxo = let eth_23_utxo =
OutputWitness::random(NoteWitness::basic(23, "ETH"), nf_b.commit(), &mut rng); OutputWitness::random(NoteWitness::basic(23, eth), nf_b.commit(), &mut rng);
let eth_23_in = InputWitness::random(eth_23_utxo, nf_b, &mut rng); let eth_23_in = InputWitness::random(eth_23_utxo, nf_b, &mut rng);
let crv_4840_out = let crv_4840_out =
OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng); OutputWitness::random(NoteWitness::basic(4840, crv), nf_c.commit(), &mut rng);
let ptx_unbalanced = PartialTxWitness { let ptx_unbalanced = PartialTxWitness {
inputs: vec![nmo_10_in, eth_23_in], inputs: vec![nmo_10_in, eth_23_in],
@ -80,12 +84,12 @@ mod test {
let crv_4840_in = InputWitness::random(crv_4840_out, nf_c, &mut rng); let crv_4840_in = InputWitness::random(crv_4840_out, nf_c, &mut rng);
let nmo_10_out = OutputWitness::random( let nmo_10_out = OutputWitness::random(
NoteWitness::basic(10, "NMO"), NoteWitness::basic(10, nmo),
NullifierSecret::random(&mut rng).commit(), // transferring to a random owner NullifierSecret::random(&mut rng).commit(), // transferring to a random owner
&mut rng, &mut rng,
); );
let eth_23_out = OutputWitness::random( let eth_23_out = OutputWitness::random(
NoteWitness::basic(23, "ETH"), NoteWitness::basic(23, eth),
NullifierSecret::random(&mut rng).commit(), // transferring to a random owner NullifierSecret::random(&mut rng).commit(), // transferring to a random owner
&mut rng, &mut rng,
); );

View File

@ -41,6 +41,17 @@ impl InputWitness {
} }
} }
pub fn public(output: crate::OutputWitness) -> Self {
let nf_sk = NullifierSecret::zero();
assert_eq!(nf_sk.commit(), output.nf_pk); // ensure the output was a public UTXO
Self {
note: output.note,
balance_blinding: BalanceWitness::unblinded(),
nf_sk,
nonce: output.nonce,
}
}
pub fn evolved_nonce(&self) -> NullifierNonce { pub fn evolved_nonce(&self) -> NullifierNonce {
self.nonce.evolve(&self.nf_sk) self.nonce.evolve(&self.nf_sk)
} }

View File

@ -15,4 +15,6 @@ pub use input::{Input, InputWitness};
pub use note::{DeathCommitment, NoteCommitment, NoteWitness}; pub use note::{DeathCommitment, NoteCommitment, NoteWitness};
pub use nullifier::{Nullifier, NullifierCommitment, NullifierNonce, NullifierSecret}; pub use nullifier::{Nullifier, NullifierCommitment, NullifierNonce, NullifierSecret};
pub use output::{Output, OutputWitness}; pub use output::{Output, OutputWitness};
pub use partial_tx::{PartialTx, PartialTxWitness, PtxRoot}; pub use partial_tx::{
PartialTx, PartialTxInputWitness, PartialTxOutputWitness, PartialTxWitness, PtxRoot,
};

View File

@ -19,7 +19,7 @@ pub fn death_commitment(death_constraint: &[u8]) -> DeathCommitment {
} }
pub fn unit_point(unit: &str) -> Unit { pub fn unit_point(unit: &str) -> Unit {
crate::crypto::hash_to_curve(unit.as_bytes()) crate::crypto::hash_to_curve(format!("NOMOS_CL_UNIT{unit}").as_bytes())
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
@ -42,25 +42,20 @@ pub struct NoteWitness {
} }
impl NoteWitness { impl NoteWitness {
pub fn new( pub fn new(value: u64, unit: Unit, death_constraint: [u8; 32], state: [u8; 32]) -> Self {
value: u64,
unit: impl Into<String>,
death_constraint: [u8; 32],
state: [u8; 32],
) -> Self {
Self { Self {
value, value,
unit: unit_point(&unit.into()), unit,
death_constraint, death_constraint,
state, state,
} }
} }
pub fn basic(value: u64, unit: impl Into<String>) -> Self { pub fn basic(value: u64, unit: Unit) -> Self {
Self::new(value, unit, [0u8; 32], [0u8; 32]) Self::new(value, unit, [0u8; 32], [0u8; 32])
} }
pub fn stateless(value: u64, unit: impl Into<String>, death_constraint: [u8; 32]) -> Self { pub fn stateless(value: u64, unit: Unit, death_constraint: [u8; 32]) -> Self {
Self::new(value, unit, death_constraint, [0u8; 32]) Self::new(value, unit, death_constraint, [0u8; 32])
} }
@ -94,18 +89,19 @@ impl NoteWitness {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::nullifier::NullifierSecret;
use super::*; use super::*;
use crate::nullifier::NullifierSecret;
#[test] #[test]
fn test_note_commit_permutations() { fn test_note_commit_permutations() {
let (nmo, eth) = (unit_point("NMO"), unit_point("ETH"));
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let nf_pk = NullifierSecret::random(&mut rng).commit(); let nf_pk = NullifierSecret::random(&mut rng).commit();
let nf_nonce = NullifierNonce::random(&mut rng); let nf_nonce = NullifierNonce::random(&mut rng);
let reference_note = NoteWitness::basic(32, "NMO"); let reference_note = NoteWitness::basic(32, nmo);
// different notes under same nullifier produce different commitments // different notes under same nullifier produce different commitments
let mutation_tests = [ let mutation_tests = [
@ -114,7 +110,7 @@ mod test {
..reference_note ..reference_note
}, },
NoteWitness { NoteWitness {
unit: unit_point("ETH"), unit: eth,
..reference_note ..reference_note
}, },
NoteWitness { NoteWitness {

View File

@ -43,6 +43,10 @@ impl NullifierSecret {
Self(sk) Self(sk)
} }
pub const fn zero() -> Self {
Self([0u8; 16])
}
pub fn commit(&self) -> NullifierCommitment { pub fn commit(&self) -> NullifierCommitment {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(b"NOMOS_CL_NULL_COMMIT"); hasher.update(b"NOMOS_CL_NULL_COMMIT");

View File

@ -6,7 +6,7 @@ use crate::{
error::Error, error::Error,
note::{NoteCommitment, NoteWitness}, note::{NoteCommitment, NoteWitness},
nullifier::{NullifierCommitment, NullifierNonce}, nullifier::{NullifierCommitment, NullifierNonce},
BalanceWitness, BalanceWitness, NullifierSecret,
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@ -37,6 +37,15 @@ impl OutputWitness {
} }
} }
pub fn public(note: NoteWitness, nonce: NullifierNonce) -> Self {
Self {
note,
balance_blinding: BalanceWitness::unblinded(),
nf_pk: NullifierSecret::zero().commit(),
nonce,
}
}
pub fn commit_note(&self) -> NoteCommitment { pub fn commit_note(&self) -> NoteCommitment {
self.note.commit(self.nf_pk, self.nonce) self.note.commit(self.nf_pk, self.nonce)
} }
@ -86,14 +95,15 @@ impl Output {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::nullifier::NullifierSecret; use crate::{note::unit_point, nullifier::NullifierSecret};
#[test] #[test]
fn test_output_proof() { fn test_output_proof() {
let (nmo, eth) = (unit_point("NMO"), unit_point("ETH"));
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let witness = OutputWitness { let witness = OutputWitness {
note: NoteWitness::basic(10, "NMO"), note: NoteWitness::basic(10, nmo),
balance_blinding: BalanceWitness::random(&mut rng), balance_blinding: BalanceWitness::random(&mut rng),
nf_pk: NullifierSecret::random(&mut rng).commit(), nf_pk: NullifierSecret::random(&mut rng).commit(),
nonce: NullifierNonce::random(&mut rng), nonce: NullifierNonce::random(&mut rng),
@ -106,11 +116,11 @@ mod test {
let wrong_witnesses = [ let wrong_witnesses = [
OutputWitness { OutputWitness {
note: NoteWitness::basic(11, "NMO"), note: NoteWitness::basic(11, nmo),
..witness ..witness
}, },
OutputWitness { OutputWitness {
note: NoteWitness::basic(10, "ETH"), note: NoteWitness::basic(10, eth),
..witness ..witness
}, },
OutputWitness { OutputWitness {

View File

@ -60,6 +60,26 @@ impl PartialTxWitness {
BalanceWitness(out_sum - in_sum) BalanceWitness(out_sum - in_sum)
} }
pub fn input_witness(&self, idx: usize) -> PartialTxInputWitness {
let input_bytes =
Vec::from_iter(self.inputs.iter().map(|i| i.commit().to_bytes().to_vec()));
let input_merkle_leaves = merkle::padded_leaves::<MAX_INPUTS>(&input_bytes);
let path = merkle::path(input_merkle_leaves, idx);
let input = self.inputs[idx];
PartialTxInputWitness { input, path }
}
pub fn output_witness(&self, idx: usize) -> PartialTxOutputWitness {
let output_bytes =
Vec::from_iter(self.outputs.iter().map(|o| o.commit().to_bytes().to_vec()));
let output_merkle_leaves = merkle::padded_leaves::<MAX_OUTPUTS>(&output_bytes);
let path = merkle::path(output_merkle_leaves, idx);
let output = self.outputs[idx];
PartialTxOutputWitness { output, path }
}
} }
impl PartialTx { impl PartialTx {
@ -81,24 +101,6 @@ impl PartialTx {
merkle::root::<MAX_OUTPUTS>(output_merkle_leaves) merkle::root::<MAX_OUTPUTS>(output_merkle_leaves)
} }
pub fn input_merkle_path(&self, idx: usize) -> Vec<merkle::PathNode> {
let input_bytes =
Vec::from_iter(self.inputs.iter().map(Input::to_bytes).map(Vec::from_iter));
let input_merkle_leaves = merkle::padded_leaves::<MAX_INPUTS>(&input_bytes);
merkle::path(input_merkle_leaves, idx)
}
pub fn output_merkle_path(&self, idx: usize) -> Vec<merkle::PathNode> {
let output_bytes = Vec::from_iter(
self.outputs
.iter()
.map(Output::to_bytes)
.map(Vec::from_iter),
);
let output_merkle_leaves = merkle::padded_leaves::<MAX_OUTPUTS>(&output_bytes);
merkle::path(output_merkle_leaves, idx)
}
pub fn root(&self) -> PtxRoot { pub fn root(&self) -> PtxRoot {
let input_root = self.input_root(); let input_root = self.input_root();
let output_root = self.output_root(); let output_root = self.output_root();
@ -114,15 +116,47 @@ impl PartialTx {
} }
} }
/// An input to a partial transaction
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartialTxInputWitness {
pub input: InputWitness,
pub path: Vec<merkle::PathNode>,
}
impl PartialTxInputWitness {
pub fn input_root(&self) -> [u8; 32] {
let leaf = merkle::leaf(&self.input.commit().to_bytes());
merkle::path_root(leaf, &self.path)
}
}
/// An output to a partial transaction
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartialTxOutputWitness {
pub output: OutputWitness,
pub path: Vec<merkle::PathNode>,
}
impl PartialTxOutputWitness {
pub fn output_root(&self) -> [u8; 32] {
let leaf = merkle::leaf(&self.output.commit().to_bytes());
merkle::path_root(leaf, &self.path)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{note::NoteWitness, nullifier::NullifierSecret}; use crate::{
note::{unit_point, NoteWitness},
nullifier::NullifierSecret,
};
use super::*; use super::*;
#[test] #[test]
fn test_partial_tx_balance() { fn test_partial_tx_balance() {
let (nmo, eth, crv) = (unit_point("NMO"), unit_point("ETH"), unit_point("CRV"));
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let nf_a = NullifierSecret::random(&mut rng); let nf_a = NullifierSecret::random(&mut rng);
@ -130,15 +164,15 @@ mod test {
let nf_c = NullifierSecret::random(&mut rng); let nf_c = NullifierSecret::random(&mut rng);
let nmo_10_utxo = let nmo_10_utxo =
OutputWitness::random(NoteWitness::basic(10, "NMO"), nf_a.commit(), &mut rng); OutputWitness::random(NoteWitness::basic(10, nmo), nf_a.commit(), &mut rng);
let nmo_10 = InputWitness::random(nmo_10_utxo, nf_a, &mut rng); let nmo_10 = InputWitness::random(nmo_10_utxo, nf_a, &mut rng);
let eth_23_utxo = let eth_23_utxo =
OutputWitness::random(NoteWitness::basic(23, "ETH"), nf_b.commit(), &mut rng); OutputWitness::random(NoteWitness::basic(23, eth), nf_b.commit(), &mut rng);
let eth_23 = InputWitness::random(eth_23_utxo, nf_b, &mut rng); let eth_23 = InputWitness::random(eth_23_utxo, nf_b, &mut rng);
let crv_4840 = let crv_4840 =
OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng); OutputWitness::random(NoteWitness::basic(4840, crv), nf_c.commit(), &mut rng);
let ptx_witness = PartialTxWitness { let ptx_witness = PartialTxWitness {
inputs: vec![nmo_10, eth_23], inputs: vec![nmo_10, eth_23],

View File

@ -1,3 +1,4 @@
use cl::note::unit_point;
use rand_core::CryptoRngCore; use rand_core::CryptoRngCore;
fn receive_utxo( fn receive_utxo(
@ -10,6 +11,7 @@ fn receive_utxo(
#[test] #[test]
fn test_simple_transfer() { fn test_simple_transfer() {
let nmo = unit_point("NMO");
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let sender_nf_sk = cl::NullifierSecret::random(&mut rng); let sender_nf_sk = cl::NullifierSecret::random(&mut rng);
@ -18,13 +20,13 @@ fn test_simple_transfer() {
let recipient_nf_pk = cl::NullifierSecret::random(&mut rng).commit(); let recipient_nf_pk = cl::NullifierSecret::random(&mut rng).commit();
// Assume the sender has received an unspent output from somewhere // Assume the sender has received an unspent output from somewhere
let utxo = receive_utxo(cl::NoteWitness::basic(10, "NMO"), sender_nf_pk, &mut rng); let utxo = receive_utxo(cl::NoteWitness::basic(10, nmo), sender_nf_pk, &mut rng);
// and wants to send 8 NMO to some recipient and return 2 NMO to itself. // and wants to send 8 NMO to some recipient and return 2 NMO to itself.
let recipient_output = let recipient_output =
cl::OutputWitness::random(cl::NoteWitness::basic(8, "NMO"), recipient_nf_pk, &mut rng); cl::OutputWitness::random(cl::NoteWitness::basic(8, nmo), recipient_nf_pk, &mut rng);
let change_output = let change_output =
cl::OutputWitness::random(cl::NoteWitness::basic(2, "NMO"), sender_nf_pk, &mut rng); cl::OutputWitness::random(cl::NoteWitness::basic(2, nmo), sender_nf_pk, &mut rng);
let ptx_witness = cl::PartialTxWitness { let ptx_witness = cl::PartialTxWitness {
inputs: vec![cl::InputWitness::random(utxo, sender_nf_sk, &mut rng)], inputs: vec![cl::InputWitness::random(utxo, sender_nf_sk, &mut rng)],

View File

@ -5,7 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
cl = { path = "../cl" } cl = { path = "../cl" }
proof_statements = { path = "../proof_statements" } ledger_proof_statements = { path = "../ledger_proof_statements" }
nomos_cl_risc0_proofs = { path = "../risc0_proofs" } nomos_cl_risc0_proofs = { path = "../risc0_proofs" }
risc0-zkvm = { version = "1.0", features = ["prove", "metal"] } risc0-zkvm = { version = "1.0", features = ["prove", "metal"] }
risc0-groth16 = { version = "1.0" } risc0-groth16 = { version = "1.0" }

View File

@ -1,4 +1,4 @@
use proof_statements::death_constraint::DeathConstraintPublic; use ledger_proof_statements::death_constraint::DeathConstraintPublic;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::error::Result; use crate::error::Result;
@ -7,11 +7,11 @@ pub type Risc0DeathConstraintId = [u32; 8];
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DeathProof { pub struct DeathProof {
constraint: Risc0DeathConstraintId, pub constraint: Risc0DeathConstraintId,
risc0_receipt: risc0_zkvm::Receipt, pub risc0_receipt: risc0_zkvm::Receipt,
} }
fn risc0_id_to_cl_death_constraint(risc0_id: Risc0DeathConstraintId) -> [u8; 32] { pub fn risc0_id_to_cl_death_constraint(risc0_id: Risc0DeathConstraintId) -> [u8; 32] {
// RISC0 proof ids have the format: [u32; 8], and cl death constraint ids have the format [u8; 32]. // RISC0 proof ids have the format: [u32; 8], and cl death constraint ids have the format [u8; 32].
// CL death constraints are meaningless beyond being binding, therefore we merely need a collision // CL death constraints are meaningless beyond being binding, therefore we merely need a collision
// resisitant mapping of RISC0 ids to cl death constraints. // resisitant mapping of RISC0 ids to cl death constraints.
@ -26,6 +26,16 @@ fn risc0_id_to_cl_death_constraint(risc0_id: Risc0DeathConstraintId) -> [u8; 32]
} }
impl DeathProof { impl DeathProof {
pub fn from_risc0(
risc0_id: Risc0DeathConstraintId,
risc0_receipt: risc0_zkvm::Receipt,
) -> Self {
Self {
constraint: risc0_id,
risc0_receipt,
}
}
pub fn death_commitment(&self) -> cl::DeathCommitment { pub fn death_commitment(&self) -> cl::DeathCommitment {
cl::note::death_commitment(&risc0_id_to_cl_death_constraint(self.constraint)) cl::note::death_commitment(&risc0_id_to_cl_death_constraint(self.constraint))
} }
@ -75,9 +85,6 @@ impl DeathProof {
// extract the receipt. // extract the receipt.
let receipt = prove_info.receipt; let receipt = prove_info.receipt;
Self { Self::from_risc0(nomos_cl_risc0_proofs::DEATH_CONSTRAINT_NOP_ID, receipt)
constraint: nomos_cl_risc0_proofs::DEATH_CONSTRAINT_NOP_ID,
risc0_receipt: receipt,
}
} }
} }

View File

@ -1,4 +1,4 @@
use proof_statements::input::{InputPrivate, InputPublic}; use ledger_proof_statements::input::{InputPrivate, InputPublic};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
@ -89,16 +89,18 @@ fn note_commitment_leaves(note_commitments: &[cl::NoteCommitment]) -> [[u8; 32];
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use rand::thread_rng;
use super::*; use super::*;
use cl::note::unit_point;
use rand::thread_rng;
#[test] #[test]
fn test_input_prover() { fn test_input_prover() {
let nmo = unit_point("NMO");
let mut rng = thread_rng(); let mut rng = thread_rng();
let input = cl::InputWitness { let input = cl::InputWitness {
note: cl::NoteWitness::basic(32, "NMO"), note: cl::NoteWitness::basic(32, nmo),
balance_blinding: cl::BalanceWitness::random(&mut rng), balance_blinding: cl::BalanceWitness::random(&mut rng),
nf_sk: cl::NullifierSecret::random(&mut rng), nf_sk: cl::NullifierSecret::random(&mut rng),
nonce: cl::NullifierNonce::random(&mut rng), nonce: cl::NullifierNonce::random(&mut rng),
@ -141,7 +143,7 @@ mod test {
InputPublic { InputPublic {
input: cl::Input { input: cl::Input {
balance: cl::BalanceWitness::random(&mut rng) balance: cl::BalanceWitness::random(&mut rng)
.commit(&cl::NoteWitness::basic(32, "NMO")), .commit(&cl::NoteWitness::basic(32, nmo)),
..expected_public_inputs.input ..expected_public_inputs.input
}, },
..expected_public_inputs ..expected_public_inputs

View File

@ -4,3 +4,5 @@ pub mod error;
pub mod input; pub mod input;
pub mod output; pub mod output;
pub mod partial_tx; pub mod partial_tx;
pub use death_constraint::DeathProof;

View File

@ -55,16 +55,18 @@ impl ProvedOutput {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use rand::thread_rng;
use super::*; use super::*;
use cl::note::unit_point;
use rand::thread_rng;
#[test] #[test]
fn test_output_prover() { fn test_output_prover() {
let nmo = unit_point("NMO");
let mut rng = thread_rng(); let mut rng = thread_rng();
let output = cl::OutputWitness { let output = cl::OutputWitness {
note: cl::NoteWitness::basic(32, "NMO"), note: cl::NoteWitness::basic(32, nmo),
balance_blinding: cl::BalanceWitness::random(&mut rng), balance_blinding: cl::BalanceWitness::random(&mut rng),
nf_pk: cl::NullifierSecret::random(&mut rng).commit(), nf_pk: cl::NullifierSecret::random(&mut rng).commit(),
nonce: cl::NullifierNonce::random(&mut rng), nonce: cl::NullifierNonce::random(&mut rng),
@ -79,19 +81,19 @@ mod test {
let wrong_output_cms = [ let wrong_output_cms = [
cl::Output { cl::Output {
note_comm: cl::NoteWitness::basic(100, "NMO").commit( note_comm: cl::NoteWitness::basic(100, nmo).commit(
cl::NullifierSecret::random(&mut rng).commit(), cl::NullifierSecret::random(&mut rng).commit(),
cl::NullifierNonce::random(&mut rng), cl::NullifierNonce::random(&mut rng),
), ),
..expected_output_cm ..expected_output_cm
}, },
cl::Output { cl::Output {
note_comm: cl::NoteWitness::basic(100, "NMO").commit( note_comm: cl::NoteWitness::basic(100, nmo).commit(
cl::NullifierSecret::random(&mut rng).commit(), cl::NullifierSecret::random(&mut rng).commit(),
cl::NullifierNonce::random(&mut rng), cl::NullifierNonce::random(&mut rng),
), ),
balance: cl::BalanceWitness::random(&mut rng) balance: cl::BalanceWitness::random(&mut rng)
.commit(&cl::NoteWitness::basic(100, "NMO")), .commit(&cl::NoteWitness::basic(100, nmo)),
}, },
]; ];
@ -103,10 +105,11 @@ mod test {
#[test] #[test]
fn test_zero_output_is_rejected() { fn test_zero_output_is_rejected() {
let nmo = unit_point("NMO");
let mut rng = thread_rng(); let mut rng = thread_rng();
let output = cl::OutputWitness::random( let output = cl::OutputWitness::random(
cl::NoteWitness::basic(0, "NMO"), cl::NoteWitness::basic(0, nmo),
cl::NullifierSecret::random(&mut rng).commit(), cl::NullifierSecret::random(&mut rng).commit(),
&mut rng, &mut rng,
); );

View File

@ -1,6 +1,6 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use proof_statements::death_constraint::DeathConstraintPublic; use ledger_proof_statements::death_constraint::DeathConstraintPublic;
use crate::{ use crate::{
death_constraint::DeathProof, error::Result, input::ProvedInput, output::ProvedOutput, death_constraint::DeathProof, error::Result, input::ProvedInput, output::ProvedOutput,

View File

@ -1,5 +1,6 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use cl::note::unit_point;
use ledger::{bundle::ProvedBundle, death_constraint::DeathProof, partial_tx::ProvedPartialTx}; use ledger::{bundle::ProvedBundle, death_constraint::DeathProof, partial_tx::ProvedPartialTx};
use rand_core::CryptoRngCore; use rand_core::CryptoRngCore;
@ -29,6 +30,8 @@ fn receive_utxo(
#[test] #[test]
fn test_simple_transfer() { fn test_simple_transfer() {
let nmo = unit_point("NMO");
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
// alice is sending 8 NMO to bob. // alice is sending 8 NMO to bob.
@ -38,19 +41,18 @@ fn test_simple_transfer() {
// Alice has an unspent note worth 10 NMO // Alice has an unspent note worth 10 NMO
let utxo = receive_utxo( let utxo = receive_utxo(
cl::NoteWitness::stateless(10, "NMO", DeathProof::nop_constraint()), cl::NoteWitness::stateless(10, nmo, DeathProof::nop_constraint()),
alice.pk(), alice.pk(),
&mut rng, &mut rng,
); );
let alices_input = cl::InputWitness::random(utxo, alice.sk(), &mut rng); let alices_input = cl::InputWitness::random(utxo, alice.sk(), &mut rng);
// Alice wants to send 8 NMO to bob // Alice wants to send 8 NMO to bob
let bobs_output = let bobs_output = cl::OutputWitness::random(cl::NoteWitness::basic(8, nmo), bob.pk(), &mut rng);
cl::OutputWitness::random(cl::NoteWitness::basic(8, "NMO"), bob.pk(), &mut rng);
// .. and return the 2 NMO in change to herself. // .. and return the 2 NMO in change to herself.
let change_output = let change_output =
cl::OutputWitness::random(cl::NoteWitness::basic(2, "NMO"), alice.pk(), &mut rng); cl::OutputWitness::random(cl::NoteWitness::basic(2, nmo), alice.pk(), &mut rng);
// Construct the ptx consuming Alices inputs and producing the two outputs. // Construct the ptx consuming Alices inputs and producing the two outputs.
let ptx_witness = cl::PartialTxWitness { let ptx_witness = cl::PartialTxWitness {

View File

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

View File

@ -1,3 +1,2 @@
pub mod death_constraint; pub mod death_constraint;
pub mod input; pub mod input;
pub mod ptx;

View File

@ -1,36 +0,0 @@
use cl::{merkle, InputWitness, OutputWitness, PtxRoot};
use serde::{Deserialize, Serialize};
/// An input to a partial transaction
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartialTxInputPrivate {
pub input: InputWitness,
pub cm_path: Vec<merkle::PathNode>,
pub ptx_path: Vec<merkle::PathNode>,
}
impl PartialTxInputPrivate {
pub fn ptx_root(&self) -> PtxRoot {
let leaf = merkle::leaf(&self.input.commit().to_bytes());
PtxRoot(merkle::path_root(leaf, &self.ptx_path))
}
pub fn cm_root(&self) -> [u8; 32] {
let leaf = merkle::leaf(self.input.note_commitment().as_bytes());
merkle::path_root(leaf, &self.cm_path)
}
}
/// An output to a partial transaction
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartialTxOutputPrivate {
pub output: OutputWitness,
pub ptx_path: Vec<merkle::PathNode>,
}
impl PartialTxOutputPrivate {
pub fn ptx_root(&self) -> PtxRoot {
let leaf = merkle::leaf(&self.output.commit().to_bytes());
PtxRoot(merkle::path_root(leaf, &self.ptx_path))
}
}

View File

@ -9,7 +9,7 @@ edition = "2021"
risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
cl = { path = "../../cl" } cl = { path = "../../cl" }
proof_statements = { path = "../../proof_statements" } ledger_proof_statements = { path = "../../ledger_proof_statements" }
[patch.crates-io] [patch.crates-io]

View File

@ -9,7 +9,7 @@ edition = "2021"
risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
cl = { path = "../../cl" } cl = { path = "../../cl" }
proof_statements = { path = "../../proof_statements" } ledger_proof_statements = { path = "../../ledger_proof_statements" }
[patch.crates-io] [patch.crates-io]

View File

@ -1,5 +1,5 @@
/// Death Constraint No-op Proof /// Death Constraint No-op Proof
use proof_statements::death_constraint::DeathConstraintPublic; use ledger_proof_statements::death_constraint::DeathConstraintPublic;
use risc0_zkvm::guest::env; use risc0_zkvm::guest::env;
fn main() { fn main() {

View File

@ -9,7 +9,7 @@ edition = "2021"
risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
cl = { path = "../../cl" } cl = { path = "../../cl" }
proof_statements = { path = "../../proof_statements" } ledger_proof_statements = { path = "../../ledger_proof_statements" }
[patch.crates-io] [patch.crates-io]

View File

@ -1,6 +1,6 @@
/// Input Proof /// Input Proof
use cl::merkle; use cl::merkle;
use proof_statements::input::{InputPrivate, InputPublic}; use ledger_proof_statements::input::{InputPrivate, InputPublic};
use risc0_zkvm::guest::env; use risc0_zkvm::guest::env;
fn main() { fn main() {

View File

@ -9,7 +9,7 @@ edition = "2021"
risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
cl = { path = "../../cl" } cl = { path = "../../cl" }
proof_statements = { path = "../../proof_statements" } ledger_proof_statements = { path = "../../ledger_proof_statements" }
[patch.crates-io] [patch.crates-io]