diff --git a/emmarin/atomic_asset_transfer/.gitignore b/emmarin/atomic_asset_transfer/.gitignore new file mode 100644 index 00000000..f444679c --- /dev/null +++ b/emmarin/atomic_asset_transfer/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +Cargo.lock +methods/guest/Cargo.lock +target/ +output/ +proof.stark diff --git a/emmarin/atomic_asset_transfer/Cargo.toml b/emmarin/atomic_asset_transfer/Cargo.toml new file mode 100644 index 00000000..b8d95ee3 --- /dev/null +++ b/emmarin/atomic_asset_transfer/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +resolver = "2" +members = [ "common", "executor", "proof_statements", "risc0_proofs", "user"] + +# Always optimize; building and running the guest takes much longer without optimization. +[profile.dev] +opt-level = 3 + +[profile.release] +debug = 1 +lto = true diff --git a/emmarin/atomic_asset_transfer/LICENSE b/emmarin/atomic_asset_transfer/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/emmarin/atomic_asset_transfer/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/emmarin/atomic_asset_transfer/README.md b/emmarin/atomic_asset_transfer/README.md new file mode 100644 index 00000000..86ce485c --- /dev/null +++ b/emmarin/atomic_asset_transfer/README.md @@ -0,0 +1,111 @@ +# RISC Zero Rust Starter Template + +Welcome to the RISC Zero Rust Starter Template! This template is intended to +give you a starting point for building a project using the RISC Zero zkVM. +Throughout the template (including in this README), you'll find comments +labelled `TODO` in places where you'll need to make changes. To better +understand the concepts behind this template, check out the [zkVM +Overview][zkvm-overview]. + +## Quick Start + +First, make sure [rustup] is installed. The +[`rust-toolchain.toml`][rust-toolchain] file will be used by `cargo` to +automatically install the correct version. + +To build all methods and execute the method within the zkVM, run the following +command: + +```bash +cargo run +``` + +This is an empty template, and so there is no expected output (until you modify +the code). + +### Executing the project locally in development mode + +During development, faster iteration upon code changes can be achieved by leveraging [dev-mode], we strongly suggest activating it during your early development phase. Furthermore, you might want to get insights into the execution statistics of your project, and this can be achieved by specifying the environment variable `RUST_LOG="[executor]=info"` before running your project. + +Put together, the command to run your project in development mode while getting execution statistics is: + +```bash +RUST_LOG="[executor]=info" RISC0_DEV_MODE=1 cargo run +``` + +### Running proofs remotely on Bonsai + +_Note: The Bonsai proving service is still in early Alpha; an API key is +required for access. [Click here to request access][bonsai access]._ + +If you have access to the URL and API key to Bonsai you can run your proofs +remotely. To prove in Bonsai mode, invoke `cargo run` with two additional +environment variables: + +```bash +BONSAI_API_KEY="YOUR_API_KEY" BONSAI_API_URL="BONSAI_URL" cargo run +``` + +## How to create a project based on this template + +Search this template for the string `TODO`, and make the necessary changes to +implement the required feature described by the `TODO` comment. Some of these +changes will be complex, and so we have a number of instructional resources to +assist you in learning how to write your own code for the RISC Zero zkVM: + +- The [RISC Zero Developer Docs][dev-docs] is a great place to get started. +- Example projects are available in the [examples folder][examples] of + [`risc0`][risc0-repo] repository. +- Reference documentation is available at [https://docs.rs][docs.rs], including + [`risc0-zkvm`][risc0-zkvm], [`cargo-risczero`][cargo-risczero], + [`risc0-build`][risc0-build], and [others][crates]. + +## Directory Structure + +It is possible to organize the files for these components in various ways. +However, in this starter template we use a standard directory structure for zkVM +applications, which we think is a good starting point for your applications. + +```text +project_name +├── Cargo.toml +├── host +│ ├── Cargo.toml +│ └── src +│ └── main.rs <-- [Host code goes here] +└── methods + ├── Cargo.toml + ├── build.rs + ├── guest + │ ├── Cargo.toml + │ └── src + │ └── method_name.rs <-- [Guest code goes here] + └── src + └── lib.rs +``` + +## Video Tutorial + +For a walk-through of how to build with this template, check out this [excerpt +from our workshop at ZK HACK III][zkhack-iii]. + +## Questions, Feedback, and Collaborations + +We'd love to hear from you on [Discord][discord] or [Twitter][twitter]. + +[bonsai access]: https://bonsai.xyz/apply +[cargo-risczero]: https://docs.rs/cargo-risczero +[crates]: https://github.com/risc0/risc0/blob/main/README.md#rust-binaries +[dev-docs]: https://dev.risczero.com +[dev-mode]: https://dev.risczero.com/api/generating-proofs/dev-mode +[discord]: https://discord.gg/risczero +[docs.rs]: https://docs.rs/releases/search?query=risc0 +[examples]: https://github.com/risc0/risc0/tree/main/examples +[risc0-build]: https://docs.rs/risc0-build +[risc0-repo]: https://www.github.com/risc0/risc0 +[risc0-zkvm]: https://docs.rs/risc0-zkvm +[rustup]: https://rustup.rs +[rust-toolchain]: rust-toolchain.toml +[twitter]: https://twitter.com/risczero +[zkvm-overview]: https://dev.risczero.com/zkvm +[zkhack-iii]: https://www.youtube.com/watch?v=Yg_BGqj_6lg&list=PLcPzhUaCxlCgig7ofeARMPwQ8vbuD6hC5&index=5 diff --git a/emmarin/atomic_asset_transfer/common/Cargo.toml b/emmarin/atomic_asset_transfer/common/Cargo.toml new file mode 100644 index 00000000..52793c2a --- /dev/null +++ b/emmarin/atomic_asset_transfer/common/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } +cl = { path = "../../cl/cl" } +ledger_proof_statements = { path = "../../cl/ledger_proof_statements" } +once_cell = "1" +sha2 = "0.10" +curve25519-dalek = { version = "4.1", features = ["serde", "digest", "rand_core"] } +ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } +rand_core = "0.6.0" +serde_arrays = "0.1.0" diff --git a/emmarin/atomic_asset_transfer/common/src/lib.rs b/emmarin/atomic_asset_transfer/common/src/lib.rs new file mode 100644 index 00000000..1829799b --- /dev/null +++ b/emmarin/atomic_asset_transfer/common/src/lib.rs @@ -0,0 +1,425 @@ +pub mod mmr; + +use cl::{balance::Unit, Constraint, NoteCommitment}; +use ed25519_dalek::{ + ed25519::{signature::SignerMut, SignatureBytes}, + Signature, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, +}; +use mmr::{MMRProof, MMR}; +use once_cell::sync::Lazy; +use rand_core::CryptoRngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; + +// state of the zone +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub struct StateCommitment(pub [u8; 32]); + +pub type AccountId = [u8; PUBLIC_KEY_LENGTH]; + +// PLACEHOLDER: this is probably going to be NMO? +pub static ZONE_CL_FUNDS_UNIT: Lazy = Lazy::new(|| cl::note::derive_unit("NMO")); + +pub fn new_account(mut rng: impl CryptoRngCore) -> SigningKey { + SigningKey::generate(&mut rng) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct ZoneMetadata { + pub zone_constraint: Constraint, + pub funds_constraint: Constraint, + pub unit: Unit, +} + +impl ZoneMetadata { + pub fn id(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(self.zone_constraint.0); + hasher.update(self.funds_constraint.0); + hasher.update(self.unit); + hasher.finalize().into() + } +} + +type ExecVK = [u8; PUBLIC_KEY_LENGTH]; +type BlockSegmentIdx = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Bid { + pub segment: BlockSegmentIdx, + pub exec_vk: ExecVK, + pub fee: u64, + // pub blinding: [u8;32] +} + +impl Bid { + pub fn commit(&self) -> BlindBid { + let mut hasher = Sha256::new(); + hasher.update(self.segment.to_le_bytes()); + hasher.update(self.exec_vk); + hasher.update(self.fee.to_le_bytes()); + let commitment: [u8; 32] = hasher.finalize().into(); + + BlindBid { + exec_vk: self.exec_vk, + segment: self.segment, + commitment, + } + } + + pub fn to_bytes(&self) -> [u8; 8 + PUBLIC_KEY_LENGTH + 8] { + let mut bytes = [0u8; 8 + PUBLIC_KEY_LENGTH + 8]; + bytes[..8].copy_from_slice(&self.segment.to_le_bytes()); + bytes[8..8 + PUBLIC_KEY_LENGTH].copy_from_slice(&self.exec_vk); + bytes[8 + PUBLIC_KEY_LENGTH..].copy_from_slice(&self.fee.to_le_bytes()); + bytes + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct BlindBid { + pub segment: BlockSegmentIdx, + pub exec_vk: ExecVK, + pub commitment: [u8; 32], +} + +impl BlindBid { + pub fn to_bytes(&self) -> [u8; 8 + PUBLIC_KEY_LENGTH + 32] { + let mut bytes = [0u8; 8 + PUBLIC_KEY_LENGTH + 32]; + bytes[..8].copy_from_slice(&self.segment.to_le_bytes()); + bytes[8..8 + PUBLIC_KEY_LENGTH].copy_from_slice(&self.exec_vk); + bytes[8 + PUBLIC_KEY_LENGTH..].copy_from_slice(&self.commitment); + bytes + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct DutchBid { + pub scd_best: Bid, + pub best: Bid, +} + +impl DutchBid { + pub fn apply(&mut self, bid: Bid) { + if bid.fee > self.best.fee { + self.scd_best = self.best; + self.best = bid; + } else if bid.fee > self.scd_best.fee { + self.scd_best = bid; + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Auction { + pub blind_bids: BTreeMap>, + pub revealed_bids: BTreeMap, +} + +pub fn segment_to_height(seg: BlockSegmentIdx) -> u64 { + seg * 10 +} + +pub fn height_to_segment(height: u64) -> BlockSegmentIdx { + height / 10 +} + +pub const REVEAL_END_OFFSET: u64 = 100; +pub const BID_END_OFFSET: u64 = REVEAL_END_OFFSET + 100; +pub const BID_START_OFFSET: u64 = BID_END_OFFSET + 100; + +impl Auction { + pub fn root(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + + for blind_bids in self.blind_bids.values() { + for blind_bid in blind_bids.iter() { + hasher.update(blind_bid.segment.to_le_bytes()); + hasher.update(blind_bid.commitment); + hasher.update(blind_bid.exec_vk); + } + } + for dutch_bids in self.revealed_bids.values() { + hasher.update(dutch_bids.scd_best.segment.to_le_bytes()); + hasher.update(dutch_bids.scd_best.exec_vk); + hasher.update(dutch_bids.scd_best.fee.to_le_bytes()); + + hasher.update(dutch_bids.best.segment.to_le_bytes()); + hasher.update(dutch_bids.best.exec_vk); + hasher.update(dutch_bids.best.fee.to_le_bytes()); + } + let root: [u8; 32] = hasher.finalize().into(); + + root + } + + pub fn bid(&mut self, current_height: u64, blind_bid: BlindBid) { + let ticket_start_height = segment_to_height(blind_bid.segment); + let bid_start_height = ticket_start_height - BID_START_OFFSET; + let bid_end_height = ticket_start_height - BID_END_OFFSET; + + if current_height < bid_start_height || current_height >= bid_end_height { + panic!("current height is not within the bidding range"); + } + + self.blind_bids + .entry(blind_bid.segment) + .or_default() + .insert(blind_bid); + } + + pub fn reveal(&mut self, current_height: u64, bid: Bid) { + let ticket_start_height = segment_to_height(bid.segment); + + let reveal_end_height = ticket_start_height - REVEAL_END_OFFSET; + + if current_height >= reveal_end_height { + panic!("attempting to reveal after reveal deadline"); + } + + let blind_bid = bid.commit(); + + if !self + .blind_bids + .get(&bid.segment) + .map(|bids| bids.contains(&blind_bid)) + .unwrap_or(false) + { + panic!("attempted to reveal a bid without commitment"); + } + + self.revealed_bids + .entry(bid.segment) + .or_insert_with(|| DutchBid { + scd_best: bid, + best: bid, + }) + .apply(bid); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct StateWitness { + pub balances: BTreeMap, + pub ticket_auction: Auction, + pub included_txs: MMR, + pub zone_metadata: ZoneMetadata, +} + +impl StateWitness { + pub fn commit(&self) -> StateCommitment { + self.state_roots().commit() + } + + pub fn state_roots(&self) -> StateRoots { + StateRoots { + included_txs: self.included_txs.clone(), + zone_id: self.zone_metadata.id(), + balance_root: self.balances_root(), + auction_root: self.ticket_auction.root(), + } + } + + pub fn apply(self, current_height: u64, tx: Tx) -> (Self, IncludedTxWitness) { + let mut state = match tx { + Tx::Withdraw(w) => self.withdraw(w), + Tx::Deposit(d) => self.deposit(d), + Tx::TicketBlindBid(blind_bid) => { + let mut state = self.clone(); + state.ticket_auction.bid(current_height, blind_bid); + state + } + Tx::TicketRevealBid(bid) => { + let mut state = self.clone(); + state.ticket_auction.reveal(current_height, bid); + state + } + }; + + let inclusion_proof = state.included_txs.push(&tx.to_bytes()); + let tx_inclusion_proof = IncludedTxWitness { + tx, + proof: inclusion_proof, + }; + + (state, tx_inclusion_proof) + } + + fn withdraw(mut self, w: Withdraw) -> Self { + let Withdraw { from, amount } = w; + + let from_balance = self.balances.entry(from).or_insert(0); + *from_balance = from_balance + .checked_sub(amount) + .expect("insufficient funds in account"); + + self + } + + fn deposit(mut self, d: Deposit) -> Self { + let Deposit { to, amount } = d; + + let to_balance = self.balances.entry(to).or_insert(0); + *to_balance += to_balance + .checked_add(amount) + .expect("overflow in account balance"); + + self + } + + pub fn balances_root(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_BALANCES_ROOT"); + + for (k, v) in self.balances.iter() { + hasher.update(k); + hasher.update(&v.to_le_bytes()); + } + + hasher.finalize().into() + } + + pub fn total_balance(&self) -> u64 { + self.balances.values().sum() + } +} + +impl From for [u8; 32] { + fn from(commitment: StateCommitment) -> [u8; 32] { + commitment.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Withdraw { + pub from: AccountId, + pub amount: u64, +} + +impl Withdraw { + pub fn to_bytes(&self) -> [u8; 40] { + let mut bytes = [0; 40]; + bytes[0..PUBLIC_KEY_LENGTH].copy_from_slice(&self.from); + bytes[PUBLIC_KEY_LENGTH..PUBLIC_KEY_LENGTH + 8].copy_from_slice(&self.amount.to_le_bytes()); + bytes + } +} + +/// A deposit of funds into the zone +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Deposit { + pub to: AccountId, + pub amount: u64, +} + +impl Deposit { + pub fn to_bytes(&self) -> [u8; 40] { + let mut bytes = [0; 40]; + bytes[0..PUBLIC_KEY_LENGTH].copy_from_slice(&self.to); + bytes[PUBLIC_KEY_LENGTH..PUBLIC_KEY_LENGTH + 8].copy_from_slice(&self.amount.to_le_bytes()); + bytes + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct SignedBoundTx { + pub bound_tx: BoundTx, + #[serde(with = "serde_arrays")] + pub sig: SignatureBytes, +} + +impl SignedBoundTx { + pub fn sign(bound_tx: BoundTx, signing_key: &mut SigningKey) -> Self { + let msg = bound_tx.to_bytes(); + let sig = signing_key.sign(&msg).to_bytes(); + + Self { bound_tx, sig } + } + + pub fn verify_and_unwrap(&self) -> BoundTx { + let msg = self.bound_tx.to_bytes(); + + let sig = Signature::from_bytes(&self.sig); + let vk = self.bound_tx.tx.verifying_key(); + vk.verify_strict(&msg, &sig).expect("Invalid tx signature"); + + self.bound_tx + } +} + +/// A Tx that is executed in the zone if and only if the bind is +/// present is the same partial transaction +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct BoundTx { + pub tx: Tx, + pub bind: NoteCommitment, +} + +impl BoundTx { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend(self.tx.to_bytes()); + bytes.extend(self.bind.as_bytes()); + bytes + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Tx { + Withdraw(Withdraw), + Deposit(Deposit), + TicketBlindBid(BlindBid), + TicketRevealBid(Bid), +} + +impl Tx { + pub fn to_bytes(&self) -> Vec { + match self { + Tx::Withdraw(withdraw) => withdraw.to_bytes().to_vec(), + Tx::Deposit(deposit) => deposit.to_bytes().to_vec(), + Tx::TicketBlindBid(bid) => bid.to_bytes().to_vec(), + Tx::TicketRevealBid(bid) => bid.to_bytes().to_vec(), + } + } + + pub fn verifying_key(&self) -> VerifyingKey { + match self { + Tx::Withdraw(w) => VerifyingKey::from_bytes(&w.from).unwrap(), + Tx::Deposit(d) => VerifyingKey::from_bytes(&d.to).unwrap(), + Tx::TicketBlindBid(blind_bid) => VerifyingKey::from_bytes(&blind_bid.exec_vk).unwrap(), + Tx::TicketRevealBid(bid) => VerifyingKey::from_bytes(&bid.exec_vk).unwrap(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IncludedTxWitness { + pub tx: Tx, + pub proof: MMRProof, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct StateRoots { + pub included_txs: MMR, + pub zone_id: [u8; 32], + pub balance_root: [u8; 32], + pub auction_root: [u8; 32], +} + +impl StateRoots { + pub fn verify_tx_inclusion(&self, tx_inclusion: &IncludedTxWitness) -> bool { + self.included_txs + .verify_proof(&tx_inclusion.tx.to_bytes(), &tx_inclusion.proof) + } + + /// Commitment to the state roots + pub fn commit(&self) -> StateCommitment { + let mut hasher = Sha256::new(); + hasher.update(self.included_txs.commit()); + hasher.update(self.zone_id); + hasher.update(self.balance_root); + hasher.update(self.auction_root); + StateCommitment(hasher.finalize().into()) + } +} diff --git a/emmarin/atomic_asset_transfer/common/src/mmr.rs b/emmarin/atomic_asset_transfer/common/src/mmr.rs new file mode 100644 index 00000000..6e6db0dd --- /dev/null +++ b/emmarin/atomic_asset_transfer/common/src/mmr.rs @@ -0,0 +1,126 @@ +use cl::merkle; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MMR { + pub roots: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Root { + pub root: [u8; 32], + pub height: u8, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MMRProof { + pub path: Vec, +} + +impl MMR { + pub fn new() -> Self { + Self { roots: vec![] } + } + + pub fn push(&mut self, elem: &[u8]) -> MMRProof { + let new_root = Root { + root: merkle::leaf(elem), + height: 1, + }; + self.roots.push(new_root); + + let mut path = vec![]; + + for i in (1..self.roots.len()).rev() { + if self.roots[i].height == self.roots[i - 1].height { + path.push(merkle::PathNode::Left(self.roots[i - 1].root)); + + self.roots[i - 1] = Root { + root: merkle::node(self.roots[i - 1].root, self.roots[i].root), + height: self.roots[i - 1].height + 1, + }; + + self.roots.remove(i); + } else { + break; + } + } + + MMRProof { path } + } + + pub fn verify_proof(&self, elem: &[u8], proof: &MMRProof) -> bool { + let path_len = proof.path.len(); + let leaf = merkle::leaf(elem); + let root = merkle::path_root(leaf, &proof.path); + + for mmr_root in self.roots.iter() { + if mmr_root.height == (path_len + 1) as u8 { + return mmr_root.root == root; + } + } + + false + } + + pub fn commit(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + for mrr_root in self.roots.iter() { + hasher.update(mrr_root.root); + hasher.update(mrr_root.height.to_le_bytes()); + } + hasher.finalize().into() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_mrr_push() { + let mut mmr = MMR::new(); + let proof = mmr.push(b"hello"); + + assert_eq!(mmr.roots.len(), 1); + assert_eq!(mmr.roots[0].height, 1); + assert_eq!(mmr.roots[0].root, merkle::leaf(b"hello")); + assert!(mmr.verify_proof(b"hello", &proof)); + + let proof = mmr.push(b"world"); + + assert_eq!(mmr.roots.len(), 1); + assert_eq!(mmr.roots[0].height, 2); + assert_eq!( + mmr.roots[0].root, + merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world")) + ); + assert!(mmr.verify_proof(b"world", &proof)); + + let proof = mmr.push(b"!"); + + assert_eq!(mmr.roots.len(), 2); + assert_eq!(mmr.roots[0].height, 2); + assert_eq!( + mmr.roots[0].root, + merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world")) + ); + assert_eq!(mmr.roots[1].height, 1); + assert_eq!(mmr.roots[1].root, merkle::leaf(b"!")); + assert!(mmr.verify_proof(b"!", &proof)); + + let proof = mmr.push(b"!"); + + assert_eq!(mmr.roots.len(), 1); + assert_eq!(mmr.roots[0].height, 3); + assert_eq!( + mmr.roots[0].root, + merkle::node( + merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world")), + merkle::node(merkle::leaf(b"!"), merkle::leaf(b"!")) + ) + ); + assert!(mmr.verify_proof(b"!", &proof)); + } +} diff --git a/emmarin/atomic_asset_transfer/executor/Cargo.toml b/emmarin/atomic_asset_transfer/executor/Cargo.toml new file mode 100644 index 00000000..ce38b7af --- /dev/null +++ b/emmarin/atomic_asset_transfer/executor/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "executor" +version = "0.1.0" +edition = "2021" +default-run = "executor" + +[dependencies] +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"] } +serde = "1.0" +blake2 = "0.10" +bincode = "1" +common = { path = "../common" } +tempfile = "3" +clap = { version = "4", features = ["derive"] } +rand = "0.8.5" +rand_core = "0.6.0" +cl = { path = "../../cl/cl" } +ledger = { path = "../../cl/ledger" } +ledger_proof_statements = { path = "../../cl/ledger_proof_statements" } +goas_proof_statements = { path = "../proof_statements" } \ No newline at end of file diff --git a/emmarin/atomic_asset_transfer/executor/profile.pb b/emmarin/atomic_asset_transfer/executor/profile.pb new file mode 100644 index 00000000..2cd573b1 --- /dev/null +++ b/emmarin/atomic_asset_transfer/executor/profile.pb @@ -0,0 +1,1066 @@ + + + + + +C +  + +  +  +4 + + + + + + + !"g + +#$%&'T +()*+, +-.. +/0a +#1$%&' +234567 +89:;l + +.. +2567 +;C +<=> +?@A +B +CDEFM +GHIX +JKL +W +MN  +OPQ +RSTU" + +VWXYZC +[\]^_` +abcdefg +hijklmno> + pqrstuv. +stuv +wxyz{|P + }~  +stuv +  + +  +  + +  +T + +' + +#'T + +C +  +27 +) + + + + + tuv + +> +# + +tuv +4 + + +#' + + +27 + +1 + + + + + +l + +#'T +s + +27 + + + +#' + + + +@ + + +#' + + +l + + +m + + +#' + + +#' +tuv + + + + +#' + + + +#' + + + + +#' + + + +#' +B +  + +#' + + + +#' +l + +$ +. +#'  + + + + +#' +u + + + + +#' + + +#' + + +27 + +h + + + + + + + + +0 + + + +e +4! +# + +2" + +! + +L + + tuv + + + +h + + + +< + + + +2 + +H + + + + + + + + + + +! + + + +p +  +! + +4 +N + + + + + + +! +`" +# + + " +" +,$ + +! +P + + +7 +S" + + +a +W +z" + + +5 + + +e! +" + + +? +  +`" + +! +" +! + + + +p +4 + + +  +k + + +  +4 + +C + +L + + +! + +2" + + + +e! +# + +4 +0 + + + + + + + + + +Z + + + +  +( + + + + +H +6 +% +: +tuv + + + +#' +6 +  + + + +C + +#' + +' + +#'T + +#' + +27 + +6 + +#' + +  +  + t + tuv\ +# ' +  +   +  +  + # + V +  + tuv +  + ^ +   +  +  " +  + + + + tuv +2 + + + + +7 +  + + + + + + + + + + + + + + + + + + + + + + + + + + +3 + + + :" + + + + + + + + + + + + + + + + + + 4# + + + ,% + + + F$ + + + + + + + + + + + + + + + +& + + + + ( +" + + + ! + + + # + + + # + + + + + + + + + + + + + +" + + + $ + + +  + + 4 +  + + + + + + + + + + + +U + + + + + + + + + + + + + + + + + + + + + + + + + + +" + + $ + +  + + + + + + + + + + + + + +! + ,# + F" + + + + + + + + + + + + + + +$ + + & + +  +  +  + + + + + + + + + + + + + +" + + $ + +  +  +  + # + + + + + + + + + + + + + +  + + tuv + + + + + + + + + +  + " + + + + + + + + + + + + + +$ + + & + + ! + B# + i +  +  + + + + + + + + + + + + tuv. + + + + + + + +  +  +  + + + + + + + + + + + + + + !" + + 4 +  +  +  + + + + + + + + + + + + + + + + + + + + + + + + + + + !" + + 4 +  + X +  +   + + + + + + + + + + + + + +  + + # +   + , + F + c + ( +  + # +  +  + + + + + + + + + + + + + +  + +  +  +  + tuv + H + + + + + + + + + + + + +  + +  + B + i +  + + + + + + + + + +U + + + + + + + + + + + +  + tuv + + + + + + + +  +  + B + i + + + + + + + + + + + + + + " + +  +  + # +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + " + +  +  + , + F +  + + + + + + + + + + + + + +  + +  + + + + + + + + + + + + + +  + +  +  +  +# 'T + ' +  +   +  + +  +  + +# ' +  +  +  +# ' +  +  + C +  +# ' +  +  +  +# ' + j +  +# ' + +  +  +  + Ϗ (8HP" "" ؂"" ؂"" "" "" "" "" ""  ""  +ق""  ق""  ""  "" "" "" "" "" "" "" "" "" " " "" "" " " " +" " " "" "" " " " "  " " !"" """ #" " $" " %" " &"" '"" (" " )" " *" " +"" ,"" -" " ." " /" " 0" " 1" " 2"" 3" " 4" " 5" " 6"" 7"" 8" " 9" " :" " ;"" <"" ="" >"" ?"" @"" A"" B"" C"" D"" E"" F"" G"" H"" I"" JȐ"" KȐ"" LȐ"" M"" N"" O"" P"" Q"" R贌"" S贌"" T贌"" U贌"" Vı"" Wı"" Xı"" Yı"" Zı"" ["" \"" ]"" ^"" _"" `"" aЂ"" bЂ"" cЂ"" dЂ"" eЂ"" fЂ"" gЂ"" hό"" iό"" jό"" kό"" lό"" mό"" nό"" oό"" p"" q"" r"" s"" t"" u"" v"" wԓ"" xԓ"" yԓ"" zԓ"" {ԓ"" |ԓ"" }"" ~"" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """"""""""""""""!"" """"""""""""""""!"" """""""""""""""""!"" """""""""""""""""""!"" """""""""""""" "ӂ"#"ӂ""ӂ""ӂ""ӂ""ӂ""ӂ""ӂ"""$""#""""""""""""""""#"""""""""""""˃"%"˃""˃""˃""˃""˃""˃""˃"""&"""""""""""""""Ϗ"'"Ϗ"&"Ϗ""Ϗ""Ϗ""Ϗ""Ϗ""Ϗ""Ϗ""Ϗ"("Ϗ"'"Ϗ"&"Ϗ""Ϗ""Ϗ""Ϗ""Ϗ""Ϗ""Ϗ"""&""&"""""""ڃ")"ڃ"&"ڃ""ڃ""ڃ""ڃ""ڃ""ڃ""ڃ"""*""&""""""""""""""""+""*""&"""""""""""""""",""+""*""&"""""""""""""""",""+""*""*""-""*""&""""""""""""""""-""*""&""-""*""&""-""-""*""&""&"豂"."豂""豂""豂""豂""豂""豂""豂"""/"".""""""""""""""""0""/""."""""""""""""""ܗ"1"ܗ"0"ܗ"/"ܗ"."ܗ""ܗ""ܗ""ܗ""ܗ""ܗ""ܗ"""2""1""0""/"".""""""""""""""""2""1""0""/"".""2""1""0""/"".""2""1""0""/"".""2""1""0""/""."П"3"П""П""П""П""П""П""П"""3""3""3""3""4""3""""""""""""""""4""3"Ϗ"5"Ϗ"4"Ϗ"3"Ϗ""Ϗ""Ϗ""Ϗ""Ϗ""Ϗ""Ϗ""ˏ"6"ˏ"5"ˏ"4"ˏ"3"ˏ""ˏ""ˏ""ˏ""ˏ""ˏ""ˏ"""4""4""4"ԧ"7"ԧ"3"ԧ""ԧ""ԧ""ԧ""ԧ""ԧ""ԧ"""7""7""8""7""3""""""""""""""""8""8""8""7""8""7""7""3""9"""""""""""""""䌅":"䌅"9"䌅""䌅""䌅""䌅""䌅""䌅""䌅""":""9"":""9"":""9"":""9"τ";"τ"9"τ""τ""τ""τ""τ""τ""τ""";"";"";""9"";"ȡ"!"ȡ";"ȡ"9"ȡ""ȡ""ȡ""ȡ""ȡ""ȡ""ȡ""";""!""9""""""""""""""""<"""""""""""""""䌅"<""<""<""<""<""<"ȡ"<"̓"="̓""̓""̓""̓""̓""̓""̓"""=""="">""="""""""""""""""">"҃"?"҃"="҃""҃""҃""҃""҃""҃""҃""օ"@"օ"?"օ"="օ""օ""օ""օ""օ""օ""օ"""A""@""?""=""""""""""""""""B""@""?""="""""""""""""""Ԉ"C"Ԉ"@"Ԉ"?"Ԉ"="Ԉ""Ԉ""Ԉ""Ԉ""Ԉ""Ԉ""Ԉ"""D""C""@""?""=""""""""""""""""D""C""C"ˇ"E"ˇ"C"ˇ"@"ˇ"?"ˇ"="ˇ""ˇ""ˇ""ˇ""ˇ""ˇ""ˇ"""E""E""F""E""C""@""?""=""""""""""""""""F""E""C""@""?""G""F""E""C""@""?""=""""""""""""""""A""G""F""E""C""@""?""=""""""""""""""""H""E""C""@""?""=""""""""""""""""H"̆"I"̆"@"̆"?"̆"="̆""̆""̆""̆""̆""̆""̆"""="С"J"С"="С""С""С""С""С""С""С""݅"K"݅"J"݅"="݅""݅""݅""݅""݅""݅""݅"""K""J""K""J""K""J""K""J""K""J""K""J""L""K""J""=""""""""""""""""L""K""J""L""M""J""=""""""""""""""""N""M""J""=""""""""""""""""O""J""=""""""""""""""""O"Ϗ"J"Ϗ"="Ϗ"J"Ϗ"=""P""J""="""""""""""""""݈"Q"݈"P"݈"J"݈"="݈""݈""݈""݈""݈""݈""݈""ۆ"R"ۆ"Q"ۆ"P"ۆ"J"ۆ"="ۆ""ۆ""ۆ""ۆ""ۆ""ۆ""ۆ"""R""Q""P""R""Q""P""R""Q""P"dž"S"dž"R"dž"Q"dž"P"dž"J"dž"="dž""dž""dž""dž""dž""dž""dž""͆"T"͆"Q"͆"P"͆"J"͆"="͆""͆""͆""͆""͆""͆""͆"""T""T""T""T""Q""P""P""J""=""P""P""U""J""="""""""""""""""Ȭ"V"Ȭ"U"Ȭ"J"Ȭ"="Ȭ""Ȭ""Ȭ""Ȭ""Ȭ""Ȭ""Ȭ"""W""V""U""J""="""""""""""""""ц"X"ц"W"ц"V"ц"U"ц"J"ц"="ц""ц""ц""ц""ц""ц""ц"""X""W""V""U""X""W""V""U""J""X""W""V""U""Y""W""V""U""J""=""""""""""""""""Y""Y""Y""W""V""U""Z""V""U""J""=""""""""""""""""Z""Z""Z""[""V""U""J""=""""""""""""""""[""V""U""["̒"\"̒"V"̒"U"̒"J"̒"="̒""̒""̒""̒""̒""̒""̒"""\""\""\""]""V""U""J""=""""""""""""""""]""]""]"ވ"^"ވ"J"ވ"="ވ""ވ""ވ""ވ""ވ""ވ""ވ""͆"^""^""^""^""^""_""^""J""=""""""""""""""""_""_""_""^"օ"J"̆"J"Ԉ"J"ˇ"J""`""J""="""""""""""""""ц"`""`""`""`""`""`""`""`"輄"a"輄"J"輄"="輄""輄""輄""輄""輄""輄""輄"""a""b""=""""""""""""""""b""=""c"",""b""=""""""""""""""""c"",""b""b"؅"d"؅""؅""؅""؅""؅""؅""؅"""d""d""d""d"ބ"e"ބ""ބ""ބ""ބ""ބ""ބ""ބ""ȡ"e""e""e""e""e""e"蘃"f"蘃""蘃""蘃""蘃""蘃""蘃""蘃"""f""f""f""f"ூ"g" ூ"" ூ"" ூ"" ூ"" ூ"" ூ"" ூ"" Ȧ"h" Ȧ"g" Ȧ"" Ȧ"" Ȧ"" Ȧ"" Ȧ"" Ȧ"" Ȧ"" "i" "h" "g" "" "" "" "" "" "" "" "i" "h" "g" "i" "h" "g" "h" "g" 쓃"j" 쓃"h" 쓃"g" 쓃"" 쓃"" 쓃"" 쓃"" 쓃"" 쓃"" 쓃"" "k" "" "" "" "" "" "" "" "l" "k" "" "" "" "" "" "" "" 䒃"m" 䒃"l" 䒃"k" 䒃"" 䒃"" 䒃"" 䒃"" 䒃"" 䒃"" 䒃"" "n" "k" "" "" "" "" "" "" "" "n" "k" "n" "k" Á"o" Á"n" Á"k" Á"" Á"" Á"" Á"" Á"" Á"" Á"" ΂"p" ΂"o" ΂"n" ΂"k" ΂"" ΂"" ΂"" ΂"" ΂"" ΂"" ΂"" "q" "p" "o" "n" "k" "" "" "" "" "" "" "" "q" "p" "o" "q" "p" "o" "n" 䒃"q" 䒃"p" 䒃"o" 䒃"n" +"q" +"p" +"o" +"q" +"p" +"o" +"n" +"k" +Ƃ"r" +Ƃ"p" +Ƃ"o" +Ƃ"n" +Ƃ"k" +Ƃ"" +Ƃ"" +Ƃ"" +Ƃ"" +Ƃ"" +Ƃ"" +Ƃ"" +س"s" +س"r" +س"p" +س"o" +س"n" +س"k" +س"" +س"" +س"" +س"" +س"" +س"" +س"" +"s" +"r" +ƀ"k" +ƀ"s" +ƀ"r" +ƀ"p" +ƀ"o" +ƀ"n" +ƀ"" +ƀ"" +ƀ"" +ƀ"" +ƀ"" +ƀ"" +ƀ"" +"s" +"r" +䒃"s" +䒃"r" +"t" +"k" +"s" +"r" +"p" +"o" +"n" +"" +"" +"" +"" +"" +"" +"" +"t" +䒃"t" +݀"k" +݀"o" +݀"n" +݀"" +݀"" +݀"" +݀"" +݀"" +݀"" +݀"" +̀"k" +̀"o" +̀"n" +̀"" +̀"" +̀"" +̀"" +̀"" +̀"" +̀"" +"k" +"o" +"n" +"" +"" +"" +"" +"" +"" +"" +"u" +"k" +"o" +"n" +"" +"" +"" +"" +"" +"" +"" +"u" +䒃"u" +"n" +"k" +"o" +"" +"" +"" +"" +"" +"" +"" +"v" +"n" +"k" +"o" +"" +"" +"" "" "" "" "" "v" "v" "v" 䒃"v" ـ"k" ـ"" ـ"" ـ"" ـ"" ـ"" ـ"" ـ"" Ē"k" Ē"" Ē"" Ē"" Ē"" Ē"" Ē"" Ē"" "k" "" "" "" "" "" "" "" "n" "k" "" "" "" "" "" "" "" "k" "" "" "" "" "" "" "" "w" "k" "" "" "" "" "" "" "" "x" "w" "k" "" "" "" "" "" "" "" "x" "w" "w" "w" "w" 䒃"w" Դ"y" Դ"" Դ"" Դ"" Դ"" Դ"" Դ"" Դ"" "y" "y" "y" „"z" „"" „"" „"" „"" „"" „"" „"" "{" "z" "" "" "" "" "" "" "" "{" "z" ȡ"{" ȡ"z" "{" "z" "{" "z" "{" "z" „"|" „"" „"" „"" „"" „"" „"" „"" ȡ"|" "|" "|" "|" "|" "|" "}" "" "" "" "" "~" "}" "" "" "" "" ό"" ό"~" ό"}" ό"" ό"" ό"" ό"" ȇ"}" ȇ"" ȇ"~" ȇ"" ȇ"" ȇ"" ȇ"* * * * * * * + * *  * + *  *  *  * * * * * * * * * * * * * * * * *! *" * # *!$ *"% *#& *$' *%( *&) *'* *(+ *), **- *+. *,/ *-0 *.1 */2 *03 *14 *25 *36 *47 *58 *69 *7: *8; *9< *:= *;> *<? *=@ *>A *?B *@C *AD *BE *CF *DG *EH *FI *GJ *HK *IL *JM *KN *LO *MP *NQ *OR *PS *QT *RU *SV *TW *UX *VY *WZ *X[ *Y\ *Z] *[^ *\_ *]` *^a *_b *`c *ad *be *cf *dg *eh *fi *gj *hk *il *jm *kn *lo *mp *nq *or *ps *qt *ru *sv *tw *ux *vy *wz *x{ *y| *z} *{~ *| *} *~ * 22cycles2count2unknown2__start2risc0_zkvm::guest::env::init2memset2sys_rand2 risc0_zkvm::guest::env::finalize2K>::try_from2memcpy2G>::as_ref2!risc0_binfmt::hash::tagged_struct2__rust_alloc_zeroed2[::hash_bytes2sys_sha_buffer22risc0_zkp::core::hash::sha::guest::copy_and_update2__rust_dealloc21risc0_zkp::core::hash::sha::Block::as_half_blocks2Y::compress2sys_sha_compress2U as risc0_binfmt::hash::Digestible>::digest2sys_halt2main2std::rt::lang_start2std::rt::lang_start_internal2 std::rt::init2%std::thread::local::LocalKey::with22std::sys_common::thread_info::THREAD_INFO::__getit2:std::sys::pal::common::thread_local::os_local::Key::get2E<[u8] as alloc::ffi::c_str::CString::new::SpecNewImpl>::spec_new_impl2-alloc::sync::arcinner_layout_for_value_layout2 std::rt::lang_start::{{closure}}28std::sys_common::backtrace::__rust_begin_short_backtrace2zone_state::main2!cl::output::OutputWitness::public2} as digest::core_api::FixedOutputCore>::finalize_fixed_core2sha2::sha256::compress2562P as core::ops::drop::Drop>::drop2alloc::collections::btree::navigate::,alloc::collections::btree::node::marker::Edge>>::deallocating_next_unchecked2#common::StateWitness::total_balance2common::StateWitness::apply2memcmp2compiler_builtins::mem::memcmp2>alloc::collections::btree::map::entry::Entry::or_insert2common::mmr::MMR::push2-alloc::raw_vec::RawVec::reserve_for_push2alloc::raw_vec::finish_grow2cl::merkle::leaf2'once_cell::imp::OnceCell::initialize2"once_cell::imp::initialize_or_wait24once_cell::imp::OnceCell::initialize::{{closure}}2&core::ops::function::FnOnce::call_once2cl::note::derive_unit2common::StateWitness::commit2common::mmr::MMR::commit2memmove2compiler_builtins::mem::memmove2!common::StateWitness::state_roots2#common::StateWitness::balances_root21cl::partial_tx::PartialTxInputWitness::input_root2cl::merkle::path_root2cl::input::InputWitness::commit23cl::partial_tx::PartialTxOutputWitness::output_root2(common::SignedBoundTx::verify_and_unwrap2:>::from2common::Tx::verifying_key29curve25519_dalek::edwards::CompressedEdwardsY::decompress2subtle::black_box2 +sys_bigint2-curve25519_dalek::edwards::decompress::step_12>::add_assign2mcurve25519_dalek::field::::sqrt_ratio_i2Icurve25519_dalek::backend::serial::risc0::field::FieldElementR0::as_bytes2qcrypto_bigint::ct_choice:: for subtle::Choice>::from2icurve25519_dalek::field::::pow225012U as subtle::ConditionallySelectable>::conditional_select25ed25519_dalek::verifying::VerifyingKey::verify_strict21curve25519_dalek::edwards::EdwardsPoint::compress2gcurve25519_dalek::field::::invert2sha2::sha512::compress5122sha2::sha512::soft::compress2G::neg2+curve25519_dalek::scalar::Scalar::from_hash2;curve25519_dalek::scalar::Scalar::from_bytes_mod_order_wide2Kcurve25519_dalek::backend::serial::risc0::scalar::ScalarR0::from_bytes_wide2Jcrypto_bigint::uint::add_mod::>::add_mod2acurve25519_dalek::scalar::::pack2Lcurve25519_dalek::edwards::EdwardsPoint::vartime_double_scalar_mul_basepoint22curve25519_dalek::backend::vartime_double_base_mul2 as core::convert::From>::from2Hcurve25519_dalek::backend::serial::curve_models::ProjectivePoint::double2curve25519_dalek::backend::serial::curve_models:: for curve25519_dalek::edwards::EdwardsPoint>::add2curve25519_dalek::backend::serial::curve_models:: for curve25519_dalek::edwards::EdwardsPoint>::add23curve25519_dalek::scalar::Scalar::non_adjacent_form2curve25519_dalek::backend::serial::curve_models:: for curve25519_dalek::edwards::EdwardsPoint>::sub2curve25519_dalek::backend::serial::curve_models:: for curve25519_dalek::edwards::EdwardsPoint>::sub26curve25519_dalek::scalar::Scalar::from_canonical_bytes2Bcurve25519_dalek::backend::serial::risc0::scalar::ScalarR0::reduce27curve25519_dalek::edwards::EdwardsPoint::is_small_order2Ied25519:: for [u8; _]>::from2;alloc::raw_vec::RawVec::reserve::do_reserve_and_handle2__rust_realloc2cl::merkle::node2(cl::input::InputWitness::note_commitment2common::ZoneMetadata::id2M as risc0_zkvm::guest::env::Write>::write2^ as risc0_zkvm::serde::serializer::WordWrite>::write_words2_::compress_slice2 sys_write2e as serde::de::Deserializer>::deserialize_struct2[::read_words2sys_read_words2>::deserialize::VecVisitor as serde::de::Visitor>::visit_seq2Z as serde::de::SeqAccess>::next_element_seed2Ocommon::_::::deserialize2B as serde::de::Visitor>::visit_seq2Icommon::_::::deserialize2s::deserialize::__Visitor as serde::de::Visitor>::visit_enum2d as serde::de::Deserializer>::deserialize_tuple2m as serde::de::Deserializer>::deserialize_newtype_struct2::deserialize::__Visitor as serde::de::Visitor>::visit_enum2b as serde::de::Deserializer>::deserialize_map27alloc::collections::btree::map::BTreeMap::insert2&cl::nullifier::NullifierSecret::commit2&cl::input::InputWitness::evolved_nonce2%cl::nullifier::NullifierNonce::evolve2"cl::input::InputWitness::nullifier2%std::sys::pal::zkvm::once::Once::call2std::io::stdio::cleanup2-std::sync::once_lock::OnceLock::initialize \ No newline at end of file diff --git a/emmarin/atomic_asset_transfer/executor/src/lib.rs b/emmarin/atomic_asset_transfer/executor/src/lib.rs new file mode 100644 index 00000000..50ea367d --- /dev/null +++ b/emmarin/atomic_asset_transfer/executor/src/lib.rs @@ -0,0 +1,383 @@ +use std::collections::BTreeMap; + +use cl::Constraint; +use common::{ + mmr::MMR, AccountId, IncludedTxWitness, SignedBoundTx, StateWitness, Tx, ZoneMetadata, +}; +use goas_proof_statements::{ + user_note::UserAtomicTransfer, zone_funds::SpendFundsPrivate, zone_state::ZoneStatePrivate, +}; +use rand_core::CryptoRngCore; + +#[derive(Debug, Clone)] +pub struct ZoneNotes { + pub state: StateWitness, + pub state_note: cl::OutputWitness, + pub fund_note: cl::OutputWitness, +} + +impl ZoneNotes { + pub fn new_with_balances( + zone_name: &str, + balances: BTreeMap, + mut rng: impl CryptoRngCore, + ) -> Self { + let state = StateWitness { + balances, + ticket_auction: Default::default(), + included_txs: MMR::new(), + zone_metadata: zone_metadata(zone_name), + }; + let state_note = zone_state_utxo(&state, &mut rng); + let fund_note = zone_fund_utxo(state.total_balance(), state.zone_metadata, &mut rng); + Self { + state, + state_note, + fund_note, + } + } + + pub fn state_input_witness(&self) -> cl::InputWitness { + cl::InputWitness::public(self.state_note) + } + + pub fn fund_input_witness(&self) -> cl::InputWitness { + cl::InputWitness::public(self.fund_note) + } + + pub fn run(mut self, block_height: u64, tx: Tx) -> (Self, IncludedTxWitness) { + let (new_state, included_tx) = self.state.apply(block_height, tx); + self.state = new_state; + + let state_in = self.state_input_witness(); + self.state_note = cl::OutputWitness::public(cl::NoteWitness { + state: self.state.commit().0, + nonce: state_in.evolved_nonce(b"STATE_NONCE"), + ..state_in.note + }); + + let fund_in = self.fund_input_witness(); + self.fund_note = cl::OutputWitness::public(cl::NoteWitness { + value: self.state.total_balance(), + nonce: state_in.evolved_nonce(b"FUND_NONCE"), + ..fund_in.note + }); + + (self, included_tx) + } +} + +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, + constraint: zone_meta.funds_constraint, + state: zone_meta.id(), + nonce: cl::Nonce::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, + constraint: zone.zone_metadata.zone_constraint, + state: zone.commit().0, + nonce: cl::Nonce::random(&mut rng), + }) +} + +pub fn user_atomic_transfer_constraint() -> Constraint { + ledger::constraint::risc0_constraint(goas_risc0_proofs::USER_ATOMIC_TRANSFER_ID) +} + +pub fn zone_state_constraint() -> Constraint { + ledger::constraint::risc0_constraint(goas_risc0_proofs::ZONE_STATE_ID) +} + +pub fn zone_fund_constraint() -> Constraint { + ledger::constraint::risc0_constraint(goas_risc0_proofs::SPEND_ZONE_FUNDS_ID) +} + +pub fn zone_metadata(zone_mnemonic: &str) -> ZoneMetadata { + ZoneMetadata { + zone_constraint: zone_state_constraint(), + funds_constraint: zone_fund_constraint(), + unit: cl::note::derive_unit(zone_mnemonic), + } +} + +pub fn prove_zone_stf( + state: StateWitness, + block_height: u64, + inputs: Vec<(SignedBoundTx, cl::PartialTxInputWitness)>, + zone_in: cl::PartialTxInputWitness, + zone_out: cl::PartialTxOutputWitness, + funds_out: cl::PartialTxOutputWitness, +) -> ledger::ConstraintProof { + let private_inputs = ZoneStatePrivate { + state, + block_height, + inputs, + zone_in, + zone_out, + funds_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?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + let receipt = prove_info.receipt; + ledger::ConstraintProof::from_risc0(goas_risc0_proofs::ZONE_STATE_ID, receipt) +} + +pub fn prove_zone_fund_constraint( + in_zone_funds: cl::PartialTxInputWitness, + zone_note: cl::PartialTxOutputWitness, + out_zone_state: &StateWitness, + block_height: u64, +) -> ledger::ConstraintProof { + let private_inputs = SpendFundsPrivate { + in_zone_funds, + zone_note, + state_roots: out_zone_state.state_roots(), + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&private_inputs) + .unwrap() + .write(&block_height) + .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?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + let receipt = prove_info.receipt; + ledger::ConstraintProof::from_risc0(goas_risc0_proofs::SPEND_ZONE_FUNDS_ID, receipt) +} + +pub fn prove_user_atomic_transfer( + atomic_transfer: UserAtomicTransfer, + block_height: u64, +) -> ledger::ConstraintProof { + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&atomic_transfer) + .unwrap() + .write(&block_height) + .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::USER_ATOMIC_TRANSFER_ELF, &opts) + .unwrap(); + println!( + "STARK 'user atomic transfer' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + let receipt = prove_info.receipt; + ledger::ConstraintProof::from_risc0(goas_risc0_proofs::USER_ATOMIC_TRANSFER_ID, receipt) +} + +#[cfg(test)] +mod tests { + use cl::{ + note::derive_unit, BalanceWitness, Nonce, NoteWitness, OutputWitness, PartialTxWitness, + }; + use common::{BoundTx, Deposit, Withdraw}; + use goas_proof_statements::user_note::UserIntent; + use ledger::ConstraintProof; + use ledger_proof_statements::constraint::ConstraintPublic; + + use super::*; + + #[test] + pub fn test_prove_zone_stf() { + let mut rng = rand::thread_rng(); + + let mut alice = common::new_account(&mut rng); + let alice_vk = alice.verifying_key().to_bytes(); + + let zone_start = + ZoneNotes::new_with_balances("ZONE", BTreeMap::from_iter([(alice_vk, 32)]), &mut rng); + + let bind = OutputWitness::public(NoteWitness::basic( + 32, + *common::ZONE_CL_FUNDS_UNIT, + &mut rng, + )); + + let signed_withdraw = SignedBoundTx::sign( + BoundTx { + tx: Tx::Withdraw(Withdraw { + from: alice_vk, + amount: 10, + }), + bind: bind.commit_note(), + }, + &mut alice, + ); + + let block_height = 0; + + let zone_end = zone_start + .clone() + .run(block_height, signed_withdraw.bound_tx.tx) + .0; + + let ptx = PartialTxWitness { + inputs: vec![ + cl::InputWitness::public(bind), + zone_start.state_input_witness(), + zone_start.fund_input_witness(), + ], + outputs: vec![zone_end.state_note, zone_end.fund_note], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let txs = vec![(signed_withdraw, ptx.input_witness(0))]; + + let proof = prove_zone_stf( + zone_start.state.clone(), + block_height, + txs, + ptx.input_witness(1), + ptx.output_witness(0), + ptx.output_witness(1), + ); + + assert!(proof.verify(ConstraintPublic { + nf: zone_start.state_input_witness().nullifier(), + ptx_root: ptx.commit().root(), + block_height: 0, + })) + } + + #[test] + fn test_prove_zone_fund_constraint() { + let mut rng = rand::thread_rng(); + let zone = ZoneNotes::new_with_balances("ZONE", BTreeMap::from_iter([]), &mut rng); + + let ptx = PartialTxWitness { + inputs: vec![zone.fund_input_witness()], + outputs: vec![zone.state_note], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let block_height = 0; + + let proof = prove_zone_fund_constraint( + ptx.input_witness(0), + ptx.output_witness(0), + &zone.state, + block_height, + ); + + assert!(proof.verify(ConstraintPublic { + nf: zone.fund_input_witness().nullifier(), + ptx_root: ptx.commit().root(), + block_height, + })) + } + + #[test] + fn test_prove_user_atomic_transfer() { + let mut rng = rand::thread_rng(); + let block_height = 0; + + let alice = common::new_account(&mut rng); + let alice_vk = alice.verifying_key().to_bytes(); + + let zone_a = + ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([(alice_vk, 40)]), &mut rng); + let zone_b = ZoneNotes::new_with_balances("ZONE_B", BTreeMap::new(), &mut rng); + + let user_intent = UserIntent { + zone_a_meta: zone_a.state.zone_metadata, + zone_b_meta: zone_b.state.zone_metadata, + withdraw: Withdraw { + from: alice_vk, + amount: 32, + }, + deposit: Deposit { + to: alice_vk, + amount: 32, + }, + }; + let user_note = cl::InputWitness::public(cl::OutputWitness::public(NoteWitness { + value: 1, + unit: derive_unit("INTENT"), + constraint: ConstraintProof::nop_constraint(), + state: user_intent.commit(), + nonce: Nonce::random(&mut rng), + })); + + let (zone_a, withdraw_included_witnesss) = + zone_a.run(block_height, Tx::Withdraw(user_intent.withdraw)); + let (zone_b, deposit_included_witnesss) = + zone_b.run(block_height, Tx::Deposit(user_intent.deposit)); + + let ptx = PartialTxWitness { + inputs: vec![user_note], + outputs: vec![zone_a.state_note, zone_b.state_note], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let user_atomic_transfer = UserAtomicTransfer { + user_note: ptx.input_witness(0), + user_intent, + zone_a: ptx.output_witness(0), + zone_b: ptx.output_witness(1), + zone_a_roots: zone_a.state.state_roots(), + zone_b_roots: zone_b.state.state_roots(), + withdraw_tx: withdraw_included_witnesss, + deposit_tx: deposit_included_witnesss, + }; + + let proof = prove_user_atomic_transfer(user_atomic_transfer, block_height); + + assert!(proof.verify(ConstraintPublic { + nf: user_note.nullifier(), + ptx_root: ptx.commit().root(), + block_height, + })) + } +} diff --git a/emmarin/atomic_asset_transfer/executor/src/main.rs b/emmarin/atomic_asset_transfer/executor/src/main.rs new file mode 100644 index 00000000..3beebf4f --- /dev/null +++ b/emmarin/atomic_asset_transfer/executor/src/main.rs @@ -0,0 +1,69 @@ +// These constants represent the RISC-V ELF and the image ID generated by risc0-build. +// The ELF is used for proving and the ID is used for verification. +use common::*; +use risc0_zkvm::{default_prover, ExecutorEnv}; +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +enum Action { + Stf { + // path to bincode-encoded state witness + state: PathBuf, + // path to bincode-encoded inputs + inputs: PathBuf, + }, +} + +fn stf_prove_stark(state: StateWitness, inputs: Vec) { + let env = ExecutorEnv::builder() + .write(&inputs) + .unwrap() + .write(&state) + .unwrap() + .build() + .unwrap(); + + // Obtain the default prover. + let prover = default_prover(); + + use std::time::Instant; + let start_t = Instant::now(); + + // Proof information by proving the specified ELF binary. + // 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, goas_risc0_proofs::ZONE_STATE_ELF, &opts) + .unwrap(); + + println!("STARK prover time: {:.2?}", start_t.elapsed()); + // extract the receipt. + let receipt = prove_info.receipt; + + // TODO: Implement code for retrieving receipt journal here. + + 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(goas_risc0_proofs::ZONE_STATE_ID).unwrap(); +} + +fn main() { + // Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run` + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) + .init(); + + let action = Action::parse(); + + match action { + Action::Stf { state, inputs } => { + let state = bincode::deserialize(&std::fs::read(state).unwrap()).unwrap(); + let inputs = bincode::deserialize(&std::fs::read(inputs).unwrap()).unwrap(); + stf_prove_stark(state, inputs); + } + } +} diff --git a/emmarin/atomic_asset_transfer/executor/tests/atomic_transfer.rs b/emmarin/atomic_asset_transfer/executor/tests/atomic_transfer.rs new file mode 100644 index 00000000..7abd8788 --- /dev/null +++ b/emmarin/atomic_asset_transfer/executor/tests/atomic_transfer.rs @@ -0,0 +1,178 @@ +use std::collections::BTreeMap; + +use cl::{BalanceWitness, BundleWitness, Nonce, NoteWitness}; +use common::{new_account, BoundTx, Deposit, SignedBoundTx, Tx, Withdraw}; +use executor::ZoneNotes; +use goas_proof_statements::user_note::{UserAtomicTransfer, UserIntent}; + +#[test] +fn test_atomic_transfer() { + let mut rng = rand::thread_rng(); + let block_height = 0; + + let mut alice = new_account(&mut rng); + let alice_vk = alice.verifying_key().to_bytes(); + + let zone_a_start = + ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([(alice_vk, 100)]), &mut rng); + + let zone_b_start = ZoneNotes::new_with_balances("ZONE_B", BTreeMap::from_iter([]), &mut rng); + + let alice_intent = UserIntent { + zone_a_meta: zone_a_start.state.zone_metadata, + zone_b_meta: zone_b_start.state.zone_metadata, + withdraw: Withdraw { + from: alice_vk, + amount: 75, + }, + deposit: Deposit { + to: alice_vk, + amount: 75, + }, + }; + + let alice_intent_out = cl::OutputWitness::public(NoteWitness { + value: 1, + unit: cl::note::derive_unit("INTENT"), + constraint: executor::user_atomic_transfer_constraint(), + state: alice_intent.commit(), + nonce: Nonce::random(&mut rng), + }); + + let user_ptx = cl::PartialTxWitness { + inputs: vec![], + outputs: vec![alice_intent_out], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let (zone_a_end, withdraw_inclusion) = zone_a_start + .clone() + .run(block_height, Tx::Withdraw(alice_intent.withdraw)); + + let (zone_b_end, deposit_inclusion) = zone_b_start + .clone() + .run(block_height, Tx::Deposit(alice_intent.deposit)); + + let alice_intent_in = cl::InputWitness::public(alice_intent_out); + let atomic_transfer_ptx = cl::PartialTxWitness { + inputs: vec![ + alice_intent_in, + zone_a_start.state_input_witness(), + zone_a_start.fund_input_witness(), + zone_b_start.state_input_witness(), + zone_b_start.fund_input_witness(), + ], + outputs: vec![ + zone_a_end.state_note, + zone_a_end.fund_note, + zone_b_end.state_note, + zone_b_end.fund_note, + ], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let signed_withdraw = SignedBoundTx::sign( + BoundTx { + tx: Tx::Withdraw(alice_intent.withdraw), + bind: alice_intent_in.note_commitment(), + }, + &mut alice, + ); + let signed_deposit = SignedBoundTx::sign( + BoundTx { + tx: Tx::Deposit(alice_intent.deposit), + bind: alice_intent_in.note_commitment(), + }, + &mut alice, + ); + + let constraint_proofs = BTreeMap::from_iter([ + ( + alice_intent_in.nullifier(), + executor::prove_user_atomic_transfer( + UserAtomicTransfer { + user_note: atomic_transfer_ptx.input_witness(0), + user_intent: alice_intent, + zone_a: atomic_transfer_ptx.output_witness(0), + zone_b: atomic_transfer_ptx.output_witness(2), + zone_a_roots: zone_a_end.state.state_roots(), + zone_b_roots: zone_b_end.state.state_roots(), + withdraw_tx: withdraw_inclusion, + deposit_tx: deposit_inclusion, + }, + block_height, + ), + ), + ( + zone_a_start.state_input_witness().nullifier(), + executor::prove_zone_stf( + zone_a_start.state.clone(), + block_height, + vec![(signed_withdraw, atomic_transfer_ptx.input_witness(0))], // withdraw bound to input intent note + atomic_transfer_ptx.input_witness(1), // input state note + atomic_transfer_ptx.output_witness(0), // output state note + atomic_transfer_ptx.output_witness(1), // output funds note + ), + ), + ( + zone_a_start.fund_input_witness().nullifier(), + executor::prove_zone_fund_constraint( + atomic_transfer_ptx.input_witness(2), // input fund note + atomic_transfer_ptx.output_witness(0), // output state note + &zone_a_end.state, + block_height, + ), + ), + ( + zone_b_start.state_input_witness().nullifier(), + executor::prove_zone_stf( + zone_b_start.state.clone(), + block_height, + vec![(signed_deposit, atomic_transfer_ptx.input_witness(0))], // deposit bound to input intent note + atomic_transfer_ptx.input_witness(3), // input state note + atomic_transfer_ptx.output_witness(2), // output state note + atomic_transfer_ptx.output_witness(3), // output funds note + ), + ), + ( + zone_b_start.fund_input_witness().nullifier(), + executor::prove_zone_fund_constraint( + atomic_transfer_ptx.input_witness(4), // input fund note (input #1) + atomic_transfer_ptx.output_witness(2), // output state note (output #0) + &zone_b_end.state, + block_height, + ), + ), + ]); + + let user_ptx_proof = + ledger::partial_tx::ProvedPartialTx::prove(&user_ptx, BTreeMap::new(), &[]) + .expect("user ptx failed to prove"); + assert!(user_ptx_proof.verify()); + + let note_commitments = vec![ + alice_intent_out.commit_note(), + zone_a_start.state_note.commit_note(), + zone_a_start.fund_note.commit_note(), + zone_b_start.state_note.commit_note(), + zone_b_start.fund_note.commit_note(), + ]; + + let atomic_transfer_proof = ledger::partial_tx::ProvedPartialTx::prove( + &atomic_transfer_ptx, + constraint_proofs, + ¬e_commitments, + ) + .expect("atomic transfer proof failed"); + + assert!(atomic_transfer_proof.verify()); + + let bundle_witness = BundleWitness { + partials: vec![user_ptx, atomic_transfer_ptx], + }; + + let bundle_proof = + ledger::bundle::ProvedBundle::prove(&bundle_witness).expect("bundle proof failed"); + + assert!(bundle_proof.verify()); +} diff --git a/emmarin/atomic_asset_transfer/executor/tests/auction.rs b/emmarin/atomic_asset_transfer/executor/tests/auction.rs new file mode 100644 index 00000000..9dcd58f9 --- /dev/null +++ b/emmarin/atomic_asset_transfer/executor/tests/auction.rs @@ -0,0 +1,185 @@ +use std::collections::BTreeMap; + +use cl::{BalanceWitness, BundleWitness, Nonce, NoteWitness}; +use common::{ + height_to_segment, new_account, segment_to_height, Bid, BoundTx, Deposit, SignedBoundTx, Tx, + Withdraw, BID_END_OFFSET, BID_START_OFFSET, REVEAL_END_OFFSET, +}; +use executor::ZoneNotes; +use goas_proof_statements::user_note::{UserAtomicTransfer, UserIntent}; +use rand::seq::SliceRandom; + +#[test] +#[should_panic] +fn test_auction_bid_before_bidding_range() { + let mut rng = rand::thread_rng(); + + let zone_start = ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([]), &mut rng); + + let target_block = 1542; + let target_segment = height_to_segment(target_block); + + assert_eq!(target_segment, 154); + + let mut rng = rand::thread_rng(); + + let mut bob = new_account(&mut rng); + let bob_vk = bob.verifying_key().to_bytes(); + + let bob_bid = Bid { + segment: target_segment, + exec_vk: bob_vk, + fee: 500, + }; + + let tx_blind_bid = Tx::TicketBlindBid(bob_bid.commit()); + + let current_height = 100; + + let (zone_end, bid_inclusion) = zone_start.clone().run(current_height, tx_blind_bid); +} + +#[test] +fn test_auction_happy_path_single_exec() { + let mut rng = rand::thread_rng(); + + let zone_init_state = ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([]), &mut rng); + + let target_block = 1542; + let target_segment = height_to_segment(target_block); + + assert_eq!(target_segment, 154); + + let mut rng = rand::thread_rng(); + + let mut bob = new_account(&mut rng); + let bob_vk = bob.verifying_key().to_bytes(); + + let bob_bid = Bid { + segment: target_segment, + exec_vk: bob_vk, + fee: 500, + }; + + let tx_blind_bid = Tx::TicketBlindBid(bob_bid.commit()); + + let bid_height = segment_to_height(target_segment) - BID_START_OFFSET; + + let (zone_blind_bid_state, bid_inclusion) = + zone_init_state.clone().run(bid_height, tx_blind_bid); + + assert!(zone_blind_bid_state + .state + .ticket_auction + .blind_bids + .get(&target_segment) + .unwrap() + .contains(&bob_bid.commit())); + + let tx_reveal_bid = Tx::TicketRevealBid(bob_bid); + + let reveal_height = segment_to_height(target_segment) - REVEAL_END_OFFSET - 1; + + let (zone_reveal_bid_state, reveal_inclusion) = zone_blind_bid_state + .clone() + .run(reveal_height, tx_reveal_bid); + + assert!(zone_reveal_bid_state + .state + .ticket_auction + .blind_bids + .get(&target_segment) + .unwrap() + .contains(&bob_bid.commit())); + + let dutch_bid = zone_reveal_bid_state + .state + .ticket_auction + .revealed_bids + .get(&target_segment) + .unwrap(); + + assert!(dutch_bid.scd_best == bob_bid); + assert!(dutch_bid.best == bob_bid); +} + +#[test] +fn test_auction_happy_path_multi_exec() { + let mut rng = rand::thread_rng(); + + let zone_init_state = ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([]), &mut rng); + + let target_block = 1542; + let target_segment = height_to_segment(target_block); + + assert_eq!(target_segment, 154); + + let mut rng = rand::thread_rng(); + + let mut nums = Vec::from_iter(0..100); + nums.shuffle(&mut rng); + + let mut bids = Vec::from_iter(nums.iter().map(|n| { + let mut executor_sk = new_account(&mut rng); + let executor_vk = executor_sk.verifying_key().to_bytes(); + + Bid { + segment: target_segment, + exec_vk: executor_vk, + fee: *n, + } + })); + + let mut zone_curr_state = zone_init_state; + for (i, executor_bid) in bids.iter().enumerate() { + let tx_blind_bid = Tx::TicketBlindBid(executor_bid.commit()); + + let bid_height = segment_to_height(target_segment) - BID_START_OFFSET + + (i as u64 % (BID_START_OFFSET - BID_END_OFFSET)); + + (zone_curr_state, _) = zone_curr_state.clone().run(bid_height, tx_blind_bid); + } + + for bid in bids.iter() { + assert!(zone_curr_state + .state + .ticket_auction + .blind_bids + .get(&target_segment) + .unwrap() + .contains(&bid.commit())); + } + + bids.shuffle(&mut rng); + + for (i, bid) in bids.iter().enumerate() { + let tx_reveal_bid = Tx::TicketRevealBid(bid.clone()); + + let reveal_height = segment_to_height(target_segment) - BID_END_OFFSET + + (i as u64 % (BID_END_OFFSET - REVEAL_END_OFFSET)); + + (zone_curr_state, _) = zone_curr_state.clone().run(reveal_height, tx_reveal_bid); + } + + for bid in bids.iter() { + assert!(zone_curr_state + .state + .ticket_auction + .blind_bids + .get(&target_segment) + .unwrap() + .contains(&bid.commit())); + } + + let dutch_bid = zone_curr_state + .state + .ticket_auction + .revealed_bids + .get(&target_segment) + .unwrap(); + + bids.sort_by_key(|b| b.fee); + + assert_eq!(dutch_bid.scd_best, bids[bids.len() - 2]); + assert_eq!(dutch_bid.best, bids[bids.len() - 1]); +} diff --git a/emmarin/atomic_asset_transfer/executor/tests/deposit_ptx.rs b/emmarin/atomic_asset_transfer/executor/tests/deposit_ptx.rs new file mode 100644 index 00000000..143e6a44 --- /dev/null +++ b/emmarin/atomic_asset_transfer/executor/tests/deposit_ptx.rs @@ -0,0 +1,106 @@ +use std::collections::BTreeMap; + +use cl::{BalanceWitness, NoteWitness, NullifierSecret}; +use common::{mmr::MMR, new_account, BoundTx, SignedBoundTx, StateWitness, Tx, ZONE_CL_FUNDS_UNIT}; +use executor::ZoneNotes; +use ledger::constraint::ConstraintProof; + +#[test] +fn test_deposit() { + let mut rng = rand::thread_rng(); + let block_height = 0; + + let mut alice = new_account(&mut rng); + let alice_vk = alice.verifying_key().to_bytes(); + let alice_cl_sk = NullifierSecret::random(&mut rng); + + let zone_start = ZoneNotes::new_with_balances("ZONE", BTreeMap::new(), &mut rng); + + let deposit = common::Deposit { + to: alice_vk, + amount: 78, + }; + + let zone_end = zone_start.clone().run(block_height, Tx::Deposit(deposit)).0; + + let alice_deposit = cl::InputWitness::from_output( + cl::OutputWitness::new( + NoteWitness::stateless( + 78, + *ZONE_CL_FUNDS_UNIT, + ConstraintProof::nop_constraint(), // alice should demand a tx inclusion proof for the deposit + &mut rng, + ), + alice_cl_sk.commit(), + ), + alice_cl_sk, + ); + + let deposit_ptx = cl::PartialTxWitness { + inputs: vec![zone_start.state_input_witness(), alice_deposit], + outputs: vec![zone_end.state_note, zone_end.fund_note], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let signed_deposit = SignedBoundTx::sign( + BoundTx { + tx: Tx::Deposit(deposit), + bind: alice_deposit.note_commitment(), + }, + &mut alice, + ); + + let constraint_proofs = BTreeMap::from_iter([ + ( + zone_start.state_input_witness().nullifier(), + executor::prove_zone_stf( + zone_start.state.clone(), + block_height, + vec![(signed_deposit, deposit_ptx.input_witness(1))], // bind it to the deposit note)], + deposit_ptx.input_witness(0), // input state note (input #0) + deposit_ptx.output_witness(0), // output state note (output #0) + deposit_ptx.output_witness(1), // output funds note (output #1) + ), + ), + ( + alice_deposit.nullifier(), + ledger::ConstraintProof::prove_nop( + alice_deposit.nullifier(), + deposit_ptx.commit().root(), + block_height, + ), + ), + ]); + + let note_commitments = vec![ + zone_start.state_note.commit_note(), + alice_deposit.note_commitment(), + ]; + + let deposit_proof = ledger::partial_tx::ProvedPartialTx::prove( + &deposit_ptx, + constraint_proofs, + ¬e_commitments, + ) + .expect("deposit proof failed"); + + assert!(deposit_proof.verify()); + + assert_eq!(deposit_proof.ptx.outputs[0], zone_end.state_note.commit()); + assert_eq!( + zone_end.state_note.note.state, + StateWitness { + balances: BTreeMap::from_iter([(alice_vk, 78)]), + ticket_auction: Default::default(), + included_txs: { + let mut mmr = MMR::new(); + mmr.push(&Tx::Deposit(deposit).to_bytes()); + mmr + }, + zone_metadata: zone_start.state.zone_metadata, + } + .commit() + .0 + ); + assert!(deposit_ptx.balance().is_zero()); +} diff --git a/emmarin/atomic_asset_transfer/executor/tests/withdraw_ptx.rs b/emmarin/atomic_asset_transfer/executor/tests/withdraw_ptx.rs new file mode 100644 index 00000000..4f0395d7 --- /dev/null +++ b/emmarin/atomic_asset_transfer/executor/tests/withdraw_ptx.rs @@ -0,0 +1,133 @@ +use std::collections::BTreeMap; + +use cl::{BalanceWitness, NoteWitness, NullifierSecret}; +use common::{mmr::MMR, new_account, BoundTx, SignedBoundTx, StateWitness, Tx, ZONE_CL_FUNDS_UNIT}; +use executor::ZoneNotes; +use ledger::constraint::ConstraintProof; + +#[test] +fn test_withdrawal() { + let mut rng = rand::thread_rng(); + + let mut alice = new_account(&mut rng); + let alice_vk = alice.verifying_key().to_bytes(); + let alice_cl_sk = NullifierSecret::random(&mut rng); + let block_height = 0; + + let zone_start = + ZoneNotes::new_with_balances("ZONE", BTreeMap::from_iter([(alice_vk, 100)]), &mut rng); + + let alice_intent = cl::InputWitness::from_output( + cl::OutputWitness::new( + NoteWitness::stateless( + 1, + *ZONE_CL_FUNDS_UNIT, + ConstraintProof::nop_constraint(), + &mut rng, + ), // TODO, intent should be in the constraint + alice_cl_sk.commit(), + ), + alice_cl_sk, + ); + + let withdraw = common::Withdraw { + from: alice_vk, + amount: 78, + }; + + let zone_end = zone_start + .clone() + .run(block_height, Tx::Withdraw(withdraw)) + .0; + + let alice_withdrawal = cl::OutputWitness::new( + NoteWitness::stateless( + withdraw.amount, + *ZONE_CL_FUNDS_UNIT, + ConstraintProof::nop_constraint(), + &mut rng, + ), + alice_cl_sk.commit(), + ); + + let withdraw_ptx = cl::PartialTxWitness { + inputs: vec![ + zone_start.state_input_witness(), + zone_start.fund_input_witness(), + alice_intent, + ], + outputs: vec![zone_end.state_note, zone_end.fund_note, alice_withdrawal], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let signed_withdraw = SignedBoundTx::sign( + BoundTx { + tx: Tx::Withdraw(withdraw), + bind: alice_intent.note_commitment(), + }, + &mut alice, + ); + + let constraint_proofs = BTreeMap::from_iter([ + ( + zone_start.state_input_witness().nullifier(), + executor::prove_zone_stf( + zone_start.state.clone(), + block_height, + vec![(signed_withdraw, withdraw_ptx.input_witness(2))], + withdraw_ptx.input_witness(0), // input state note (input #0) + withdraw_ptx.output_witness(0), // output state note (output #0) + withdraw_ptx.output_witness(1), // output funds note (output #1) + ), + ), + ( + zone_start.fund_input_witness().nullifier(), + executor::prove_zone_fund_constraint( + withdraw_ptx.input_witness(1), // input fund note (input #1) + withdraw_ptx.output_witness(0), // output state note (output #0) + &zone_end.state, + block_height, + ), + ), + ( + alice_intent.nullifier(), + ConstraintProof::prove_nop( + alice_intent.nullifier(), + withdraw_ptx.commit().root(), + block_height, + ), + ), + ]); + + let note_commitments = vec![ + zone_start.state_note.commit_note(), + zone_start.fund_note.commit_note(), + alice_intent.note_commitment(), + ]; + + let withdraw_proof = ledger::partial_tx::ProvedPartialTx::prove( + &withdraw_ptx, + constraint_proofs, + ¬e_commitments, + ) + .expect("withdraw proof failed"); + + assert!(withdraw_proof.verify()); + + assert_eq!(withdraw_proof.ptx.outputs[0], zone_end.state_note.commit()); + assert_eq!( + zone_end.state_note.note.state, + StateWitness { + balances: BTreeMap::from_iter([(alice_vk, 22)]), + ticket_auction: Default::default(), + included_txs: { + let mut mmr = MMR::new(); + mmr.push(&Tx::Withdraw(withdraw).to_bytes()); + mmr + }, + zone_metadata: zone_start.state.zone_metadata, + } + .commit() + .0 + ) +} diff --git a/emmarin/atomic_asset_transfer/proof_statements/Cargo.toml b/emmarin/atomic_asset_transfer/proof_statements/Cargo.toml new file mode 100644 index 00000000..81b97392 --- /dev/null +++ b/emmarin/atomic_asset_transfer/proof_statements/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "goas_proof_statements" +version = "0.1.0" +edition = "2021" + +[dependencies] +common = { path = "../common" } +cl = { path = "../../cl/cl" } +ledger_proof_statements = { path = "../../cl/ledger_proof_statements" } +serde = { version = "1.0", features = ["derive"] } +sha2 = "0.10" diff --git a/emmarin/atomic_asset_transfer/proof_statements/src/lib.rs b/emmarin/atomic_asset_transfer/proof_statements/src/lib.rs new file mode 100644 index 00000000..d0525df9 --- /dev/null +++ b/emmarin/atomic_asset_transfer/proof_statements/src/lib.rs @@ -0,0 +1,14 @@ +pub mod user_note; +pub mod zone_funds; +pub mod zone_state; + +pub fn assert_is_zone_note( + zone_meta: &common::ZoneMetadata, + note: &cl::NoteWitness, + state_roots: &common::StateRoots, +) { + assert_eq!(state_roots.commit().0, note.state); + assert_eq!(zone_meta.id(), state_roots.zone_id); + assert_eq!(zone_meta.zone_constraint, note.constraint); + assert_eq!(zone_meta.unit, note.unit); +} diff --git a/emmarin/atomic_asset_transfer/proof_statements/src/user_note.rs b/emmarin/atomic_asset_transfer/proof_statements/src/user_note.rs new file mode 100644 index 00000000..4cbfcab2 --- /dev/null +++ b/emmarin/atomic_asset_transfer/proof_statements/src/user_note.rs @@ -0,0 +1,112 @@ +/// 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: +/// +/// [] -> [user note] +/// +/// Thep User Note will encode the logic that orchestrates the withdrawal from zone A +/// and deposit to zone B. +/// +/// The User Notes 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. +use ledger_proof_statements::constraint::ConstraintPublic; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserIntent { + pub zone_a_meta: common::ZoneMetadata, + pub zone_b_meta: common::ZoneMetadata, + pub withdraw: common::Withdraw, + pub deposit: common::Deposit, +} + +impl UserIntent { + pub fn commit(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(b"USER_INTENT_STATE"); + hasher.update(self.zone_a_meta.id()); + hasher.update(self.zone_b_meta.id()); + hasher.update(self.withdraw.to_bytes()); + hasher.update(self.deposit.to_bytes()); + hasher.finalize().into() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserAtomicTransfer { + // user's note + pub user_note: cl::PartialTxInputWitness, + pub user_intent: UserIntent, + + // the output state notes which should have included both tx's + pub zone_a: cl::PartialTxOutputWitness, + pub zone_b: cl::PartialTxOutputWitness, + + // proofs of identies of the above notes + pub zone_a_roots: common::StateRoots, + pub zone_b_roots: common::StateRoots, + + // proof that zone_a has included this withdrawal + pub withdraw_tx: common::IncludedTxWitness, + // proof that zone_b has included this deposit + pub deposit_tx: common::IncludedTxWitness, +} + +impl UserAtomicTransfer { + pub fn assert_constraints(&self, block_height: u64) -> ConstraintPublic { + // user committed to these actions in the user note + assert_eq!(self.user_intent.commit(), self.user_note.input.note.state); + + // ensure we are interacting with the correct zone notes + crate::assert_is_zone_note( + &self.user_intent.zone_a_meta, + &self.zone_a.output.note, + &self.zone_a_roots, + ); + crate::assert_is_zone_note( + &self.user_intent.zone_b_meta, + &self.zone_b.output.note, + &self.zone_b_roots, + ); + + // ensure txs were included in the respective zones + assert!(self.zone_a_roots.verify_tx_inclusion(&self.withdraw_tx)); + assert!(self.zone_b_roots.verify_tx_inclusion(&self.deposit_tx)); + + // ensure the txs are the same ones the user requested + assert_eq!( + common::Tx::Withdraw(self.user_intent.withdraw), + self.withdraw_tx.tx + ); + assert_eq!( + common::Tx::Deposit(self.user_intent.deposit), + self.deposit_tx.tx + ); + + let input_root = self.user_note.input_root(); + let output_root = self.zone_a.output_root(); + assert_eq!(output_root, self.zone_b.output_root()); + + let ptx_root = cl::PtxRoot(cl::merkle::node(input_root, output_root)); + let nf = self.user_note.input.nullifier(); + ConstraintPublic { + ptx_root, + nf, + block_height, + } + } +} diff --git a/emmarin/atomic_asset_transfer/proof_statements/src/zone_funds.rs b/emmarin/atomic_asset_transfer/proof_statements/src/zone_funds.rs new file mode 100644 index 00000000..48f236bf --- /dev/null +++ b/emmarin/atomic_asset_transfer/proof_statements/src/zone_funds.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SpendFundsPrivate { + /// The note we're spending + pub in_zone_funds: cl::PartialTxInputWitness, + /// The zone note that is authorizing the spend + pub zone_note: cl::PartialTxOutputWitness, + /// Proof of identity of the above note + pub state_roots: common::StateRoots, +} diff --git a/emmarin/atomic_asset_transfer/proof_statements/src/zone_state.rs b/emmarin/atomic_asset_transfer/proof_statements/src/zone_state.rs new file mode 100644 index 00000000..dba28ad4 --- /dev/null +++ b/emmarin/atomic_asset_transfer/proof_statements/src/zone_state.rs @@ -0,0 +1,16 @@ +use common::{SignedBoundTx, StateWitness}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ZoneStatePrivate { + pub state: StateWitness, + pub block_height: u64, + pub inputs: Vec<(SignedBoundTx, cl::PartialTxInputWitness)>, + pub zone_in: cl::PartialTxInputWitness, + pub zone_out: cl::PartialTxOutputWitness, + /// While the absence of birth constraints does not guarantee uniqueness of a note that can be used as + /// zone funds, deposits and withdrawals make sure the funds are merged in a single note. + /// This means that while there's nothing to prevent creation of notes with the same characteristics of zone + /// funds, those would not be tracked by the zone state and can be ignored. + pub funds_out: cl::PartialTxOutputWitness, +} diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/Cargo.toml b/emmarin/atomic_asset_transfer/risc0_proofs/Cargo.toml new file mode 100644 index 00000000..435092f5 --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "goas_risc0_proofs" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "1.0" } + +[package.metadata.risc0] +methods = ["spend_zone_funds", "zone_state", "user_atomic_transfer"] \ No newline at end of file diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/build.rs b/emmarin/atomic_asset_transfer/risc0_proofs/build.rs new file mode 100644 index 00000000..08a8a4eb --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/spend_zone_funds/Cargo.toml b/emmarin/atomic_asset_transfer/risc0_proofs/spend_zone_funds/Cargo.toml new file mode 100644 index 00000000..1d349a53 --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/spend_zone_funds/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "spend-zone-funds" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +serde = { version = "1.0", features = ["derive"] } +cl = { path = "../../../cl/cl" } +goas_proof_statements = { path = "../../proof_statements" } +ledger_proof_statements = { path = "../../../cl/ledger_proof_statements" } +sha2 = "0.10" + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs b/emmarin/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs new file mode 100644 index 00000000..90d1ca25 --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs @@ -0,0 +1,30 @@ +/// Zone Funds Spend Proof +/// +/// Our goal: prove the zone authorized spending of funds +use cl::merkle; +use cl::partial_tx::PtxRoot; +use goas_proof_statements::zone_funds::SpendFundsPrivate; +use ledger_proof_statements::constraint::ConstraintPublic; +use risc0_zkvm::guest::env; + +fn main() { + let SpendFundsPrivate { + in_zone_funds, + zone_note, + state_roots, + } = env::read(); + let block_height: u64 = env::read(); + + let input_root = in_zone_funds.input_root(); + let output_root = zone_note.output_root(); + + let ptx_root = PtxRoot(merkle::node(input_root, output_root)); + + // 1) Check the zone note is the correct one + assert_eq!(in_zone_funds.input.note.state, state_roots.zone_id); + assert_eq!(zone_note.output.note.state, state_roots.commit().0); + + let nf = in_zone_funds.input.nullifier(); + + env::commit(&ConstraintPublic { ptx_root, nf, block_height }); +} diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/src/lib.rs b/emmarin/atomic_asset_transfer/risc0_proofs/src/lib.rs new file mode 100644 index 00000000..1bdb3085 --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/user_atomic_transfer/Cargo.toml b/emmarin/atomic_asset_transfer/risc0_proofs/user_atomic_transfer/Cargo.toml new file mode 100644 index 00000000..abcbde99 --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/user_atomic_transfer/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "user_atomic_transfer" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +blake2 = "0.10" +serde = { version = "1.0", features = ["derive"] } +bincode = "1" +common = { path = "../../common" } +cl = { path = "../../../cl/cl" } +goas_proof_statements = { path = "../../proof_statements" } +ledger_proof_statements = { path = "../../../cl/ledger_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 +# multiplication accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +# k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.3-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/user_atomic_transfer/src/main.rs b/emmarin/atomic_asset_transfer/risc0_proofs/user_atomic_transfer/src/main.rs new file mode 100644 index 00000000..478e3911 --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/user_atomic_transfer/src/main.rs @@ -0,0 +1,10 @@ +use goas_proof_statements::user_note::UserAtomicTransfer; +use ledger_proof_statements::constraint::ConstraintPublic; +use risc0_zkvm::guest::env; + +fn main() { + let transfer: UserAtomicTransfer = env::read(); + let block_height: u64 = env::read(); + let public: ConstraintPublic = transfer.assert_constraints(block_height); + env::commit(&public); +} diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/zone_state/Cargo.toml b/emmarin/atomic_asset_transfer/risc0_proofs/zone_state/Cargo.toml new file mode 100644 index 00000000..7f10de51 --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/zone_state/Cargo.toml @@ -0,0 +1,27 @@ + + +[package] +name = "zone_state" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +blake2 = "0.10" +serde = { version = "1.0", features = ["derive"] } +bincode = "1" +common = { path = "../../common" } +cl = { path = "../../../cl/cl" } +goas_proof_statements = { path = "../../proof_statements" } +ledger_proof_statements = { path = "../../../cl/ledger_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 +# multiplication accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +# k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.3-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/atomic_asset_transfer/risc0_proofs/zone_state/src/main.rs b/emmarin/atomic_asset_transfer/risc0_proofs/zone_state/src/main.rs new file mode 100644 index 00000000..8d64dfb3 --- /dev/null +++ b/emmarin/atomic_asset_transfer/risc0_proofs/zone_state/src/main.rs @@ -0,0 +1,90 @@ +use cl::{ + note::NoteWitness, output::OutputWitness, + PtxRoot, +}; + +use common::*; +use goas_proof_statements::zone_state::ZoneStatePrivate; +use ledger_proof_statements::constraint::ConstraintPublic; +use risc0_zkvm::guest::env; + +fn validate_zone_transition( + in_note: cl::PartialTxInputWitness, + out_note: cl::PartialTxOutputWitness, + out_funds: cl::PartialTxOutputWitness, + in_state_cm: StateCommitment, + out_state: StateWitness, +) { + let metadata = out_state.zone_metadata; + let out_state_cm = out_state.commit().0; + // 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_cm); + + // ensure units match metadata + assert_eq!(in_note.input.note.unit, metadata.unit); + assert_eq!(out_note.output.note.unit, metadata.unit); + + // ensure constraints match metadata + assert_eq!(in_note.input.note.constraint, metadata.zone_constraint); + assert_eq!(out_note.output.note.constraint, metadata.zone_constraint); + + // nullifier secret is propagated + assert_eq!(in_note.input.nf_sk.commit(), out_note.output.nf_pk); + + // the nonce is correctly evolved + assert_eq!(in_note.input.evolved_nonce(b"STATE_NONCE"), out_note.output.note.nonce); + + // funds are still under control of the zone + let expected_note_witness = NoteWitness { + value: out_state.total_balance(), + unit: *ZONE_CL_FUNDS_UNIT, + constraint: metadata.funds_constraint, + state: metadata.id(), + nonce: in_note.input.evolved_nonce(b"FUND_NONCE") + }; + assert_eq!( + out_funds.output, + OutputWitness::public(expected_note_witness) + ); + // funds belong to the same partial tx + assert_eq!(out_funds.output_root(), out_note.output_root()); +} + +fn main() { + let ZoneStatePrivate { + mut state, + block_height, + inputs, + zone_in, + zone_out, + funds_out, + } = env::read(); + + let input_root = zone_in.input_root(); + let output_root = zone_out.output_root(); + + let pub_inputs = ConstraintPublic { + ptx_root: PtxRoot(cl::merkle::node(input_root, output_root)), + nf: zone_in.input.nullifier(), + block_height, + }; + + let in_state_cm = state.commit(); + + for (signed_bound_tx, ptx_input_witness) in inputs { + // verify the signature + let bound_tx = signed_bound_tx.verify_and_unwrap(); + + // ensure the note this tx is bound to is present in the ptx + assert_eq!(bound_tx.bind, ptx_input_witness.input.note_commitment()); + assert_eq!(ptx_input_witness.input_root(), input_root); + + // apply the ptx + state = state.apply(block_height, bound_tx.tx).0 + } + + validate_zone_transition(zone_in, zone_out, funds_out, in_state_cm, state); + + env::commit(&pub_inputs); +} diff --git a/emmarin/atomic_asset_transfer/rust-toolchain.toml b/emmarin/atomic_asset_transfer/rust-toolchain.toml new file mode 100644 index 00000000..36614c30 --- /dev/null +++ b/emmarin/atomic_asset_transfer/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "rust-src"] +profile = "minimal" diff --git a/emmarin/atomic_asset_transfer/user/Cargo.toml b/emmarin/atomic_asset_transfer/user/Cargo.toml new file mode 100644 index 00000000..5ca776ff --- /dev/null +++ b/emmarin/atomic_asset_transfer/user/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "user" +version = "0.1.0" +edition = "2021" + +[dependencies] +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"] } +serde = "1.0" +blake2 = "0.10" +bincode = "1" +common = { path = "../common" } +tempfile = "3" +clap = { version = "4", features = ["derive"] } +rand = "0.8.5" +cl = { path = "../../cl/cl" } diff --git a/emmarin/atomic_asset_transfer/user/src/main.rs b/emmarin/atomic_asset_transfer/user/src/main.rs new file mode 100644 index 00000000..bc2aa136 --- /dev/null +++ b/emmarin/atomic_asset_transfer/user/src/main.rs @@ -0,0 +1,8 @@ +fn main() { + // Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run` + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) + .init(); + + println!("TODO: impl user side of the atomic asset transfer"); +} diff --git a/emmarin/cl/.gitignore b/emmarin/cl/.gitignore new file mode 100644 index 00000000..b354aec7 --- /dev/null +++ b/emmarin/cl/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/ \ No newline at end of file diff --git a/emmarin/cl/Cargo.toml b/emmarin/cl/Cargo.toml new file mode 100644 index 00000000..bcfe0763 --- /dev/null +++ b/emmarin/cl/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +resolver = "2" +members = [ "cl", "ledger", "ledger_proof_statements", "risc0_proofs"] + +# Always optimize; building and running the risc0_proofs takes much longer without optimization. +[profile.dev] +opt-level = 3 + +[profile.release] +debug = 1 +lto = true diff --git a/emmarin/cl/cl/Cargo.toml b/emmarin/cl/cl/Cargo.toml new file mode 100644 index 00000000..b73749b9 --- /dev/null +++ b/emmarin/cl/cl/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "cl" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = {version="1.0", features = ["derive"]} +group = "0.13.0" +rand = "0.8.5" +rand_core = "0.6.0" +hex = "0.4.3" +curve25519-dalek = {version = "4.1", features = ["serde", "digest", "rand_core"]} +sha2 = "0.10" diff --git a/emmarin/cl/cl/src/balance.rs b/emmarin/cl/cl/src/balance.rs new file mode 100644 index 00000000..10db851b --- /dev/null +++ b/emmarin/cl/cl/src/balance.rs @@ -0,0 +1,149 @@ +use rand_core::CryptoRngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::PartialTxWitness; + +pub type Value = u64; +pub type Unit = [u8; 32]; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub struct Balance([u8; 32]); + +impl Balance { + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct UnitBalance { + pub unit: Unit, + pub pos: u64, + pub neg: u64, +} + +impl UnitBalance { + pub fn is_zero(&self) -> bool { + self.pos == self.neg + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct BalanceWitness { + pub balances: Vec, + pub blinding: [u8; 16], +} + +impl BalanceWitness { + pub fn random_blinding(mut rng: impl CryptoRngCore) -> [u8; 16] { + let mut blinding = [0u8; 16]; + rng.fill_bytes(&mut blinding); + + blinding + } + + pub fn zero(blinding: [u8; 16]) -> Self { + Self { + balances: Default::default(), + blinding, + } + } + + pub fn from_ptx(ptx: &PartialTxWitness, blinding: [u8; 16]) -> Self { + let mut balance = Self::zero(blinding); + + for input in ptx.inputs.iter() { + balance.insert_negative(input.note.unit, input.note.value); + } + + for output in ptx.outputs.iter() { + balance.insert_positive(output.note.unit, output.note.value); + } + + balance.clear_zeros(); + + balance + } + + pub fn insert_positive(&mut self, unit: Unit, value: Value) { + for unit_bal in self.balances.iter_mut() { + if unit_bal.unit == unit { + unit_bal.pos += value; + return; + } + } + + // Unit was not found, so we must create one. + self.balances.push(UnitBalance { + unit, + pos: value, + neg: 0, + }); + } + + pub fn insert_negative(&mut self, unit: Unit, value: Value) { + for unit_bal in self.balances.iter_mut() { + if unit_bal.unit == unit { + unit_bal.neg += value; + return; + } + } + + self.balances.push(UnitBalance { + unit, + pos: 0, + neg: value, + }); + } + + pub fn clear_zeros(&mut self) { + let mut i = 0usize; + while i < self.balances.len() { + if self.balances[i].is_zero() { + self.balances.swap_remove(i); + // don't increment `i` since the last element has been swapped into the + // `i`'th place + } else { + i += 1; + } + } + } + + pub fn combine(balances: impl IntoIterator, blinding: [u8; 16]) -> Self { + let mut combined = BalanceWitness::zero(blinding); + + for balance in balances { + for unit_bal in balance.balances.iter() { + if unit_bal.pos > unit_bal.neg { + combined.insert_positive(unit_bal.unit, unit_bal.pos - unit_bal.neg); + } else { + combined.insert_negative(unit_bal.unit, unit_bal.neg - unit_bal.pos); + } + } + } + + combined.clear_zeros(); + + combined + } + + pub fn is_zero(&self) -> bool { + self.balances.is_empty() + } + + pub fn commit(&self) -> Balance { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_BAL_COMMIT"); + + for unit_balance in self.balances.iter() { + hasher.update(unit_balance.unit); + hasher.update(unit_balance.pos.to_le_bytes()); + hasher.update(unit_balance.neg.to_le_bytes()); + } + hasher.update(self.blinding); + + let commit_bytes: [u8; 32] = hasher.finalize().into(); + Balance(commit_bytes) + } +} diff --git a/emmarin/cl/cl/src/bundle.rs b/emmarin/cl/cl/src/bundle.rs new file mode 100644 index 00000000..0ea98c87 --- /dev/null +++ b/emmarin/cl/cl/src/bundle.rs @@ -0,0 +1,117 @@ +use serde::{Deserialize, Serialize}; + +use crate::{partial_tx::PartialTx, BalanceWitness, PartialTxWitness}; + +/// The transaction bundle is a collection of partial transactions. +/// The goal in bundling transactions is to produce a set of partial transactions +/// that balance each other. + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bundle { + pub partials: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BundleWitness { + pub partials: Vec, +} + +impl BundleWitness { + pub fn balance(&self) -> BalanceWitness { + BalanceWitness::combine(self.partials.iter().map(|ptx| ptx.balance()), [0u8; 16]) + } + + pub fn commit(&self) -> Bundle { + Bundle { + partials: Vec::from_iter(self.partials.iter().map(|ptx| ptx.commit())), + } + } +} + +#[cfg(test)] +mod test { + use crate::{ + balance::UnitBalance, + input::InputWitness, + note::{derive_unit, NoteWitness}, + nullifier::NullifierSecret, + output::OutputWitness, + partial_tx::PartialTxWitness, + }; + + use super::*; + + #[test] + fn test_bundle_balance() { + let mut rng = rand::thread_rng(); + let (nmo, eth, crv) = (derive_unit("NMO"), derive_unit("ETH"), derive_unit("CRV")); + + let nf_a = NullifierSecret::random(&mut rng); + let nf_b = NullifierSecret::random(&mut rng); + let nf_c = NullifierSecret::random(&mut rng); + + let nmo_10_utxo = OutputWitness::new(NoteWitness::basic(10, nmo, &mut rng), nf_a.commit()); + let nmo_10_in = InputWitness::from_output(nmo_10_utxo, nf_a); + + let eth_23_utxo = OutputWitness::new(NoteWitness::basic(23, eth, &mut rng), nf_b.commit()); + let eth_23_in = InputWitness::from_output(eth_23_utxo, nf_b); + + let crv_4840_out = + OutputWitness::new(NoteWitness::basic(4840, crv, &mut rng), nf_c.commit()); + + let ptx_unbalanced = PartialTxWitness { + inputs: vec![nmo_10_in, eth_23_in], + outputs: vec![crv_4840_out], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let bundle_witness = BundleWitness { + partials: vec![ptx_unbalanced.clone()], + }; + + assert!(!bundle_witness.balance().is_zero()); + assert_eq!( + bundle_witness.balance().balances, + vec![ + UnitBalance { + unit: nmo, + pos: 0, + neg: 10 + }, + UnitBalance { + unit: eth, + pos: 0, + neg: 23 + }, + UnitBalance { + unit: crv, + pos: 4840, + neg: 0 + }, + ] + ); + + let crv_4840_in = InputWitness::from_output(crv_4840_out, nf_c); + let nmo_10_out = OutputWitness::new( + NoteWitness::basic(10, nmo, &mut rng), + NullifierSecret::random(&mut rng).commit(), // transferring to a random owner + ); + let eth_23_out = OutputWitness::new( + NoteWitness::basic(23, eth, &mut rng), + NullifierSecret::random(&mut rng).commit(), // transferring to a random owner + ); + + let ptx_solved = PartialTxWitness { + inputs: vec![crv_4840_in], + outputs: vec![nmo_10_out, eth_23_out], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let witness = BundleWitness { + partials: vec![ptx_unbalanced, ptx_solved], + }; + + assert!(witness.balance().is_zero()); + assert_eq!(witness.balance().balances, vec![]); + } +} diff --git a/emmarin/cl/cl/src/crypto.rs b/emmarin/cl/cl/src/crypto.rs new file mode 100644 index 00000000..e0be5f5e --- /dev/null +++ b/emmarin/cl/cl/src/crypto.rs @@ -0,0 +1,6 @@ +use sha2::Sha512; +use curve25519_dalek::ristretto::RistrettoPoint; + +pub fn hash_to_curve(bytes: &[u8]) -> RistrettoPoint { + RistrettoPoint::hash_from_bytes::(bytes) +} diff --git a/emmarin/cl/cl/src/error.rs b/emmarin/cl/cl/src/error.rs new file mode 100644 index 00000000..03d01d1f --- /dev/null +++ b/emmarin/cl/cl/src/error.rs @@ -0,0 +1,4 @@ +#[derive(Debug)] +pub enum Error { + ProofFailed, +} diff --git a/emmarin/cl/cl/src/input.rs b/emmarin/cl/cl/src/input.rs new file mode 100644 index 00000000..e47bfb0e --- /dev/null +++ b/emmarin/cl/cl/src/input.rs @@ -0,0 +1,85 @@ +/// This module defines the partial transaction structure. +/// +/// Partial transactions, as the name suggests, are transactions +/// which on their own may not balance (i.e. \sum inputs != \sum outputs) +use crate::{ + note::{Constraint, NoteWitness}, + nullifier::{Nullifier, NullifierSecret}, + Nonce, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Input { + pub nullifier: Nullifier, + pub constraint: Constraint, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct InputWitness { + pub note: NoteWitness, + pub nf_sk: NullifierSecret, +} + +impl InputWitness { + pub fn new(note: NoteWitness, nf_sk: NullifierSecret) -> Self { + Self { note, nf_sk } + } + + pub fn from_output(output: crate::OutputWitness, nf_sk: NullifierSecret) -> Self { + assert_eq!(nf_sk.commit(), output.nf_pk); + Self::new(output.note, nf_sk) + } + + 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::new(output.note, nf_sk) + } + + pub fn evolved_nonce(&self, domain: &[u8]) -> Nonce { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_COIN_EVOLVE"); + hasher.update(domain); + hasher.update(self.nf_sk.0); + hasher.update(self.note.commit(self.nf_sk.commit()).0); + + let nonce_bytes: [u8; 32] = hasher.finalize().into(); + Nonce::from_bytes(nonce_bytes) + } + + pub fn evolve_output(&self, domain: &[u8]) -> crate::OutputWitness { + crate::OutputWitness { + note: NoteWitness { + nonce: self.evolved_nonce(domain), + ..self.note + }, + nf_pk: self.nf_sk.commit(), + } + } + + pub fn nullifier(&self) -> Nullifier { + Nullifier::new(self.nf_sk, self.note_commitment()) + } + + pub fn commit(&self) -> Input { + Input { + nullifier: self.nullifier(), + constraint: self.note.constraint, + } + } + + pub fn note_commitment(&self) -> crate::NoteCommitment { + self.note.commit(self.nf_sk.commit()) + } +} + +impl Input { + pub fn to_bytes(&self) -> [u8; 64] { + let mut bytes = [0u8; 64]; + bytes[..32].copy_from_slice(self.nullifier.as_bytes()); + bytes[32..64].copy_from_slice(&self.constraint.0); + bytes + } +} diff --git a/emmarin/cl/cl/src/lib.rs b/emmarin/cl/cl/src/lib.rs new file mode 100644 index 00000000..e1d559ea --- /dev/null +++ b/emmarin/cl/cl/src/lib.rs @@ -0,0 +1,20 @@ +pub mod balance; +pub mod bundle; +pub mod crypto; +pub mod error; +pub mod input; +pub mod merkle; +pub mod note; +pub mod nullifier; +pub mod output; +pub mod partial_tx; + +pub use balance::{Balance, BalanceWitness}; +pub use bundle::{Bundle, BundleWitness}; +pub use input::{Input, InputWitness}; +pub use note::{Constraint, Nonce, NoteCommitment, NoteWitness}; +pub use nullifier::{Nullifier, NullifierCommitment, NullifierSecret}; +pub use output::{Output, OutputWitness}; +pub use partial_tx::{ + PartialTx, PartialTxInputWitness, PartialTxOutputWitness, PartialTxWitness, PtxRoot, +}; diff --git a/emmarin/cl/cl/src/merkle.rs b/emmarin/cl/cl/src/merkle.rs new file mode 100644 index 00000000..bb10e3c3 --- /dev/null +++ b/emmarin/cl/cl/src/merkle.rs @@ -0,0 +1,239 @@ +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +pub fn padded_leaves(elements: &[Vec]) -> [[u8; 32]; N] { + let mut leaves = [[0u8; 32]; N]; + + for (i, element) in elements.iter().enumerate() { + assert!(i < N); + leaves[i] = leaf(element); + } + + leaves +} + +pub fn leaf(data: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_MERKLE_LEAF"); + hasher.update(data); + hasher.finalize().into() +} + +pub fn node(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_MERKLE_NODE"); + hasher.update(a); + hasher.update(b); + hasher.finalize().into() +} + +pub fn root(elements: [[u8; 32]; N]) -> [u8; 32] { + let n = elements.len(); + + assert!(n.is_power_of_two()); + + let mut nodes = elements; + + for h in (1..=n.ilog2()).rev() { + for i in 0..2usize.pow(h - 1) { + nodes[i] = node(nodes[i * 2], nodes[i * 2 + 1]); + } + } + + nodes[0] +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PathNode { + Left([u8; 32]), + Right([u8; 32]), +} + +pub fn path_root(leaf: [u8; 32], path: &[PathNode]) -> [u8; 32] { + let mut computed_hash = leaf; + + for path_node in path { + match path_node { + PathNode::Left(sibling_hash) => { + computed_hash = node(*sibling_hash, computed_hash); + } + PathNode::Right(sibling_hash) => { + computed_hash = node(computed_hash, *sibling_hash); + } + } + } + + computed_hash +} + +pub fn path(leaves: [[u8; 32]; N], idx: usize) -> Vec { + assert!(N.is_power_of_two()); + assert!(idx < N); + + let mut nodes = leaves; + let mut path = Vec::new(); + let mut idx = idx; + + for h in (1..=N.ilog2()).rev() { + if idx % 2 == 0 { + path.push(PathNode::Right(nodes[idx + 1])); + } else { + path.push(PathNode::Left(nodes[idx - 1])); + } + + idx /= 2; + + for i in 0..2usize.pow(h - 1) { + nodes[i] = node(nodes[i * 2], nodes[i * 2 + 1]); + } + } + + path +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_root_height_1() { + let r = root::<1>(padded_leaves(&[b"sand".into()])); + + let expected = leaf(b"sand"); + + assert_eq!(r, expected); + } + + #[test] + fn test_root_height_2() { + let r = root::<2>(padded_leaves(&[b"desert".into(), b"sand".into()])); + + let expected = node(leaf(b"desert"), leaf(b"sand")); + + assert_eq!(r, expected); + } + + #[test] + fn test_root_height_3() { + let r = root::<4>(padded_leaves(&[ + b"desert".into(), + b"sand".into(), + b"feels".into(), + b"warm".into(), + ])); + + let expected = node( + node(leaf(b"desert"), leaf(b"sand")), + node(leaf(b"feels"), leaf(b"warm")), + ); + + assert_eq!(r, expected); + } + + #[test] + fn test_root_height_4() { + let r = root::<8>(padded_leaves(&[ + b"desert".into(), + b"sand".into(), + b"feels".into(), + b"warm".into(), + b"at".into(), + b"night".into(), + ])); + + let expected = node( + node( + node(leaf(b"desert"), leaf(b"sand")), + node(leaf(b"feels"), leaf(b"warm")), + ), + node( + node(leaf(b"at"), leaf(b"night")), + node([0u8; 32], [0u8; 32]), + ), + ); + + assert_eq!(r, expected); + } + + #[test] + fn test_path_height_1() { + let leaves = padded_leaves(&[b"desert".into()]); + let r = root::<1>(leaves); + + let p = path::<1>(leaves, 0); + let expected = vec![]; + assert_eq!(p, expected); + assert_eq!(path_root(leaf(b"desert"), &p), r); + } + + #[test] + fn test_path_height_2() { + let leaves = padded_leaves(&[b"desert".into(), b"sand".into()]); + let r = root::<2>(leaves); + + // --- proof for element at idx 0 + + let p0 = path(leaves, 0); + let expected0 = vec![PathNode::Right(leaf(b"sand"))]; + assert_eq!(p0, expected0); + assert_eq!(path_root(leaf(b"desert"), &p0), r); + + // --- proof for element at idx 1 + + let p1 = path(leaves, 1); + let expected1 = vec![PathNode::Left(leaf(b"desert"))]; + assert_eq!(p1, expected1); + assert_eq!(path_root(leaf(b"sand"), &p1), r); + } + + #[test] + fn test_path_height_3() { + let leaves = padded_leaves(&[ + b"desert".into(), + b"sand".into(), + b"feels".into(), + b"warm".into(), + ]); + let r = root::<4>(leaves); + + // --- proof for element at idx 0 + + let p0 = path(leaves, 0); + let expected0 = vec![ + PathNode::Right(leaf(b"sand")), + PathNode::Right(node(leaf(b"feels"), leaf(b"warm"))), + ]; + assert_eq!(p0, expected0); + assert_eq!(path_root(leaf(b"desert"), &p0), r); + + // --- proof for element at idx 1 + + let p1 = path(leaves, 1); + let expected1 = vec![ + PathNode::Left(leaf(b"desert")), + PathNode::Right(node(leaf(b"feels"), leaf(b"warm"))), + ]; + assert_eq!(p1, expected1); + assert_eq!(path_root(leaf(b"sand"), &p1), r); + + // --- proof for element at idx 2 + + let p2 = path(leaves, 2); + let expected2 = vec![ + PathNode::Right(leaf(b"warm")), + PathNode::Left(node(leaf(b"desert"), leaf(b"sand"))), + ]; + assert_eq!(p2, expected2); + assert_eq!(path_root(leaf(b"feels"), &p2), r); + + // --- proof for element at idx 3 + + let p3 = path(leaves, 3); + let expected3 = vec![ + PathNode::Left(leaf(b"feels")), + PathNode::Left(node(leaf(b"desert"), leaf(b"sand"))), + ]; + assert_eq!(p3, expected3); + assert_eq!(path_root(leaf(b"warm"), &p3), r); + } +} diff --git a/emmarin/cl/cl/src/note.rs b/emmarin/cl/cl/src/note.rs new file mode 100644 index 00000000..d28a9204 --- /dev/null +++ b/emmarin/cl/cl/src/note.rs @@ -0,0 +1,171 @@ +use rand::RngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::{balance::Unit, nullifier::NullifierCommitment}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct Constraint(pub [u8; 32]); + +impl Constraint { + pub fn from_vk(constraint_vk: &[u8]) -> Self { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_CONSTRAINT_COMMIT"); + hasher.update(constraint_vk); + let constraint_cm: [u8; 32] = hasher.finalize().into(); + + Self(constraint_cm) + } +} + +pub fn derive_unit(unit: &str) -> Unit { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_UNIT"); + hasher.update(unit.as_bytes()); + let unit: Unit = hasher.finalize().into(); + unit +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct NoteCommitment(pub [u8; 32]); + +impl NoteCommitment { + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub struct NoteWitness { + pub value: u64, + pub unit: Unit, + pub constraint: Constraint, + pub state: [u8; 32], + pub nonce: Nonce, +} + +impl NoteWitness { + pub fn new( + value: u64, + unit: Unit, + constraint: Constraint, + state: [u8; 32], + nonce: Nonce, + ) -> Self { + Self { + value, + unit, + constraint, + state, + nonce, + } + } + + pub fn basic(value: u64, unit: Unit, rng: impl RngCore) -> Self { + let constraint = Constraint([0u8; 32]); + let nonce = Nonce::random(rng); + Self::new(value, unit, constraint, [0u8; 32], nonce) + } + + pub fn stateless(value: u64, unit: Unit, constraint: Constraint, rng: impl RngCore) -> Self { + Self::new(value, unit, constraint, [0u8; 32], Nonce::random(rng)) + } + + pub fn commit(&self, nf_pk: NullifierCommitment) -> NoteCommitment { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_NOTE_COMMIT"); + + // COMMIT TO BALANCE + hasher.update(self.value.to_le_bytes()); + hasher.update(self.unit); + // Important! we don't commit to the balance blinding factor as that may make the notes linkable. + + // COMMIT TO STATE + hasher.update(self.state); + + // COMMIT TO CONSTRAINT + hasher.update(self.constraint.0); + + // COMMIT TO NONCE + hasher.update(self.nonce.as_bytes()); + + // COMMIT TO NULLIFIER + hasher.update(nf_pk.as_bytes()); + + let commit_bytes: [u8; 32] = hasher.finalize().into(); + NoteCommitment(commit_bytes) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Nonce([u8; 32]); + +impl Nonce { + pub fn random(mut rng: impl RngCore) -> Self { + let mut nonce = [0u8; 32]; + rng.fill_bytes(&mut nonce); + Self(nonce) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::nullifier::NullifierSecret; + + #[test] + fn test_note_commit_permutations() { + let (nmo, eth) = (derive_unit("NMO"), derive_unit("ETH")); + + let mut rng = rand::thread_rng(); + + let nf_pk = NullifierSecret::random(&mut rng).commit(); + + let reference_note = NoteWitness::basic(32, nmo, &mut rng); + + // different notes under same nullifier produce different commitments + let mutation_tests = [ + NoteWitness { + value: 12, + ..reference_note + }, + NoteWitness { + unit: eth, + ..reference_note + }, + NoteWitness { + constraint: Constraint::from_vk(&[1u8; 32]), + ..reference_note + }, + NoteWitness { + state: [1u8; 32], + ..reference_note + }, + NoteWitness { + nonce: Nonce::random(&mut rng), + ..reference_note + }, + ]; + + for n in mutation_tests { + assert_ne!(n.commit(nf_pk), reference_note.commit(nf_pk)); + } + + // commitment to same note with different nullifiers produce different commitments + + let other_nf_pk = NullifierSecret::random(&mut rng).commit(); + + assert_ne!( + reference_note.commit(nf_pk), + reference_note.commit(other_nf_pk) + ); + } +} diff --git a/emmarin/cl/cl/src/nullifier.rs b/emmarin/cl/cl/src/nullifier.rs new file mode 100644 index 00000000..ab39f8ff --- /dev/null +++ b/emmarin/cl/cl/src/nullifier.rs @@ -0,0 +1,161 @@ +// The Nullifier is used to detect if a note has +// already been consumed. + +// The same nullifier secret may be used across multiple +// notes to allow users to hold fewer secrets. A note +// nonce is used to disambiguate when the same nullifier +// secret is used for multiple notes. + +use rand_core::RngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::NoteCommitment; + +// Maintained privately by note holder +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct NullifierSecret(pub [u8; 16]); + +// Nullifier commitment is public information that +// can be provided to anyone wishing to transfer +// you a note +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct NullifierCommitment([u8; 32]); + +// The nullifier attached to input notes to prove an input has not +// already been spent. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Nullifier([u8; 32]); + +impl NullifierSecret { + pub fn random(mut rng: impl RngCore) -> Self { + let mut sk = [0u8; 16]; + rng.fill_bytes(&mut sk); + Self(sk) + } + + pub const fn zero() -> Self { + Self([0u8; 16]) + } + + pub fn commit(&self) -> NullifierCommitment { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_NULL_COMMIT"); + hasher.update(self.0); + + let commit_bytes: [u8; 32] = hasher.finalize().into(); + NullifierCommitment(commit_bytes) + } + + pub fn from_bytes(bytes: [u8; 16]) -> Self { + Self(bytes) + } +} + +impl NullifierCommitment { + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn hex(&self) -> String { + hex::encode(self.0) + } + + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl Nullifier { + pub fn new(sk: NullifierSecret, note_cm: NoteCommitment) -> Self { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_NULLIFIER"); + hasher.update(sk.0); + hasher.update(note_cm.0); + + let nf_bytes: [u8; 32] = hasher.finalize().into(); + Self(nf_bytes) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[cfg(test)] +mod test { + use crate::{note::derive_unit, Constraint, Nonce, NoteWitness}; + + use super::*; + + #[ignore = "nullifier test vectors not stable yet"] + #[test] + fn test_nullifier_commitment_vectors() { + assert_eq!( + NullifierSecret([0u8; 16]).commit().hex(), + "384318f9864fe57647bac344e2afdc500a672dedb29d2dc63b004e940e4b382a" + ); + assert_eq!( + NullifierSecret([1u8; 16]).commit().hex(), + "0fd667e6bb39fbdc35d6265726154b839638ea90bcf4e736953ccf27ca5f870b" + ); + assert_eq!( + NullifierSecret([u8::MAX; 16]).commit().hex(), + "1cb78e487eb0b3116389311fdde84cd3f619a4d7f487b29bf5a002eed3784d75" + ); + } + + #[test] + fn test_nullifier_same_sk_different_nonce() { + let mut rng = rand::thread_rng(); + let sk = NullifierSecret::random(&mut rng); + let note_1 = NoteWitness { + value: 1, + unit: derive_unit("NMO"), + constraint: Constraint::from_vk(&[]), + state: [0u8; 32], + nonce: Nonce::random(&mut rng), + }; + let note_2 = NoteWitness { + nonce: Nonce::random(&mut rng), + ..note_1 + }; + + let note_cm_1 = note_1.commit(sk.commit()); + let note_cm_2 = note_2.commit(sk.commit()); + + let nf_1 = Nullifier::new(sk, note_cm_1); + let nf_2 = Nullifier::new(sk, note_cm_2); + + assert_ne!(nf_1, nf_2); + } + + #[test] + fn test_same_sk_same_nonce_different_note() { + let mut rng = rand::thread_rng(); + + let sk = NullifierSecret::random(&mut rng); + let nonce = Nonce::random(&mut rng); + + let note_1 = NoteWitness { + value: 1, + unit: derive_unit("NMO"), + constraint: Constraint::from_vk(&[]), + state: [0u8; 32], + nonce, + }; + + let note_2 = NoteWitness { + unit: derive_unit("ETH"), + ..note_1 + }; + + let note_cm_1 = note_1.commit(sk.commit()); + let note_cm_2 = note_2.commit(sk.commit()); + + let nf_1 = Nullifier::new(sk, note_cm_1); + let nf_2 = Nullifier::new(sk, note_cm_2); + + assert_ne!(nf_1, nf_2); + } +} diff --git a/emmarin/cl/cl/src/output.rs b/emmarin/cl/cl/src/output.rs new file mode 100644 index 00000000..5ddf2439 --- /dev/null +++ b/emmarin/cl/cl/src/output.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + note::{NoteCommitment, NoteWitness}, + nullifier::NullifierCommitment, + NullifierSecret, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Output { + pub note_comm: NoteCommitment, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct OutputWitness { + pub note: NoteWitness, + pub nf_pk: NullifierCommitment, +} + +impl OutputWitness { + pub fn new(note: NoteWitness, nf_pk: NullifierCommitment) -> Self { + Self { note, nf_pk } + } + + pub fn public(note: NoteWitness) -> Self { + let nf_pk = NullifierSecret::zero().commit(); + Self { note, nf_pk } + } + + pub fn commit_note(&self) -> NoteCommitment { + self.note.commit(self.nf_pk) + } + + pub fn commit(&self) -> Output { + Output { + note_comm: self.commit_note(), + } + } +} + +impl Output { + pub fn to_bytes(&self) -> [u8; 32] { + self.note_comm.0 + } +} diff --git a/emmarin/cl/cl/src/partial_tx.rs b/emmarin/cl/cl/src/partial_tx.rs new file mode 100644 index 00000000..844957c6 --- /dev/null +++ b/emmarin/cl/cl/src/partial_tx.rs @@ -0,0 +1,211 @@ +use rand_core::{CryptoRngCore, RngCore}; +use serde::{Deserialize, Serialize}; + +use crate::balance::{Balance, BalanceWitness}; +use crate::input::{Input, InputWitness}; +use crate::merkle; +use crate::output::{Output, OutputWitness}; + +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. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct PtxRoot(pub [u8; 32]); + +impl From<[u8; 32]> for PtxRoot { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl PtxRoot { + pub fn random(mut rng: impl RngCore) -> Self { + let mut sk = [0u8; 32]; + rng.fill_bytes(&mut sk); + Self(sk) + } + + pub fn hex(&self) -> String { + hex::encode(self.0) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTx { + pub inputs: Vec, + pub outputs: Vec, + pub balance: Balance, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTxWitness { + pub inputs: Vec, + pub outputs: Vec, + pub balance_blinding: [u8; 16], +} + +impl PartialTxWitness { + pub fn random( + inputs: Vec, + outputs: Vec, + mut rng: impl CryptoRngCore, + ) -> Self { + Self { + inputs, + outputs, + balance_blinding: BalanceWitness::random_blinding(&mut rng), + } + } + + pub fn balance(&self) -> BalanceWitness { + BalanceWitness::from_ptx(self, self.balance_blinding) + } + + pub fn commit(&self) -> PartialTx { + PartialTx { + inputs: Vec::from_iter(self.inputs.iter().map(InputWitness::commit)), + outputs: Vec::from_iter(self.outputs.iter().map(OutputWitness::commit)), + balance: self.balance().commit(), + } + } + + 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::(&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::(&output_bytes); + + let path = merkle::path(output_merkle_leaves, idx); + let output = self.outputs[idx]; + PartialTxOutputWitness { output, path } + } +} + +impl PartialTx { + pub fn input_root(&self) -> [u8; 32] { + let input_bytes = + Vec::from_iter(self.inputs.iter().map(Input::to_bytes).map(Vec::from_iter)); + let input_merkle_leaves = merkle::padded_leaves(&input_bytes); + merkle::root::(input_merkle_leaves) + } + + pub fn output_root(&self) -> [u8; 32] { + let output_bytes = Vec::from_iter( + self.outputs + .iter() + .map(Output::to_bytes) + .map(Vec::from_iter), + ); + let output_merkle_leaves = merkle::padded_leaves(&output_bytes); + merkle::root::(output_merkle_leaves) + } + + pub fn root(&self) -> PtxRoot { + let input_root = self.input_root(); + let output_root = self.output_root(); + let root = merkle::node(input_root, output_root); + PtxRoot(root) + } +} + +/// An input to a partial transaction +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTxInputWitness { + pub input: InputWitness, + pub path: Vec, +} + +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, +} + +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)] +mod test { + + use crate::{ + balance::UnitBalance, + note::{derive_unit, NoteWitness}, + nullifier::NullifierSecret, + }; + + use super::*; + + #[test] + fn test_partial_tx_balance() { + let (nmo, eth, crv) = (derive_unit("NMO"), derive_unit("ETH"), derive_unit("CRV")); + let mut rng = rand::thread_rng(); + + let nf_a = NullifierSecret::random(&mut rng); + let nf_b = NullifierSecret::random(&mut rng); + let nf_c = NullifierSecret::random(&mut rng); + + let nmo_10_utxo = OutputWitness::new(NoteWitness::basic(10, nmo, &mut rng), nf_a.commit()); + let nmo_10 = InputWitness::from_output(nmo_10_utxo, nf_a); + + let eth_23_utxo = OutputWitness::new(NoteWitness::basic(23, eth, &mut rng), nf_b.commit()); + let eth_23 = InputWitness::from_output(eth_23_utxo, nf_b); + + let crv_4840 = OutputWitness::new(NoteWitness::basic(4840, crv, &mut rng), nf_c.commit()); + + let ptx_witness = PartialTxWitness { + inputs: vec![nmo_10, eth_23], + outputs: vec![crv_4840], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let ptx = ptx_witness.commit(); + + assert_eq!( + ptx.balance, + BalanceWitness { + balances: vec![ + UnitBalance { + unit: nmo, + pos: 0, + neg: 10 + }, + UnitBalance { + unit: eth, + pos: 0, + neg: 23 + }, + UnitBalance { + unit: crv, + pos: 4840, + neg: 0 + }, + ], + blinding: ptx_witness.balance_blinding + } + .commit() + ); + } +} diff --git a/emmarin/cl/cl/tests/simple_transfer.rs b/emmarin/cl/cl/tests/simple_transfer.rs new file mode 100644 index 00000000..f1a7a958 --- /dev/null +++ b/emmarin/cl/cl/tests/simple_transfer.rs @@ -0,0 +1,37 @@ +use cl::{note::derive_unit, BalanceWitness}; + +fn receive_utxo(note: cl::NoteWitness, nf_pk: cl::NullifierCommitment) -> cl::OutputWitness { + cl::OutputWitness::new(note, nf_pk) +} + +#[test] +fn test_simple_transfer() { + let nmo = derive_unit("NMO"); + let mut rng = rand::thread_rng(); + + let sender_nf_sk = cl::NullifierSecret::random(&mut rng); + let sender_nf_pk = sender_nf_sk.commit(); + + let recipient_nf_pk = cl::NullifierSecret::random(&mut rng).commit(); + + // Assume the sender has received an unspent output from somewhere + let utxo = receive_utxo(cl::NoteWitness::basic(10, nmo, &mut rng), sender_nf_pk); + + // and wants to send 8 NMO to some recipient and return 2 NMO to itself. + let recipient_output = + cl::OutputWitness::new(cl::NoteWitness::basic(8, nmo, &mut rng), recipient_nf_pk); + let change_output = + cl::OutputWitness::new(cl::NoteWitness::basic(2, nmo, &mut rng), sender_nf_pk); + + let ptx_witness = cl::PartialTxWitness { + inputs: vec![cl::InputWitness::from_output(utxo, sender_nf_sk)], + outputs: vec![recipient_output, change_output], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let bundle = cl::BundleWitness { + partials: vec![ptx_witness], + }; + + assert!(bundle.balance().is_zero()) +} diff --git a/emmarin/cl/ledger/Cargo.toml b/emmarin/cl/ledger/Cargo.toml new file mode 100644 index 00000000..ac37f565 --- /dev/null +++ b/emmarin/cl/ledger/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ledger" +version = "0.1.0" +edition = "2021" + +[dependencies] +cl = { path = "../cl" } +ledger_proof_statements = { path = "../ledger_proof_statements" } +nomos_cl_risc0_proofs = { path = "../risc0_proofs" } +risc0-zkvm = { version = "1.0", features = ["prove", "metal"] } +risc0-groth16 = { version = "1.0" } +rand = "0.8.5" +rand_core = "0.6.0" +thiserror = "1.0.62" +sha2 = "0.10" diff --git a/emmarin/cl/ledger/src/bundle.rs b/emmarin/cl/ledger/src/bundle.rs new file mode 100644 index 00000000..df157fbb --- /dev/null +++ b/emmarin/cl/ledger/src/bundle.rs @@ -0,0 +1,67 @@ +use ledger_proof_statements::bundle::BundlePrivate; + +use crate::error::{Error, Result}; + +pub struct ProvedBundle { + pub bundle: cl::Bundle, + pub risc0_receipt: risc0_zkvm::Receipt, +} + +impl ProvedBundle { + pub fn prove(bundle_witness: &cl::BundleWitness) -> Result { + // need to show that bundle is balanced. + // i.e. the sum of ptx balances is 0 + + let bundle_private = BundlePrivate { + balances: bundle_witness + .partials + .iter() + .map(|ptx| ptx.balance()) + .collect(), + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&bundle_private) + .unwrap() + .build() + .unwrap(); + + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, nomos_cl_risc0_proofs::BUNDLE_ELF, &opts) + .map_err(|_| Error::Risc0ProofFailed)?; + + println!( + "STARK 'bundle' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + let receipt = prove_info.receipt; + + Ok(Self { + bundle: bundle_witness.commit(), + risc0_receipt: receipt, + }) + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self) -> bool { + let Ok(bundle_public) = self.public() else { + return false; + }; + + Vec::from_iter(self.bundle.partials.iter().map(|ptx| ptx.balance)) == bundle_public.balances + && self + .risc0_receipt + .verify(nomos_cl_risc0_proofs::BUNDLE_ID) + .is_ok() + } +} diff --git a/emmarin/cl/ledger/src/constraint.rs b/emmarin/cl/ledger/src/constraint.rs new file mode 100644 index 00000000..b94302d2 --- /dev/null +++ b/emmarin/cl/ledger/src/constraint.rs @@ -0,0 +1,87 @@ +use cl::Constraint; +use ledger_proof_statements::constraint::ConstraintPublic; + +use crate::error::Result; + +#[derive(Debug, Clone)] +pub struct ConstraintProof { + pub risc0_id: [u32; 8], + pub risc0_receipt: risc0_zkvm::Receipt, +} + +pub fn risc0_constraint(risc0_id: [u32; 8]) -> Constraint { + // Commit to a RISC0 ID for use as a note constraint + + let mut bytes = [0u8; 32]; + + for (i, word) in risc0_id.iter().enumerate() { + let word_bytes = word.to_le_bytes(); + bytes[i * 4] = word_bytes[0]; + bytes[i * 4 + 1] = word_bytes[1]; + bytes[i * 4 + 2] = word_bytes[2]; + bytes[i * 4 + 3] = word_bytes[3]; + } + + Constraint::from_vk(&bytes) +} + +impl ConstraintProof { + pub fn from_risc0(risc0_id: [u32; 8], risc0_receipt: risc0_zkvm::Receipt) -> Self { + Self { + risc0_id, + risc0_receipt, + } + } + + pub fn constraint(&self) -> Constraint { + risc0_constraint(self.risc0_id) + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self, expected_public: ConstraintPublic) -> bool { + let Ok(public) = self.public() else { + return false; + }; + + expected_public == public && self.risc0_receipt.verify(self.risc0_id).is_ok() + } + + pub fn nop_constraint() -> Constraint { + risc0_constraint(nomos_cl_risc0_proofs::CONSTRAINT_NOP_ID) + } + + pub fn prove_nop(nf: cl::Nullifier, ptx_root: cl::PtxRoot, block_height: u64) -> Self { + let constraint_public = ConstraintPublic { nf, ptx_root, block_height }; + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&constraint_public) + .unwrap() + .build() + .unwrap(); + + // Obtain the default prover. + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + // Proof information by proving the specified ELF binary. + // This struct contains the receipt along with statistics about execution of the guest + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, nomos_cl_risc0_proofs::CONSTRAINT_NOP_ELF, &opts) + .unwrap(); + + println!( + "STARK 'constraint-nop' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + // extract the receipt. + let receipt = prove_info.receipt; + + Self::from_risc0(nomos_cl_risc0_proofs::CONSTRAINT_NOP_ID, receipt) + } +} diff --git a/emmarin/cl/ledger/src/error.rs b/emmarin/cl/ledger/src/error.rs new file mode 100644 index 00000000..4431a25f --- /dev/null +++ b/emmarin/cl/ledger/src/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +pub type Result = core::result::Result; + +#[derive(Error, Debug)] +pub enum Error { + #[error("risc0 failed to serde")] + Risc0Serde(#[from] risc0_zkvm::serde::Error), + #[error("risc0 failed to prove execution of the zkvm")] + Risc0ProofFailed, +} diff --git a/emmarin/cl/ledger/src/lib.rs b/emmarin/cl/ledger/src/lib.rs new file mode 100644 index 00000000..cb6d4cac --- /dev/null +++ b/emmarin/cl/ledger/src/lib.rs @@ -0,0 +1,6 @@ +pub mod bundle; +pub mod constraint; +pub mod error; +pub mod partial_tx; + +pub use constraint::ConstraintProof; diff --git a/emmarin/cl/ledger/src/partial_tx.rs b/emmarin/cl/ledger/src/partial_tx.rs new file mode 100644 index 00000000..0e87e06c --- /dev/null +++ b/emmarin/cl/ledger/src/partial_tx.rs @@ -0,0 +1,124 @@ +use std::collections::BTreeMap; + +use ledger_proof_statements::{ + constraint::ConstraintPublic, + ptx::{PtxPrivate, PtxPublic}, +}; + +use crate::{ + constraint::ConstraintProof, + error::{Error, Result}, +}; + +const MAX_NOTE_COMMS: usize = 2usize.pow(8); + +pub struct ProvedPartialTx { + pub ptx: cl::PartialTx, + pub cm_root: [u8; 32], + pub constraint_proofs: BTreeMap, + pub risc0_receipt: risc0_zkvm::Receipt, +} + +impl ProvedPartialTx { + pub fn prove( + ptx: &cl::PartialTxWitness, + constraint_proofs: BTreeMap, + note_commitments: &[cl::NoteCommitment], + ) -> Result { + let cm_leaves = note_commitment_leaves(note_commitments); + + let input_cm_paths = Vec::from_iter(ptx.inputs.iter().map(|input| { + let output_cm = input.note_commitment(); + + let cm_idx = note_commitments + .iter() + .position(|c| c == &output_cm) + .unwrap(); + + cl::merkle::path(cm_leaves, cm_idx) + })); + let cm_root = cl::merkle::root(cm_leaves); + let ptx_private = PtxPrivate { + ptx: ptx.clone(), + input_cm_paths, + cm_root, + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&ptx_private) + .unwrap() + .build() + .unwrap(); + + // Obtain the default prover. + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + // Proof information by proving the specified ELF binary. + // This struct contains the receipt along with statistics about execution of the guest + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, nomos_cl_risc0_proofs::PTX_ELF, &opts) + .map_err(|_| Error::Risc0ProofFailed)?; + + println!( + "STARK 'ptx' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + Ok(Self { + ptx: ptx.commit(), + cm_root, + risc0_receipt: prove_info.receipt, + constraint_proofs, + }) + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self) -> bool { + let Ok(proved_ptx_inputs) = self.public() else { + return false; + }; + let expected_ptx_inputs = PtxPublic { + ptx: self.ptx.clone(), + cm_root: self.cm_root, + }; + if expected_ptx_inputs != proved_ptx_inputs { + return false; + } + + let ptx_root = self.ptx.root(); + + for input in self.ptx.inputs.iter() { + let nf = input.nullifier; + let Some(constraint_proof) = self.constraint_proofs.get(&nf) else { + return false; + }; + if input.constraint != constraint_proof.constraint() { + // ensure the constraint proof is actually for this input + return false; + } + + let proved_public = constraint_proof.public().unwrap(); + // TODO: validator must validate that the proved block height is within the range of the executor ticket + if !constraint_proof.verify(ConstraintPublic { nf, ptx_root, block_height: proved_public.block_height }) { + // verify the constraint was satisfied + return false; + } + } + + self.risc0_receipt + .verify(nomos_cl_risc0_proofs::PTX_ID) + .is_ok() + } +} + +fn note_commitment_leaves(note_commitments: &[cl::NoteCommitment]) -> [[u8; 32]; MAX_NOTE_COMMS] { + let note_comm_bytes = Vec::from_iter(note_commitments.iter().map(|c| c.as_bytes().to_vec())); + cl::merkle::padded_leaves::(¬e_comm_bytes) +} diff --git a/emmarin/cl/ledger/tests/simple_transfer.rs b/emmarin/cl/ledger/tests/simple_transfer.rs new file mode 100644 index 00000000..e209f3de --- /dev/null +++ b/emmarin/cl/ledger/tests/simple_transfer.rs @@ -0,0 +1,80 @@ +use std::collections::BTreeMap; + +use cl::{note::derive_unit, BalanceWitness}; +use ledger::{bundle::ProvedBundle, constraint::ConstraintProof, partial_tx::ProvedPartialTx}; +use rand_core::CryptoRngCore; + +struct User(cl::NullifierSecret); + +impl User { + fn random(mut rng: impl CryptoRngCore) -> Self { + Self(cl::NullifierSecret::random(&mut rng)) + } + + fn pk(&self) -> cl::NullifierCommitment { + self.0.commit() + } + + fn sk(&self) -> cl::NullifierSecret { + self.0 + } +} + +fn receive_utxo(note: cl::NoteWitness, nf_pk: cl::NullifierCommitment) -> cl::OutputWitness { + cl::OutputWitness::new(note, nf_pk) +} + +#[test] +fn test_simple_transfer() { + let nmo = derive_unit("NMO"); + + let mut rng = rand::thread_rng(); + + // alice is sending 8 NMO to bob. + + let alice = User::random(&mut rng); + let bob = User::random(&mut rng); + + // Alice has an unspent note worth 10 NMO + let utxo = receive_utxo( + cl::NoteWitness::stateless(10, nmo, ConstraintProof::nop_constraint(), &mut rng), + alice.pk(), + ); + let alices_input = cl::InputWitness::from_output(utxo, alice.sk()); + + // Alice wants to send 8 NMO to bob + let bobs_output = cl::OutputWitness::new(cl::NoteWitness::basic(8, nmo, &mut rng), bob.pk()); + + // .. and return the 2 NMO in change to herself. + let change_output = + cl::OutputWitness::new(cl::NoteWitness::basic(2, nmo, &mut rng), alice.pk()); + + // Construct the ptx consuming Alices inputs and producing the two outputs. + let ptx_witness = cl::PartialTxWitness { + inputs: vec![alices_input], + outputs: vec![bobs_output, change_output], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + // Prove the constraints for alices input (she uses the no-op constraint) + let constraint_proofs = BTreeMap::from_iter(ptx_witness.inputs.iter().map(|i| { + ( + i.nullifier(), + ConstraintProof::prove_nop(i.nullifier(), ptx_witness.commit().root()), + ) + })); + + // assume we only have one note commitment on chain for now ... + let note_commitments = vec![utxo.commit_note()]; + let proved_ptx = + ProvedPartialTx::prove(&ptx_witness, constraint_proofs, ¬e_commitments).unwrap(); + + assert!(proved_ptx.verify()); // It's a valid ptx. + + let bundle_witness = cl::BundleWitness { + partials: vec![ptx_witness], + }; + + let proved_bundle = ProvedBundle::prove(&bundle_witness).unwrap(); + assert!(proved_bundle.verify()); // The bundle is balanced. +} diff --git a/emmarin/cl/ledger_proof_statements/Cargo.toml b/emmarin/cl/ledger_proof_statements/Cargo.toml new file mode 100644 index 00000000..65ea695a --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ledger_proof_statements" +version = "0.1.0" +edition = "2021" + +[dependencies] +cl = { path = "../cl" } +serde = { version = "1.0", features = ["derive"] } diff --git a/emmarin/cl/ledger_proof_statements/src/bundle.rs b/emmarin/cl/ledger_proof_statements/src/bundle.rs new file mode 100644 index 00000000..f45feb90 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/bundle.rs @@ -0,0 +1,12 @@ +use cl::{Balance, BalanceWitness}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BundlePublic { + pub balances: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BundlePrivate { + pub balances: Vec, +} diff --git a/emmarin/cl/ledger_proof_statements/src/constraint.rs b/emmarin/cl/ledger_proof_statements/src/constraint.rs new file mode 100644 index 00000000..fbac81ef --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/constraint.rs @@ -0,0 +1,10 @@ +use cl::{Nullifier, PtxRoot}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConstraintPublic { + pub nf: Nullifier, + pub ptx_root: PtxRoot, + pub block_height: u64, + // pub ticket: Option +} diff --git a/emmarin/cl/ledger_proof_statements/src/lib.rs b/emmarin/cl/ledger_proof_statements/src/lib.rs new file mode 100644 index 00000000..9d5377f0 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/lib.rs @@ -0,0 +1,3 @@ +pub mod constraint; +pub mod ptx; +pub mod bundle; diff --git a/emmarin/cl/ledger_proof_statements/src/ptx.rs b/emmarin/cl/ledger_proof_statements/src/ptx.rs new file mode 100644 index 00000000..c65eab6d --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/ptx.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; +use cl::{PartialTx, PartialTxWitness}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PtxPublic { + pub ptx: PartialTx, + pub cm_root: [u8; 32], +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PtxPrivate { + pub ptx: PartialTxWitness, + pub input_cm_paths: Vec>, + pub cm_root: [u8; 32], +} diff --git a/emmarin/cl/risc0_proofs/Cargo.toml b/emmarin/cl/risc0_proofs/Cargo.toml new file mode 100644 index 00000000..21cc2961 --- /dev/null +++ b/emmarin/cl/risc0_proofs/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nomos_cl_risc0_proofs" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "1.0" } + +[package.metadata.risc0] +methods = ["bundle", "constraint_nop", "ptx"] + diff --git a/emmarin/cl/risc0_proofs/build.rs b/emmarin/cl/risc0_proofs/build.rs new file mode 100644 index 00000000..08a8a4eb --- /dev/null +++ b/emmarin/cl/risc0_proofs/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/emmarin/cl/risc0_proofs/bundle/Cargo.toml b/emmarin/cl/risc0_proofs/bundle/Cargo.toml new file mode 100644 index 00000000..f207737e --- /dev/null +++ b/emmarin/cl/risc0_proofs/bundle/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bundle" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +serde = { version = "1.0", features = ["derive"] } +cl = { path = "../../cl" } +ledger_proof_statements = { path = "../../ledger_proof_statements" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/cl/risc0_proofs/bundle/src/main.rs b/emmarin/cl/risc0_proofs/bundle/src/main.rs new file mode 100644 index 00000000..9beec67b --- /dev/null +++ b/emmarin/cl/risc0_proofs/bundle/src/main.rs @@ -0,0 +1,23 @@ +/// Bundle Proof +/// +/// The bundle proof demonstrates that the set of partial transactions +/// balance to zero. i.e. \sum inputs = \sum outputs. +/// +/// This is done by proving knowledge of some blinding factor `r` s.t. +/// \sum outputs - \sum input = 0*G + r*H +/// +/// To avoid doing costly ECC in stark, we compute only the RHS in stark. +/// The sums and equality is checked outside of stark during proof verification. +use risc0_zkvm::guest::env; + +fn main() { + let bundle_private: ledger_proof_statements::bundle::BundlePrivate = env::read(); + + let bundle_public = ledger_proof_statements::bundle::BundlePublic { + balances: Vec::from_iter(bundle_private.balances.iter().map(|b| b.commit())), + }; + + assert!(cl::BalanceWitness::combine(bundle_private.balances, [0u8; 16]).is_zero()); + + env::commit(&bundle_public); +} diff --git a/emmarin/cl/risc0_proofs/constraint_nop/Cargo.toml b/emmarin/cl/risc0_proofs/constraint_nop/Cargo.toml new file mode 100644 index 00000000..1aac2426 --- /dev/null +++ b/emmarin/cl/risc0_proofs/constraint_nop/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "constraint_nop" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +serde = { version = "1.0", features = ["derive"] } +cl = { path = "../../cl" } +ledger_proof_statements = { path = "../../ledger_proof_statements" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/cl/risc0_proofs/constraint_nop/src/main.rs b/emmarin/cl/risc0_proofs/constraint_nop/src/main.rs new file mode 100644 index 00000000..25d9c0c1 --- /dev/null +++ b/emmarin/cl/risc0_proofs/constraint_nop/src/main.rs @@ -0,0 +1,8 @@ +/// Constraint No-op Proof +use ledger_proof_statements::constraint::ConstraintPublic; +use risc0_zkvm::guest::env; + +fn main() { + let public: ConstraintPublic = env::read(); + env::commit(&public); +} diff --git a/emmarin/cl/risc0_proofs/ptx/Cargo.toml b/emmarin/cl/risc0_proofs/ptx/Cargo.toml new file mode 100644 index 00000000..181aef72 --- /dev/null +++ b/emmarin/cl/risc0_proofs/ptx/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ptx" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } +serde = { version = "1.0", features = ["derive"] } +cl = { path = "../../cl" } +ledger_proof_statements = { path = "../../ledger_proof_statements" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/cl/risc0_proofs/ptx/src/main.rs b/emmarin/cl/risc0_proofs/ptx/src/main.rs new file mode 100644 index 00000000..4b3a2517 --- /dev/null +++ b/emmarin/cl/risc0_proofs/ptx/src/main.rs @@ -0,0 +1,28 @@ +/// Input Proof +use cl::merkle; +use ledger_proof_statements::ptx::{PtxPrivate, PtxPublic}; +use risc0_zkvm::guest::env; + +fn main() { + let PtxPrivate { + ptx, + input_cm_paths, + cm_root, + } = env::read(); + + assert_eq!(ptx.inputs.len(), input_cm_paths.len()); + for (input, cm_path) in ptx.inputs.iter().zip(input_cm_paths) { + let note_cm = input.note_commitment(); + let cm_leaf = merkle::leaf(note_cm.as_bytes()); + assert_eq!(cm_root, merkle::path_root(cm_leaf, &cm_path)); + } + + for output in ptx.outputs.iter() { + assert!(output.note.value > 0); + } + + env::commit(&PtxPublic { + ptx: ptx.commit(), + cm_root, + }); +} diff --git a/emmarin/cl/risc0_proofs/src/lib.rs b/emmarin/cl/risc0_proofs/src/lib.rs new file mode 100644 index 00000000..1bdb3085 --- /dev/null +++ b/emmarin/cl/risc0_proofs/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs"));