* Add native zone deposits

* check note unit and death constraints

* fix deposit logic

* fix ptx merkle root derivation

* restrict ptx inputs/outputs

* re-org directories
This commit is contained in:
Giacomo Pasini 2024-08-01 12:46:17 +02:00 committed by GitHub
parent e9d43eaee9
commit 7e67014042
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 216 additions and 102 deletions

View File

@ -13,17 +13,20 @@ lazy_static! {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub struct Balance(pub RistrettoPoint);
pub type Value = u64;
pub type Unit = RistrettoPoint;
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub struct BalanceWitness(pub Scalar);
impl Balance {
/// A commitment to zero, blinded by the provided balance witness
pub fn zero(blinding: BalanceWitness) -> Self {
// Since, balance commitments are `value * UnitPoint + blinding * H`, when value=0, the commmitment is unitless.
// So we use the generator point as a stand in for the unit point.
//
// TAI: we can optimize this further from `0*G + r*H` to just `r*H` to save a point scalar mult + point addition.
let unit = curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
// Since, balance commitments are `value * UnitPoint + blinding * H`, when value=0, the commmitment is unitless.
// So we use the generator point as a stand in for the unit point.
//
// TAI: we can optimize this further from `0*G + r*H` to just `r*H` to save a point scalar mult + point addition.
let unit = curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
Self(balance(0, unit, blinding.0))
}
@ -46,7 +49,7 @@ impl BalanceWitness {
}
}
pub fn balance(value: u64, unit: RistrettoPoint, blinding: Scalar) -> RistrettoPoint {
pub fn balance(value: u64, unit: Unit, blinding: Scalar) -> Unit {
let value_scalar = Scalar::from(value);
// can vartime leak the number of cycles through the stark proof?
RistrettoPoint::vartime_multiscalar_mul(

View File

@ -1,8 +1,10 @@
use curve25519_dalek::RistrettoPoint;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::nullifier::{NullifierCommitment, NullifierNonce};
use crate::{
balance::Unit,
nullifier::{NullifierCommitment, NullifierNonce},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct DeathCommitment(pub [u8; 32]);
@ -16,7 +18,7 @@ pub fn death_commitment(death_constraint: &[u8]) -> DeathCommitment {
DeathCommitment(death_cm)
}
pub fn unit_point(unit: &str) -> RistrettoPoint {
pub fn unit_point(unit: &str) -> Unit {
crate::crypto::hash_to_curve(unit.as_bytes())
}
@ -34,7 +36,7 @@ impl NoteCommitment {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub struct NoteWitness {
pub value: u64,
pub unit: RistrettoPoint,
pub unit: Unit,
pub death_constraint: [u8; 32], // death constraint verification key
pub state: [u8; 32],
}

View File

@ -65,6 +65,10 @@ impl NullifierCommitment {
pub fn hex(&self) -> String {
hex::encode(self.0)
}
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl NullifierNonce {

View File

@ -8,8 +8,8 @@ use crate::input::{Input, InputWitness};
use crate::merkle;
use crate::output::{Output, OutputWitness};
const MAX_INPUTS: usize = 8;
const MAX_OUTPUTS: usize = 8;
pub const MAX_INPUTS: usize = 8;
pub const MAX_OUTPUTS: usize = 8;
/// The partial transaction commitment couples an input to a partial transaction.
/// Prevents partial tx unbundling.

View File

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

View File

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

View File

@ -1,4 +1,11 @@
use cl::nullifier::{Nullifier, NullifierCommitment};
use cl::{
balance::Unit,
crypto,
input::InputWitness,
nullifier::{Nullifier, NullifierCommitment},
output::OutputWitness,
};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
@ -11,6 +18,17 @@ pub const MAX_EVENTS: usize = 1 << 8;
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct StateCommitment([u8; 32]);
pub type AccountId = u32;
// PLACEHOLDER: replace with the death constraint vk of the zone funds
pub const ZONE_FUNDS_VK: [u8; 32] = [0; 32];
// PLACEHOLDER: this is probably going to be NMO?
pub static ZONE_CL_FUNDS_UNIT: Lazy<Unit> = Lazy::new(|| crypto::hash_to_curve(b"NMO"));
// PLACEHOLDER
pub static ZONE_UNIT: Lazy<Unit> = Lazy::new(|| crypto::hash_to_curve(b"ZONE_UNIT"));
// PLACEHOLDER
pub const ZONE_NF_PK: NullifierCommitment = NullifierCommitment::from_bytes([0; 32]);
#[derive(Clone, Serialize, Deserialize)]
pub struct StateWitness {
pub balances: BTreeMap<u32, u32>,
@ -38,12 +56,8 @@ impl StateWitness {
}
fn included_txs_root(&self) -> [u8; 32] {
let tx_bytes = Vec::from_iter(
self.included_txs
.iter()
.map(Input::to_bytes)
.map(Vec::from_iter),
);
// this is a placeholder
let tx_bytes = [vec![0u8; 32]];
let tx_merkle_leaves = cl::merkle::padded_leaves(&tx_bytes);
cl::merkle::root::<MAX_TXS>(tx_merkle_leaves)
}
@ -62,8 +76,8 @@ impl StateWitness {
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Withdraw {
pub from: u32,
pub amount: u32,
pub from: AccountId,
pub amount: AccountId,
pub to: NullifierCommitment,
pub nf: Nullifier,
}
@ -79,22 +93,30 @@ impl Withdraw {
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Input {
Withdraw(Withdraw),
/// A deposit of funds into the zone
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deposit {
/// The note that is used to deposit funds into the zone
pub deposit: InputWitness,
// This zone state note
pub zone_note_in: InputWitness,
pub zone_note_out: OutputWitness,
// The zone funds note
pub zone_funds_in: InputWitness,
pub zone_funds_out: OutputWitness,
}
impl Input {
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Input::Withdraw(withdraw) => withdraw.to_bytes().to_vec(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Input {
Withdraw(Withdraw),
Deposit(Deposit),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Event {
Spend(proof_statements::zone_funds::Spend),
Spend(goas_proof_statements::zone_funds::Spend),
}
impl Event {

View File

@ -5,7 +5,7 @@ edition = "2021"
default-run = "host"
[dependencies]
methods = { path = "../methods" }
goas_risc0_proofs = { path = "../risc0_proofs", package = "goas_risc0_proofs" }
risc0-zkvm = { version = "1.0", features = ["prove", "metal"] }
risc0-groth16 = { version = "1.0" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@ -36,7 +36,7 @@ fn stf_prove_stark(state: StateWitness, inputs: Vec<Input>) {
// 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, methods::METHOD_ELF, &opts)
.prove_with_opts(env, goas_risc0_proofs::ZONE_STATE_ELF, &opts)
.unwrap();
println!("STARK prover time: {:.2?}", start_t.elapsed());
@ -48,7 +48,7 @@ fn stf_prove_stark(state: StateWitness, inputs: Vec<Input>) {
std::fs::write("proof.stark", bincode::serialize(&receipt).unwrap()).unwrap();
// The receipt was verified at the end of proving, but the below code is an
// example of how someone else could verify this receipt.
receipt.verify(methods::METHOD_ID).unwrap();
receipt.verify(goas_risc0_proofs::ZONE_STATE_ID).unwrap();
}
fn main() {

View File

@ -1,10 +0,0 @@
[package]
name = "methods"
version = "0.1.0"
edition = "2021"
[build-dependencies]
risc0-build = { version = "1.0" }
[package.metadata.risc0]
methods = ["guest"]

View File

@ -1,3 +0,0 @@
fn main() {
risc0_build::embed_methods();
}

View File

@ -1,40 +0,0 @@
use common::*;
use proof_statements::zone_funds::Spend;
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 main() {
let inputs: Vec<Input> = env::read();
let mut state: StateWitness = env::read();
for input in inputs {
match input {
Input::Withdraw(input) => {
state = withdraw(state, input);
}
}
}
env::commit(&state.commit());
}

View File

@ -1 +0,0 @@
include!(concat!(env!("OUT_DIR"), "/methods.rs"));

View File

@ -22,7 +22,6 @@ impl Spend {
}
}
/// There are two kind of paths
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpendFundsPrivate {
/// The note we're spending

View File

@ -7,5 +7,5 @@ edition = "2021"
risc0-build = { version = "1.0" }
[package.metadata.risc0]
methods = ["spend_zone_funds"]
methods = ["spend_zone_funds", "zone_state"]

View File

@ -60,12 +60,9 @@ fn main() {
out_zone_funds.output.balance_blinding,
in_zone_funds.input.balance_blinding
);
let mut evolved_nonce = [0; 16];
evolved_nonce[..16]
.copy_from_slice(&Sha256::digest(&out_zone_funds.output.nonce.as_bytes())[..16]);
assert_eq!(
out_zone_funds.output.nonce,
NullifierNonce::from_bytes(evolved_nonce)
NullifierNonce::from_bytes(Sha256::digest(&out_zone_funds.output.nonce.as_bytes()).into())
);
assert_eq!(ptx_root, spent_note.ptx_root());
@ -76,8 +73,5 @@ fn main() {
// check the correct recipient is being paid
assert_eq!(spent_note.output.nf_pk, spend_event.to);
env::commit(&DeathConstraintPublic {
ptx_root,
nf,
});
env::commit(&DeathConstraintPublic { ptx_root, nf });
}

View File

@ -1,7 +1,7 @@
[package]
name = "method"
name = "zone_state"
version = "0.1.0"
edition = "2021"
@ -14,7 +14,9 @@ serde = { version = "1.0", features = ["derive"] }
bincode = "1"
common = { path = "../../common" }
cl = { path = "../../../cl/cl" }
proof_statements = { path = "../../proof_statements", package = "goas_proof_statements" }
goas_proof_statements = { path = "../../proof_statements" }
proof_statements = { path = "../../../cl/proof_statements" }
sha2 = "0.10"
[patch.crates-io]
# Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint

View File

@ -0,0 +1,140 @@
use cl::{
merkle,
nullifier::{Nullifier, NullifierNonce, NullifierSecret},
partial_tx::{MAX_INPUTS, MAX_OUTPUTS},
PtxRoot,
};
use common::*;
use goas_proof_statements::zone_funds::Spend;
use proof_statements::death_constraint::DeathConstraintPublic;
use risc0_zkvm::guest::env;
use sha2::{Digest, Sha256};
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(
mut state: StateWitness,
deposit: Deposit,
pub_inputs: DeathConstraintPublic,
) -> StateWitness {
state.included_txs.push(Input::Deposit(deposit.clone()));
let Deposit {
deposit,
zone_note_in,
zone_note_out,
zone_funds_in,
zone_funds_out,
} = deposit;
// 1) Check there are no more input/output notes than expected
let inputs = [
deposit.commit().to_bytes().to_vec(),
zone_note_in.commit().to_bytes().to_vec(),
zone_funds_in.commit().to_bytes().to_vec(),
];
let inputs_root = merkle::root(merkle::padded_leaves::<MAX_INPUTS>(&inputs));
let outputs = [
zone_note_out.commit().to_bytes().to_vec(),
zone_funds_out.commit().to_bytes().to_vec(),
];
let outputs_root = merkle::root(merkle::padded_leaves::<MAX_OUTPUTS>(&outputs));
let ptx_root = PtxRoot(merkle::node(inputs_root, outputs_root));
assert_eq!(ptx_root, pub_inputs.ptx_root);
// 2) Check the deposit note is not already under control of the zone
assert_ne!(deposit.note.death_constraint, ZONE_FUNDS_VK);
// 3) Check the ptx is balanced. This is not a requirement for standard ptxs, but we need it
// in deposits (at least in a first version) to ensure fund tracking
assert_eq!(deposit.note.unit, *ZONE_UNIT);
assert_eq!(zone_funds_in.note.unit, *ZONE_UNIT);
assert_eq!(zone_funds_out.note.unit, *ZONE_UNIT);
let in_sum = deposit.note.value + zone_funds_in.note.value;
let out_sum = zone_note_out.note.value;
assert_eq!(out_sum, in_sum, "deposit ptx is unbalanced");
// 4) Check the zone fund notes are correctly created
assert_eq!(zone_funds_in.note.death_constraint, ZONE_FUNDS_VK);
assert_eq!(zone_funds_out.note.death_constraint, ZONE_FUNDS_VK);
assert_eq!(zone_funds_in.nf_sk, NullifierSecret::from_bytes([0; 16])); // there is no secret in the zone funds
assert_eq!(zone_funds_out.nf_pk, zone_funds_in.nf_sk.commit()); // the sk is the same
// nonce is correctly evolved
assert_eq!(
zone_funds_out.nonce,
NullifierNonce::from_bytes(Sha256::digest(&zone_funds_in.nonce.as_bytes()).into())
);
// 5) Check zone state notes are correctly created
assert_eq!(
zone_note_in.note.death_constraint,
zone_note_out.note.death_constraint
);
assert_eq!(zone_note_in.nf_sk, NullifierSecret::from_bytes([0; 16])); //// there is no secret in the zone state
assert_eq!(zone_note_out.nf_pk, zone_note_in.nf_sk.commit()); // the sk is the same
assert_eq!(zone_note_in.note.unit, zone_note_out.note.unit);
assert_eq!(zone_note_in.note.value, zone_note_out.note.value);
// nonce is correctly evolved
assert_eq!(
zone_note_out.nonce,
NullifierNonce::from_bytes(Sha256::digest(&zone_note_in.nonce.as_bytes()).into())
);
let nullifier = Nullifier::new(zone_note_in.nf_sk, zone_note_in.nonce);
assert_eq!(nullifier, pub_inputs.nf);
// 6) We're now ready to do the deposit!
let amount = deposit.note.value as u32;
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);
*to_balance = to_balance
.checked_add(amount)
.expect("overflow when depositing");
state
}
fn main() {
let public_inputs: DeathConstraintPublic = env::read();
let inputs: Vec<Input> = env::read();
let mut state: StateWitness = env::read();
for input in inputs {
match input {
Input::Withdraw(input) => state = withdraw(state, input),
Input::Deposit(input) => state = deposit(state, input, public_inputs),
}
}
env::commit(&state.commit());
}