Deposit (#8)
* 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:
parent
e9d43eaee9
commit
7e67014042
|
@ -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(
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"
|
|
@ -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 {
|
||||
|
|
|
@ -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"] }
|
|
@ -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() {
|
|
@ -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"]
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
risc0_build::embed_methods();
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
include!(concat!(env!("OUT_DIR"), "/methods.rs"));
|
|
@ -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
|
||||
|
|
|
@ -7,5 +7,5 @@ edition = "2021"
|
|||
risc0-build = { version = "1.0" }
|
||||
|
||||
[package.metadata.risc0]
|
||||
methods = ["spend_zone_funds"]
|
||||
methods = ["spend_zone_funds", "zone_state"]
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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());
|
||||
}
|
Loading…
Reference in New Issue