From 680b10960c0b1bed3f742e0bd89f9c2d3be8577f Mon Sep 17 00:00:00 2001 From: oleh Date: Thu, 6 Feb 2025 06:06:18 +0100 Subject: [PATCH 01/16] feat: setup co-noir & switch to U252 (#9) --- packages/contracts/contracts/PoolERC20.sol | 4 +- packages/contracts/contracts/Utils.sol | 9 +- packages/contracts/noir/common/Nargo.toml | 1 - .../contracts/noir/common/src/erc20_note.nr | 6 +- packages/contracts/noir/common/src/lib.nr | 6 +- packages/contracts/noir/common/src/uint252.nr | 192 ++++++++++++++++++ .../noir/erc20_transfer/Prover1.toml | 116 +++++++++++ .../noir/erc20_transfer/Prover2.toml | 3 + packages/contracts/noir/run.sh | 87 ++++++++ packages/contracts/noir/timer.sh | 19 ++ packages/contracts/sdk/RollupService.ts | 10 +- packages/contracts/sdk/utils.ts | 11 +- 12 files changed, 442 insertions(+), 22 deletions(-) create mode 100644 packages/contracts/noir/common/src/uint252.nr create mode 100644 packages/contracts/noir/erc20_transfer/Prover1.toml create mode 100644 packages/contracts/noir/erc20_transfer/Prover2.toml create mode 100755 packages/contracts/noir/run.sh create mode 100644 packages/contracts/noir/timer.sh diff --git a/packages/contracts/contracts/PoolERC20.sol b/packages/contracts/contracts/PoolERC20.sol index 3ac4995..ea9731c 100644 --- a/packages/contracts/contracts/PoolERC20.sol +++ b/packages/contracts/contracts/PoolERC20.sol @@ -43,7 +43,7 @@ contract PoolERC20 is PoolGeneric { ) external { token.safeTransferFrom(msg.sender, address(this), amount); - PublicInputs.Type memory pi = PublicInputs.create(2 + 2 + U256_LIMBS); + PublicInputs.Type memory pi = PublicInputs.create(2 + 2 + 1); pi.push(getNoteHashTree().root); pi.push(getNullifierTree().root); pi.push(address(token)); @@ -70,7 +70,7 @@ contract PoolERC20 is PoolGeneric { bytes32 nullifier, NoteInput calldata changeNote ) external { - PublicInputs.Type memory pi = PublicInputs.create(6 + U256_LIMBS); + PublicInputs.Type memory pi = PublicInputs.create(6 + 1); // params pi.push(getNoteHashTree().root); pi.push(getNullifierTree().root); diff --git a/packages/contracts/contracts/Utils.sol b/packages/contracts/contracts/Utils.sol index c95a819..92e5d13 100644 --- a/packages/contracts/contracts/Utils.sol +++ b/packages/contracts/contracts/Utils.sol @@ -89,10 +89,11 @@ library PublicInputs { Type memory publicInputs, uint256 value ) internal pure { - uint256[U256_LIMBS] memory limbs = toNoirU256(value); - for (uint256 i = 0; i < limbs.length; i++) { - push(publicInputs, limbs[i]); - } + push(publicInputs, value); + // uint256[U256_LIMBS] memory limbs = toNoirU256(value); + // for (uint256 i = 0; i < limbs.length; i++) { + // push(publicInputs, limbs[i]); + // } } function finish( diff --git a/packages/contracts/noir/common/Nargo.toml b/packages/contracts/noir/common/Nargo.toml index 362610b..39cb628 100644 --- a/packages/contracts/noir/common/Nargo.toml +++ b/packages/contracts/noir/common/Nargo.toml @@ -6,5 +6,4 @@ compiler_version = ">=0.39.0" [dependencies] protocol_types = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "aztec-packages-v0.67.0", directory = "noir-projects/noir-protocol-circuits/crates/types" } -bignum = { tag = "v0.4.2", git = "https://github.com/noir-lang/noir-bignum/" } nodash = { tag = "v0.39.4", git = "https://github.com/olehmisar/nodash/" } diff --git a/packages/contracts/noir/common/src/erc20_note.nr b/packages/contracts/noir/common/src/erc20_note.nr index 74d66ec..115a361 100644 --- a/packages/contracts/noir/common/src/erc20_note.nr +++ b/packages/contracts/noir/common/src/erc20_note.nr @@ -28,13 +28,13 @@ impl Erc20Note { } } -impl crate::Serialize<6> for Erc20Note { - fn serialize(self) -> [Field; 6] { +impl crate::Serialize<4> for Erc20Note { + fn serialize(self) -> [Field; 4] { self .owner .serialize() .concat(self.amount.token.serialize()) - .concat(self.amount.amount.limbs) + .concat([self.amount.amount.to_integer()]) .concat([self.randomness]) } } diff --git a/packages/contracts/noir/common/src/lib.nr b/packages/contracts/noir/common/src/lib.nr index fb1cad5..4c892cb 100644 --- a/packages/contracts/noir/common/src/lib.nr +++ b/packages/contracts/noir/common/src/lib.nr @@ -1,7 +1,7 @@ -use bignum::{BigNum, fields::U256::U256Params}; use protocol_types::hash::poseidon2_hash_with_separator; mod context; +mod uint252; mod erc20_note; mod note; mod owned_note; @@ -45,7 +45,7 @@ pub global GENERATOR_INDEX__NOTE_NULLIFIER: Field = 2; // Note: keep in sync with other languages pub global U256_LIMBS: u32 = 3; -pub type U256 = BigNum; +pub type U256 = uint252::U252; /// Walmart Aztec address #[derive(Eq, Serialize)] @@ -73,7 +73,7 @@ pub struct TokenAmount { impl TokenAmount { pub fn zero(token: crate::EthAddress) -> Self { - Self { token, amount: U256::new() } + Self { token, amount: U256::zero() } } fn _check(self, other: Self) { diff --git a/packages/contracts/noir/common/src/uint252.nr b/packages/contracts/noir/common/src/uint252.nr new file mode 100644 index 0000000..bf39797 --- /dev/null +++ b/packages/contracts/noir/common/src/uint252.nr @@ -0,0 +1,192 @@ +// Copyright (c) 2025 Clarified Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::cmp::{Eq, Ord, Ordering}; +use std::ops::{Add, Div, Mul, Rem, Sub}; + +// Maximum value for U252 (2^252 - 1), chosen to fit within Aztec's field arithmetic bounds +pub global MAX_U252: Field = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + +pub global U252_PACKED_LEN: u32 = 1; + +pub struct U252 { + value: Field, +} + +impl U252 { + pub fn new(value: Field) -> Self { + value.assert_max_bit_size::<252>(); + Self { value } + } + + pub fn new_unchecked(value: Field) -> Self { + Self { value } + } + + pub fn from_integer(value: Field) -> Self { + value.assert_max_bit_size::<252>(); + Self { value } + } + + pub fn to_integer(self) -> Field { + self.value + } + + pub fn zero() -> Self { + Self { value: 0 } + } + + pub fn one() -> Self { + Self { value: 1 } + } + + pub fn max() -> Self { + Self { value: MAX_U252 } + } + + pub fn is_zero(self) -> bool { + self.value == 0 + } + + // Performs division with remainder using binary long division algorithm + // Returns (quotient, remainder) tuple + pub unconstrained fn div_rem_unconstrained(self, other: Self) -> (Self, Self) { + assert(!(other.value == 0), "Division by zero"); + + self.value.assert_max_bit_size::<252>(); + other.value.assert_max_bit_size::<252>(); + + let bits: [u1; 252] = self.value.to_be_bits(); + let divisor = other.value; + + let mut quotient: Field = 0; + let mut remainder: Field = 0; + + // Process each bit from MSB to LSB, similar to paper-and-pencil division + for i in 0..252 { + // Shift remainder left by 1 bit and add next bit + remainder = remainder * 2 + (bits[i] as Field); + + // Single comparison to determine if we should subtract divisor + // Changed to just !remainder.lt(divisor) which means remainder >= divisor + if !remainder.lt(divisor) { + remainder = remainder - divisor; + quotient = quotient * 2 + 1; + } else { + quotient = quotient * 2; + } + } + (Self { value: quotient }, Self { value: remainder }) + } + + // Performs division with remainder using unconstrained binary long division algorithm, then + // constrains the result via multiplicative properties + // Returns (quotient, remainder) tuple + pub fn div_rem(self, other: Self) -> (Self, Self) { + assert(!(other.value == 0), "Division by zero"); + + if self.value == other.value { + (Self::one(), Self::zero()) + } else if self.is_zero() { + (Self::zero(), Self::zero()) + } else if other.value == 1 { + (self, Self::zero()) + } else if self.value.lt(other.value) { + (Self::zero(), self) + } else { + //Safety: constraining this immediately after by checking the division property + let (quotient, remainder) = unsafe { self.div_rem_unconstrained(other) }; + + // Verify quotient * other + remainder == self + assert( + quotient * other + remainder == self, + "Unconstrained division result is incorrect", + ); + + (quotient, remainder) + } + } + + // Adds two U252 values without overflow checks - use with caution + pub fn add_unchecked(self, other: Self) -> Self { + Self { value: self.value + other.value } + } + + // Subtracts two U252 values without underflow checks - use with caution + pub fn sub_unchecked(self, other: Self) -> Self { + Self { value: self.value - other.value } + } +} + + +impl Add for U252 { + fn add(self, other: Self) -> Self { + let result = self.value + other.value; + result.assert_max_bit_size::<252>(); + + assert(!MAX_U252.lt(result), "U252 addition overflow"); + assert(!result.lt(self.value), "U252 addition overflow"); + assert(!result.lt(other.value), "U252 addition overflow"); + Self { value: result } + } +} + +impl Sub for U252 { + fn sub(self, other: Self) -> Self { + assert( + other.value.lt(self.value) | other.value.eq(self.value), + "U252 subtraction underflow", + ); + let result = self.value - other.value; + result.assert_max_bit_size::<252>(); + Self { value: result } + } +} + +impl Mul for U252 { + fn mul(self, other: Self) -> Self { + let result = self.value * other.value; + + result.assert_max_bit_size::<252>(); + // Allow multiplication by 1 without additional checks, otherwise check for overflow + assert( + (self.value == 1) + | (other.value == 1) + | (result.lt(MAX_U252 + 1) & !result.lt(self.value) & !result.lt(other.value)), + "U252 multiplication overflow", + ); + Self { value: result } + } +} + +impl Div for U252 { + fn div(self, other: Self) -> Self { + let (quotient, _) = self.div_rem(other); + quotient + } +} + +impl Rem for U252 { + fn rem(self, other: Self) -> Self { + let (_, remainder) = self.div_rem(other); + remainder + } +} + +impl Ord for U252 { + fn cmp(self, other: Self) -> Ordering { + if self.value.lt(other.value) { + Ordering::less() + } else if self.value.eq(other.value) { + Ordering::equal() + } else { + Ordering::greater() + } + } +} + +impl Eq for U252 { + fn eq(self, other: Self) -> bool { + self.value.eq(other.value) + } +} diff --git a/packages/contracts/noir/erc20_transfer/Prover1.toml b/packages/contracts/noir/erc20_transfer/Prover1.toml new file mode 100644 index 0000000..f37e4b8 --- /dev/null +++ b/packages/contracts/noir/erc20_transfer/Prover1.toml @@ -0,0 +1,116 @@ +from_secret_key = "0x118f09bc73ec486db2030077142f2bceba2a4d4c9e0f6147d776f8ca8ec02ff1" +change_randomness = "0x0577601b056366fa61051149b73897c8dc91c8563d08d17c50d9322b7a5098ae" + +[tree_roots] +note_hash_root = "0x12c082c76e5eb67cec5d7db4471729b5fb15d495fa6a2d4e5c2b3406946223a0" +nullifier_root = "0x0aa63c509390ad66ecd821998aabb16a818bcc5db5cf4accc0ce1821745244e9" + +[from_note_inputs] +note_sibling_path = [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0b63a53787021a4a962a452c2921b3663aff1ffd8d5510540f8e659e782956f1", + "0x0e34ac2c09f45a503d2908bcb12f1cbae5fa4065759c88d501c097506a8b2290", + "0x21f9172d72fdcdafc312eee05cf5092980dda821da5b760a9fb8dbdf607c8a20", + "0x2373ea368857ec7af97e7b470d705848e2bf93ed7bef142a490f2119bcf82d8e", + "0x120157cfaaa49ce3da30f8b47879114977c24b266d58b0ac18b325d878aafddf", + "0x01c28fe1059ae0237b72334700697bdf465e03df03986fe05200cadeda66bd76", + "0x2d78ed82f93b61ba718b17c2dfe5b52375b4d37cbbed6f1fc98b47614b0cf21b", + "0x067243231eddf4222f3911defbba7705aff06ed45960b27f6f91319196ef97e1", + "0x1849b85f3c693693e732dfc4577217acc18295193bede09ce8b97ad910310972", + "0x2a775ea761d20435b31fa2c33ff07663e24542ffb9e7b293dfce3042eb104686", + "0x0f320b0703439a8114f81593de99cd0b8f3b9bf854601abb5b2ea0e8a3dda4a7", + "0x0d07f6e7a8a0e9199d6d92801fff867002ff5b4808962f9da2ba5ce1bdd26a73", + "0x1c4954081e324939350febc2b918a293ebcdaead01be95ec02fcbe8d2c1635d1", + "0x0197f2171ef99c2d053ee1fb5ff5ab288d56b9b41b4716c9214a4d97facc4c4a", + "0x2b9cdd484c5ba1e4d6efcc3f18734b5ac4c4a0b9102e2aeb48521a661d3feee9", + "0x14f44d672eb357739e42463497f9fdac46623af863eea4d947ca00a497dcdeb3", + "0x071d7627ae3b2eabda8a810227bf04206370ac78dbf6c372380182dbd3711fe3", + "0x2fdc08d9fe075ac58cb8c00f98697861a13b3ab6f9d41a4e768f75e477475bf5", + "0x20165fe405652104dceaeeca92950aa5adc571b8cafe192878cba58ff1be49c5", + "0x1c8c3ca0b3a3d75850fcd4dc7bf1e3445cd0cfff3ca510630fd90b47e8a24755", + "0x1f0c1a8fb16b0d2ac9a146d7ae20d8d179695a92a79ed66fc45d9da4532459b3", + "0x038146ec5a2573e1c30d2fb32c66c8440f426fbd108082df41c7bebd1d521c30", + "0x17d3d12b17fe762de4b835b2180b012e808816a7f2ff69ecb9d65188235d8fd4", + "0x0e1a6b7d63a6e5a9e54e8f391dd4e9d49cdfedcbc87f02cd34d4641d2eb30491", + "0x09244eec34977ff795fc41036996ce974136377f521ac8eb9e04642d204783d2", + "0x1646d6f544ec36df9dc41f778a7ef1690a53c730b501471b6acd202194a7e8e9", + "0x064769603ba3f6c41f664d266ecb9a3a0f6567cd3e48b40f34d4894ee4c361b3", + "0x1595bb3cd19f84619dc2e368175a88d8627a7439eda9397202cdb1167531fd3f", + "0x2a529be462b81ca30265b558763b1498289c9d88277ab14f0838cb1fce4b472c", + "0x0c08da612363088ad0bbc78abd233e8ace4c05a56fdabdd5e5e9b05e428bdaee", + "0x14748d0241710ef47f54b931ac5a58082b1d56b0f0c30d55fb71a6e8c9a6be14", + "0x0b59baa35b9dc267744f0ccb4e3b0255c1fc512460d91130c6bc19fb2668568d", + "0x2c45bb0c3d5bc1dc98e0baef09ff46d18c1a451e724f41c2b675549bb5c80e59", + "0x121468e6710bf1ffec6d0f26743afe6f88ef55dab40b83ca0a39bc44b196374c", + "0x2042c32c823a7440ceb6c342f9125f1fe426b02c527cd8fb28c85d02b705e759", + "0x0d582c10ff8115413aa5b70564fdd2f3cefe1f33a1e43a47bc495081e91e73e5", + "0x0f55a0d491a9da093eb999fa0dffaf904620cbc78d07e63c6f795c5c7512b523", + "0x21849764e1aa64b83a69e39d27eedaec2a8f97066e5ddb74634ffdb11388dd9a", + "0x2e33ee2008411c04b99c24b313513d097a0d21a5040b6193d1f978b8226892d6", +] +note_index = "0x0" + +[from_note_inputs.nullifier_low_leaf_preimage] +nullifier = "16803307459015040401852171866520504004549441014858337312433795419933165652271" +next_nullifier = "16947817896211750602831357878879841536953337186959472835195250218326948105343" +next_index = "38" + +[from_note_inputs.nullifier_low_leaf_membership_witness] +leaf_index = "53" +sibling_path = [ + "89608827205400042212603164533674623076734752674846833106321039558861918097", + "11301393563187046258491469457343761693629010154592613576413167355835381418907", + "12390023533950167391600770646920656387356437563612710359669154167054707439741", + "6737950067533114345487670792281012677572324355958600670420132499294961450278", + "6708144597151111882649888870478814964992609871804461820795238719880019958165", + "7997675936567769706222583500490338185379747281415057124249902920940843577209", + "796074195456137668475057404256202455048248910468542119987582633322559749494", + "20567739078944838550556895816409602128127282297589578747131836752205066334747", + "2915761020738377646169465098196184536995852317462848975418156916828302972897", + "10985760690611977917867463287126968335324276333731556907069004868774077204850", + "19208047717975195819992968481289292904158208618635067144381052124352153142918", + "6873111190261103763395069460662520014470628472871405490586772273844549690535", + "5894139036143562089612233756205231544611692010506775540918923829608719739507", + "12794319561613039897672261721253788651586435024857268094532550402122135778769", + "720777601321551456724742356376872832235514487302799006897322578639686749258", + "19726607866286112953874979389205149577323021278529259017954198462517737418473", + "9477901871732605408863140319391985875503693577321165842544029785283526188723", + "3218243980816964110015535469652973420290887819006413761652914020854170460131", + "21647471328696313483506044180817939310547082363167430262013183074005768690677", + "14513543603428597604998785424833526732416414663942895493375066920249255152069", + "12912536786691007423957206067517486813236154886763950786309034005218474477397", + "14043083790220302639747777938344554939065181077244110063366429914379380349363", + "1585351311391412912983327123858240918248160277807437690996718569990466444336", + "10777443874874414095971316544407516389536700701770254896428522920996906045396", + "6379059771196981783531842116523729103253487220527074934863013362203865842833", + "4134966835882445041808556871336951519851774168539778125719567488662398010322", + "10076045549529902860501553551276683947306207322373274327817594791372714928361", + "2840050510901032730295917342517416830172237966947356225177945514569852215731", + "9763122299140113778865332540237465612993478463904652910937740892142312750399", + "19143097027756952285085354173215061221134962025215187577014441848927452612396", + "5443396159062520964690002960163216626227972885246029607890253069325844601582", + "9252184438226219647561583700014163671525787751930886881150609055515912093204", + "5133978852121333258945738158023312408330097040934952294511033236283137218189", + "20024968741681435992617962040670787662064588050911389666593896948755442699865", + "8177692210107365315198900131750690704270322702202776727756223630359548802892", + "14591970101429819852875418865351062544941910746983613947047278625268406019929", + "6035853708389102381677690046728119823868576653788717783661700952558999073765", + "6935984739519493649769258491382157191948193966139936850002438965559842485539", + "15160592699256931057355536169588270738517810359373901881744156960877034659226", + "20898143714352063775313258090973641367368749294647965931365904988797017821910", +] + +[from_note_inputs.note] +randomness = "0x2f6e3831b5312229f58e719b7ef458465987a7f8473c592fe56a4c4b1e2fdf48" + +[from_note_inputs.note.owner] +inner = "0x28c7eef33d7e5d31b9d2cc09c783294d91c36e05b7b815d96549f91fe0d3b0d4" + +[from_note_inputs.note.amount.token] +inner = "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6" + +[from_note_inputs.note.amount.amount] +value = "500" + +[amount] +value = "123" diff --git a/packages/contracts/noir/erc20_transfer/Prover2.toml b/packages/contracts/noir/erc20_transfer/Prover2.toml new file mode 100644 index 0000000..9f47ffa --- /dev/null +++ b/packages/contracts/noir/erc20_transfer/Prover2.toml @@ -0,0 +1,3 @@ +to_randomness = "0x0d71211e0e89af974bf816f24ba78f9c99ab50ab9f19367490cedb993d0526fe" +[to] +inner = "0x2b7bd70beb13e310f4593dbc807332acba0f01c4586f17cf984eedd7e1437414" diff --git a/packages/contracts/noir/run.sh b/packages/contracts/noir/run.sh new file mode 100755 index 0000000..af6d6c5 --- /dev/null +++ b/packages/contracts/noir/run.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source timer.sh + +nargo compile --silence-warnings +echo "Compiled" + +CIRCUIT_NAME=erc20_transfer +CIRCUIT=target/$CIRCUIT_NAME.json + +# merge Prover1.toml and Prover2.toml into Prover.toml +# Convert TOML to JSON +dasel -f "$CIRCUIT_NAME/Prover1.toml" -r toml -w json > prover1.json +dasel -f "$CIRCUIT_NAME/Prover2.toml" -r toml -w json > prover2.json +# Merge JSON with jq +jq -s '.[0] * .[1]' prover1.json prover2.json > merged.json +# Convert back to TOML +dasel -f merged.json -r json -w toml > "$CIRCUIT_NAME/Prover.toml" +rm prover1.json prover2.json merged.json + +# split input into shares +co-noir split-input --circuit $CIRCUIT --input $CIRCUIT_NAME/Prover1.toml --protocol REP3 --out-dir target +co-noir split-input --circuit $CIRCUIT --input $CIRCUIT_NAME/Prover2.toml --protocol REP3 --out-dir target +echo "Inputs split" + +# merge inputs into single input file +timeStart "merge-input-shares" +co-noir merge-input-shares --inputs target/Prover1.toml.0.shared --inputs target/Prover2.toml.0.shared --protocol REP3 --out target/Prover.toml.0.shared +co-noir merge-input-shares --inputs target/Prover1.toml.1.shared --inputs target/Prover2.toml.1.shared --protocol REP3 --out target/Prover.toml.1.shared +co-noir merge-input-shares --inputs target/Prover1.toml.2.shared --inputs target/Prover2.toml.2.shared --protocol REP3 --out target/Prover.toml.2.shared +timeEnd "merge-input-shares" + +# run witness extension in MPC +timeStart "mpc-generate-witness" +co-noir generate-witness --input target/Prover.toml.0.shared --circuit $CIRCUIT --protocol REP3 --config configs/party0.toml --out target/witness.gz.0.shared & +co-noir generate-witness --input target/Prover.toml.1.shared --circuit $CIRCUIT --protocol REP3 --config configs/party1.toml --out target/witness.gz.1.shared & +co-noir generate-witness --input target/Prover.toml.2.shared --circuit $CIRCUIT --protocol REP3 --config configs/party2.toml --out target/witness.gz.2.shared +wait $(jobs -p) +timeEnd "mpc-generate-witness" + +# run proving in MPC +timeStart "mpc-build-proving-key" +co-noir build-proving-key --witness target/witness.gz.0.shared --circuit $CIRCUIT --crs bn254_g1.dat --protocol REP3 --config configs/party0.toml --out target/proving_key.0 & +co-noir build-proving-key --witness target/witness.gz.1.shared --circuit $CIRCUIT --crs bn254_g1.dat --protocol REP3 --config configs/party1.toml --out target/proving_key.1 & +co-noir build-proving-key --witness target/witness.gz.2.shared --circuit $CIRCUIT --crs bn254_g1.dat --protocol REP3 --config configs/party2.toml --out target/proving_key.2 +wait $(jobs -p) +timeEnd "mpc-build-proving-key" + +timeStart "mpc-generate-proof" +co-noir generate-proof --proving-key target/proving_key.0 --protocol REP3 --hasher KECCAK --config configs/party0.toml --out target/proof.0.proof --public-input target/public_input.json & +co-noir generate-proof --proving-key target/proving_key.1 --protocol REP3 --hasher KECCAK --config configs/party1.toml --out target/proof.1.proof & +co-noir generate-proof --proving-key target/proving_key.2 --protocol REP3 --hasher KECCAK --config configs/party2.toml --out target/proof.2.proof +wait $(jobs -p) +timeEnd "mpc-generate-proof" + +timeStart "bb-generate-witness" +nargo execute --package $CIRCUIT_NAME --silence-warnings +timeEnd "bb-generate-witness" +timeStart "bb-generate-proof" +bb prove_ultra_keccak_honk -b $CIRCUIT -w target/$CIRCUIT_NAME.gz -o target/proof_bb.proof +timeEnd "bb-generate-proof" + + +# Create verification key +co-noir create-vk --circuit $CIRCUIT --crs bn254_g1.dat --hasher KECCAK --vk target/verification_key +echo "Verification key created" + +# verify proof +co-noir verify --proof target/proof.0.proof --vk target/verification_key --hasher KECCAK --crs bn254_g2.dat +echo "Proof verified" + +bb write_vk_ultra_keccak_honk -b $CIRCUIT -o target/verification_key_bb +echo "Verification key created with bb" + +# check if verification keys are the same (yes/no) +cmp -s target/verification_key target/verification_key_bb && echo "Verification keys are the same" || echo "Verification keys are different" +cmp -s target/proof.0.proof target/proof_bb.proof && echo "Proofs are the same" || echo "Proofs are different" + +# Double check with bb +echo "Verifying with bb" +bb verify_ultra_keccak_honk -p target/proof.0.proof -k target/verification_key_bb +echo "Proof verified with bb" + +# Check the bb proof +bb verify_ultra_keccak_honk -p target/proof_bb.proof -k target/verification_key_bb diff --git a/packages/contracts/noir/timer.sh b/packages/contracts/noir/timer.sh new file mode 100644 index 0000000..84fa0c6 --- /dev/null +++ b/packages/contracts/noir/timer.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +declare -A TIMERS + +timeStart() { + TIMERS["$1"]=$(date +%s) # Use seconds instead +} + +timeEnd() { + local start=${TIMERS["$1"]} + if [[ -z "$start" ]]; then + echo "Timer '$1' not found" + return 1 + fi + local end=$(date +%s) + local duration=$(( end - start )) + echo "$1 took ${duration}s" + unset TIMERS["$1"] +} diff --git a/packages/contracts/sdk/RollupService.ts b/packages/contracts/sdk/RollupService.ts index 390557b..5351edc 100644 --- a/packages/contracts/sdk/RollupService.ts +++ b/packages/contracts/sdk/RollupService.ts @@ -8,7 +8,7 @@ import { assert, type AsyncOrSync } from "ts-essentials"; import { type PoolERC20 } from "../typechain-types"; import { EncryptionService } from "./EncryptionService"; import type { ITreesService } from "./RemoteTreesService"; -import { fromNoirU256, prove, toNoirU256, U256_LIMBS } from "./utils.js"; +import { prove, toNoirU256 } from "./utils.js"; // Note: keep in sync with other languages export const NOTE_HASH_TREE_HEIGHT = 40; @@ -393,7 +393,8 @@ export class Erc20Note { return [ BigInt(this.owner.address), BigInt(this.amount.token), - ...amount.amount.limbs.map((x) => BigInt(x)), + // ...amount.amount.limbs.map((x) => BigInt(x)), + BigInt(amount.amount.value), BigInt(this.randomness), ]; } @@ -410,9 +411,10 @@ export class Erc20Note { ), amount: await TokenAmount.from({ token: ethers.zeroPadValue(fieldsStr[1]!, 20), - amount: fromNoirU256({ limbs: fields.slice(2, 2 + U256_LIMBS) }), + // amount: fromNoirU256({ limbs: fields.slice(2, 2 + U256_LIMBS) }), + amount: ethers.toBigInt(fieldsStr[2]!), }), - randomness: ethers.zeroPadValue(fieldsStr[2 + U256_LIMBS]!, 32), + randomness: ethers.zeroPadValue(fieldsStr[3]!, 32), }); } diff --git a/packages/contracts/sdk/utils.ts b/packages/contracts/sdk/utils.ts index 947af9a..ae91f79 100644 --- a/packages/contracts/sdk/utils.ts +++ b/packages/contracts/sdk/utils.ts @@ -48,11 +48,12 @@ export const U256_LIMBS = 3; export const U256_LIMB_SIZE = 120; export function toNoirU256(value: bigint) { - assert(value >= 0n && value < 2n ** 256n, "invalid U256 value"); - const limbs = splitBigIntToLimbs(value, U256_LIMB_SIZE, U256_LIMBS).map( - (x) => "0x" + x.toString(16), - ); - return { limbs }; + return { value: value.toString() }; + // assert(value >= 0n && value < 2n ** 256n, "invalid U256 value"); + // const limbs = splitBigIntToLimbs(value, U256_LIMB_SIZE, U256_LIMBS).map( + // (x) => "0x" + x.toString(16), + // ); + // return { limbs }; } export function fromNoirU256(value: { limbs: (bigint | string)[] }) { From 4512652857bed699137485ec374ada36325628a7 Mon Sep 17 00:00:00 2001 From: oleh Date: Thu, 6 Feb 2025 06:10:55 +0100 Subject: [PATCH 02/16] feat: dark pool router POC (#10) --- packages/contracts/contracts/PoolERC20.sol | 37 ++++++ packages/contracts/deploy/00_deploy.ts | 5 + packages/contracts/noir/Nargo.toml | 2 + packages/contracts/noir/common/src/lib.nr | 7 ++ packages/contracts/noir/lob_router/Nargo.toml | 8 ++ packages/contracts/noir/lob_router/src/lib.nr | 34 +++++ .../contracts/noir/lob_router_swap/Nargo.toml | 9 ++ .../noir/lob_router_swap/Prover1.toml | 117 ++++++++++++++++++ .../noir/lob_router_swap/Prover2.toml | 112 +++++++++++++++++ .../noir/lob_router_swap/src/main.nr | 29 +++++ packages/contracts/noir/run.sh | 2 +- packages/contracts/sdk/LobService.ts | 113 +++++++++++++++++ packages/contracts/sdk/sdk.ts | 5 +- packages/contracts/test/PoolERC20.test.ts | 36 ++++++ 14 files changed, 514 insertions(+), 2 deletions(-) create mode 100644 packages/contracts/noir/lob_router/Nargo.toml create mode 100644 packages/contracts/noir/lob_router/src/lib.nr create mode 100644 packages/contracts/noir/lob_router_swap/Nargo.toml create mode 100644 packages/contracts/noir/lob_router_swap/Prover1.toml create mode 100644 packages/contracts/noir/lob_router_swap/Prover2.toml create mode 100644 packages/contracts/noir/lob_router_swap/src/main.nr create mode 100644 packages/contracts/sdk/LobService.ts diff --git a/packages/contracts/contracts/PoolERC20.sol b/packages/contracts/contracts/PoolERC20.sol index ea9731c..67a69ba 100644 --- a/packages/contracts/contracts/PoolERC20.sol +++ b/packages/contracts/contracts/PoolERC20.sol @@ -20,6 +20,7 @@ contract PoolERC20 is PoolGeneric { IVerifier unshieldVerifier; IVerifier joinVerifier; IVerifier transferVerifier; + IVerifier swapVerifier; } constructor( @@ -27,12 +28,14 @@ contract PoolERC20 is PoolGeneric { IVerifier unshieldVerifier, IVerifier joinVerifier, IVerifier transferVerifier, + IVerifier swapVerifier, IVerifier rollupVerifier ) PoolGeneric(rollupVerifier) { _poolErc20Storage().shieldVerifier = shieldVerifier; _poolErc20Storage().unshieldVerifier = unshieldVerifier; _poolErc20Storage().joinVerifier = joinVerifier; _poolErc20Storage().transferVerifier = transferVerifier; + _poolErc20Storage().swapVerifier = swapVerifier; } function shield( @@ -154,6 +157,40 @@ contract PoolERC20 is PoolGeneric { } } + // TODO: move to a separate contract + function swap( + bytes calldata proof, + NoteInput[4] calldata notes, + bytes32[2] calldata nullifiers + ) external { + PublicInputs.Type memory pi = PublicInputs.create(8); + pi.push(getNoteHashTree().root); + pi.push(getNullifierTree().root); + pi.push(notes[0].noteHash); + pi.push(notes[1].noteHash); + pi.push(notes[2].noteHash); + pi.push(notes[3].noteHash); + pi.push(nullifiers[0]); + pi.push(nullifiers[1]); + require( + _poolErc20Storage().swapVerifier.verify(proof, pi.finish()), + "Invalid swap proof" + ); + + { + NoteInput[] memory noteInputs = new NoteInput[](4); + noteInputs[0] = notes[0]; + noteInputs[1] = notes[1]; + noteInputs[2] = notes[2]; + noteInputs[3] = notes[3]; + bytes32[] memory nullifiersDyn = new bytes32[](nullifiers.length); + for (uint256 i = 0; i < nullifiers.length; i++) { + nullifiersDyn[i] = nullifiers[i]; + } + _PoolGeneric_addPendingTx(noteInputs, nullifiersDyn); + } + } + function _poolErc20Storage() private pure diff --git a/packages/contracts/deploy/00_deploy.ts b/packages/contracts/deploy/00_deploy.ts index 840652c..2dde18a 100644 --- a/packages/contracts/deploy/00_deploy.ts +++ b/packages/contracts/deploy/00_deploy.ts @@ -34,6 +34,10 @@ const deploy: DeployFunction = async ({ "Erc20TransferVerifier", "erc20_transfer", ); + const swapVerifier = await deployVerifier( + "LobRouterSwapVerifier", + "lob_router_swap", + ); const rollupVerifier = await deployVerifier("RollupVerifier", "rollup"); const pool = await typedDeployments.deploy("PoolERC20", { @@ -44,6 +48,7 @@ const deploy: DeployFunction = async ({ unshieldVerifier.address, joinVerifier.address, transferVerifier.address, + swapVerifier.address, rollupVerifier.address, ], }); diff --git a/packages/contracts/noir/Nargo.toml b/packages/contracts/noir/Nargo.toml index 7e79d22..c980995 100644 --- a/packages/contracts/noir/Nargo.toml +++ b/packages/contracts/noir/Nargo.toml @@ -5,6 +5,8 @@ members = [ "erc20_unshield", "erc20_join", "erc20_transfer", + "lob_router", + "lob_router_swap", "rollup", "common", ] diff --git a/packages/contracts/noir/common/src/lib.nr b/packages/contracts/noir/common/src/lib.nr index 4c892cb..8a50cd3 100644 --- a/packages/contracts/noir/common/src/lib.nr +++ b/packages/contracts/noir/common/src/lib.nr @@ -95,6 +95,13 @@ impl std::ops::Sub for TokenAmount { } } +impl std::cmp::Ord for TokenAmount { + fn cmp(self, other: Self) -> std::cmp::Ordering { + self._check(other); + self.amount.cmp(other.amount) + } +} + pub struct TreeRoots { pub note_hash_root: Field, pub nullifier_root: Field, diff --git a/packages/contracts/noir/lob_router/Nargo.toml b/packages/contracts/noir/lob_router/Nargo.toml new file mode 100644 index 0000000..8e8b236 --- /dev/null +++ b/packages/contracts/noir/lob_router/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "lob_router" +type = "lib" +authors = ["Oleh Misarosh "] + +[dependencies] +common = { path = "../common" } +erc20 = { path = "../erc20" } diff --git a/packages/contracts/noir/lob_router/src/lib.nr b/packages/contracts/noir/lob_router/src/lib.nr new file mode 100644 index 0000000..2124347 --- /dev/null +++ b/packages/contracts/noir/lob_router/src/lib.nr @@ -0,0 +1,34 @@ +mod LobRouter { + pub fn swap( + context: &mut common::Context, + seller_secret_key: Field, + seller_note: erc20::Erc20NoteConsumptionInputs, + seller_amount: common::U256, + seller_randomness: Field, + buyer_secret_key: Field, + buyer_note: erc20::Erc20NoteConsumptionInputs, + buyer_amount: common::U256, + buyer_randomness: Field, + ) { + // TODO(security): orders must be signed by parties and the prices should match + erc20::Token::transfer( + context, + seller_secret_key, + seller_note, + buyer_note.note.owner(), + seller_amount, + buyer_randomness, + seller_randomness, + ); + + erc20::Token::transfer( + context, + buyer_secret_key, + buyer_note, + seller_note.note.owner(), + buyer_amount, + seller_randomness, + buyer_randomness, + ); + } +} diff --git a/packages/contracts/noir/lob_router_swap/Nargo.toml b/packages/contracts/noir/lob_router_swap/Nargo.toml new file mode 100644 index 0000000..1e9d5a5 --- /dev/null +++ b/packages/contracts/noir/lob_router_swap/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "lob_router_swap" +type = "bin" +authors = ["Oleh Misarosh pub common::Result<4, 2> { + let mut context = common::Context::from(tree_roots); + + lob_router::LobRouter::swap( + &mut context, + seller_secret_key, + seller_note, + seller_amount, + seller_randomness, + buyer_secret_key, + buyer_note, + buyer_amount, + buyer_randomness, + ); + + context.finish() +} diff --git a/packages/contracts/noir/run.sh b/packages/contracts/noir/run.sh index af6d6c5..bf43861 100755 --- a/packages/contracts/noir/run.sh +++ b/packages/contracts/noir/run.sh @@ -7,7 +7,7 @@ source timer.sh nargo compile --silence-warnings echo "Compiled" -CIRCUIT_NAME=erc20_transfer +CIRCUIT_NAME=lob_router_swap CIRCUIT=target/$CIRCUIT_NAME.json # merge Prover1.toml and Prover2.toml into Prover.toml diff --git a/packages/contracts/sdk/LobService.ts b/packages/contracts/sdk/LobService.ts new file mode 100644 index 0000000..92774c3 --- /dev/null +++ b/packages/contracts/sdk/LobService.ts @@ -0,0 +1,113 @@ +import { type AsyncOrSync } from "ts-essentials"; +import { type PoolERC20 } from "../typechain-types"; +import { NoteInputStruct } from "../typechain-types/contracts/PoolERC20"; +import { type ITreesService } from "./RemoteTreesService.js"; +import { + CompleteWaAddress, + Erc20Note, + TokenAmount, + type NoirAndBackend, + type PoolErc20Service, +} from "./RollupService.js"; +import { prove, toNoirU256 } from "./utils.js"; + +export class LobService { + constructor( + private contract: PoolERC20, + private trees: ITreesService, + private poolErc20: PoolErc20Service, + private circuits: AsyncOrSync<{ + swap: NoirAndBackend; + }>, + ) {} + + async swap(params: { + sellerSecretKey: string; + sellerNote: Erc20Note; + sellerAmount: bigint; + buyerSecretKey: string; + buyerNote: Erc20Note; + buyerAmount: bigint; + }) { + const { Fr } = await import("@aztec/aztec.js"); + + const swapCircuit = (await this.circuits).swap; + const sellerRandomness = Fr.random().toString(); + const buyerRandomness = Fr.random().toString(); + + const sellerChangeNote = await Erc20Note.from({ + owner: await CompleteWaAddress.fromSecretKey(params.sellerSecretKey), + amount: await TokenAmount.from({ + token: params.sellerNote.amount.token, + amount: params.sellerNote.amount.amount - params.sellerAmount, + }), + randomness: sellerRandomness, + }); + const buyerChangeNote = await Erc20Note.from({ + owner: await CompleteWaAddress.fromSecretKey(params.buyerSecretKey), + amount: await TokenAmount.from({ + token: params.buyerNote.amount.token, + amount: params.buyerNote.amount.amount - params.buyerAmount, + }), + randomness: buyerRandomness, + }); + const sellerSwapNote = await Erc20Note.from({ + owner: await CompleteWaAddress.fromSecretKey(params.sellerSecretKey), + amount: await TokenAmount.from({ + token: params.buyerNote.amount.token, + amount: params.buyerAmount, + }), + randomness: sellerRandomness, + }); + const buyerSwapNote = await Erc20Note.from({ + owner: await CompleteWaAddress.fromSecretKey(params.buyerSecretKey), + amount: await TokenAmount.from({ + token: params.sellerNote.amount.token, + amount: params.sellerAmount, + }), + randomness: buyerRandomness, + }); + + const input = { + tree_roots: await this.trees.getTreeRoots(), + seller_secret_key: params.sellerSecretKey, + seller_note: await this.poolErc20.toNoteConsumptionInputs( + params.sellerSecretKey, + params.sellerNote, + ), + seller_amount: toNoirU256(params.sellerAmount), + seller_randomness: sellerRandomness, + + buyer_secret_key: params.buyerSecretKey, + buyer_note: await this.poolErc20.toNoteConsumptionInputs( + params.buyerSecretKey, + params.buyerNote, + ), + buyer_amount: toNoirU256(params.buyerAmount), + buyer_randomness: buyerRandomness, + }; + const { proof } = await prove("swap", swapCircuit, input); + const noteInputs: [ + NoteInputStruct, + NoteInputStruct, + NoteInputStruct, + NoteInputStruct, + ] = [ + await sellerChangeNote.toSolidityNoteInput(), + await buyerSwapNote.toSolidityNoteInput(), + await buyerChangeNote.toSolidityNoteInput(), + await sellerSwapNote.toSolidityNoteInput(), + ]; + const nullifiers: [string, string] = [ + ( + await params.sellerNote.computeNullifier(params.sellerSecretKey) + ).toString(), + ( + await params.buyerNote.computeNullifier(params.buyerSecretKey) + ).toString(), + ]; + const tx = await this.contract.swap(proof, noteInputs, nullifiers); + const receipt = await tx.wait(); + console.log("swap gas used", receipt?.gasUsed); + } +} diff --git a/packages/contracts/sdk/sdk.ts b/packages/contracts/sdk/sdk.ts index 5fff704..633a0df 100644 --- a/packages/contracts/sdk/sdk.ts +++ b/packages/contracts/sdk/sdk.ts @@ -3,6 +3,7 @@ import { mapValues } from "lodash-es"; import type { AsyncOrSync } from "ts-essentials"; import type { PoolERC20 } from "../typechain-types/index.js"; import { EncryptionService } from "./EncryptionService.js"; +import { LobService } from "./LobService.js"; import { type ITreesService } from "./RemoteTreesService.js"; import { PoolErc20Service } from "./RollupService.js"; @@ -24,7 +25,7 @@ export function createInterfaceSdk( coreSdk: ReturnType, trees: ITreesService, compiledCircuits: Record< - "shield" | "unshield" | "join" | "transfer", + "shield" | "unshield" | "join" | "transfer" | "swap", AsyncOrSync >, ) { @@ -37,9 +38,11 @@ export function createInterfaceSdk( trees, circuits, ); + const lob = new LobService(coreSdk.contract, trees, poolErc20, circuits); return { poolErc20, + lob, }; } diff --git a/packages/contracts/test/PoolERC20.test.ts b/packages/contracts/test/PoolERC20.test.ts index 784f6e5..b689fb8 100644 --- a/packages/contracts/test/PoolERC20.test.ts +++ b/packages/contracts/test/PoolERC20.test.ts @@ -41,6 +41,8 @@ describe("PoolERC20", () => { await usdc.mintForTests(alice, await parseUnits(usdc, "1000000")); await usdc.connect(alice).approve(pool, ethers.MaxUint256); + await btc.mintForTests(bob, await parseUnits(btc, "1000000")); + await btc.connect(bob).approve(pool, ethers.MaxUint256); CompleteWaAddress = (await tsImport("../sdk", __filename)).sdk .CompleteWaAddress; @@ -64,6 +66,7 @@ describe("PoolERC20", () => { unshield: noir.getCircuitJson("erc20_unshield"), join: noir.getCircuitJson("erc20_join"), transfer: noir.getCircuitJson("erc20_transfer"), + swap: noir.getCircuitJson("lob_router_swap"), }); backendSdk = createBackendSdk(coreSdk, trees, { @@ -349,4 +352,37 @@ describe("PoolERC20", () => { await sdk.poolErc20.getBalanceNotesOf(usdc, aliceSecretKey), ).to.deep.equal([changeNote]); }); + + it("swaps", async () => { + const { note: aliceNote } = await sdk.poolErc20.shield({ + account: alice, + token: usdc, + amount: 100n, + secretKey: aliceSecretKey, + }); + const { note: bobNote } = await sdk.poolErc20.shield({ + account: bob, + token: btc, + amount: 10n, + secretKey: bobSecretKey, + }); + + await backendSdk.rollup.rollup(); + + await sdk.lob.swap({ + sellerSecretKey: aliceSecretKey, + sellerNote: aliceNote, + sellerAmount: 70n, + buyerSecretKey: bobSecretKey, + buyerNote: bobNote, + buyerAmount: 2n, + }); + + await backendSdk.rollup.rollup(); + + expect(await sdk.poolErc20.balanceOf(usdc, aliceSecretKey)).to.equal(30n); + expect(await sdk.poolErc20.balanceOf(btc, aliceSecretKey)).to.equal(2n); + expect(await sdk.poolErc20.balanceOf(usdc, bobSecretKey)).to.equal(70n); + expect(await sdk.poolErc20.balanceOf(btc, bobSecretKey)).to.equal(8n); + }); }); From c332570ff506bb39a8ce3a6f00495affb36ca66f Mon Sep 17 00:00:00 2001 From: oleh Date: Thu, 6 Feb 2025 15:11:53 +0100 Subject: [PATCH 03/16] feat: add simplest order matching (#12) --- packages/contracts/noir/lob_router/src/lib.nr | 28 ++++++++++-- .../noir/lob_router_swap/Prover1.toml | 43 +++++++++++------- .../noir/lob_router_swap/Prover2.toml | 44 ++++++++++++------- .../noir/lob_router_swap/src/main.nr | 8 ++-- packages/contracts/sdk/LobService.ts | 15 ++++++- 5 files changed, 97 insertions(+), 41 deletions(-) diff --git a/packages/contracts/noir/lob_router/src/lib.nr b/packages/contracts/noir/lob_router/src/lib.nr index 7e90e6a..2b1e7e7 100644 --- a/packages/contracts/noir/lob_router/src/lib.nr +++ b/packages/contracts/noir/lob_router/src/lib.nr @@ -3,14 +3,28 @@ mod LobRouter { context: &mut common::Context, seller_secret_key: Field, seller_note: erc20::Erc20NoteConsumptionInputs, - seller_amount: common::TokenAmount, + seller_order: crate::Order, seller_randomness: Field, buyer_secret_key: Field, buyer_note: erc20::Erc20NoteConsumptionInputs, - buyer_amount: common::TokenAmount, + buyer_order: crate::Order, buyer_randomness: Field, ) { - // TODO(security): orders must be signed by parties and the prices should match + // TODO(security): orders must be signed by parties + + assert( + seller_order.sell_amount == buyer_order.buy_amount, + "seller order amount does not match buyer order amount", + ); + assert( + seller_order.buy_amount == buyer_order.sell_amount, + "buyer order amount does not match seller order amount", + ); + let seller_amount = seller_order.sell_amount; + let buyer_amount = seller_order.buy_amount; + assert(seller_amount.token == seller_note.note.amount.token, "invalid seller note token"); + assert(buyer_amount.token == buyer_note.note.amount.token, "invalid buyer note token"); + erc20::Token::transfer( context, seller_secret_key, @@ -32,3 +46,11 @@ mod LobRouter { ); } } + +pub struct Order { + pub sell_amount: common::TokenAmount, + pub buy_amount: common::TokenAmount, + /// Hide order contents from other parties and outside world + // TODO(perf): not sure if this is needed because orders are secret shared in an MPC network + pub randomness: Field, +} diff --git a/packages/contracts/noir/lob_router_swap/Prover1.toml b/packages/contracts/noir/lob_router_swap/Prover1.toml index 05a9056..ce5b4e3 100644 --- a/packages/contracts/noir/lob_router_swap/Prover1.toml +++ b/packages/contracts/noir/lob_router_swap/Prover1.toml @@ -1,14 +1,13 @@ seller_secret_key = "0x118f09bc73ec486db2030077142f2bceba2a4d4c9e0f6147d776f8ca8ec02ff1" -seller_randomness = "0x182b64fb6668fe979510b16b36a7e9468e7a5063c1912e48f7a206d28e4d8e6a" - +seller_randomness = "0x0b0caedd7f1d06928a708301d43f00811de6c3894a1f8acfdfc3752bc32c9f91" [tree_roots] -note_hash_root = "0x25c912389a3b4dae01c0c9ce451affa51175104d34fb0741cb2ded0c690ce0c6" +note_hash_root = "0x26cb85e05fd3cd1fbd91297569b8b8780a8ab5ed65107fb1e96d785c911f9fda" nullifier_root = "0x0aa63c509390ad66ecd821998aabb16a818bcc5db5cf4accc0ce1821745244e9" [seller_note] note_sibling_path = [ - "0x127d42b3838a09a1d2a77d51d45efc2d872f7441434c8d73a2a65addeb63a26f", + "0x26a278428101c2dbba8f872d9a5e3f70507c085e3df8671ac4951ac86f3440bf", "0x0b63a53787021a4a962a452c2921b3663aff1ffd8d5510540f8e659e782956f1", "0x0e34ac2c09f45a503d2908bcb12f1cbae5fa4065759c88d501c097506a8b2290", "0x21f9172d72fdcdafc312eee05cf5092980dda821da5b760a9fb8dbdf607c8a20", @@ -52,18 +51,18 @@ note_sibling_path = [ note_index = "0x0" [seller_note.nullifier_low_leaf_preimage] -nullifier = "3502761326491897903912307329680840616114346130080727477006195110249521578463" -next_nullifier = "4622624008295404567211139969641762687984164848421786975202499265153077664709" -next_index = "37" +nullifier = "13830031299663409915258849247374171055586994045724027710947409933442635819679" +next_nullifier = "14448247143185075808205507000294482673348150188671637055844838122639309371309" +next_index = "54" [seller_note.nullifier_low_leaf_membership_witness] -leaf_index = "11" +leaf_index = "16" sibling_path = [ - "9592323143927693440852487918340834996926447034477944741845172904821136138140", - "5497606502829525072786717709301021256722113035609372121941390198982065775880", - "12013063095546063372417655624755911539489891906351320936318682923960676894657", - "10285196304867229525576259386556584044657339834734116745467765038834977104298", - "2903488935950838990208405343998070456796334091752591474019785544969867651224", + "18612157520988203038829918955464149978245584422880519644509407060774751013869", + "12324420131452739431074831828474476646824710186358500890131801772704214614256", + "6183856203401291583559187713266357947024267372151080140631247835359697422923", + "13090692521104750119417174288415102276391152553208717353765562027256507892105", + "5598734783864514682111263155111541308279258928439310216065335426757541199664", "3766428923666518331463526421481938811267455056246707954327363314469768904474", "796074195456137668475057404256202455048248910468542119987582633322559749494", "20567739078944838550556895816409602128127282297589578747131836752205066334747", @@ -102,16 +101,28 @@ sibling_path = [ ] [seller_note.note] -randomness = "0x0737cddb7da2fbd46f2fa3eabafd8f7c67b0930d5a91bb86def7057e0eef6715" +randomness = "0x07c1f0f96711747754a2932b1102e02b83f2eac65762db6278d447dd1f111efa" [seller_note.note.owner] inner = "0x28c7eef33d7e5d31b9d2cc09c783294d91c36e05b7b815d96549f91fe0d3b0d4" [seller_note.note.amount.token] -inner = "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" +inner = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" [seller_note.note.amount.amount] value = "100" -[seller_amount] +[seller_order] +randomness = "0x0b0caedd7f1d06928a708301d43f00811de6c3894a1f8acfdfc3752bc32c9f91" + +[seller_order.sell_amount.token] +inner = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + +[seller_order.sell_amount.amount] value = "70" + +[seller_order.buy_amount.token] +inner = "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + +[seller_order.buy_amount.amount] +value = "2" diff --git a/packages/contracts/noir/lob_router_swap/Prover2.toml b/packages/contracts/noir/lob_router_swap/Prover2.toml index 403229b..1ddfe1d 100644 --- a/packages/contracts/noir/lob_router_swap/Prover2.toml +++ b/packages/contracts/noir/lob_router_swap/Prover2.toml @@ -1,9 +1,9 @@ buyer_secret_key = "0x2120f33c0d324bfe571a18c1d5a1c9cdc6db60621e35bc78be1ced339f936a71" -buyer_randomness = "0x134fc3cd998fcbba3e4826e491da44a0b0e41072baadc927e21544b69ba7f91e" +buyer_randomness = "0x1170de694d6d728817d9bb8ac5d92198e60e34c33c57e1915682e3f541461059" [buyer_note] note_sibling_path = [ - "0x24e551d406fc3f56d3b74a5a58c83645f9ad424e384e0fd97e5cf309a4695ab3", + "0x1a0351e9ad9a380c68ef5300baabd7b40b34c912577bd9d012e2df9fef45264a", "0x0b63a53787021a4a962a452c2921b3663aff1ffd8d5510540f8e659e782956f1", "0x0e34ac2c09f45a503d2908bcb12f1cbae5fa4065759c88d501c097506a8b2290", "0x21f9172d72fdcdafc312eee05cf5092980dda821da5b760a9fb8dbdf607c8a20", @@ -47,19 +47,19 @@ note_sibling_path = [ note_index = "0x1" [buyer_note.nullifier_low_leaf_preimage] -nullifier = "14955778689319551303378818347778024347630018933433821705130993688099992966438" -next_nullifier = "16244632380179304245573134952201909709066181424366511358474662925880755537391" -next_index = "63" +nullifier = "3020367463424511551486341097408743328878406344336466401765430080394102349391" +next_nullifier = "3163318570942166511124552723585457577379612198132068460434913196460425889368" +next_index = "60" [buyer_note.nullifier_low_leaf_membership_witness] -leaf_index = "44" +leaf_index = "14" sibling_path = [ - "19153318808262274360025480944602576430653382995793665568014074536604617664453", - "12204236326854526973223496557357200239778090370172071879236879436466357624621", - "405351999194507832591771161941330294949683334785194602943911205188154186380", - "2959426397026183366069050464650984242887556596265649681973607242888685387779", - "2492782479763675264093896873817694335040885070876443070196661113661962129135", - "7997675936567769706222583500490338185379747281415057124249902920940843577209", + "1513595028644017075204557416826288607860918101548629507045554740956732507395", + "1697818760348717895994122047875291423154940241675140322766248342327844012824", + "16835990894927923730876449688911465722193030824506892140772366596798846027145", + "10285196304867229525576259386556584044657339834734116745467765038834977104298", + "2903488935950838990208405343998070456796334091752591474019785544969867651224", + "3766428923666518331463526421481938811267455056246707954327363314469768904474", "796074195456137668475057404256202455048248910468542119987582633322559749494", "20567739078944838550556895816409602128127282297589578747131836752205066334747", "2915761020738377646169465098196184536995852317462848975418156916828302972897", @@ -97,16 +97,28 @@ sibling_path = [ ] [buyer_note.note] -randomness = "0x28eae219c97727f63d73b3cd468d3325302536239c824ca1bcfed2941c8d6ded" +randomness = "0x1af08c095db372d1373088e89772122824ecdb13dfc27754f32b00d4974d4f01" [buyer_note.note.owner] inner = "0x2b7bd70beb13e310f4593dbc807332acba0f01c4586f17cf984eedd7e1437414" [buyer_note.note.amount.token] -inner = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +inner = "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" [buyer_note.note.amount.amount] value = "10" -[buyer_amount] -value = "3" +[buyer_order] +randomness = "0x1170de694d6d728817d9bb8ac5d92198e60e34c33c57e1915682e3f541461059" + +[buyer_order.sell_amount.token] +inner = "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + +[buyer_order.sell_amount.amount] +value = "2" + +[buyer_order.buy_amount.token] +inner = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + +[buyer_order.buy_amount.amount] +value = "70" diff --git a/packages/contracts/noir/lob_router_swap/src/main.nr b/packages/contracts/noir/lob_router_swap/src/main.nr index fa50182..fda9862 100644 --- a/packages/contracts/noir/lob_router_swap/src/main.nr +++ b/packages/contracts/noir/lob_router_swap/src/main.nr @@ -3,12 +3,12 @@ fn main( // seller seller_secret_key: Field, seller_note: erc20::Erc20NoteConsumptionInputs, - seller_amount: common::TokenAmount, + seller_order: lob_router::Order, seller_randomness: Field, // buyer buyer_secret_key: Field, buyer_note: erc20::Erc20NoteConsumptionInputs, - buyer_amount: common::TokenAmount, + buyer_order: lob_router::Order, buyer_randomness: Field, ) -> pub common::Result<4, 2> { let mut context = common::Context::from(tree_roots); @@ -17,11 +17,11 @@ fn main( &mut context, seller_secret_key, seller_note, - seller_amount, + seller_order, seller_randomness, buyer_secret_key, buyer_note, - buyer_amount, + buyer_order, buyer_randomness, ); diff --git a/packages/contracts/sdk/LobService.ts b/packages/contracts/sdk/LobService.ts index 88284ca..9c55066 100644 --- a/packages/contracts/sdk/LobService.ts +++ b/packages/contracts/sdk/LobService.ts @@ -56,6 +56,17 @@ export class LobService { randomness: buyerRandomness, }); + const seller_order = { + sell_amount: await params.sellerAmount.toNoir(), + buy_amount: await params.buyerAmount.toNoir(), + randomness: sellerRandomness, + }; + const buyer_order = { + sell_amount: await params.buyerAmount.toNoir(), + buy_amount: await params.sellerAmount.toNoir(), + randomness: buyerRandomness, + }; + const input = { tree_roots: await this.trees.getTreeRoots(), seller_secret_key: params.sellerSecretKey, @@ -63,7 +74,7 @@ export class LobService { params.sellerSecretKey, params.sellerNote, ), - seller_amount: await params.sellerAmount.toNoir(), + seller_order, seller_randomness: sellerRandomness, buyer_secret_key: params.buyerSecretKey, @@ -71,7 +82,7 @@ export class LobService { params.buyerSecretKey, params.buyerNote, ), - buyer_amount: await params.buyerAmount.toNoir(), + buyer_order, buyer_randomness: buyerRandomness, }; const { proof } = await prove("swap", swapCircuit, input); From 53550c01f5b6eb723a599a994804604021617d87 Mon Sep 17 00:00:00 2001 From: oleh Date: Fri, 7 Feb 2025 00:44:53 +0100 Subject: [PATCH 04/16] feat: mpc swap (#13) --- packages/contracts/package.json | 1 + packages/contracts/sdk/LobService.ts | 111 +++++++++- packages/contracts/sdk/RollupService.ts | 24 +-- packages/contracts/sdk/backendSdk.ts | 12 +- packages/contracts/sdk/mpc/.gitignore | 2 + .../contracts/sdk/mpc/MpcNetworkService.ts | 191 ++++++++++++++++++ packages/contracts/sdk/mpc/run-party.sh | 37 ++++ packages/contracts/sdk/mpc/split-inputs.sh | 15 ++ packages/contracts/sdk/mpc/utils.ts | 60 ++++++ packages/contracts/sdk/sdk.ts | 12 +- packages/contracts/sdk/utils.ts | 13 ++ packages/contracts/test/PoolERC20.test.ts | 61 ++++++ pnpm-lock.yaml | 10 + 13 files changed, 523 insertions(+), 26 deletions(-) create mode 100644 packages/contracts/sdk/mpc/.gitignore create mode 100644 packages/contracts/sdk/mpc/MpcNetworkService.ts create mode 100755 packages/contracts/sdk/mpc/run-party.sh create mode 100755 packages/contracts/sdk/mpc/split-inputs.sh create mode 100644 packages/contracts/sdk/mpc/utils.ts diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 7cbc74a..a301d99 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -56,6 +56,7 @@ "ethers": "^6.13.4", "ky": "^1.7.2", "lodash-es": "^4.17.21", + "smol-toml": "^1.3.1", "ts-essentials": "^9.4.1", "zod": "^3.23.8" } diff --git a/packages/contracts/sdk/LobService.ts b/packages/contracts/sdk/LobService.ts index 9c55066..8281e00 100644 --- a/packages/contracts/sdk/LobService.ts +++ b/packages/contracts/sdk/LobService.ts @@ -1,10 +1,14 @@ -import { type AsyncOrSync } from "ts-essentials"; +import { uniq } from "lodash"; +import { assert, type AsyncOrSync } from "ts-essentials"; import { type PoolERC20 } from "../typechain-types"; import { NoteInputStruct } from "../typechain-types/contracts/PoolERC20"; +import { MpcProverService, type Side } from "./mpc/MpcNetworkService.js"; +import { splitInput } from "./mpc/utils.js"; import { type ITreesService } from "./RemoteTreesService.js"; import { CompleteWaAddress, Erc20Note, + getRandomness, TokenAmount, type NoirAndBackend, type PoolErc20Service, @@ -16,6 +20,7 @@ export class LobService { private contract: PoolERC20, private trees: ITreesService, private poolErc20: PoolErc20Service, + private mpcProver: MpcProverService, private circuits: AsyncOrSync<{ swap: NoirAndBackend; }>, @@ -29,11 +34,9 @@ export class LobService { buyerNote: Erc20Note; buyerAmount: TokenAmount; }) { - const { Fr } = await import("@aztec/aztec.js"); - const swapCircuit = (await this.circuits).swap; - const sellerRandomness = Fr.random().toString(); - const buyerRandomness = Fr.random().toString(); + const sellerRandomness = await getRandomness(); + const buyerRandomness = await getRandomness(); const sellerChangeNote = await Erc20Note.from({ owner: await CompleteWaAddress.fromSecretKey(params.sellerSecretKey), @@ -76,7 +79,6 @@ export class LobService { ), seller_order, seller_randomness: sellerRandomness, - buyer_secret_key: params.buyerSecretKey, buyer_note: await this.poolErc20.toNoteConsumptionInputs( params.buyerSecretKey, @@ -109,4 +111,101 @@ export class LobService { const receipt = await tx.wait(); console.log("swap gas used", receipt?.gasUsed); } + + async requestSwap(params: { + secretKey: string; + note: Erc20Note; + sellAmount: TokenAmount; + buyAmount: TokenAmount; + }) { + const swapCircuit = (await this.circuits).swap; + const randomness = await getRandomness(); + + const changeNote = await Erc20Note.from({ + owner: await CompleteWaAddress.fromSecretKey(params.secretKey), + amount: params.note.amount.sub(params.sellAmount), + randomness, + }); + const swapNote = await Erc20Note.from({ + owner: await CompleteWaAddress.fromSecretKey(params.secretKey), + amount: params.buyAmount, + randomness, + }); + + const order = { + sell_amount: await params.sellAmount.toNoir(), + buy_amount: await params.buyAmount.toNoir(), + randomness, + }; + + // deterministic side + const side: Side = + params.sellAmount.token.toLowerCase() < + params.buyAmount.token.toLowerCase() + ? "seller" + : "buyer"; + const input = { + [`${side}_secret_key`]: params.secretKey, + [`${side}_note`]: await this.poolErc20.toNoteConsumptionInputs( + params.secretKey, + params.note, + ), + [`${side}_order`]: order, + [`${side}_randomness`]: randomness, + }; + console.log("side", side, randomness); + // only one trading party need to provide public inputs + const inputPublic = + side === "seller" + ? { + tree_roots: await this.trees.getTreeRoots(), + } + : undefined; + const inputsShared = await splitInput(swapCircuit.circuit, { + // merge public inputs into first input because it does not matter how public inputs are passed + ...input, + ...inputPublic, + }); + const orderId = randomness; // TODO: is randomness a good order id? + const proofs = await this.mpcProver.prove(inputsShared, { + orderId, + side, + circuit: swapCircuit.circuit, + numPublicInputs: 8, + }); + assert(uniq(proofs).length === 1, "proofs mismatch"); + const proof = proofs[0]!; + return { + proof, + side, + changeNote: await changeNote.toSolidityNoteInput(), + swapNote: await swapNote.toSolidityNoteInput(), + nullifier: ( + await params.note.computeNullifier(params.secretKey) + ).toString(), + }; + } + + async commitSwap(sellerSwap: SwapResult, buyerSwap: SwapResult) { + assert( + sellerSwap.proof === buyerSwap.proof, + "seller & buyer proof mismatch", + ); + const proof = sellerSwap.proof; + + const tx = await this.contract.swap( + proof, + [ + sellerSwap.changeNote, + buyerSwap.swapNote, + buyerSwap.changeNote, + sellerSwap.swapNote, + ], + [sellerSwap.nullifier, buyerSwap.nullifier], + ); + const receipt = await tx.wait(); + console.log("swap gas used", receipt?.gasUsed); + } } + +type SwapResult = Awaited>; diff --git a/packages/contracts/sdk/RollupService.ts b/packages/contracts/sdk/RollupService.ts index dae0cd5..18d2968 100644 --- a/packages/contracts/sdk/RollupService.ts +++ b/packages/contracts/sdk/RollupService.ts @@ -1,6 +1,6 @@ import type { Fr } from "@aztec/aztec.js"; import type { UltraHonkBackend } from "@aztec/bb.js"; -import type { Noir } from "@noir-lang/noir_js"; +import type { CompiledCircuit, Noir } from "@noir-lang/noir_js"; import { utils } from "@repo/utils"; import { ethers } from "ethers"; import { compact, orderBy, times } from "lodash-es"; @@ -69,8 +69,7 @@ export class PoolErc20Service { amount: bigint; secretKey: string; }) { - const { Fr } = await import("@aztec/aztec.js"); - const randomness = Fr.random().toString(); + const randomness = await getRandomness(); const note = await Erc20Note.from({ owner: await CompleteWaAddress.fromSecretKey(secretKey), amount: await TokenAmount.from({ @@ -110,10 +109,8 @@ export class PoolErc20Service { to: string; amount: bigint; }) { - const { Fr } = await import("@aztec/aztec.js"); - assert(utils.isAddressEqual(token, fromNote.amount.token), "invalid token"); - const change_randomness = Fr.random().toString(); + const change_randomness = await getRandomness(); const changeNote = await Erc20Note.from({ owner: fromNote.owner, amount: await TokenAmount.from({ @@ -166,10 +163,9 @@ export class PoolErc20Service { notes: Erc20Note[]; to?: WaAddress; }) { - const { Fr } = await import("@aztec/aztec.js"); assert(notes.length === MAX_NOTES_TO_JOIN, "invalid notes length"); - const join_randomness = Fr.random().toString(); + const join_randomness = await getRandomness(); to ??= ( await CompleteWaAddress.fromSecretKey(secretKey) @@ -220,12 +216,10 @@ export class PoolErc20Service { to: CompleteWaAddress; amount: TokenAmount; }) { - const { Fr } = await import("@aztec/aztec.js"); - const nullifier = await fromNote.computeNullifier(secretKey); - const to_randomness = Fr.random().toString(); - const change_randomness = Fr.random().toString(); + const to_randomness = await getRandomness(); + const change_randomness = await getRandomness(); const input = { tree_roots: await this.trees.getTreeRoots(), from_note_inputs: await this.toNoteConsumptionInputs(secretKey, fromNote), @@ -538,6 +532,7 @@ export class CompleteWaAddress { } export type NoirAndBackend = { + circuit: CompiledCircuit; noir: Noir; backend: UltraHonkBackend; }; @@ -560,3 +555,8 @@ function sortEvents< (e) => `${e.blockNumber}-${e.transactionIndex}-${e.index}`, ); } + +export async function getRandomness() { + const { Fr } = await import("@aztec/aztec.js"); + return Fr.random().toString(); +} diff --git a/packages/contracts/sdk/backendSdk.ts b/packages/contracts/sdk/backendSdk.ts index 9629669..f5d23e3 100644 --- a/packages/contracts/sdk/backendSdk.ts +++ b/packages/contracts/sdk/backendSdk.ts @@ -14,17 +14,17 @@ export function createBackendSdk( rollup: utils.iife(async () => { const { Noir } = await import("@noir-lang/noir_js"); const { UltraHonkBackend } = await import("@aztec/bb.js"); - const noir = new Noir(await compiledCircuits.rollup); + const circuit = await compiledCircuits.rollup; + const noir = new Noir(circuit); // TODO(perf): write and use a NativeUltraHonkBackend // const backend = new NativeUltraPlonkBackend( // `${process.env.HOME}/.bb/bb`, // await compiledCircuits.rollup, // ) as unknown as UltraPlonkBackend; - const backend = new UltraHonkBackend( - (await compiledCircuits.rollup).bytecode, - { threads: os.cpus().length }, - ); - return { noir, backend }; + const backend = new UltraHonkBackend(circuit.bytecode, { + threads: os.cpus().length, + }); + return { circuit, noir, backend }; }), }); return { diff --git a/packages/contracts/sdk/mpc/.gitignore b/packages/contracts/sdk/mpc/.gitignore new file mode 100644 index 0000000..3f38e0c --- /dev/null +++ b/packages/contracts/sdk/mpc/.gitignore @@ -0,0 +1,2 @@ +work-dirs +configs diff --git a/packages/contracts/sdk/mpc/MpcNetworkService.ts b/packages/contracts/sdk/mpc/MpcNetworkService.ts new file mode 100644 index 0000000..32c08b7 --- /dev/null +++ b/packages/contracts/sdk/mpc/MpcNetworkService.ts @@ -0,0 +1,191 @@ +import type { CompiledCircuit } from "@noir-lang/noir_js"; +import { ethers } from "ethers"; +import { omit } from "lodash"; +import fs from "node:fs"; +import path from "node:path"; +import { z } from "zod"; +import { promiseWithResolvers } from "../utils.js"; +import { inWorkingDir, makeRunCommand, splitInput } from "./utils.js"; + +export class MpcProverService { + readonly #parties = { + 0: new MpcProverPartyService(0), + 1: new MpcProverPartyService(1), + 2: new MpcProverPartyService(2), + }; + + async prove( + inputsShared: Awaited>, + params: { + orderId: OrderId; + side: Side; + circuit: CompiledCircuit; + // TODO: infer number of public inputs + numPublicInputs: number; + }, + ) { + return await Promise.all( + inputsShared.map(async ({ partyIndex, inputShared }) => { + return await this.#parties[partyIndex].requestProveAsParty({ + ...params, + inputShared, + }); + }), + ); + } +} + +class MpcProverPartyService { + #storage: Map = new Map(); + + constructor(readonly partyIndex: PartyIndex) {} + + async requestProveAsParty(params: { + orderId: OrderId; + side: Side; + inputShared: string; + circuit: CompiledCircuit; + // TODO: infer number of public inputs + numPublicInputs: number; + }) { + // TODO(security): authorization + if (this.#storage.has(params.orderId)) { + throw new Error(`order already exists ${params.orderId}`); + } + const order: Order = { + id: params.orderId, + inputShared: params.inputShared, + side: params.side, + result: promiseWithResolvers(), + }; + this.#storage.set(params.orderId, order); + + this.#tryExecuteOrder(params.orderId, { + circuit: params.circuit, + numPublicInputs: params.numPublicInputs, + }); + + return await order.result.promise; + } + + async #tryExecuteOrder( + orderId: OrderId, + + params: { + circuit: CompiledCircuit; + numPublicInputs: number; + }, + ) { + const order = this.#storage.get(orderId); + if (!order) { + throw new Error( + `order not found in party storage ${this.partyIndex}: ${orderId}`, + ); + } + + const otherOrders = Array.from(this.#storage.values()).filter( + (o) => o.id !== order.id && o.side !== order.side, + ); + if (otherOrders.length === 0) { + return; + } + const otherOrder = otherOrders[0]!; + const inputsShared = + order.side === "seller" + ? ([order.inputShared, otherOrder.inputShared] as const) + : ([otherOrder.inputShared, order.inputShared] as const); + console.log( + "executing orders", + this.partyIndex, + omit(order, ["inputShared", "result"]), + omit(otherOrder, ["inputShared", "result"]), + ); + try { + const { proof } = await this.#prove({ + circuit: params.circuit, + input0Shared: inputsShared[0], + input1Shared: inputsShared[1], + numPublicInputs: params.numPublicInputs, + }); + const proofHex = ethers.hexlify(proof); + order.result.resolve(proofHex); + otherOrder.result.resolve(proofHex); + } catch (error) { + order.result.reject(error); + otherOrder.result.reject(error); + } + } + + async #prove(params: { + circuit: CompiledCircuit; + input0Shared: string; + input1Shared: string; + // TODO: infer number of public inputs + numPublicInputs: number; + }) { + console.log("proving as party", this.partyIndex); + return await inWorkingDir(async (workingDir) => { + for (const [traderIndex, inputShared] of [ + params.input0Shared, + params.input1Shared, + ].entries()) { + fs.writeFileSync( + path.join( + workingDir, + `Prover${traderIndex}.toml.${this.partyIndex}.shared`, + ), + ethers.getBytes(inputShared), + ); + } + + const circuitPath = path.join(workingDir, "circuit.json"); + fs.writeFileSync(circuitPath, JSON.stringify(params.circuit)); + + const runCommand = makeRunCommand(__dirname); + await runCommand( + `./run-party.sh ${workingDir} ${circuitPath} ${this.partyIndex}`, + ); + + const publicInputs = z + .string() + .array() + .parse( + JSON.parse( + fs.readFileSync( + path.join(workingDir, "public_input.json"), + "utf-8", + ), + ), + ); + const proofData = Uint8Array.from( + fs.readFileSync( + path.join(workingDir, `proof.${this.partyIndex}.proof`), + ), + ); + // arcane magic + const proof = ethers.getBytes( + ethers.concat([ + proofData.slice(0, 2), + proofData.slice(6, 100), + proofData.slice(100 + params.numPublicInputs * 32), + ]), + ); + // console.log("proof native\n", JSON.stringify(Array.from(proof))); + return { proof, publicInputs }; + }); + } +} + +export type OrderId = string & { __brand: "OrderId" }; +export type PartyIndex = 0 | 1 | 2; +/** + * Deterministically determined based on the tokens being swapped + */ +export type Side = "seller" | "buyer"; + +type Order = { + side: Side; + id: OrderId; + inputShared: string; + result: ReturnType>; +}; diff --git a/packages/contracts/sdk/mpc/run-party.sh b/packages/contracts/sdk/mpc/run-party.sh new file mode 100755 index 0000000..c28628d --- /dev/null +++ b/packages/contracts/sdk/mpc/run-party.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source ../../noir/timer.sh + +if [ $# -ne 3 ]; then + echo "Usage: $0 " + exit 1 +fi +WORK_DIR=$1 +CIRCUIT=$2 +PARTY_INDEX=$3 + +PROVER0_TOML=$WORK_DIR/Prover0.toml +PROVER1_TOML=$WORK_DIR/Prover1.toml +# copy from https://github.com/TaceoLabs/co-snarks/tree/e96a712dfa987fb39e17232ef11d067b29b62aef/co-noir/co-noir/examples/configs +PARTY_CONFIGS_DIR=configs + +# merge inputs into single input file +timeStart "merge-input-shares" +co-noir merge-input-shares --inputs $PROVER0_TOML.$PARTY_INDEX.shared --inputs $PROVER1_TOML.$PARTY_INDEX.shared --protocol REP3 --out $WORK_DIR/Prover.toml.$PARTY_INDEX.shared +timeEnd "merge-input-shares" + +# run witness extension in MPC +timeStart "mpc-generate-witness" +co-noir generate-witness --input $WORK_DIR/Prover.toml.$PARTY_INDEX.shared --circuit $CIRCUIT --protocol REP3 --config $PARTY_CONFIGS_DIR/party$PARTY_INDEX.toml --out $WORK_DIR/witness.gz.$PARTY_INDEX.shared +timeEnd "mpc-generate-witness" + +# run proving in MPC +timeStart "mpc-build-proving-key" +co-noir build-proving-key --witness $WORK_DIR/witness.gz.$PARTY_INDEX.shared --circuit $CIRCUIT --crs ~/.bb-crs/bn254_g1.dat --protocol REP3 --config $PARTY_CONFIGS_DIR/party$PARTY_INDEX.toml --out $WORK_DIR/proving_key.$PARTY_INDEX +timeEnd "mpc-build-proving-key" + +timeStart "mpc-generate-proof" +co-noir generate-proof --proving-key $WORK_DIR/proving_key.$PARTY_INDEX --protocol REP3 --hasher KECCAK --config $PARTY_CONFIGS_DIR/party$PARTY_INDEX.toml --out $WORK_DIR/proof.$PARTY_INDEX.proof --public-input $WORK_DIR/public_input.json +timeEnd "mpc-generate-proof" diff --git a/packages/contracts/sdk/mpc/split-inputs.sh b/packages/contracts/sdk/mpc/split-inputs.sh new file mode 100755 index 0000000..1846efb --- /dev/null +++ b/packages/contracts/sdk/mpc/split-inputs.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +PROVER_TOML=$1 +CIRCUIT=$2 + +WORK_DIR=$(dirname $PROVER_TOML) + +co-noir split-input --circuit $CIRCUIT --input $PROVER_TOML --protocol REP3 --out-dir $WORK_DIR diff --git a/packages/contracts/sdk/mpc/utils.ts b/packages/contracts/sdk/mpc/utils.ts new file mode 100644 index 0000000..bf09013 --- /dev/null +++ b/packages/contracts/sdk/mpc/utils.ts @@ -0,0 +1,60 @@ +import type { CompiledCircuit, InputMap } from "@noir-lang/noir_js"; +import { ethers } from "ethers"; +import { range } from "lodash"; +import fs from "node:fs"; +import path from "node:path"; +import toml from "smol-toml"; +import type { PartyIndex } from "./MpcNetworkService.js"; + +export async function splitInput(circuit: CompiledCircuit, input: InputMap) { + return await inWorkingDir(async (workingDir) => { + const proverPath = path.join(workingDir, "ProverX.toml"); + fs.writeFileSync(proverPath, toml.stringify(input)); + const circuitPath = path.join(workingDir, "circuit.json"); + fs.writeFileSync(circuitPath, JSON.stringify(circuit)); + const runCommand = makeRunCommand(__dirname); + await runCommand(`./split-inputs.sh ${proverPath} ${circuitPath}`); + const shared = range(3).map((i) => { + const x = Uint8Array.from(fs.readFileSync(`${proverPath}.${i}.shared`)); + return ethers.hexlify(x); + }); + return Array.from(shared.entries()).map(([partyIndex, inputShared]) => ({ + partyIndex: partyIndex as PartyIndex, + inputShared, + })); + }); +} + +export async function inWorkingDir(f: (workingDir: string) => Promise) { + const id = crypto.randomUUID(); + const workingDir = path.join(__dirname, "work-dirs", id); + fs.mkdirSync(workingDir, { recursive: true }); + try { + return await f(workingDir); + } finally { + fs.rmSync(workingDir, { recursive: true }); + } +} + +export const makeRunCommand = (cwd?: string) => async (command: string) => { + const { exec } = await import("child_process"); + const { promisify } = await import("util"); + const execAsync = promisify(exec); + // TODO(security): escape command arguments (use spawn) + try { + const { stdout, stderr } = await execAsync(command, { + cwd, + maxBuffer: Infinity, + }); + if (stdout) { + console.log(stdout); + } + if (stderr) { + console.error(stderr); + } + } catch (error) { + console.error(`Error executing command: ${command}`); + console.error((error as any).stderr || (error as any).message); + throw new Error(`Error executing command: ${command}`); + } +}; diff --git a/packages/contracts/sdk/sdk.ts b/packages/contracts/sdk/sdk.ts index 633a0df..b0740ea 100644 --- a/packages/contracts/sdk/sdk.ts +++ b/packages/contracts/sdk/sdk.ts @@ -6,6 +6,7 @@ import { EncryptionService } from "./EncryptionService.js"; import { LobService } from "./LobService.js"; import { type ITreesService } from "./RemoteTreesService.js"; import { PoolErc20Service } from "./RollupService.js"; +import { MpcProverService } from "./mpc/MpcNetworkService.js"; export * from "./EncryptionService.js"; export * from "./NonMembershipTree.js"; @@ -38,7 +39,14 @@ export function createInterfaceSdk( trees, circuits, ); - const lob = new LobService(coreSdk.contract, trees, poolErc20, circuits); + const mpcProver = new MpcProverService(); + const lob = new LobService( + coreSdk.contract, + trees, + poolErc20, + mpcProver, + circuits, + ); return { poolErc20, @@ -57,5 +65,5 @@ async function getCircuit(artifact: AsyncOrSync) { artifact = await artifact; const noir = new Noir(artifact); const backend = new UltraHonkBackend(artifact.bytecode); - return { noir, backend }; + return { circuit: artifact, noir, backend }; } diff --git a/packages/contracts/sdk/utils.ts b/packages/contracts/sdk/utils.ts index ae91f79..584248a 100644 --- a/packages/contracts/sdk/utils.ts +++ b/packages/contracts/sdk/utils.ts @@ -78,3 +78,16 @@ export async function prove( proof = proof.slice(4); // remove length return { proof, witness, returnValue, publicInputs }; } + +export function promiseWithResolvers(): { + promise: Promise; + resolve: (value: T) => void; + reject: (reason: unknown) => void; +} { + const ret: any = {}; + ret.promise = new Promise((resolve, reject) => { + ret.resolve = resolve; + ret.reject = reject; + }); + return ret; +} diff --git a/packages/contracts/test/PoolERC20.test.ts b/packages/contracts/test/PoolERC20.test.ts index 909074d..020eac1 100644 --- a/packages/contracts/test/PoolERC20.test.ts +++ b/packages/contracts/test/PoolERC20.test.ts @@ -408,4 +408,65 @@ describe("PoolERC20", () => { expect(await sdk.poolErc20.balanceOf(usdc, bobSecretKey)).to.equal(70n); expect(await sdk.poolErc20.balanceOf(btc, bobSecretKey)).to.equal(8n); }); + + it("swaps mpc", async () => { + if (process.env.CI) { + // TODO: install co-noir on github actions and remove this + console.log("skipping mpc swap test"); + return; + } + + const { note: aliceNote } = await sdk.poolErc20.shield({ + account: alice, + token: usdc, + amount: 100n, + secretKey: aliceSecretKey, + }); + const { note: bobNote } = await sdk.poolErc20.shield({ + account: bob, + token: btc, + amount: 10n, + secretKey: bobSecretKey, + }); + + await backendSdk.rollup.rollup(); + + const sellerAmount = await TokenAmount.from({ + token: await usdc.getAddress(), + amount: 70n, + }); + const buyerAmount = await TokenAmount.from({ + token: await btc.getAddress(), + amount: 2n, + }); + + const swapAlicePromise = sdk.lob.requestSwap({ + secretKey: aliceSecretKey, + note: aliceNote, + sellAmount: sellerAmount, + buyAmount: buyerAmount, + }); + const swapBobPromise = sdk.lob.requestSwap({ + secretKey: bobSecretKey, + note: bobNote, + sellAmount: buyerAmount, + buyAmount: sellerAmount, + }); + const [swapAlice, swapBob] = await Promise.all([ + swapAlicePromise, + swapBobPromise, + ]); + const args = + swapAlice.side === "seller" + ? ([swapAlice, swapBob] as const) + : ([swapBob, swapAlice] as const); + await sdk.lob.commitSwap(...args); + + await backendSdk.rollup.rollup(); + + expect(await sdk.poolErc20.balanceOf(usdc, aliceSecretKey)).to.equal(30n); + expect(await sdk.poolErc20.balanceOf(btc, aliceSecretKey)).to.equal(2n); + expect(await sdk.poolErc20.balanceOf(usdc, bobSecretKey)).to.equal(70n); + expect(await sdk.poolErc20.balanceOf(btc, bobSecretKey)).to.equal(8n); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00a4037..13ee737 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: lodash-es: specifier: ^4.17.21 version: 4.17.21 + smol-toml: + specifier: ^1.3.1 + version: 1.3.1 ts-essentials: specifier: ^9.4.1 version: 9.4.2(typescript@5.6.3) @@ -4075,6 +4078,7 @@ packages: lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} @@ -5091,6 +5095,10 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + smol-toml@1.3.1: + resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==} + engines: {node: '>= 18'} + solc@0.8.26: resolution: {integrity: sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==} engines: {node: '>=10.0.0'} @@ -11372,6 +11380,8 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + smol-toml@1.3.1: {} + solc@0.8.26(debug@4.3.7): dependencies: command-exists: 1.2.9 From 9c6095607393b80c626f178479a4169afc7780ee Mon Sep 17 00:00:00 2001 From: oleh Date: Fri, 7 Feb 2025 17:40:58 +0100 Subject: [PATCH 05/16] feat: pre-verify mpc proofs (#14) --- .../contracts/sdk/mpc/MpcNetworkService.ts | 142 +++++++++++------- packages/contracts/test/PoolERC20.test.ts | 50 ++++++ 2 files changed, 138 insertions(+), 54 deletions(-) diff --git a/packages/contracts/sdk/mpc/MpcNetworkService.ts b/packages/contracts/sdk/mpc/MpcNetworkService.ts index 32c08b7..7fe1f8a 100644 --- a/packages/contracts/sdk/mpc/MpcNetworkService.ts +++ b/packages/contracts/sdk/mpc/MpcNetworkService.ts @@ -1,7 +1,9 @@ +import { UltraHonkBackend } from "@aztec/bb.js"; import type { CompiledCircuit } from "@noir-lang/noir_js"; import { ethers } from "ethers"; import { omit } from "lodash"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { z } from "zod"; import { promiseWithResolvers } from "../utils.js"; @@ -70,7 +72,6 @@ class MpcProverPartyService { async #tryExecuteOrder( orderId: OrderId, - params: { circuit: CompiledCircuit; numPublicInputs: number; @@ -101,8 +102,9 @@ class MpcProverPartyService { omit(otherOrder, ["inputShared", "result"]), ); try { - const { proof } = await this.#prove({ + const { proof } = await proveAsParty({ circuit: params.circuit, + partyIndex: this.partyIndex, input0Shared: inputsShared[0], input1Shared: inputsShared[1], numPublicInputs: params.numPublicInputs, @@ -110,70 +112,102 @@ class MpcProverPartyService { const proofHex = ethers.hexlify(proof); order.result.resolve(proofHex); otherOrder.result.resolve(proofHex); + this.#storage.delete(order.id); + this.#storage.delete(otherOrder.id); } catch (error) { order.result.reject(error); otherOrder.result.reject(error); } } +} - async #prove(params: { - circuit: CompiledCircuit; - input0Shared: string; - input1Shared: string; - // TODO: infer number of public inputs - numPublicInputs: number; - }) { - console.log("proving as party", this.partyIndex); - return await inWorkingDir(async (workingDir) => { - for (const [traderIndex, inputShared] of [ - params.input0Shared, - params.input1Shared, - ].entries()) { - fs.writeFileSync( - path.join( - workingDir, - `Prover${traderIndex}.toml.${this.partyIndex}.shared`, - ), - ethers.getBytes(inputShared), - ); - } +async function proveAsParty(params: { + partyIndex: number; + circuit: CompiledCircuit; + input0Shared: string; + input1Shared: string; + // TODO: infer number of public inputs + numPublicInputs: number; +}) { + console.log("proving as party", params.partyIndex); + return await inWorkingDir(async (workingDir) => { + for (const [traderIndex, inputShared] of [ + params.input0Shared, + params.input1Shared, + ].entries()) { + fs.writeFileSync( + path.join( + workingDir, + `Prover${traderIndex}.toml.${params.partyIndex}.shared`, + ), + ethers.getBytes(inputShared), + ); + } - const circuitPath = path.join(workingDir, "circuit.json"); - fs.writeFileSync(circuitPath, JSON.stringify(params.circuit)); + const circuitPath = path.join(workingDir, "circuit.json"); + fs.writeFileSync(circuitPath, JSON.stringify(params.circuit)); - const runCommand = makeRunCommand(__dirname); - await runCommand( - `./run-party.sh ${workingDir} ${circuitPath} ${this.partyIndex}`, - ); + const runCommand = makeRunCommand(__dirname); + await runCommand( + `./run-party.sh ${workingDir} ${circuitPath} ${params.partyIndex}`, + ); - const publicInputs = z - .string() - .array() - .parse( - JSON.parse( - fs.readFileSync( - path.join(workingDir, "public_input.json"), - "utf-8", - ), - ), - ); - const proofData = Uint8Array.from( - fs.readFileSync( - path.join(workingDir, `proof.${this.partyIndex}.proof`), + const publicInputs = z + .string() + .array() + .parse( + JSON.parse( + fs.readFileSync(path.join(workingDir, "public_input.json"), "utf-8"), ), ); - // arcane magic - const proof = ethers.getBytes( - ethers.concat([ - proofData.slice(0, 2), - proofData.slice(6, 100), - proofData.slice(100 + params.numPublicInputs * 32), - ]), - ); - // console.log("proof native\n", JSON.stringify(Array.from(proof))); - return { proof, publicInputs }; + const proofData = Uint8Array.from( + fs.readFileSync( + path.join(workingDir, `proof.${params.partyIndex}.proof`), + ), + ); + // arcane magic + const proof = ethers.getBytes( + ethers.concat([ + proofData.slice(0, 2), + proofData.slice(6, 100), + proofData.slice(100 + params.numPublicInputs * 32), + ]), + ); + + // pre-verify proof + const backend = new UltraHonkBackend(params.circuit.bytecode, { + threads: os.cpus().length, }); - } + let verified: boolean; + try { + verified = await backend.verifyProof( + { + // prepend length as 4 bytes + proof: ethers.getBytes( + ethers.concat([ + ethers.zeroPadValue(ethers.toBeArray(proof.length), 4), + proof, + ]), + ), + publicInputs, + }, + { keccak: true }, + ); + } catch (e: any) { + if (e.message?.includes("unreachable")) { + throw new Error("mpc generated invalid proof: failed in runtime"); + } + throw e; + } finally { + await backend.destroy(); + } + if (!verified) { + throw new Error("mpc generated invalid proof: returned false"); + } + + // console.log("proof native\n", JSON.stringify(Array.from(proof))); + return { proof, publicInputs }; + }); } export type OrderId = string & { __brand: "OrderId" }; diff --git a/packages/contracts/test/PoolERC20.test.ts b/packages/contracts/test/PoolERC20.test.ts index 020eac1..9ceee74 100644 --- a/packages/contracts/test/PoolERC20.test.ts +++ b/packages/contracts/test/PoolERC20.test.ts @@ -469,4 +469,54 @@ describe("PoolERC20", () => { expect(await sdk.poolErc20.balanceOf(usdc, bobSecretKey)).to.equal(70n); expect(await sdk.poolErc20.balanceOf(btc, bobSecretKey)).to.equal(8n); }); + + it("fails to swap if order amounts do not match", async () => { + if (process.env.CI) { + // TODO: install co-noir on github actions and remove this + return; + } + + const { note: aliceNote } = await sdk.poolErc20.shield({ + account: alice, + token: usdc, + amount: 100n, + secretKey: aliceSecretKey, + }); + const { note: bobNote } = await sdk.poolErc20.shield({ + account: bob, + token: btc, + amount: 10n, + secretKey: bobSecretKey, + }); + + await backendSdk.rollup.rollup(); + + const sellerAmount = await TokenAmount.from({ + token: await usdc.getAddress(), + amount: 70n, + }); + const buyerAmount = await TokenAmount.from({ + token: await btc.getAddress(), + amount: 2n, + }); + + const swapAlicePromise = sdk.lob.requestSwap({ + secretKey: aliceSecretKey, + note: aliceNote, + sellAmount: sellerAmount, + buyAmount: buyerAmount, + }); + const swapBobPromise = sdk.lob.requestSwap({ + secretKey: bobSecretKey, + note: bobNote, + sellAmount: buyerAmount, + buyAmount: await TokenAmount.from({ + token: await usdc.getAddress(), + amount: 71n, // amount differs + }), + }); + await expect( + Promise.all([swapAlicePromise, swapBobPromise]), + ).to.be.rejectedWith("mpc generated invalid proof"); + }); }); From b2d6741af4649c24ca7fc2f6a9b00ce2977a85b5 Mon Sep 17 00:00:00 2001 From: oleh Date: Fri, 7 Feb 2025 17:43:59 +0100 Subject: [PATCH 06/16] refactor: remove numPublicInputs param --- packages/contracts/sdk/LobService.ts | 1 - packages/contracts/sdk/mpc/MpcNetworkService.ts | 11 +---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/contracts/sdk/LobService.ts b/packages/contracts/sdk/LobService.ts index 8281e00..f8741cf 100644 --- a/packages/contracts/sdk/LobService.ts +++ b/packages/contracts/sdk/LobService.ts @@ -171,7 +171,6 @@ export class LobService { orderId, side, circuit: swapCircuit.circuit, - numPublicInputs: 8, }); assert(uniq(proofs).length === 1, "proofs mismatch"); const proof = proofs[0]!; diff --git a/packages/contracts/sdk/mpc/MpcNetworkService.ts b/packages/contracts/sdk/mpc/MpcNetworkService.ts index 7fe1f8a..d939171 100644 --- a/packages/contracts/sdk/mpc/MpcNetworkService.ts +++ b/packages/contracts/sdk/mpc/MpcNetworkService.ts @@ -22,8 +22,6 @@ export class MpcProverService { orderId: OrderId; side: Side; circuit: CompiledCircuit; - // TODO: infer number of public inputs - numPublicInputs: number; }, ) { return await Promise.all( @@ -47,8 +45,6 @@ class MpcProverPartyService { side: Side; inputShared: string; circuit: CompiledCircuit; - // TODO: infer number of public inputs - numPublicInputs: number; }) { // TODO(security): authorization if (this.#storage.has(params.orderId)) { @@ -64,7 +60,6 @@ class MpcProverPartyService { this.#tryExecuteOrder(params.orderId, { circuit: params.circuit, - numPublicInputs: params.numPublicInputs, }); return await order.result.promise; @@ -74,7 +69,6 @@ class MpcProverPartyService { orderId: OrderId, params: { circuit: CompiledCircuit; - numPublicInputs: number; }, ) { const order = this.#storage.get(orderId); @@ -107,7 +101,6 @@ class MpcProverPartyService { partyIndex: this.partyIndex, input0Shared: inputsShared[0], input1Shared: inputsShared[1], - numPublicInputs: params.numPublicInputs, }); const proofHex = ethers.hexlify(proof); order.result.resolve(proofHex); @@ -126,8 +119,6 @@ async function proveAsParty(params: { circuit: CompiledCircuit; input0Shared: string; input1Shared: string; - // TODO: infer number of public inputs - numPublicInputs: number; }) { console.log("proving as party", params.partyIndex); return await inWorkingDir(async (workingDir) => { @@ -170,7 +161,7 @@ async function proveAsParty(params: { ethers.concat([ proofData.slice(0, 2), proofData.slice(6, 100), - proofData.slice(100 + params.numPublicInputs * 32), + proofData.slice(100 + publicInputs.length * 32), ]), ); From 327139de9cc666e6cec897598ef2439a0a606d7d Mon Sep 17 00:00:00 2001 From: oleh Date: Sat, 8 Feb 2025 01:04:31 +0100 Subject: [PATCH 07/16] refactor: simpler native proof decoding (#16) --- .../contracts/sdk/mpc/MpcNetworkService.ts | 38 ++++--------------- packages/contracts/sdk/utils.ts | 20 ++++++++++ 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/packages/contracts/sdk/mpc/MpcNetworkService.ts b/packages/contracts/sdk/mpc/MpcNetworkService.ts index d939171..c42579e 100644 --- a/packages/contracts/sdk/mpc/MpcNetworkService.ts +++ b/packages/contracts/sdk/mpc/MpcNetworkService.ts @@ -5,8 +5,7 @@ import { omit } from "lodash"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { z } from "zod"; -import { promiseWithResolvers } from "../utils.js"; +import { decodeNativeHonkProof, promiseWithResolvers } from "../utils.js"; import { inWorkingDir, makeRunCommand, splitInput } from "./utils.js"; export class MpcProverService { @@ -143,27 +142,11 @@ async function proveAsParty(params: { `./run-party.sh ${workingDir} ${circuitPath} ${params.partyIndex}`, ); - const publicInputs = z - .string() - .array() - .parse( - JSON.parse( - fs.readFileSync(path.join(workingDir, "public_input.json"), "utf-8"), - ), - ); - const proofData = Uint8Array.from( + const { proof, publicInputs } = decodeNativeHonkProof( fs.readFileSync( path.join(workingDir, `proof.${params.partyIndex}.proof`), ), ); - // arcane magic - const proof = ethers.getBytes( - ethers.concat([ - proofData.slice(0, 2), - proofData.slice(6, 100), - proofData.slice(100 + publicInputs.length * 32), - ]), - ); // pre-verify proof const backend = new UltraHonkBackend(params.circuit.bytecode, { @@ -172,16 +155,7 @@ async function proveAsParty(params: { let verified: boolean; try { verified = await backend.verifyProof( - { - // prepend length as 4 bytes - proof: ethers.getBytes( - ethers.concat([ - ethers.zeroPadValue(ethers.toBeArray(proof.length), 4), - proof, - ]), - ), - publicInputs, - }, + { proof, publicInputs }, { keccak: true }, ); } catch (e: any) { @@ -196,8 +170,10 @@ async function proveAsParty(params: { throw new Error("mpc generated invalid proof: returned false"); } - // console.log("proof native\n", JSON.stringify(Array.from(proof))); - return { proof, publicInputs }; + return { + proof: proof.slice(4), // remove length + publicInputs, + }; }); } diff --git a/packages/contracts/sdk/utils.ts b/packages/contracts/sdk/utils.ts index 584248a..e857a2c 100644 --- a/packages/contracts/sdk/utils.ts +++ b/packages/contracts/sdk/utils.ts @@ -1,4 +1,5 @@ import type { Fr } from "@aztec/aztec.js"; +import { splitHonkProof } from "@aztec/bb.js"; import type { InputMap } from "@noir-lang/noir_js"; import { ethers } from "ethers"; import { assert } from "ts-essentials"; @@ -91,3 +92,22 @@ export function promiseWithResolvers(): { }); return ret; } + +export function decodeNativeHonkProof(nativeProof: Uint8Array) { + const { proof, publicInputs: publicInputsRaw } = splitHonkProof(nativeProof); + const publicInputs = deflattenFields(publicInputsRaw); + return { proof, publicInputs }; +} + +// TODO: import from @aztec/bb.js when available +function deflattenFields(flattenedFields: Uint8Array): string[] { + const publicInputSize = 32; + const chunkedFlattenedPublicInputs: Uint8Array[] = []; + + for (let i = 0; i < flattenedFields.length; i += publicInputSize) { + const publicInput = flattenedFields.slice(i, i + publicInputSize); + chunkedFlattenedPublicInputs.push(publicInput); + } + + return chunkedFlattenedPublicInputs.map((x) => ethers.hexlify(x)); +} From adb01167e31649c26478c008bce43edbf1d68ab0 Mon Sep 17 00:00:00 2001 From: oleh Date: Sat, 8 Feb 2025 07:27:26 +0100 Subject: [PATCH 08/16] refactor: use spawn instead of exec --- .../contracts/sdk/mpc/MpcNetworkService.ts | 8 ++- packages/contracts/sdk/mpc/utils.ts | 61 ++++++++++++------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/packages/contracts/sdk/mpc/MpcNetworkService.ts b/packages/contracts/sdk/mpc/MpcNetworkService.ts index c42579e..e62cf14 100644 --- a/packages/contracts/sdk/mpc/MpcNetworkService.ts +++ b/packages/contracts/sdk/mpc/MpcNetworkService.ts @@ -138,9 +138,11 @@ async function proveAsParty(params: { fs.writeFileSync(circuitPath, JSON.stringify(params.circuit)); const runCommand = makeRunCommand(__dirname); - await runCommand( - `./run-party.sh ${workingDir} ${circuitPath} ${params.partyIndex}`, - ); + await runCommand("./run-party.sh", [ + workingDir, + circuitPath, + params.partyIndex, + ]); const { proof, publicInputs } = decodeNativeHonkProof( fs.readFileSync( diff --git a/packages/contracts/sdk/mpc/utils.ts b/packages/contracts/sdk/mpc/utils.ts index bf09013..b532392 100644 --- a/packages/contracts/sdk/mpc/utils.ts +++ b/packages/contracts/sdk/mpc/utils.ts @@ -13,7 +13,7 @@ export async function splitInput(circuit: CompiledCircuit, input: InputMap) { const circuitPath = path.join(workingDir, "circuit.json"); fs.writeFileSync(circuitPath, JSON.stringify(circuit)); const runCommand = makeRunCommand(__dirname); - await runCommand(`./split-inputs.sh ${proverPath} ${circuitPath}`); + await runCommand("./split-inputs.sh", [proverPath, circuitPath]); const shared = range(3).map((i) => { const x = Uint8Array.from(fs.readFileSync(`${proverPath}.${i}.shared`)); return ethers.hexlify(x); @@ -36,25 +36,42 @@ export async function inWorkingDir(f: (workingDir: string) => Promise) { } } -export const makeRunCommand = (cwd?: string) => async (command: string) => { - const { exec } = await import("child_process"); - const { promisify } = await import("util"); - const execAsync = promisify(exec); - // TODO(security): escape command arguments (use spawn) - try { - const { stdout, stderr } = await execAsync(command, { - cwd, - maxBuffer: Infinity, +export const makeRunCommand = + (cwd?: string) => + async (command: string, args: (string | number)[] = []) => { + const { spawn } = await import("node:child_process"); + + const spawned = spawn( + command, + args.map((arg) => arg.toString()), + { cwd }, + ); + spawned.stdout.on("data", (data) => { + process.stdout.write(data); }); - if (stdout) { - console.log(stdout); - } - if (stderr) { - console.error(stderr); - } - } catch (error) { - console.error(`Error executing command: ${command}`); - console.error((error as any).stderr || (error as any).message); - throw new Error(`Error executing command: ${command}`); - } -}; + + spawned.stderr.on("data", (data) => { + process.stderr.write(data); + }); + + return await new Promise((resolve, reject) => { + spawned.on("close", (code: number) => { + if (code !== 0) { + reject(new Error(`Process exited with code ${code}`)); + return; + } + + resolve(); + }); + + spawned.on("error", (err) => { + reject( + new Error( + `Error executing command \`${ + command + " " + args.join(" ") + }\`: ${err.message}`, + ), + ); + }); + }); + }; From 2a2faab0c1c7234a1466cb10f685f75a91cfdf15 Mon Sep 17 00:00:00 2001 From: oleh Date: Sat, 8 Feb 2025 22:16:11 +0100 Subject: [PATCH 09/16] feat: order matching queue (#21) --- packages/contracts/package.json | 1 + packages/contracts/sdk/LobService.ts | 28 ++++- .../contracts/sdk/mpc/MpcNetworkService.ts | 117 +++++++++-------- packages/contracts/test/PoolERC20.test.ts | 118 +++++++++++++++++- pnpm-lock.yaml | 18 +++ 5 files changed, 221 insertions(+), 61 deletions(-) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index a301d99..05e889b 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -56,6 +56,7 @@ "ethers": "^6.13.4", "ky": "^1.7.2", "lodash-es": "^4.17.21", + "p-queue": "^8.1.0", "smol-toml": "^1.3.1", "ts-essentials": "^9.4.1", "zod": "^3.23.8" diff --git a/packages/contracts/sdk/LobService.ts b/packages/contracts/sdk/LobService.ts index f8741cf..9fd30a0 100644 --- a/packages/contracts/sdk/LobService.ts +++ b/packages/contracts/sdk/LobService.ts @@ -118,6 +118,15 @@ export class LobService { sellAmount: TokenAmount; buyAmount: TokenAmount; }) { + const orderId = await getRandomness(); + console.log( + "order ID", + orderId, + params.sellAmount.amount, + "->", + params.buyAmount.amount, + ); + const swapCircuit = (await this.circuits).swap; const randomness = await getRandomness(); @@ -153,7 +162,6 @@ export class LobService { [`${side}_order`]: order, [`${side}_randomness`]: randomness, }; - console.log("side", side, randomness); // only one trading party need to provide public inputs const inputPublic = side === "seller" @@ -166,7 +174,6 @@ export class LobService { ...input, ...inputPublic, }); - const orderId = randomness; // TODO: is randomness a good order id? const proofs = await this.mpcProver.prove(inputsShared, { orderId, side, @@ -175,6 +182,7 @@ export class LobService { assert(uniq(proofs).length === 1, "proofs mismatch"); const proof = proofs[0]!; return { + orderId, proof, side, changeNote: await changeNote.toSolidityNoteInput(), @@ -185,10 +193,20 @@ export class LobService { }; } - async commitSwap(sellerSwap: SwapResult, buyerSwap: SwapResult) { + async commitSwap(params: { swapA: SwapResult; swapB: SwapResult }) { + const [sellerSwap, buyerSwap] = + params.swapA.side === "seller" + ? [params.swapA, params.swapB] + : [params.swapB, params.swapA]; + + assert( + sellerSwap.orderId !== buyerSwap.orderId, + "order ids must be different", + ); // sanity check + assert( sellerSwap.proof === buyerSwap.proof, - "seller & buyer proof mismatch", + `seller & buyer proof mismatch: ${sellerSwap.orderId} ${buyerSwap.orderId}`, ); const proof = sellerSwap.proof; @@ -207,4 +225,4 @@ export class LobService { } } -type SwapResult = Awaited>; +export type SwapResult = Awaited>; diff --git a/packages/contracts/sdk/mpc/MpcNetworkService.ts b/packages/contracts/sdk/mpc/MpcNetworkService.ts index e62cf14..6570ecc 100644 --- a/packages/contracts/sdk/mpc/MpcNetworkService.ts +++ b/packages/contracts/sdk/mpc/MpcNetworkService.ts @@ -1,10 +1,11 @@ import { UltraHonkBackend } from "@aztec/bb.js"; import type { CompiledCircuit } from "@noir-lang/noir_js"; +import { utils } from "@repo/utils"; import { ethers } from "ethers"; -import { omit } from "lodash"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import PQueue, { type QueueAddOptions } from "p-queue"; import { decodeNativeHonkProof, promiseWithResolvers } from "../utils.js"; import { inWorkingDir, makeRunCommand, splitInput } from "./utils.js"; @@ -36,6 +37,7 @@ export class MpcProverService { class MpcProverPartyService { #storage: Map = new Map(); + #queue = new PQueue({ concurrency: 1 }); constructor(readonly partyIndex: PartyIndex) {} @@ -57,59 +59,75 @@ class MpcProverPartyService { }; this.#storage.set(params.orderId, order); - this.#tryExecuteOrder(params.orderId, { - circuit: params.circuit, - }); + // add this order to other order's queue + // TODO(perf): this is O(N^2) but we should do better + for (const otherOrder of this.#storage.values()) { + this.#addOrdersToQueue({ + orderAId: order.id, + orderBId: otherOrder.id, + circuit: params.circuit, + }); + } return await order.result.promise; } - async #tryExecuteOrder( - orderId: OrderId, - params: { - circuit: CompiledCircuit; - }, - ) { - const order = this.#storage.get(orderId); - if (!order) { - throw new Error( - `order not found in party storage ${this.partyIndex}: ${orderId}`, - ); - } + #addOrdersToQueue(params: { + orderAId: OrderId; + orderBId: OrderId; + circuit: CompiledCircuit; + }) { + const options: QueueAddOptions = { + throwOnTimeout: true, + // this is a hack to enforce the order of execution matches across all MPC parties + priority: Number( + ethers.getBigInt( + ethers.id([params.orderAId, params.orderBId].sort().join("")), + ) % BigInt(Number.MAX_SAFE_INTEGER), + ), + }; + this.#queue.add(async () => { + await utils.sleep(500); // just to make sure all parties got the order over network + const orderA = this.#storage.get(params.orderAId); + const orderB = this.#storage.get(params.orderBId); + if (!orderA || !orderB) { + // one of the orders was already matched + return; + } + if (orderA.id === orderB.id) { + // can't match with itself + return; + } + if (orderA.side === orderB.side) { + // pre-check that orders are on opposite sides + return; + } - const otherOrders = Array.from(this.#storage.values()).filter( - (o) => o.id !== order.id && o.side !== order.side, - ); - if (otherOrders.length === 0) { - return; - } - const otherOrder = otherOrders[0]!; - const inputsShared = - order.side === "seller" - ? ([order.inputShared, otherOrder.inputShared] as const) - : ([otherOrder.inputShared, order.inputShared] as const); - console.log( - "executing orders", - this.partyIndex, - omit(order, ["inputShared", "result"]), - omit(otherOrder, ["inputShared", "result"]), - ); - try { - const { proof } = await proveAsParty({ - circuit: params.circuit, - partyIndex: this.partyIndex, - input0Shared: inputsShared[0], - input1Shared: inputsShared[1], - }); - const proofHex = ethers.hexlify(proof); - order.result.resolve(proofHex); - otherOrder.result.resolve(proofHex); - this.#storage.delete(order.id); - this.#storage.delete(otherOrder.id); - } catch (error) { - order.result.reject(error); - otherOrder.result.reject(error); - } + // deterministic ordering + const [order0, order1] = + orderA.side === "seller" ? [orderA, orderB] : [orderB, orderA]; + console.log("executing orders", this.partyIndex, order0.id, order1.id); + try { + const { proof } = await proveAsParty({ + circuit: params.circuit, + partyIndex: this.partyIndex, + input0Shared: order0.inputShared, + input1Shared: order1.inputShared, + }); + const proofHex = ethers.hexlify(proof); + order0.result.resolve(proofHex); + order1.result.resolve(proofHex); + this.#storage.delete(order0.id); + this.#storage.delete(order1.id); + console.log( + `orders matched: ${this.partyIndex} ${order0.id} ${order1.id}`, + ); + } catch (error) { + console.log( + `orders did not match: ${this.partyIndex} ${order0.id} ${order1.id}`, + ); + } + }, options); } } @@ -119,7 +137,6 @@ async function proveAsParty(params: { input0Shared: string; input1Shared: string; }) { - console.log("proving as party", params.partyIndex); return await inWorkingDir(async (workingDir) => { for (const [traderIndex, inputShared] of [ params.input0Shared, diff --git a/packages/contracts/test/PoolERC20.test.ts b/packages/contracts/test/PoolERC20.test.ts index fa22b81..b70edeb 100644 --- a/packages/contracts/test/PoolERC20.test.ts +++ b/packages/contracts/test/PoolERC20.test.ts @@ -3,6 +3,7 @@ import { expect } from "chai"; import { ethers, noir, typedDeployments } from "hardhat"; import type { sdk as interfaceSdk } from "../sdk"; import type { createBackendSdk } from "../sdk/backendSdk"; +import { SwapResult } from "../sdk/LobService"; import { parseUnits, snapshottedBeforeEach } from "../shared/utils"; import { MockERC20, @@ -44,6 +45,8 @@ describe("PoolERC20", () => { await usdc.connect(alice).approve(pool, ethers.MaxUint256); await btc.mintForTests(bob, await parseUnits(btc, "1000000")); await btc.connect(bob).approve(pool, ethers.MaxUint256); + await btc.mintForTests(charlie, await parseUnits(btc, "1000000")); + await btc.connect(charlie).approve(pool, ethers.MaxUint256); ({ CompleteWaAddress, TokenAmount } = ( await tsImport("../sdk", __filename) @@ -460,11 +463,7 @@ describe("PoolERC20", () => { swapAlicePromise, swapBobPromise, ]); - const args = - swapAlice.side === "seller" - ? ([swapAlice, swapBob] as const) - : ([swapBob, swapAlice] as const); - await sdk.lob.commitSwap(...args); + await sdk.lob.commitSwap({ swapA: swapAlice, swapB: swapBob }); await backendSdk.rollup.rollup(); @@ -474,7 +473,114 @@ describe("PoolERC20", () => { expect(await sdk.poolErc20.balanceOf(btc, bobSecretKey)).to.equal(8n); }); - it("fails to swap if order amounts do not match", async () => { + it("swaps 4 orders", async () => { + if (process.env.CI) { + // TODO: install co-noir on github actions and remove this + return; + } + + const { note: aliceNote0 } = await sdk.poolErc20.shield({ + account: alice, + token: usdc, + amount: 100n, + secretKey: aliceSecretKey, + }); + const { note: aliceNote1 } = await sdk.poolErc20.shield({ + account: alice, + token: usdc, + amount: 100n, + secretKey: aliceSecretKey, + }); + const { note: bobNote } = await sdk.poolErc20.shield({ + account: bob, + token: btc, + amount: 10n, + secretKey: bobSecretKey, + }); + const { note: charlieNote } = await sdk.poolErc20.shield({ + account: charlie, + token: btc, + amount: 20n, + secretKey: charlieSecretKey, + }); + await backendSdk.rollup.rollup(); + + let swaps0Promise: Promise<[SwapResult, SwapResult]>; + { + // alice <-> bob + const sellerAmount = await TokenAmount.from({ + token: await usdc.getAddress(), + amount: 70n, + }); + const buyerAmount = await TokenAmount.from({ + token: await btc.getAddress(), + amount: 2n, + }); + swaps0Promise = Promise.all([ + sdk.lob.requestSwap({ + secretKey: aliceSecretKey, + note: aliceNote0, + sellAmount: sellerAmount, + buyAmount: buyerAmount, + }), + sdk.lob.requestSwap({ + secretKey: bobSecretKey, + note: bobNote, + sellAmount: buyerAmount, + buyAmount: sellerAmount, + }), + ]); + } + + let swaps1Promise: Promise<[SwapResult, SwapResult]>; + { + // alice <-> charlie + const sellerAmount = await TokenAmount.from({ + token: await usdc.getAddress(), + amount: 30n, + }); + const buyerAmount = await TokenAmount.from({ + token: await btc.getAddress(), + amount: 1n, + }); + swaps1Promise = Promise.all([ + sdk.lob.requestSwap({ + secretKey: aliceSecretKey, + note: aliceNote1, + sellAmount: sellerAmount, + buyAmount: buyerAmount, + }), + sdk.lob.requestSwap({ + secretKey: charlieSecretKey, + note: charlieNote, + sellAmount: buyerAmount, + buyAmount: sellerAmount, + }), + ]); + } + + const swaps0 = await swaps0Promise; + const swaps1 = await swaps1Promise; + await sdk.lob.commitSwap({ swapA: swaps0[0], swapB: swaps0[1] }); + await sdk.lob.commitSwap({ swapA: swaps1[0], swapB: swaps1[1] }); + await backendSdk.rollup.rollup(); + + expect(await sdk.poolErc20.balanceOf(usdc, aliceSecretKey)).to.equal( + 200n - 70n - 30n, + ); + expect(await sdk.poolErc20.balanceOf(btc, aliceSecretKey)).to.equal( + 2n + 1n, + ); + + expect(await sdk.poolErc20.balanceOf(usdc, bobSecretKey)).to.equal(70n); + expect(await sdk.poolErc20.balanceOf(btc, bobSecretKey)).to.equal(8n); + + expect(await sdk.poolErc20.balanceOf(usdc, charlieSecretKey)).to.equal(30n); + expect(await sdk.poolErc20.balanceOf(btc, charlieSecretKey)).to.equal(19n); + }); + + // TODO: fix this test and re-enable. It never finishes because it does not throw if orders do no match anymore. + it.skip("fails to swap if order amounts do not match", async () => { if (process.env.CI) { // TODO: install co-noir on github actions and remove this return; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13ee737..9167621 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: lodash-es: specifier: ^4.17.21 version: 4.17.21 + p-queue: + specifier: ^8.1.0 + version: 8.1.0 smol-toml: specifier: ^1.3.1 version: 1.3.1 @@ -4508,6 +4511,14 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + p-queue@8.1.0: + resolution: {integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==} + engines: {node: '>=18'} + + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -10822,6 +10833,13 @@ snapshots: dependencies: aggregate-error: 3.1.0 + p-queue@8.1.0: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 6.1.4 + + p-timeout@6.1.4: {} + package-json-from-dist@1.0.1: {} pako@1.0.11: {} From c7111d2d053247161d49ca2a1957e68d26d000c7 Mon Sep 17 00:00:00 2001 From: oleh Date: Fri, 14 Feb 2025 15:28:04 +0100 Subject: [PATCH 10/16] chore(docs): explain what the project is about --- README.md | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1ba6e71..6fd58c8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ # Mezcal -Mezcal (Nahuatl: mexcalli) - agave booze. +Mezcal (Nahuatl: mexcalli - agave booze) - on-chain dark pool implementation using [Noir](https://noir-lang.org) and [Taceo coNoir](https://taceo.io). Hides EVERYTHING about orders and traders(tokens, amounts and addresses of traders are completely hidden). Trades settled on an EVM chain using a very simplified version of [Aztec Protocol](https://aztec.network). The tradeoff is O(N^2) order matching engine. + +The code is highly experimental. The core code is located in `packages/contracts`. ## TODO @@ -21,22 +23,3 @@ Mezcal (Nahuatl: mexcalli) - agave booze. - [ ] deploy as proxy - [ ] test contracts with larger token amounts - [ ] TODO(security): parse inputs to circuits instead of assuming they are correct. Same applies to types returned from `unconstrained` functions. - -### Backend - -- [x] prove using native bb -- [ ] persist merkle trees -- [ ] return pending tree roots - -### UI - -- [x] shield -- [x] transfer -- [ ] join (maybe behind the scenes, multicall) -- [ ] unshield - -### compliance - -- [ ] unshield only mode -- [ ] set shield limit to 10 USDC -- [ ] disclaimer that the rollup is not audited From 51cc360dc17615f6d12474c1a36f3ac75c73902e Mon Sep 17 00:00:00 2001 From: oleh Date: Thu, 27 Feb 2025 12:43:56 +0100 Subject: [PATCH 11/16] feat: switch to U253 (#23) Was previously on `U252` because of this https://github.com/TaceoLabs/co-snarks/issues/317 --- packages/contracts/noir/common/src/lib.nr | 4 +- .../common/src/{uint252.nr => uint253.nr} | 61 +++++++++---------- 2 files changed, 32 insertions(+), 33 deletions(-) rename packages/contracts/noir/common/src/{uint252.nr => uint253.nr} (77%) diff --git a/packages/contracts/noir/common/src/lib.nr b/packages/contracts/noir/common/src/lib.nr index 639d83d..cc61519 100644 --- a/packages/contracts/noir/common/src/lib.nr +++ b/packages/contracts/noir/common/src/lib.nr @@ -1,7 +1,7 @@ use protocol_types::hash::poseidon2_hash_with_separator; mod context; -mod uint252; +mod uint253; mod erc20_note; pub(crate) mod note; mod owned_note; @@ -46,7 +46,7 @@ pub global GENERATOR_INDEX__NOTE_HASH: Field = 3; // Note: keep in sync with other languages pub global U256_LIMBS: u32 = 3; -pub type U256 = uint252::U252; +pub type U256 = uint253::U253; /// User address within the rollup #[derive(Eq, Serialize)] diff --git a/packages/contracts/noir/common/src/uint252.nr b/packages/contracts/noir/common/src/uint253.nr similarity index 77% rename from packages/contracts/noir/common/src/uint252.nr rename to packages/contracts/noir/common/src/uint253.nr index bf39797..db17fb9 100644 --- a/packages/contracts/noir/common/src/uint252.nr +++ b/packages/contracts/noir/common/src/uint253.nr @@ -4,18 +4,18 @@ use std::cmp::{Eq, Ord, Ordering}; use std::ops::{Add, Div, Mul, Rem, Sub}; -// Maximum value for U252 (2^252 - 1), chosen to fit within Aztec's field arithmetic bounds -pub global MAX_U252: Field = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; +// Maximum value for U253 (2^253 - 1), chosen to fit within Aztec's field arithmetic bounds +pub global MAX_U253: Field = 0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; -pub global U252_PACKED_LEN: u32 = 1; +pub global U253_PACKED_LEN: u32 = 1; -pub struct U252 { +pub struct U253 { value: Field, } -impl U252 { +impl U253 { pub fn new(value: Field) -> Self { - value.assert_max_bit_size::<252>(); + value.assert_max_bit_size::<253>(); Self { value } } @@ -24,7 +24,7 @@ impl U252 { } pub fn from_integer(value: Field) -> Self { - value.assert_max_bit_size::<252>(); + value.assert_max_bit_size::<253>(); Self { value } } @@ -41,7 +41,7 @@ impl U252 { } pub fn max() -> Self { - Self { value: MAX_U252 } + Self { value: MAX_U253 } } pub fn is_zero(self) -> bool { @@ -53,17 +53,17 @@ impl U252 { pub unconstrained fn div_rem_unconstrained(self, other: Self) -> (Self, Self) { assert(!(other.value == 0), "Division by zero"); - self.value.assert_max_bit_size::<252>(); - other.value.assert_max_bit_size::<252>(); + self.value.assert_max_bit_size::<253>(); + other.value.assert_max_bit_size::<253>(); - let bits: [u1; 252] = self.value.to_be_bits(); + let bits: [u1; 253] = self.value.to_be_bits(); let divisor = other.value; let mut quotient: Field = 0; let mut remainder: Field = 0; // Process each bit from MSB to LSB, similar to paper-and-pencil division - for i in 0..252 { + for i in 0..253 { // Shift remainder left by 1 bit and add next bit remainder = remainder * 2 + (bits[i] as Field); @@ -107,73 +107,72 @@ impl U252 { } } - // Adds two U252 values without overflow checks - use with caution + // Adds two U253 values without overflow checks - use with caution pub fn add_unchecked(self, other: Self) -> Self { Self { value: self.value + other.value } } - // Subtracts two U252 values without underflow checks - use with caution + // Subtracts two U253 values without underflow checks - use with caution pub fn sub_unchecked(self, other: Self) -> Self { Self { value: self.value - other.value } } } - -impl Add for U252 { +impl Add for U253 { fn add(self, other: Self) -> Self { let result = self.value + other.value; - result.assert_max_bit_size::<252>(); + result.assert_max_bit_size::<253>(); - assert(!MAX_U252.lt(result), "U252 addition overflow"); - assert(!result.lt(self.value), "U252 addition overflow"); - assert(!result.lt(other.value), "U252 addition overflow"); + assert(!MAX_U253.lt(result), "U253 addition overflow"); + assert(!result.lt(self.value), "U253 addition overflow"); + assert(!result.lt(other.value), "U253 addition overflow"); Self { value: result } } } -impl Sub for U252 { +impl Sub for U253 { fn sub(self, other: Self) -> Self { assert( other.value.lt(self.value) | other.value.eq(self.value), - "U252 subtraction underflow", + "U253 subtraction underflow", ); let result = self.value - other.value; - result.assert_max_bit_size::<252>(); + result.assert_max_bit_size::<253>(); Self { value: result } } } -impl Mul for U252 { +impl Mul for U253 { fn mul(self, other: Self) -> Self { let result = self.value * other.value; - result.assert_max_bit_size::<252>(); + result.assert_max_bit_size::<253>(); // Allow multiplication by 1 without additional checks, otherwise check for overflow assert( (self.value == 1) | (other.value == 1) - | (result.lt(MAX_U252 + 1) & !result.lt(self.value) & !result.lt(other.value)), - "U252 multiplication overflow", + | (result.lt(MAX_U253 + 1) & !result.lt(self.value) & !result.lt(other.value)), + "U253 multiplication overflow", ); Self { value: result } } } -impl Div for U252 { +impl Div for U253 { fn div(self, other: Self) -> Self { let (quotient, _) = self.div_rem(other); quotient } } -impl Rem for U252 { +impl Rem for U253 { fn rem(self, other: Self) -> Self { let (_, remainder) = self.div_rem(other); remainder } } -impl Ord for U252 { +impl Ord for U253 { fn cmp(self, other: Self) -> Ordering { if self.value.lt(other.value) { Ordering::less() @@ -185,7 +184,7 @@ impl Ord for U252 { } } -impl Eq for U252 { +impl Eq for U253 { fn eq(self, other: Self) -> bool { self.value.eq(other.value) } From 00720c17feebc5ee5b2d9b9bb938e3d39a45be27 Mon Sep 17 00:00:00 2001 From: oleh Date: Wed, 12 Mar 2025 01:04:25 +0100 Subject: [PATCH 12/16] chore: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fd58c8..5eddcec 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + # Mezcal From 7b07a93e68fcf831d141a353e9f043d885f559df Mon Sep 17 00:00:00 2001 From: oleh Date: Wed, 4 Jun 2025 16:12:08 +0200 Subject: [PATCH 13/16] chore(ci): bump ubuntu & nodejs --- .github/workflows/test.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3609459..68476f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,17 +4,16 @@ on: [push] jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - node-version: [20] + node-version: [22] steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "pnpm" From ed2f7204843ea95516c469d7de1c7355883085ea Mon Sep 17 00:00:00 2001 From: oleh Date: Wed, 4 Jun 2025 16:47:11 +0200 Subject: [PATCH 14/16] fix hasher --- packages/contracts/noir/run.sh | 19 +++++++++---------- packages/contracts/sdk/mpc/run-party.sh | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/contracts/noir/run.sh b/packages/contracts/noir/run.sh index 1b8f89b..c2871dc 100755 --- a/packages/contracts/noir/run.sh +++ b/packages/contracts/noir/run.sh @@ -12,12 +12,12 @@ CIRCUIT=target/$CIRCUIT_NAME.json # merge Prover1.toml and Prover2.toml into Prover.toml # Convert TOML to JSON -dasel -f "$CIRCUIT_NAME/Prover1.toml" -r toml -w json > prover1.json -dasel -f "$CIRCUIT_NAME/Prover2.toml" -r toml -w json > prover2.json +dasel -f "$CIRCUIT_NAME/Prover1.toml" -r toml -w json >prover1.json +dasel -f "$CIRCUIT_NAME/Prover2.toml" -r toml -w json >prover2.json # Merge JSON with jq -jq -s '.[0] * .[1]' prover1.json prover2.json > merged.json +jq -s '.[0] * .[1]' prover1.json prover2.json >merged.json # Convert back to TOML -dasel -f merged.json -r json -w toml > "$CIRCUIT_NAME/Prover.toml" +dasel -f merged.json -r json -w toml >"$CIRCUIT_NAME/Prover.toml" rm prover1.json prover2.json merged.json # split input into shares @@ -49,9 +49,9 @@ wait $(jobs -p) timeEnd "mpc-build-proving-key" timeStart "mpc-generate-proof" -co-noir generate-proof --proving-key target/proving_key.0 --protocol REP3 --hasher KECCAK --crs ~/.bb-crs/bn254_g1.dat --config configs/party0.toml --out target/proof.0.proof --public-input target/public_input.json & -co-noir generate-proof --proving-key target/proving_key.1 --protocol REP3 --hasher KECCAK --crs ~/.bb-crs/bn254_g1.dat --config configs/party1.toml --out target/proof.1.proof & -co-noir generate-proof --proving-key target/proving_key.2 --protocol REP3 --hasher KECCAK --crs ~/.bb-crs/bn254_g1.dat --config configs/party2.toml --out target/proof.2.proof +co-noir generate-proof --proving-key target/proving_key.0 --protocol REP3 --hasher keccak --crs ~/.bb-crs/bn254_g1.dat --config configs/party0.toml --out target/proof.0.proof --public-input target/public_input.json & +co-noir generate-proof --proving-key target/proving_key.1 --protocol REP3 --hasher keccak --crs ~/.bb-crs/bn254_g1.dat --config configs/party1.toml --out target/proof.1.proof & +co-noir generate-proof --proving-key target/proving_key.2 --protocol REP3 --hasher keccak --crs ~/.bb-crs/bn254_g1.dat --config configs/party2.toml --out target/proof.2.proof wait $(jobs -p) timeEnd "mpc-generate-proof" @@ -62,13 +62,12 @@ timeStart "bb-generate-proof" bb prove_ultra_keccak_honk -b $CIRCUIT -w target/$CIRCUIT_NAME.gz -o target/proof_bb.proof timeEnd "bb-generate-proof" - # Create verification key -co-noir create-vk --circuit $CIRCUIT --crs bn254_g1.dat --hasher KECCAK --vk target/verification_key +co-noir create-vk --circuit $CIRCUIT --crs bn254_g1.dat --hasher keccak --vk target/verification_key echo "Verification key created" # verify proof -co-noir verify --proof target/proof.0.proof --vk target/verification_key --hasher KECCAK --crs bn254_g2.dat +co-noir verify --proof target/proof.0.proof --vk target/verification_key --hasher keccak --crs bn254_g2.dat echo "Proof verified" bb write_vk_ultra_keccak_honk -b $CIRCUIT -o target/verification_key_bb diff --git a/packages/contracts/sdk/mpc/run-party.sh b/packages/contracts/sdk/mpc/run-party.sh index 36886cc..7107762 100755 --- a/packages/contracts/sdk/mpc/run-party.sh +++ b/packages/contracts/sdk/mpc/run-party.sh @@ -33,5 +33,5 @@ co-noir build-proving-key --witness $WORK_DIR/witness.gz.$PARTY_INDEX.shared --c timeEnd "mpc-build-proving-key" timeStart "mpc-generate-proof" -co-noir generate-proof --proving-key $WORK_DIR/proving_key.$PARTY_INDEX --protocol REP3 --hasher KECCAK --crs ~/.bb-crs/bn254_g1.dat --config $PARTY_CONFIGS_DIR/party$PARTY_INDEX.toml --out $WORK_DIR/proof.$PARTY_INDEX.proof --public-input $WORK_DIR/public_input.json +co-noir generate-proof --proving-key $WORK_DIR/proving_key.$PARTY_INDEX --protocol REP3 --hasher keccak --crs ~/.bb-crs/bn254_g1.dat --config $PARTY_CONFIGS_DIR/party$PARTY_INDEX.toml --out $WORK_DIR/proof.$PARTY_INDEX.proof --public-input $WORK_DIR/public_input.json timeEnd "mpc-generate-proof" From 6a24b6f4deaf0b946023f0873838f87822e41d54 Mon Sep 17 00:00:00 2001 From: oleh Date: Wed, 4 Jun 2025 17:05:06 +0200 Subject: [PATCH 15/16] Merge branch 'main' into dark-pool --- .../contracts/sdk/NativeUltraHonkBackend.ts | 21 ++---------------- .../contracts/sdk/mpc/MpcNetworkService.ts | 11 +++++----- packages/contracts/sdk/utils.ts | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/contracts/sdk/NativeUltraHonkBackend.ts b/packages/contracts/sdk/NativeUltraHonkBackend.ts index 856d1b2..341d88f 100644 --- a/packages/contracts/sdk/NativeUltraHonkBackend.ts +++ b/packages/contracts/sdk/NativeUltraHonkBackend.ts @@ -1,11 +1,9 @@ import type { ProofData } from "@aztec/bb.js"; import type { CompiledCircuit } from "@noir-lang/noir_js"; -import { chunk } from "lodash-es"; import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { Hex } from "ox"; -import { assert } from "ts-essentials"; +import { readNativeHonkProof } from "./utils.js"; export class NativeUltraHonkBackend { constructor( @@ -62,22 +60,7 @@ export class NativeUltraHonkBackend { reject(new Error(`Process exited with code ${code}`)); return; } - - const proof = fs.readFileSync(path.join(proofOutputPath, "proof")); - const publicInputs = fs.readFileSync( - path.join(proofOutputPath, "public_inputs"), - ); - assert( - publicInputs.length % 32 === 0, - "publicInputs length must be divisible by 32", - ); - resolve({ - proof, - // TODO: not sure if this publicInputs decoding is correct - publicInputs: chunk(Array.from(publicInputs), 32).map((x) => - Hex.fromBytes(Uint8Array.from(x)), - ), - }); + resolve(readNativeHonkProof(proofOutputPath)); }); bbProcess.on("error", (err) => { diff --git a/packages/contracts/sdk/mpc/MpcNetworkService.ts b/packages/contracts/sdk/mpc/MpcNetworkService.ts index 6570ecc..c85e346 100644 --- a/packages/contracts/sdk/mpc/MpcNetworkService.ts +++ b/packages/contracts/sdk/mpc/MpcNetworkService.ts @@ -6,7 +6,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import PQueue, { type QueueAddOptions } from "p-queue"; -import { decodeNativeHonkProof, promiseWithResolvers } from "../utils.js"; +import { promiseWithResolvers } from "../utils.js"; import { inWorkingDir, makeRunCommand, splitInput } from "./utils.js"; export class MpcProverService { @@ -161,10 +161,11 @@ async function proveAsParty(params: { params.partyIndex, ]); - const { proof, publicInputs } = decodeNativeHonkProof( - fs.readFileSync( - path.join(workingDir, `proof.${params.partyIndex}.proof`), - ), + const proof = fs.readFileSync( + path.join(workingDir, `proof.${params.partyIndex}.proof`), + ); + const publicInputs = JSON.parse( + fs.readFileSync(path.join(workingDir, "public-input.json"), "utf-8"), ); // pre-verify proof diff --git a/packages/contracts/sdk/utils.ts b/packages/contracts/sdk/utils.ts index 9f32efe..229bc97 100644 --- a/packages/contracts/sdk/utils.ts +++ b/packages/contracts/sdk/utils.ts @@ -1,6 +1,10 @@ import type { Fr } from "@aztec/aztec.js"; import type { InputMap } from "@noir-lang/noir_js"; import { ethers } from "ethers"; +import { chunk } from "lodash-es"; +import fs from "node:fs"; +import path from "node:path"; +import { Hex } from "ox"; import { assert } from "ts-essentials"; import type { NoirAndBackend } from "./sdk.js"; @@ -90,3 +94,21 @@ export function promiseWithResolvers(): { }); return ret; } + +export function readNativeHonkProof(pathToProofDir: string) { + const proof = fs.readFileSync(path.join(pathToProofDir, "proof")); + const publicInputs = fs.readFileSync( + path.join(pathToProofDir, "public_inputs"), + ); + assert( + publicInputs.length % 32 === 0, + "publicInputs length must be divisible by 32", + ); + return { + proof, + // TODO: not sure if this publicInputs decoding is correct + publicInputs: chunk(Array.from(publicInputs), 32).map((x) => + Hex.fromBytes(Uint8Array.from(x)), + ), + }; +} From adc146dba53793a6883ac9fc1055f4fa8fd25c64 Mon Sep 17 00:00:00 2001 From: oleh Date: Wed, 4 Jun 2025 17:09:48 +0200 Subject: [PATCH 16/16] chore(ci): downgrade node --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68476f3..25d669a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,13 +7,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [22] + node-version: [20] steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "pnpm"