From be50e146ec4f4774310dfeaa059c003f38764f18 Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Wed, 14 Aug 2024 12:04:00 -0400 Subject: [PATCH 1/7] Add new circuit with non_repudiation Signed-off-by: Jim Zhang --- ...ier_anon_enc_nullifier_non_repudiation.sol | 345 ++++++++++++++++++ ...eto_anon_enc_nullifier_non_repudiation.sol | 151 ++++++++ .../anon_enc_nullifier_non_repudiation.circom | 198 ++++++++++ zkp/circuits/gen-config.json | 4 + .../anon_enc_nullifier_non_repudiation.js | 131 +++++++ .../anon_enc_nullifier_non_repudiation.js | 327 +++++++++++++++++ 6 files changed, 1156 insertions(+) create mode 100644 solidity/contracts/lib/verifier_anon_enc_nullifier_non_repudiation.sol create mode 100644 solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol create mode 100644 zkp/circuits/anon_enc_nullifier_non_repudiation.circom create mode 100644 zkp/js/integration-test/anon_enc_nullifier_non_repudiation.js create mode 100644 zkp/js/test/anon_enc_nullifier_non_repudiation.js diff --git a/solidity/contracts/lib/verifier_anon_enc_nullifier_non_repudiation.sol b/solidity/contracts/lib/verifier_anon_enc_nullifier_non_repudiation.sol new file mode 100644 index 0000000..37efa35 --- /dev/null +++ b/solidity/contracts/lib/verifier_anon_enc_nullifier_non_repudiation.sol @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright 2021 0KIMS association. + + This file is generated with [snarkJS](https://github.com/iden3/snarkjs). + + snarkJS is a free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + snarkJS is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with snarkJS. If not, see . +*/ + +pragma solidity >=0.7.0 <0.9.0; + +contract Groth16Verifier_AnonEncNullifierNonRepudiation { + // Scalar field size + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + // Base field size + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + // Verification Key data + uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; + uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; + uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; + uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; + uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; + uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; + uint256 constant deltax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; + uint256 constant deltax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; + uint256 constant deltay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; + uint256 constant deltay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; + + + uint256 constant IC0x = 18730365562460670693262104413994931450806245038342991074603702551145897446352; + uint256 constant IC0y = 1986693462786640810426767380703648796254793448064011972797702315349632815679; + + uint256 constant IC1x = 16870277633353431582745757456114551991543220125333910640950906223222428246600; + uint256 constant IC1y = 11763303529950153436167850321952253781150101539550635974422884602967931704529; + + uint256 constant IC2x = 7492139160872683688606874534836480114199794859856431341520104211895185468105; + uint256 constant IC2y = 14813280517590580695434122977657319147316681485832507107118965404727369155658; + + uint256 constant IC3x = 10541581144684677245563567719009299858168100252975530701085184594846686922442; + uint256 constant IC3y = 12423010272711947932214571551592641513351836227790619198037432672075736715985; + + uint256 constant IC4x = 20547896192668194748705496940293250349138487712849921548951135754449028100114; + uint256 constant IC4y = 19380018835465602880669597932303754588516556906579156141998369090215267943905; + + uint256 constant IC5x = 3497934937374698861458082648510524862735904576538121762683780666720217803943; + uint256 constant IC5y = 19358419081217534482456243431622745184097057178054796594003426205353779408371; + + uint256 constant IC6x = 1222897936315596126927341482014264094130869267003109149428325722924149269544; + uint256 constant IC6y = 17679781720025747485721341325889092064011165262210358207787805343692728587982; + + uint256 constant IC7x = 11041809683612507882649266123341775357582386776766315442375607468404277810808; + uint256 constant IC7y = 3008732445284909178337061418859181110676925452270748699360912213378069885455; + + uint256 constant IC8x = 16630729143544500445197281156182928567976009331142950423286548026584143292576; + uint256 constant IC8y = 15034306132328240152778386149567086676475530969294098655761124191402795904536; + + uint256 constant IC9x = 6615333468444900552853977016808455783428501574114697288106266932708507275234; + uint256 constant IC9y = 16816650594233630741501106371904203042605156553324566045620319363635119273611; + + uint256 constant IC10x = 19071460928967456065019966088451226983659340250314978502397921634594661669202; + uint256 constant IC10y = 1521670794961263537550750326668826805956417807016117659581659908684770081357; + + uint256 constant IC11x = 13503065048439891388718111176106509690741021175819398503545089877797023109062; + uint256 constant IC11y = 21886449946977333776493126189347465297374951226924400836489580610683320809827; + + uint256 constant IC12x = 21207934520129176934656802371776642692782385589914235168175964200339939576592; + uint256 constant IC12y = 12880605648408212290905605663986734167225069025715670443566570886545917308568; + + uint256 constant IC13x = 4470575671008036988174637795369279391874438048582101033974877701807604256199; + uint256 constant IC13y = 18492119776600731115965678551473405638918810395454855183680152154460897121480; + + uint256 constant IC14x = 15896685925262803443305015925978470163054181332116352355052362643495981122283; + uint256 constant IC14y = 12995306051876963863577325360971867180815611496349906990707568670067468512950; + + uint256 constant IC15x = 18033269819713943042093661001497349646150994442336376524396808633510060209739; + uint256 constant IC15y = 10544134106796260597673035021134916267400471907673885489742471019506346162054; + + uint256 constant IC16x = 2560487462210122212235515281409354012727657975873197679206210024307867678616; + uint256 constant IC16y = 7203891636392831834367382568036152128346258845664303724798991382219198140918; + + uint256 constant IC17x = 19152890826403662893915072520364303958088562532449137774109654696678125025272; + uint256 constant IC17y = 14889762080799876036275653222861494734297362274127956469643055095088097647418; + + uint256 constant IC18x = 9758106434739296718600285374091185957814948920034065486612914894379346523770; + uint256 constant IC18y = 2147670986683875116249580783099190055441110350250107174763612136319896606412; + + uint256 constant IC19x = 3642620546912522589210619468605528736123026162679018804543412112786896355315; + uint256 constant IC19y = 16561150047952056323284926141498954409510866389749288759478417043104414698685; + + uint256 constant IC20x = 13667792973725615282792658402930901973096877991170633528092314792437179745509; + uint256 constant IC20y = 15176548165974713660213461970024778773513106069242307529675799567993647827740; + + uint256 constant IC21x = 3401740095211941486627752855321162965271093704950562655273080740404285838459; + uint256 constant IC21y = 20813590650444227797445106581638546347192161163972098804515811798279457555196; + + uint256 constant IC22x = 14318409529302451147666664224574182782932618312346884007209627458110610824487; + uint256 constant IC22y = 10286111568804634144434693103438828848600605075707241771599436632738726403506; + + uint256 constant IC23x = 20683375524482501889803664991669117821831708552700209056098686828065110662392; + uint256 constant IC23y = 11285260080724050878304520482563733329085324990327931630100108118825710008932; + + uint256 constant IC24x = 14565397267148370495951677799635085595349972097735345951141956797330097348134; + uint256 constant IC24y = 6309317111022179093636887003452256235598047420461201829067740835945116280014; + + uint256 constant IC25x = 4907509403349732366967349924067172021197773973197524936861077474343993802133; + uint256 constant IC25y = 15442826017665100925677027196087926402204596775212426379303274305999559794446; + + uint256 constant IC26x = 20809625668304932099387825941210510287331745765481116747023403902550180813519; + uint256 constant IC26y = 21787365501555085079883514800551444640048687379978439395138172888432053378843; + + + // Memory data + uint16 constant pVk = 0; + uint16 constant pPairing = 128; + + uint16 constant pLastMem = 896; + + function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[26] calldata _pubSignals) public view returns (bool) { + assembly { + function checkField(v) { + if iszero(lt(v, q)) { + mstore(0, 0) + return(0, 0x20) + } + } + + // G1 function to multiply a G1 value(x,y) to value in an address + function g1_mulAccC(pR, x, y, s) { + let success + let mIn := mload(0x40) + mstore(mIn, x) + mstore(add(mIn, 32), y) + mstore(add(mIn, 64), s) + + success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + + mstore(add(mIn, 64), mload(pR)) + mstore(add(mIn, 96), mload(add(pR, 32))) + + success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + } + + function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk { + let _pPairing := add(pMem, pPairing) + let _pVk := add(pMem, pVk) + + mstore(_pVk, IC0x) + mstore(add(_pVk, 32), IC0y) + + // Compute the linear combination vk_x + + g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0))) + + g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32))) + + g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64))) + + g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96))) + + g1_mulAccC(_pVk, IC5x, IC5y, calldataload(add(pubSignals, 128))) + + g1_mulAccC(_pVk, IC6x, IC6y, calldataload(add(pubSignals, 160))) + + g1_mulAccC(_pVk, IC7x, IC7y, calldataload(add(pubSignals, 192))) + + g1_mulAccC(_pVk, IC8x, IC8y, calldataload(add(pubSignals, 224))) + + g1_mulAccC(_pVk, IC9x, IC9y, calldataload(add(pubSignals, 256))) + + g1_mulAccC(_pVk, IC10x, IC10y, calldataload(add(pubSignals, 288))) + + g1_mulAccC(_pVk, IC11x, IC11y, calldataload(add(pubSignals, 320))) + + g1_mulAccC(_pVk, IC12x, IC12y, calldataload(add(pubSignals, 352))) + + g1_mulAccC(_pVk, IC13x, IC13y, calldataload(add(pubSignals, 384))) + + g1_mulAccC(_pVk, IC14x, IC14y, calldataload(add(pubSignals, 416))) + + g1_mulAccC(_pVk, IC15x, IC15y, calldataload(add(pubSignals, 448))) + + g1_mulAccC(_pVk, IC16x, IC16y, calldataload(add(pubSignals, 480))) + + g1_mulAccC(_pVk, IC17x, IC17y, calldataload(add(pubSignals, 512))) + + g1_mulAccC(_pVk, IC18x, IC18y, calldataload(add(pubSignals, 544))) + + g1_mulAccC(_pVk, IC19x, IC19y, calldataload(add(pubSignals, 576))) + + g1_mulAccC(_pVk, IC20x, IC20y, calldataload(add(pubSignals, 608))) + + g1_mulAccC(_pVk, IC21x, IC21y, calldataload(add(pubSignals, 640))) + + g1_mulAccC(_pVk, IC22x, IC22y, calldataload(add(pubSignals, 672))) + + g1_mulAccC(_pVk, IC23x, IC23y, calldataload(add(pubSignals, 704))) + + g1_mulAccC(_pVk, IC24x, IC24y, calldataload(add(pubSignals, 736))) + + g1_mulAccC(_pVk, IC25x, IC25y, calldataload(add(pubSignals, 768))) + + g1_mulAccC(_pVk, IC26x, IC26y, calldataload(add(pubSignals, 800))) + + + // -A + mstore(_pPairing, calldataload(pA)) + mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) + + // B + mstore(add(_pPairing, 64), calldataload(pB)) + mstore(add(_pPairing, 96), calldataload(add(pB, 32))) + mstore(add(_pPairing, 128), calldataload(add(pB, 64))) + mstore(add(_pPairing, 160), calldataload(add(pB, 96))) + + // alpha1 + mstore(add(_pPairing, 192), alphax) + mstore(add(_pPairing, 224), alphay) + + // beta2 + mstore(add(_pPairing, 256), betax1) + mstore(add(_pPairing, 288), betax2) + mstore(add(_pPairing, 320), betay1) + mstore(add(_pPairing, 352), betay2) + + // vk_x + mstore(add(_pPairing, 384), mload(add(pMem, pVk))) + mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) + + + // gamma2 + mstore(add(_pPairing, 448), gammax1) + mstore(add(_pPairing, 480), gammax2) + mstore(add(_pPairing, 512), gammay1) + mstore(add(_pPairing, 544), gammay2) + + // C + mstore(add(_pPairing, 576), calldataload(pC)) + mstore(add(_pPairing, 608), calldataload(add(pC, 32))) + + // delta2 + mstore(add(_pPairing, 640), deltax1) + mstore(add(_pPairing, 672), deltax2) + mstore(add(_pPairing, 704), deltay1) + mstore(add(_pPairing, 736), deltay2) + + + let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) + + isOk := and(success, mload(_pPairing)) + } + + let pMem := mload(0x40) + mstore(0x40, add(pMem, pLastMem)) + + // Validate that all evaluations ∈ F + + checkField(calldataload(add(_pubSignals, 0))) + + checkField(calldataload(add(_pubSignals, 32))) + + checkField(calldataload(add(_pubSignals, 64))) + + checkField(calldataload(add(_pubSignals, 96))) + + checkField(calldataload(add(_pubSignals, 128))) + + checkField(calldataload(add(_pubSignals, 160))) + + checkField(calldataload(add(_pubSignals, 192))) + + checkField(calldataload(add(_pubSignals, 224))) + + checkField(calldataload(add(_pubSignals, 256))) + + checkField(calldataload(add(_pubSignals, 288))) + + checkField(calldataload(add(_pubSignals, 320))) + + checkField(calldataload(add(_pubSignals, 352))) + + checkField(calldataload(add(_pubSignals, 384))) + + checkField(calldataload(add(_pubSignals, 416))) + + checkField(calldataload(add(_pubSignals, 448))) + + checkField(calldataload(add(_pubSignals, 480))) + + checkField(calldataload(add(_pubSignals, 512))) + + checkField(calldataload(add(_pubSignals, 544))) + + checkField(calldataload(add(_pubSignals, 576))) + + checkField(calldataload(add(_pubSignals, 608))) + + checkField(calldataload(add(_pubSignals, 640))) + + checkField(calldataload(add(_pubSignals, 672))) + + checkField(calldataload(add(_pubSignals, 704))) + + checkField(calldataload(add(_pubSignals, 736))) + + checkField(calldataload(add(_pubSignals, 768))) + + checkField(calldataload(add(_pubSignals, 800))) + + checkField(calldataload(add(_pubSignals, 832))) + + + // Validate all evaluations + let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) + + mstore(0, isValid) + return(0, 0x20) + } + } + } diff --git a/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol b/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol new file mode 100644 index 0000000..58860a1 --- /dev/null +++ b/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol @@ -0,0 +1,151 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma solidity ^0.8.20; + +import {Groth16Verifier_CheckHashesValue} from "./lib/verifier_check_hashes_value.sol"; +import {Groth16Verifier_CheckNullifierValue} from "./lib/verifier_check_nullifier_value.sol"; +import {Groth16Verifier_AnonEncNullifier} from "./lib/verifier_anon_enc_nullifier.sol"; +import {ZetoNullifier} from "./lib/zeto_nullifier.sol"; +import {ZetoFungibleWithdrawWithNullifiers} from "./lib/zeto_fungible_withdraw_nullifier.sol"; +import {Registry} from "./lib/registry.sol"; +import {Commonlib} from "./lib/common.sol"; +import "hardhat/console.sol"; + +/// @title A sample implementation of a Zeto based fungible token with anonymity, encryption and history masking +/// @author Kaleido, Inc. +/// @dev The proof has the following statements: +/// - each value in the output commitments must be a positive number in the range 0 ~ (2\*\*40 - 1) +/// - the sum of the nullified values match the sum of output values +/// - the hashes in the input and output match the hash(value, salt, owner public key) formula +/// - the sender possesses the private BabyJubjub key, whose public key is part of the pre-image of the input commitment hashes, which match the corresponding nullifiers +/// - the encrypted value in the input is derived from the receiver's UTXO value and encrypted with a shared secret using the ECDH protocol between the sender and receiver (this guarantees data availability for the receiver) +/// - the nullifiers represent input commitments that are included in a Sparse Merkle Tree represented by the root hash +contract Zeto_AnonEncNullifierNonRepudiation is + ZetoNullifier, + ZetoFungibleWithdrawWithNullifiers +{ + Groth16Verifier_AnonEncNullifierNonRepudiation verifier; + // the authority public key that must be used to + // encrypt the secrets of every transaction + uint256[2] private authority; + + constructor( + Groth16Verifier_CheckHashesValue _depositVerifier, + Groth16Verifier_CheckNullifierValue _withdrawVerifier, + Groth16Verifier_AnonEncNullifierNonRepudiation _verifier + ) + ZetoNullifier() + ZetoFungibleWithdrawWithNullifiers(_depositVerifier, _withdrawVerifier) + { + verifier = _verifier; + } + + /** + * @dev the main function of the contract, which transfers values from one account (represented by Babyjubjub public keys) + * to one or more receiver accounts (also represented by Babyjubjub public keys). One of the two nullifiers may be zero + * if the transaction only needs one UTXO to be spent. Equally one of the two outputs may be zero if the transaction + * only needs to create one new UTXO. + * + * @param nullifiers Array of nullifiers that are secretly bound to UTXOs to be spent by the transaction. + * @param outputs Array of new UTXOs to generate, for future transactions to spend. + * @param root The root hash of the Sparse Merkle Tree that contains the nullifiers. + * @param proof A zero knowledge proof that the submitter is authorized to spend the inputs, and + * that the outputs are valid in terms of obeying mass conservation rules. + * + * Emits a {UTXOTransferWithEncryptedValues} event. + */ + function transfer( + uint256[2] memory nullifiers, + uint256[2] memory outputs, + uint256 root, + uint256 encryptionNonce, + uint256[2] memory encryptedValues, + Commonlib.Proof calldata proof + ) public returns (bool) { + require( + validateTransactionProposal(nullifiers, outputs, root), + "Invalid transaction proposal" + ); + + // construct the public inputs + uint256[10] memory publicInputs; + publicInputs[0] = encryptedValues[0]; // encrypted value for the receiver UTXO + publicInputs[1] = encryptedValues[1]; // encrypted salt for the receiver UTXO + publicInputs[2] = nullifiers[0]; + publicInputs[3] = nullifiers[1]; + publicInputs[4] = root; + publicInputs[5] = (nullifiers[0] == 0) ? 0 : 1; // if the first nullifier is empty, disable its MT proof verification + publicInputs[6] = (nullifiers[1] == 0) ? 0 : 1; // if the second nullifier is empty, disable its MT proof verification + publicInputs[7] = outputs[0]; + publicInputs[8] = outputs[1]; + publicInputs[9] = encryptionNonce; + + // // Check the proof + require( + verifier.verifyProof(proof.pA, proof.pB, proof.pC, publicInputs), + "Invalid proof" + ); + + // accept the transaction to consume the input UTXOs and produce new UTXOs + processInputsAndOutputs(nullifiers, outputs); + + uint256[] memory nullifierArray = new uint256[](nullifiers.length); + uint256[] memory outputArray = new uint256[](outputs.length); + uint256[] memory encryptedValuesArray = new uint256[]( + encryptedValues.length + ); + for (uint256 i = 0; i < nullifiers.length; ++i) { + nullifierArray[i] = nullifiers[i]; + outputArray[i] = outputs[i]; + encryptedValuesArray[i] = encryptedValues[i]; + } + + emit UTXOTransferWithEncryptedValues( + nullifierArray, + outputArray, + encryptionNonce, + encryptedValuesArray, + msg.sender + ); + return true; + } + + function deposit( + uint256 amount, + uint256 utxo, + Commonlib.Proof calldata proof + ) public { + _deposit(amount, utxo, proof); + uint256[] memory utxos = new uint256[](1); + utxos[0] = utxo; + _mint(utxos); + } + + function withdraw( + uint256 amount, + uint256[2] memory nullifiers, + uint256 output, + uint256 root, + Commonlib.Proof calldata proof + ) public { + _withdrawWithNullifiers(amount, nullifiers, output, root, proof); + processInputsAndOutputs(nullifiers, [output, 0]); + } + + function mint(uint256[] memory utxos) public onlyOwner { + _mint(utxos); + } +} diff --git a/zkp/circuits/anon_enc_nullifier_non_repudiation.circom b/zkp/circuits/anon_enc_nullifier_non_repudiation.circom new file mode 100644 index 0000000..b02b6fa --- /dev/null +++ b/zkp/circuits/anon_enc_nullifier_non_repudiation.circom @@ -0,0 +1,198 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma circom 2.1.4; + +include "./lib/check-positive.circom"; +include "./lib/check-hashes.circom"; +include "./lib/check-sum.circom"; +include "./lib/check-nullifiers.circom"; +include "./lib/check-smt-proof.circom"; +include "./lib/ecdh.circom"; +include "./lib/encrypt.circom"; +include "./node_modules/circomlib/circuits/babyjub.circom"; + +// This version of the circuit performs the following operations: +// - derive the sender's public key from the sender's private key +// - check the input and output commitments match the expected hashes +// - check the input and output values sum to the same amount +// - perform encryption of the receiver's output UTXO value and salt +// - check the nullifiers are derived from the input commitments and the sender's private key +// - check the nullifiers are included in the Merkle tree +// - encrypt all secrets with an authority's public key (for non-repudiation purposes) +template Zeto(nInputs, nOutputs, nSMTLevels) { + signal input nullifiers[nInputs]; + signal input inputCommitments[nInputs]; + signal input inputValues[nInputs]; + signal input inputSalts[nInputs]; + // must be properly hashed and trimmed to be compatible with the BabyJub curve. + // Reference: https://github.com/iden3/circomlib/blob/master/test/babyjub.js#L103 + signal input inputOwnerPrivateKey; + signal input root; + signal input merkleProof[nInputs][nSMTLevels]; + signal input enabled[nInputs]; + signal input outputCommitments[nOutputs]; + signal input outputValues[nOutputs]; + signal input outputSalts[nOutputs]; + signal input outputOwnerPublicKeys[nOutputs][2]; + signal input encryptionNonce; + signal input authorityPublicKey[2]; + + signal output cipherText[2]; + signal output authorityCipherText_inputOwner[2]; + // two encrypted values for each input: value and salt + signal output authorityCipherText_inputs[2 * nInputs]; + signal output authorityCipherText_outputOwner[2 * nOutputs]; + // two encrypted values for each input: value and salt + signal output authorityCipherText_outputs[2 * nOutputs]; + + // derive the sender's public key from the secret input + // for the sender's private key. This step demonstrates + // the sender really owns the private key for the input + // UTXOs + var inputOwnerPublicKey[2]; + component pub = BabyPbk(); + pub.in <== inputOwnerPrivateKey; + inputOwnerPublicKey[0] = pub.Ax; + inputOwnerPublicKey[1] = pub.Ay; + var inputOwnerPublicKeys[nInputs][2]; + for (var i = 0; i < nInputs; i++) { + inputOwnerPublicKeys[i][0] = inputOwnerPublicKey[0]; + inputOwnerPublicKeys[i][1] = inputOwnerPublicKey[1]; + } + + component checkPositives = CheckPositive(nOutputs); + checkPositives.outputValues <== outputValues; + + component checkInputHashes = CheckHashes(nInputs); + checkInputHashes.commitments <== inputCommitments; + checkInputHashes.values <== inputValues; + checkInputHashes.salts <== inputSalts; + checkInputHashes.ownerPublicKeys <== inputOwnerPublicKeys; + + component checkOutputHashes = CheckHashes(nOutputs); + checkOutputHashes.commitments <== outputCommitments; + checkOutputHashes.values <== outputValues; + checkOutputHashes.salts <== outputSalts; + checkOutputHashes.ownerPublicKeys <== outputOwnerPublicKeys; + + component checkNullifiers = CheckNullifiers(nInputs); + checkNullifiers.nullifiers <== nullifiers; + checkNullifiers.values <== inputValues; + checkNullifiers.salts <== inputSalts; + checkNullifiers.ownerPrivateKey <== inputOwnerPrivateKey; + + component checkSum = CheckSum(nInputs, nOutputs); + checkSum.inputValues <== inputValues; + checkSum.outputValues <== outputValues; + + // With the above steps, we demonstrated that the nullifiers + // are securely bound to the input commitments. Now we need to + // demonstrate that the input commitments belong to the Sparse + // Merkle Tree with the root `root`. + component checkSMTProof = CheckSMTProof(nInputs, nSMTLevels); + checkSMTProof.root <== root; + checkSMTProof.merkleProof <== merkleProof; + checkSMTProof.enabled <== enabled; + checkSMTProof.leafNodeIndexes <== inputCommitments; + + // generate shared secret for the receiver + var sharedSecretReceiver[2]; + component ecdh1 = Ecdh(); + ecdh1.privKey <== inputOwnerPrivateKey; + // our circuit requires that the output UTXO for the receiver must be the first in the array + ecdh1.pubKey[0] <== outputOwnerPublicKeys[0][0]; + ecdh1.pubKey[1] <== outputOwnerPublicKeys[0][1]; + sharedSecretReceiver[0] = ecdh1.sharedKey[0]; + sharedSecretReceiver[1] = ecdh1.sharedKey[1]; + + // encrypt the value for the receiver + component encrypt1 = SymmetricEncrypt(2); + // our circuit requires that the output UTXO for the receiver must be the first in the array + encrypt1.plainText[0] <== outputValues[0]; + encrypt1.plainText[1] <== outputSalts[0]; + encrypt1.key <== sharedSecretReceiver; + encrypt1.nonce <== encryptionNonce; + encrypt1.cipherText[0] --> cipherText[0]; + encrypt1.cipherText[1] --> cipherText[1]; + + // generate shared secret for the authority + var sharedSecretAuthority[2]; + component ecdh2 = Ecdh(); + ecdh2.privKey <== inputOwnerPrivateKey; + // our circuit requires that the output UTXO for the receiver must be the first in the array + ecdh2.pubKey[0] <== authorityPublicKey[0]; + ecdh2.pubKey[1] <== authorityPublicKey[1]; + sharedSecretAuthority[0] = ecdh2.sharedKey[0]; + sharedSecretAuthority[1] = ecdh2.sharedKey[1]; + + // encrypt the values for the authority + component encrypt2 = SymmetricEncrypt(2 + 2 * nInputs + 4 * nOutputs); + encrypt2.plainText[0] <== inputOwnerPublicKey[0]; + encrypt2.plainText[1] <== inputOwnerPublicKey[1]; + var idx1 = 2; + for (var i = 0; i < nInputs; i++) { + encrypt2.plainText[idx1] <== inputValues[i]; + idx1++; + encrypt2.plainText[idx1] <== inputSalts[i]; + idx1++; + } + for (var i = 0; i < nOutputs; i++) { + encrypt2.plainText[idx1] <== outputOwnerPublicKeys[i][0]; + idx1++; + encrypt2.plainText[idx1] <== outputOwnerPublicKeys[i][1]; + idx1++; + } + for (var i = 0; i < nOutputs; i++) { + encrypt2.plainText[idx1] <== outputValues[i]; + idx1++; + encrypt2.plainText[idx1] <== outputSalts[i]; + idx1++; + } + encrypt2.key <== sharedSecretAuthority; + encrypt2.nonce <== encryptionNonce; + encrypt2.cipherText[0] --> authorityCipherText_inputOwner[0]; + encrypt2.cipherText[1] --> authorityCipherText_inputOwner[1]; + var idx2 = 2; + var j1 = 0; + for (var i = 0; i < nInputs; i++) { + encrypt2.cipherText[idx2] --> authorityCipherText_inputs[j1]; + idx2++; + j1++; + encrypt2.cipherText[idx2] --> authorityCipherText_inputs[j1]; + idx2++; + j1++; + } + var j2 = 0; + for (var i = 0; i < nOutputs; i++) { + encrypt2.cipherText[idx2] --> authorityCipherText_outputOwner[j2]; + idx2++; + j2++; + encrypt2.cipherText[idx2] --> authorityCipherText_outputOwner[j2]; + idx2++; + j2++; + } + var j3 = 0; + for (var i = 0; i < nOutputs; i++) { + encrypt2.cipherText[idx2] --> authorityCipherText_outputs[j3]; + idx2++; + j3++; + encrypt2.cipherText[idx2] --> authorityCipherText_outputs[j3]; + idx2++; + j3++; + } +} + +component main { public [ nullifiers, outputCommitments, encryptionNonce, root, enabled, authorityPublicKey ] } = Zeto(2, 2, 64); \ No newline at end of file diff --git a/zkp/circuits/gen-config.json b/zkp/circuits/gen-config.json index 12817a7..9f8e36a 100644 --- a/zkp/circuits/gen-config.json +++ b/zkp/circuits/gen-config.json @@ -15,6 +15,10 @@ "ptau": "powersOfTau28_hez_final_16", "skipSolidityGenaration": false }, + "anon_enc_nullifier_non_repudiation": { + "ptau": "powersOfTau28_hez_final_16", + "skipSolidityGenaration": false + }, "anon_enc_nullifier": { "ptau": "powersOfTau28_hez_final_16", "skipSolidityGenaration": false diff --git a/zkp/js/integration-test/anon_enc_nullifier_non_repudiation.js b/zkp/js/integration-test/anon_enc_nullifier_non_repudiation.js new file mode 100644 index 0000000..8e7b5ac --- /dev/null +++ b/zkp/js/integration-test/anon_enc_nullifier_non_repudiation.js @@ -0,0 +1,131 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const { expect } = require('chai'); +const { groth16 } = require('snarkjs'); +const { genRandomSalt, genKeypair, formatPrivKeyForBabyJub, stringifyBigInts } = require('maci-crypto'); +const { Merkletree, InMemoryDB, str2Bytes, ZERO_HASH } = require('@iden3/js-merkletree'); +const { Poseidon, newSalt, loadCircuit } = require('../index.js'); +const { loadProvingKeys } = require('./utils.js'); + +const SMT_HEIGHT = 64; +const poseidonHash = Poseidon.poseidon4; +const poseidonHash3 = Poseidon.poseidon3; + +describe('main circuit tests for Zeto fungible tokens with encryption fro non-repudiation and anonymity using nullifiers', () => { + let circuit, provingKeyFile, verificationKey, smtAlice, smtBob; + + const Alice = {}; + const Bob = {}; + const Regulator = {}; + let senderPrivateKey; + + before(async () => { + circuit = await loadCircuit('anon_enc_nullifier_non_repudiation'); + ({ provingKeyFile, verificationKey } = loadProvingKeys('anon_enc_nullifier_non_repudiation')); + + let keypair = genKeypair(); + Alice.privKey = keypair.privKey; + Alice.pubKey = keypair.pubKey; + senderPrivateKey = formatPrivKeyForBabyJub(Alice.privKey); + + keypair = genKeypair(); + Bob.privKey = keypair.privKey; + Bob.pubKey = keypair.pubKey; + + keypair = genKeypair(); + Regulator.privKey = keypair.privKey; + Regulator.pubKey = keypair.pubKey; + + // initialize the local storage for Alice to manage her UTXOs in the Spart Merkle Tree + const storage1 = new InMemoryDB(str2Bytes('')); + smtAlice = new Merkletree(storage1, true, SMT_HEIGHT); + + // initialize the local storage for Bob to manage his UTXOs in the Spart Merkle Tree + const storage2 = new InMemoryDB(str2Bytes('')); + smtBob = new Merkletree(storage2, true, SMT_HEIGHT); + }); + + it('should generate a valid proof that can be verified successfully', async () => { + const inputValues = [15, 100]; + const outputValues = [80, 35]; + // create two input UTXOs, each has their own salt, but same owner + const senderPrivateKey = formatPrivKeyForBabyJub(Alice.privKey); + const salt1 = newSalt(); + const input1 = poseidonHash([BigInt(inputValues[0]), salt1, ...Alice.pubKey]); + const salt2 = newSalt(); + const input2 = poseidonHash([BigInt(inputValues[1]), salt2, ...Alice.pubKey]); + const inputCommitments = [input1, input2]; + + // create the nullifiers for the input UTXOs + const nullifier1 = poseidonHash3([BigInt(inputValues[0]), salt1, senderPrivateKey]); + const nullifier2 = poseidonHash3([BigInt(inputValues[1]), salt2, senderPrivateKey]); + const nullifiers = [nullifier1, nullifier2]; + + // calculate the root of the SMT + await smtAlice.add(input1, input1); + await smtAlice.add(input2, input2); + + // generate the merkle proof for the inputs + const proof1 = await smtAlice.generateCircomVerifierProof(input1, ZERO_HASH); + const proof2 = await smtAlice.generateCircomVerifierProof(input2, ZERO_HASH); + + // create two output UTXOs, they share the same salt, and different owner + const salt3 = newSalt(); + const output1 = poseidonHash([BigInt(outputValues[0]), salt3, ...Bob.pubKey]); + const output2 = poseidonHash([BigInt(outputValues[1]), salt3, ...Alice.pubKey]); + const outputCommitments = [output1, output2]; + + const encryptionNonce = genRandomSalt(); + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + const startTime = Date.now(); + const witness = await circuit.calculateWTNSBin( + { + nullifiers, + inputCommitments, + inputValues, + inputSalts: [salt1, salt2], + inputOwnerPrivateKey: senderPrivateKey, + root: proof1.root.bigInt(), + merkleProof: [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())], + enabled: [1, 1], + outputCommitments, + outputValues, + outputSalts: [salt3, salt3], + outputOwnerPublicKeys: [Bob.pubKey, Alice.pubKey], + authorityPublicKey: Regulator.pubKey, + ...encryptInputs, + }, + true + ); + + const { proof, publicSignals } = await groth16.prove(provingKeyFile, witness); + console.log('Proving time: ', (Date.now() - startTime) / 1000, 's'); + + const success = await groth16.verify(verificationKey, publicSignals, proof); + console.log('nullifiers', nullifiers); + console.log('inputCommitments', inputCommitments); + console.log('outputCommitments', outputCommitments); + console.log('root', proof1.root.bigInt()); + console.log('encryptionNonce', encryptionNonce); + console.log('authorityPublicKey', Regulator.pubKey); + console.log('publicSignals', publicSignals); + expect(success, true); + }).timeout(600000); +}); diff --git a/zkp/js/test/anon_enc_nullifier_non_repudiation.js b/zkp/js/test/anon_enc_nullifier_non_repudiation.js new file mode 100644 index 0000000..25820a3 --- /dev/null +++ b/zkp/js/test/anon_enc_nullifier_non_repudiation.js @@ -0,0 +1,327 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const { expect } = require('chai'); +const { join } = require('path'); +const { wasm: wasm_tester } = require('circom_tester'); +const { genRandomSalt, genKeypair, genEcdhSharedKey, formatPrivKeyForBabyJub, stringifyBigInts } = require('maci-crypto'); +const { Merkletree, InMemoryDB, str2Bytes, ZERO_HASH } = require('@iden3/js-merkletree'); +const { Poseidon, newSalt, poseidonDecrypt } = require('../index.js'); + +const SMT_HEIGHT = 64; +const poseidonHash = Poseidon.poseidon4; +const poseidonHash3 = Poseidon.poseidon3; + +describe('main circuit tests for Zeto fungible tokens with encryption for non-repudiation and anonymity using nullifiers', () => { + let circuit, smtAlice, smtBob; + + const Alice = {}; + const Bob = {}; + const Regulator = {}; + let senderPrivateKey; + + before(async function () { + this.timeout(60000); + + circuit = await wasm_tester(join(__dirname, '../../circuits/anon_enc_nullifier_non_repudiation.circom')); + + let keypair = genKeypair(); + Alice.privKey = keypair.privKey; + Alice.pubKey = keypair.pubKey; + senderPrivateKey = formatPrivKeyForBabyJub(Alice.privKey); + + keypair = genKeypair(); + Bob.privKey = keypair.privKey; + Bob.pubKey = keypair.pubKey; + + keypair = genKeypair(); + Regulator.privKey = keypair.privKey; + Regulator.pubKey = keypair.pubKey; + + // initialize the local storage for Alice to manage her UTXOs in the Spart Merkle Tree + const storage1 = new InMemoryDB(str2Bytes('')); + smtAlice = new Merkletree(storage1, true, SMT_HEIGHT); + + // initialize the local storage for Bob to manage his UTXOs in the Spart Merkle Tree + const storage2 = new InMemoryDB(str2Bytes('')); + smtBob = new Merkletree(storage2, true, SMT_HEIGHT); + }); + + it('should succeed for valid witness and produce an encypted value', async () => { + const inputValues = [32, 40]; + const outputValues = [20, 52]; + + // create two input UTXOs, each has their own salt, but same owner + const salt1 = newSalt(); + const input1 = poseidonHash([BigInt(inputValues[0]), salt1, ...Alice.pubKey]); + const salt2 = newSalt(); + const input2 = poseidonHash([BigInt(inputValues[1]), salt2, ...Alice.pubKey]); + const inputCommitments = [input1, input2]; + + // create the nullifiers for the inputs + const nullifier1 = poseidonHash3([BigInt(inputValues[0]), salt1, senderPrivateKey]); + const nullifier2 = poseidonHash3([BigInt(inputValues[1]), salt2, senderPrivateKey]); + const nullifiers = [nullifier1, nullifier2]; + + // calculate the root of the SMT + await smtAlice.add(input1, input1); + await smtAlice.add(input2, input2); + + // generate the merkle proof for the inputs + const proof1 = await smtAlice.generateCircomVerifierProof(input1, ZERO_HASH); + const proof2 = await smtAlice.generateCircomVerifierProof(input2, ZERO_HASH); + + // create two output UTXOs, they share the same salt, and different owner + const salt3 = newSalt(); + const output1 = poseidonHash([BigInt(outputValues[0]), salt3, ...Bob.pubKey]); + const salt4 = newSalt(); + const output2 = poseidonHash([BigInt(outputValues[1]), salt4, ...Alice.pubKey]); + const outputCommitments = [output1, output2]; + + const encryptionNonce = genRandomSalt(); + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + const witness = await circuit.calculateWitness( + { + nullifiers, + inputCommitments, + inputValues, + inputSalts: [salt1, salt2], + inputOwnerPrivateKey: senderPrivateKey, + root: proof1.root.bigInt(), + merkleProof: [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())], + enabled: [1, 1], + outputCommitments, + outputValues, + outputSalts: [salt3, salt4], + outputOwnerPublicKeys: [Bob.pubKey, Alice.pubKey], + authorityPublicKey: Regulator.pubKey, + ...encryptInputs, + }, + true + ); + + // console.log('witness', witness.slice(0, 25)); + // console.log('nullifiers', nullifiers); + // console.log('inputCommitments', inputCommitments); + // console.log('inputValues', inputValues); + // console.log('inputSalts', [salt1, salt2]); + // console.log('outputCommitments', outputCommitments); + // console.log('root', proof1.root.bigInt()); + // console.log('outputValues', outputValues); + // console.log('outputSalt', salt3); + // console.log('outputOwnerPublicKeys', [Bob.pubKey, Alice.pubKey]); + // console.log('encryptionNonce', encryptionNonce); + + expect(witness[17]).to.equal(BigInt(nullifiers[0])); + expect(witness[18]).to.equal(BigInt(nullifiers[1])); + expect(witness[19]).to.equal(proof1.root.bigInt()); + + // take the output from the proof circuit and attempt to decrypt + // as the receiver + const cipherText = [witness[1], witness[2]]; // index 1 is the encrypted value, index 2 is the encrypted salt + const recoveredKey = genEcdhSharedKey(Bob.privKey, Alice.pubKey); + const plainText = poseidonDecrypt(cipherText, recoveredKey, encryptionNonce); + expect(plainText).to.deep.equal([20n, salt3]); + + // take the output from the proof circuit and attempt to decrypt + // as the regulator + const recoveredKey2 = genEcdhSharedKey(Regulator.privKey, Alice.pubKey); + const cipherText2 = witness.slice(3, 17); + const plainText2 = poseidonDecrypt(cipherText2, recoveredKey2, encryptionNonce); + expect(plainText2).to.deep.equal([ + Alice.pubKey[0], // input owner public key + Alice.pubKey[1], + BigInt(inputValues[0]), // input values + salt1, // input salts + BigInt(inputValues[1]), + salt2, + Bob.pubKey[0], // output owner public key + Bob.pubKey[1], + Alice.pubKey[0], + Alice.pubKey[1], + BigInt(outputValues[0]), // output values + salt3, // output salts + BigInt(outputValues[1]), + salt4, + ]); + }); + + it('should succeed for valid witness and produce an encypted value - single input', async () => { + const inputValues = [72, 0]; + const outputValues = [20, 52]; + + // create two input UTXOs, each has their own salt, but same owner + const salt1 = newSalt(); + const input1 = poseidonHash([BigInt(inputValues[0]), salt1, ...Alice.pubKey]); + const inputCommitments = [input1, 0]; + + // create the nullifiers for the inputs + const nullifier1 = poseidonHash3([BigInt(inputValues[0]), salt1, senderPrivateKey]); + const nullifiers = [nullifier1, 0]; + + // calculate the root of the SMT + await smtAlice.add(input1, input1); + + // generate the merkle proof for the inputs + const proof1 = await smtAlice.generateCircomVerifierProof(input1, ZERO_HASH); + const proof2 = await smtAlice.generateCircomVerifierProof(0, ZERO_HASH); + + // create two output UTXOs, they share the same salt, and different owner + const salt3 = newSalt(); + const output1 = poseidonHash([BigInt(outputValues[0]), salt3, ...Bob.pubKey]); + const salt4 = newSalt(); + const output2 = poseidonHash([BigInt(outputValues[1]), salt4, ...Alice.pubKey]); + const outputCommitments = [output1, output2]; + + const encryptionNonce = genRandomSalt(); + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + const witness = await circuit.calculateWitness( + { + nullifiers, + inputCommitments, + inputValues, + inputSalts: [salt1, 0], + inputOwnerPrivateKey: senderPrivateKey, + root: proof1.root.bigInt(), + merkleProof: [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())], + enabled: [1, 0], + outputCommitments, + outputValues, + outputSalts: [salt3, salt4], + outputOwnerPublicKeys: [Bob.pubKey, Alice.pubKey], + authorityPublicKey: Regulator.pubKey, + ...encryptInputs, + }, + true + ); + + // console.log('witness', witness); + // console.log('nullifiers', nullifiers); + // console.log('inputCommitments', inputCommitments); + // console.log('inputValues', inputValues); + // console.log('inputSalts', [salt1, salt2]); + // console.log('outputCommitments', outputCommitments); + // console.log('root', proof1.root.bigInt()); + // console.log('outputValues', outputValues); + // console.log('outputSalt', salt3); + // console.log('outputOwnerPublicKeys', [receiver.pubKey, sender.pubKey]); + // console.log('encryptionNonce', encryptionNonce); + + expect(witness[17]).to.equal(BigInt(nullifiers[0])); + expect(witness[18]).to.equal(BigInt(nullifiers[1])); + expect(witness[19]).to.equal(proof1.root.bigInt()); + + // take the output from the proof circuit and attempt to decrypt + // as the receiver + const cipherText = [witness[1], witness[2]]; // index 1 is the encrypted value, index 2 is the encrypted salt + const recoveredKey = genEcdhSharedKey(Bob.privKey, Alice.pubKey); + const plainText = poseidonDecrypt(cipherText, recoveredKey, encryptionNonce); + expect(plainText).to.deep.equal([20n, salt3]); + + // take the output from the proof circuit and attempt to decrypt + // as the regulator + const recoveredKey2 = genEcdhSharedKey(Regulator.privKey, Alice.pubKey); + const cipherText2 = witness.slice(3, 17); + const plainText2 = poseidonDecrypt(cipherText2, recoveredKey2, encryptionNonce); + expect(plainText2).to.deep.equal([ + Alice.pubKey[0], + Alice.pubKey[1], + BigInt(inputValues[0]), + salt1, + BigInt(inputValues[1]), + 0n, + Bob.pubKey[0], + Bob.pubKey[1], + Alice.pubKey[0], + Alice.pubKey[1], + BigInt(outputValues[0]), + salt3, + BigInt(outputValues[1]), + salt4, + ]); + }); + + it('should fail to generate a witness because mass conservation is not obeyed', async () => { + const inputValues = [15, 100]; + const outputValues = [90, 35]; + + // create two input UTXOs, each has their own salt, but same owner + const salt1 = newSalt(); + const input1 = poseidonHash([BigInt(inputValues[0]), salt1, ...Alice.pubKey]); + const salt2 = newSalt(); + const input2 = poseidonHash([BigInt(inputValues[1]), salt2, ...Alice.pubKey]); + const inputCommitments = [input1, input2]; + + // create the nullifiers for the input UTXOs + const nullifier1 = poseidonHash3([BigInt(inputValues[0]), salt1, senderPrivateKey]); + const nullifier2 = poseidonHash3([BigInt(inputValues[1]), salt2, senderPrivateKey]); + const nullifiers = [nullifier1, nullifier2]; + + // calculate the root of the SMT + await smtAlice.add(input1, input1); + await smtAlice.add(input2, input2); + + // generate the merkle proof for the inputs + const proof1 = await smtAlice.generateCircomVerifierProof(input1, ZERO_HASH); + const proof2 = await smtAlice.generateCircomVerifierProof(input2, ZERO_HASH); + + // create two output UTXOs, they share the same salt, and different owner + const salt3 = newSalt(); + const output1 = poseidonHash([BigInt(outputValues[0]), salt3, ...Bob.pubKey]); + const output2 = poseidonHash([BigInt(outputValues[1]), salt3, ...Alice.pubKey]); + const outputCommitments = [output1, output2]; + + const sharedSecret = genEcdhSharedKey(Alice.privKey, Bob.pubKey); + const encryptionNonce = genRandomSalt(); + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + let err; + try { + await circuit.calculateWitness( + { + nullifiers, + inputCommitments, + inputValues, + inputSalts: [salt1, salt2], + inputOwnerPrivateKey: senderPrivateKey, + root: proof1.root.bigInt(), + merkleProof: [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())], + enabled: [1, 1], + outputCommitments, + outputValues, + outputSalts: [salt3, salt3], + outputOwnerPublicKeys: [Bob.pubKey, Alice.pubKey], + authorityPublicKey: Regulator.pubKey, + ...encryptInputs, + }, + true + ); + } catch (e) { + err = e; + } + // console.log(err); + expect(err).to.match(/Error in template CheckSum_161 line: 44/); + expect(err).to.match(/Error in template Zeto_259 line: 99/); + }); +}); From 933521e38ef2e508f0fe2fdf0108136ff40d6b12 Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Wed, 14 Aug 2024 14:41:51 -0400 Subject: [PATCH 2/7] Add the fungilble token sample implementation Signed-off-by: Jim Zhang --- ...eto_anon_enc_nullifier_non_repudiation.sol | 48 ++- ...zeto_anon_enc_nullifier_non_repudiation.ts | 41 ++ ...zeto_anon_enc_nullifier_non_repudiation.ts | 400 ++++++++++++++++++ 3 files changed, 478 insertions(+), 11 deletions(-) create mode 100644 solidity/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts create mode 100644 solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts diff --git a/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol b/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol index 58860a1..f2d7e81 100644 --- a/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +++ b/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol @@ -17,7 +17,7 @@ pragma solidity ^0.8.20; import {Groth16Verifier_CheckHashesValue} from "./lib/verifier_check_hashes_value.sol"; import {Groth16Verifier_CheckNullifierValue} from "./lib/verifier_check_nullifier_value.sol"; -import {Groth16Verifier_AnonEncNullifier} from "./lib/verifier_anon_enc_nullifier.sol"; +import {Groth16Verifier_AnonEncNullifierNonRepudiation} from "./lib/verifier_anon_enc_nullifier_non_repudiation.sol"; import {ZetoNullifier} from "./lib/zeto_nullifier.sol"; import {ZetoFungibleWithdrawWithNullifiers} from "./lib/zeto_fungible_withdraw_nullifier.sol"; import {Registry} from "./lib/registry.sol"; @@ -53,6 +53,14 @@ contract Zeto_AnonEncNullifierNonRepudiation is verifier = _verifier; } + function setAuthority(uint256[2] memory _authority) public onlyOwner { + authority = _authority; + } + + function getAuthority() public view returns (uint256[2] memory) { + return authority; + } + /** * @dev the main function of the contract, which transfers values from one account (represented by Babyjubjub public keys) * to one or more receiver accounts (also represented by Babyjubjub public keys). One of the two nullifiers may be zero @@ -72,7 +80,7 @@ contract Zeto_AnonEncNullifierNonRepudiation is uint256[2] memory outputs, uint256 root, uint256 encryptionNonce, - uint256[2] memory encryptedValues, + uint256[16] memory encryptedValues, Commonlib.Proof calldata proof ) public returns (bool) { require( @@ -81,17 +89,33 @@ contract Zeto_AnonEncNullifierNonRepudiation is ); // construct the public inputs - uint256[10] memory publicInputs; + uint256[26] memory publicInputs; publicInputs[0] = encryptedValues[0]; // encrypted value for the receiver UTXO publicInputs[1] = encryptedValues[1]; // encrypted salt for the receiver UTXO - publicInputs[2] = nullifiers[0]; - publicInputs[3] = nullifiers[1]; - publicInputs[4] = root; - publicInputs[5] = (nullifiers[0] == 0) ? 0 : 1; // if the first nullifier is empty, disable its MT proof verification - publicInputs[6] = (nullifiers[1] == 0) ? 0 : 1; // if the second nullifier is empty, disable its MT proof verification - publicInputs[7] = outputs[0]; - publicInputs[8] = outputs[1]; - publicInputs[9] = encryptionNonce; + publicInputs[2] = encryptedValues[2]; // encrypted input owner public key[0] + publicInputs[3] = encryptedValues[3]; // encrypted input owner public key[1] + publicInputs[4] = encryptedValues[4]; // encrypted input value[0] + publicInputs[5] = encryptedValues[5]; // encrypted input salt[0] + publicInputs[6] = encryptedValues[6]; // encrypted input value[1] + publicInputs[7] = encryptedValues[7]; // encrypted input salt[1] + publicInputs[8] = encryptedValues[8]; // encrypted first output owner public key[0] + publicInputs[9] = encryptedValues[9]; // encrypted first output owner public key[1] + publicInputs[10] = encryptedValues[10]; // encrypted second output owner public key[0] + publicInputs[11] = encryptedValues[11]; // encrypted second output owner public key[1] + publicInputs[12] = encryptedValues[12]; // encrypted output value[0] + publicInputs[13] = encryptedValues[13]; // encrypted output salt[0] + publicInputs[14] = encryptedValues[14]; // encrypted output value[1] + publicInputs[15] = encryptedValues[15]; // encrypted output salt[1] + publicInputs[16] = nullifiers[0]; + publicInputs[17] = nullifiers[1]; + publicInputs[18] = root; + publicInputs[19] = (nullifiers[0] == 0) ? 0 : 1; // if the first nullifier is empty, disable its MT proof verification + publicInputs[20] = (nullifiers[1] == 0) ? 0 : 1; // if the second nullifier is empty, disable its MT proof verification + publicInputs[21] = outputs[0]; + publicInputs[22] = outputs[1]; + publicInputs[23] = encryptionNonce; + publicInputs[24] = authority[0]; + publicInputs[25] = authority[1]; // // Check the proof require( @@ -110,6 +134,8 @@ contract Zeto_AnonEncNullifierNonRepudiation is for (uint256 i = 0; i < nullifiers.length; ++i) { nullifierArray[i] = nullifiers[i]; outputArray[i] = outputs[i]; + } + for (uint256 i = 0; i < encryptedValues.length; ++i) { encryptedValuesArray[i] = encryptedValues[i]; } diff --git a/solidity/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts b/solidity/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts new file mode 100644 index 0000000..b12ee3b --- /dev/null +++ b/solidity/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts @@ -0,0 +1,41 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; +import { SmtLibModule, DepositVerifierModule, WithdrawNullifierVerifierModule } from "./lib/deps"; + +const VerifierModule = buildModule("Groth16Verifier_AnonEncNullifierNonRepudiation", (m) => { + const verifier = m.contract('Groth16Verifier_AnonEncNullifierNonRepudiation', []); + return { verifier }; +}); + +export default buildModule("Zeto_AnonEncNullifierNonRepudiation", (m) => { + const { smtLib, poseidon3 } = m.useModule(SmtLibModule); + const { verifier } = m.useModule(VerifierModule); + const { verifier: depositVerifier } = m.useModule(DepositVerifierModule); + const { verifier: withdrawVerifier } = m.useModule(WithdrawNullifierVerifierModule); + const commonlib = m.library('Commonlib'); + + const zeto = m.contract('Zeto_AnonEncNullifierNonRepudiation', [depositVerifier, withdrawVerifier, verifier], { + libraries: { + SmtLib: smtLib, + PoseidonUnit3L: poseidon3, + Commonlib: commonlib, + }, + }); + + return { zeto }; +}); diff --git a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts new file mode 100644 index 0000000..dd58ab9 --- /dev/null +++ b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts @@ -0,0 +1,400 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ethers, ignition } from 'hardhat'; +import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; +import { expect } from 'chai'; +import { loadCircuit, poseidonDecrypt, encodeProof } from "zeto-js"; +import { groth16 } from 'snarkjs'; +import { genRandomSalt, genEcdhSharedKey, stringifyBigInts } from 'maci-crypto'; +import { Merkletree, InMemoryDB, str2Bytes } from '@iden3/js-merkletree'; +import zetoModule from '../ignition/modules/zeto_anon_enc_nullifier_non_repudiation'; +import erc20Module from '../ignition/modules/erc20'; +import { UTXO, User, newUser, newUTXO, newNullifier, doMint, ZERO_UTXO, parseUTXOEvents } from './lib/utils'; +import { loadProvingKeys, prepareDepositProof, prepareNullifierWithdrawProof } from './utils'; + +describe("Zeto based fungible token with anonymity using nullifiers and encryption for non-repudiation", function () { + let deployer: Signer; + let Alice: User; + let Bob: User; + let Charlie: User; + let Authority: User; + let erc20: any; + let zeto: any; + let utxo100: UTXO; + let utxo1: UTXO; + let utxo2: UTXO; + let utxo3: UTXO; + let utxo4: UTXO; + let utxo7: UTXO; + let circuit: any, provingKey: any; + let smtAlice: Merkletree; + let smtBob: Merkletree; + + before(async function () { + let [d, a, b, c, e] = await ethers.getSigners(); + deployer = d; + Alice = await newUser(a); + Bob = await newUser(b); + Charlie = await newUser(c); + Authority = await newUser(e); + + ({ zeto } = await ignition.deploy(zetoModule)); + ({ erc20 } = await ignition.deploy(erc20Module)); + const tx4 = await zeto.connect(deployer).setERC20(erc20.target); + await tx4.wait(); + const tx5 = await zeto.connect(deployer).setAuthority(Authority.babyJubPublicKey); + await tx5.wait(); + + circuit = await loadCircuit('anon_enc_nullifier_non_repudiation'); + ({ provingKeyFile: provingKey } = loadProvingKeys('anon_enc_nullifier_non_repudiation')); + + const storage1 = new InMemoryDB(str2Bytes("")) + smtAlice = new Merkletree(storage1, true, 64); + + const storage2 = new InMemoryDB(str2Bytes("")) + smtBob = new Merkletree(storage2, true, 64); + }); + + it("onchain SMT root should be equal to the offchain SMT root", async function () { + const root = await smtAlice.root(); + const onchainRoot = await zeto.getRoot(); + expect(onchainRoot).to.equal(0n); + expect(root.string()).to.equal(onchainRoot.toString()); + }); + + it("mint ERC20 tokens to Alice to deposit to Zeto should succeed", async function () { + const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100); + await tx.wait(); + const balance = await erc20.balanceOf(Alice.ethAddress); + expect(balance).to.equal(100); + + const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100); + await tx1.wait(); + + utxo100 = newUTXO(100, Alice); + const { outputCommitments, encodedProof } = await prepareDepositProof(Alice, utxo100); + const tx2 = await zeto.connect(Alice.signer).deposit(100, outputCommitments[0], encodedProof); + await tx2.wait(); + + await smtAlice.add(utxo100.hash, utxo100.hash); + await smtBob.add(utxo100.hash, utxo100.hash); + }); + + it("mint to Alice and transfer UTXOs honestly to Bob should succeed", async function () { + // The authority mints a new UTXO and assigns it to Alice + utxo1 = newUTXO(10, Alice); + utxo2 = newUTXO(20, Alice); + const result1 = await doMint(zeto, deployer, [utxo1, utxo2]); + + // Alice locally tracks the UTXOs inside the Sparse Merkle Tree + // hardhat doesn't have a good way to subscribe to events so we have to parse the Tx result object + const mintEvents = parseUTXOEvents(zeto, result1); + const [_utxo1, _utxo2] = mintEvents[0].outputs; + await smtAlice.add(_utxo1, _utxo1); + await smtAlice.add(_utxo2, _utxo2); + let root = await smtAlice.root(); + let onchainRoot = await zeto.getRoot(); + expect(root.string()).to.equal(onchainRoot.toString()); + // Bob also locally tracks the UTXOs inside the Sparse Merkle Tree + await smtBob.add(_utxo1, _utxo1); + await smtBob.add(_utxo2, _utxo2); + + // Alice proposes the output UTXOs for the transfer to Bob + const _utxo3 = newUTXO(25, Bob); + utxo4 = newUTXO(5, Alice); + + // Alice generates the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo1, Alice); + const nullifier2 = newNullifier(utxo2, Alice); + + // Alice generates inclusion proofs for the UTXOs to be spent + const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // Alice transfers her UTXOs to Bob + const result2 = await doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo3, utxo4], root.bigInt(), merkleProofs, [Bob, Alice]); + + // Alice locally tracks the UTXOs inside the Sparse Merkle Tree + await smtAlice.add(_utxo3.hash, _utxo3.hash); + await smtAlice.add(utxo4.hash, utxo4.hash); + root = await smtAlice.root(); + onchainRoot = await zeto.getRoot(); + expect(root.string()).to.equal(onchainRoot.toString()); + + // Bob locally tracks the UTXOs inside the Sparse Merkle Tree + // Bob parses the UTXOs from the onchain event + const signerAddress = await Alice.signer.getAddress(); + const events = parseUTXOEvents(zeto, result2.txResult!); + expect(events[0].submitter).to.equal(signerAddress); + expect(events[0].inputs).to.deep.equal([nullifier1.hash, nullifier2.hash]); + expect(events[0].outputs).to.deep.equal([_utxo3.hash, utxo4.hash]); + await smtBob.add(events[0].outputs[0], events[0].outputs[0]); + await smtBob.add(events[0].outputs[1], events[0].outputs[1]); + + // Bob uses the encrypted values in the event to decrypt and recover the UTXO value and salt + const sharedKey1 = genEcdhSharedKey(Bob.babyJubPrivateKey, Alice.babyJubPublicKey); + const plainText1 = poseidonDecrypt(events[0].encryptedValues.slice(0, 2), sharedKey1, events[0].encryptionNonce); + expect(plainText1).to.deep.equal([ + 25n, + result2.plainTextSalt, + ]); + + // The regulator uses the encrypted values in the event to decrypt and recover the UTXO value and salt + const sharedKey2 = genEcdhSharedKey(Authority.babyJubPrivateKey, Alice.babyJubPublicKey); + const plainText2 = poseidonDecrypt(events[0].encryptedValues.slice(2, 16), sharedKey2, events[0].encryptionNonce); + expect(plainText2).to.deep.equal([ + Alice.babyJubPublicKey[0], + Alice.babyJubPublicKey[1], + 10n, + utxo1.salt, + 20n, + utxo2.salt, + Bob.babyJubPublicKey[0], + Bob.babyJubPublicKey[1], + Alice.babyJubPublicKey[0], + Alice.babyJubPublicKey[1], + 25n, + _utxo3.salt, + utxo4.value, + utxo4.salt + ]); + + // Bob uses the decrypted values to construct the UTXO received from the transaction + utxo3 = newUTXO(Number(plainText1[0]), Bob, plainText1[1]); + }).timeout(600000); + + it("Bob transfers UTXOs, previously received from Alice, honestly to Charlie should succeed", async function () { + // Bob generates the nullifiers for the UTXO to be spent + const nullifier1 = newNullifier(utxo3, Bob); + + // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation + const root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo3.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(0n, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // Bob proposes the output UTXOs + const utxo6 = newUTXO(10, Charlie); + utxo7 = newUTXO(15, Bob); + + // Bob should be able to spend the UTXO that was reconstructed from the previous transaction + const result = await doTransfer(Bob, [utxo3, ZERO_UTXO], [nullifier1, ZERO_UTXO], [utxo6, utxo7], root.bigInt(), merkleProofs, [Charlie, Bob]); + + // Bob keeps the local SMT in sync + await smtBob.add(utxo6.hash, utxo6.hash); + await smtBob.add(utxo7.hash, utxo7.hash); + + // Alice gets the new UTXOs from the onchain event and keeps the local SMT in sync + const events = parseUTXOEvents(zeto, result.txResult!); + await smtAlice.add(events[0].outputs[0], events[0].outputs[0]); + await smtAlice.add(events[0].outputs[1], events[0].outputs[1]); + }).timeout(600000); + + it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () { + // Alice generates the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo100, Alice); + + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(0n, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // Alice proposes the output ERC20 tokens + const outputCommitment = newUTXO(20, Alice); + + const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); + + // Alice withdraws her UTXOs to ERC20 tokens + const tx = await zeto.connect(Alice.signer).withdraw(80, nullifiers, outputCommitments[0], root.bigInt(), encodedProof); + await tx.wait(); + + // Alice checks her ERC20 balance + const balance = await erc20.balanceOf(Alice.ethAddress); + expect(balance).to.equal(80); + }); + + it("mint existing unspent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); + }); + + it("mint existing spent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); + }); + + it("transfer spent UTXOs should fail (double spend protection)", async function () { + // create outputs + const _utxo1 = newUTXO(25, Bob); + const _utxo2 = newUTXO(5, Alice); + + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo1, Alice); + const nullifier2 = newNullifier(utxo2, Alice); + + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Bob, Alice])).rejectedWith("UTXOAlreadySpent") + }).timeout(600000); + + it("transfer with existing UTXOs in the output should fail (mass conservation protection)", async function () { + // give Bob another UTXO to be able to spend + const _utxo1 = newUTXO(15, Bob); + await doMint(zeto, deployer, [_utxo1]); + await smtBob.add(_utxo1.hash, _utxo1.hash); + + const nullifier1 = newNullifier(utxo7, Bob); + const nullifier2 = newNullifier(_utxo1, Bob); + let root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(_utxo1.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, [Alice, Alice])).rejectedWith("UTXOAlreadyOwned") + }).timeout(600000); + + it("spend by using the same UTXO as both inputs should fail", async function () { + const _utxo1 = newUTXO(20, Alice); + const _utxo2 = newUTXO(10, Bob); + const nullifier1 = newNullifier(utxo7, Bob); + const nullifier2 = newNullifier(utxo7, Bob); + // generate inclusion proofs for the UTXOs to be spent + let root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Alice, Bob])).rejectedWith(`UTXODuplicate`); + }).timeout(600000); + + it("transfer non-existing UTXOs should fail", async function () { + const nonExisting1 = newUTXO(25, Alice); + const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); + + // add to our local SMT (but they don't exist on the chain) + await smtAlice.add(nonExisting1.hash, nonExisting1.hash); + await smtAlice.add(nonExisting2.hash, nonExisting2.hash); + + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(nonExisting1, Alice); + const nullifier2 = newNullifier(nonExisting2, Alice); + + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(nonExisting2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // propose the output UTXOs + const _utxo1 = newUTXO(30, Charlie); + utxo7 = newUTXO(15, Bob); + + await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); + }).timeout(600000); + + async function doTransfer(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], root: BigInt, merkleProofs: BigInt[][], owners: User[]) { + let nullifiers: [BigNumberish, BigNumberish]; + let outputCommitments: [BigNumberish, BigNumberish]; + let encryptedValues: [BigNumberish, BigNumberish]; + let encryptionNonce: BigNumberish; + let encodedProof: any; + const result = await prepareProof(signer, inputs, _nullifiers, outputs, root, merkleProofs, owners); + nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [BigNumberish, BigNumberish]; + outputCommitments = result.outputCommitments; + encodedProof = result.encodedProof; + encryptedValues = result.encryptedValues; + encryptionNonce = result.encryptionNonce; + + const txResult = await sendTx(signer, nullifiers, outputCommitments, root, encryptedValues, encryptionNonce, encodedProof); + // add the clear text value so that it can be used by tests to compare with the decrypted value + return { txResult, plainTextSalt: outputs[0].salt }; + } + + async function prepareProof(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], root: BigInt, merkleProof: BigInt[][], owners: User[]) { + const nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [BigNumberish, BigNumberish]; + const inputCommitments: [BigNumberish, BigNumberish] = inputs.map((input) => input.hash) as [BigNumberish, BigNumberish]; + const inputValues = inputs.map((input) => BigInt(input.value || 0n)); + const inputSalts = inputs.map((input) => input.salt || 0n); + const outputCommitments: [BigNumberish, BigNumberish] = outputs.map((output) => output.hash) as [BigNumberish, BigNumberish]; + const outputValues = outputs.map((output) => BigInt(output.value || 0n)); + const outputOwnerPublicKeys: [[BigNumberish, BigNumberish], [BigNumberish, BigNumberish]] = owners.map(owner => owner.babyJubPublicKey) as [[BigNumberish, BigNumberish], [BigNumberish, BigNumberish]]; + const encryptionNonce: BigNumberish = genRandomSalt() as BigNumberish; + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + const startWitnessCalculation = Date.now(); + const inputObj = { + nullifiers, + inputCommitments, + inputValues, + inputSalts, + inputOwnerPrivateKey: signer.formattedPrivateKey, + root, + enabled: [nullifiers[0] !== 0n ? 1 : 0, nullifiers[1] !== 0n ? 1 : 0], + merkleProof, + outputCommitments, + outputValues, + outputSalts: outputs.map((output) => output.salt), + outputOwnerPublicKeys, + authorityPublicKey: Authority.babyJubPublicKey, + ...encryptInputs + }; + const witness = await circuit.calculateWTNSBin( + inputObj, + true + ); + const timeWithnessCalculation = Date.now() - startWitnessCalculation; + + const startProofGeneration = Date.now(); + const { proof, publicSignals } = await groth16.prove(provingKey, witness) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; + const timeProofGeneration = Date.now() - startProofGeneration; + + console.log(`Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.`); + + const encodedProof = encodeProof(proof); + const encryptedValue = publicSignals[0]; + const encryptedSalt = publicSignals[1]; + return { + inputCommitments, + outputCommitments, + encryptedValues: publicSignals.slice(0, 16), + encryptionNonce, + encodedProof + }; + } + + async function sendTx( + signer: User, + nullifiers: [BigNumberish, BigNumberish], + outputCommitments: [BigNumberish, BigNumberish], + root: BigNumberish, + encryptedValues: [BigNumberish, BigNumberish], + encryptionNonce: BigNumberish, + encodedProof: any + ) { + const startTx = Date.now(); + const tx = await zeto.connect(signer.signer).transfer(nullifiers, outputCommitments, root, encryptionNonce, encryptedValues, encodedProof); + const results: ContractTransactionReceipt | null = await tx.wait(); + console.log(`Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${results?.gasUsed}`); + return results; + } +}); From 84555833434215a4a57f16adc6c058c6093dd870 Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Wed, 14 Aug 2024 15:25:19 -0400 Subject: [PATCH 3/7] Regulator to check decrypted values for correctness Signed-off-by: Jim Zhang --- solidity/contracts/zeto_anon_enc_nullifier.sol | 2 ++ .../test/zeto_anon_enc_nullifier_non_repudiation.ts | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/solidity/contracts/zeto_anon_enc_nullifier.sol b/solidity/contracts/zeto_anon_enc_nullifier.sol index 9c1a84d..3025a63 100644 --- a/solidity/contracts/zeto_anon_enc_nullifier.sol +++ b/solidity/contracts/zeto_anon_enc_nullifier.sol @@ -107,6 +107,8 @@ contract Zeto_AnonEncNullifier is for (uint256 i = 0; i < nullifiers.length; ++i) { nullifierArray[i] = nullifiers[i]; outputArray[i] = outputs[i]; + } + for (uint256 i = 0; i < encryptedValues.length; ++i) { encryptedValuesArray[i] = encryptedValues[i]; } diff --git a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts index dd58ab9..9efcb8e 100644 --- a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts +++ b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts @@ -17,7 +17,7 @@ import { ethers, ignition } from 'hardhat'; import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; import { expect } from 'chai'; -import { loadCircuit, poseidonDecrypt, encodeProof } from "zeto-js"; +import { loadCircuit, poseidonDecrypt, encodeProof, Poseidon } from "zeto-js"; import { groth16 } from 'snarkjs'; import { genRandomSalt, genEcdhSharedKey, stringifyBigInts } from 'maci-crypto'; import { Merkletree, InMemoryDB, str2Bytes } from '@iden3/js-merkletree'; @@ -173,6 +173,15 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti utxo4.value, utxo4.salt ]); + // the regulator verifies that the decrypted values are correct, against the UTXO hashes + const checkInputUTXO1 = Poseidon.poseidon4([plainText2[2], plainText2[3], plainText2[0], plainText2[1]]); + expect(checkInputUTXO1).to.equal(utxo1.hash); // "utxo1" hash is available in the event + const checkInputUTXO2 = Poseidon.poseidon4([plainText2[4], plainText2[5], plainText2[0], plainText2[1]]); + expect(checkInputUTXO2).to.equal(utxo2.hash); // "utxo2" hash is available in the event + const checkOutputUTXO1 = Poseidon.poseidon4([plainText2[10], plainText2[11], plainText2[6], plainText2[7]]); + expect(checkOutputUTXO1).to.equal(_utxo3.hash); // "_utxo3" hash is available in the event + const checkOutputUTXO2 = Poseidon.poseidon4([plainText2[12], plainText2[13], plainText2[8], plainText2[9]]); + expect(checkOutputUTXO2).to.equal(utxo4.hash); // "utxo4" hash is available in the event // Bob uses the decrypted values to construct the UTXO received from the transaction utxo3 = newUTXO(Number(plainText1[0]), Bob, plainText1[1]); @@ -371,8 +380,6 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti console.log(`Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.`); const encodedProof = encodeProof(proof); - const encryptedValue = publicSignals[0]; - const encryptedSalt = publicSignals[1]; return { inputCommitments, outputCommitments, From 53dd9a929bf86707663b8d5490d91c270d6eb76d Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Wed, 14 Aug 2024 16:36:02 -0400 Subject: [PATCH 4/7] Add unit test for the sql storage Signed-off-by: Jim Zhang --- .../sparse-merkle-tree/smt/smt_test.go | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/go-sdk/internal/sparse-merkle-tree/smt/smt_test.go b/go-sdk/internal/sparse-merkle-tree/smt/smt_test.go index 5842dd9..6d48ad0 100644 --- a/go-sdk/internal/sparse-merkle-tree/smt/smt_test.go +++ b/go-sdk/internal/sparse-merkle-tree/smt/smt_test.go @@ -20,14 +20,18 @@ import ( "fmt" "math/big" "math/rand" + "os" "testing" "github.com/hyperledger-labs/zeto/go-sdk/internal/sparse-merkle-tree/node" "github.com/hyperledger-labs/zeto/go-sdk/internal/sparse-merkle-tree/storage" "github.com/hyperledger-labs/zeto/go-sdk/internal/testutils" "github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/core" + "github.com/hyperledger-labs/zeto/go-sdk/pkg/utxo" "github.com/iden3/go-iden3-crypto/babyjub" "github.com/stretchr/testify/assert" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) func TestNewMerkleTree(t *testing.T) { @@ -191,3 +195,57 @@ func TestVerifyProof(t *testing.T) { fmt.Println("All done") } + +type testSqlProvider struct { + db *gorm.DB +} + +func (s *testSqlProvider) DB() *gorm.DB { + return s.db +} + +func (s *testSqlProvider) Close() {} + +func TestSqliteStorage(t *testing.T) { + dbfile, err := os.CreateTemp("", "gorm.db") + assert.NoError(t, err) + defer func() { + os.Remove(dbfile.Name()) + }() + db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{}) + assert.NoError(t, err) + err = db.Table(core.TreeRootsTable).AutoMigrate(&core.SMTRoot{}) + assert.NoError(t, err) + err = db.Table(core.NodesTablePrefix + "test_1").AutoMigrate(&core.SMTNode{}) + assert.NoError(t, err) + + provider := &testSqlProvider{db: db} + s := storage.NewSqlStorage(provider, "test_1") + assert.NoError(t, err) + + mt, err := NewMerkleTree(s, 10) + assert.NoError(t, err) + + tokenId := big.NewInt(1001) + uriString := "https://example.com/token/1001" + assert.NoError(t, err) + sender := testutils.NewKeypair() + salt1 := utxo.NewSalt() + + utxo1 := node.NewNonFungible(tokenId, uriString, sender.PublicKey, salt1) + n1, err := node.NewLeafNode(utxo1) + assert.NoError(t, err) + err = mt.AddLeaf(n1) + assert.NoError(t, err) + + root := mt.Root() + dbRoot := core.SMTRoot{Name: "test_1"} + err = db.Table(core.TreeRootsTable).First(&dbRoot).Error + assert.NoError(t, err) + assert.Equal(t, root.Hex(), dbRoot.RootIndex) + + dbNode := core.SMTNode{RefKey: n1.Ref().Hex()} + err = db.Table(core.NodesTablePrefix + "test_1").First(&dbNode).Error + assert.NoError(t, err) + assert.Equal(t, n1.Ref().Hex(), dbNode.RefKey) +} From 5be9380654c83ec81951dd8ed4e9c270f7c60e19 Mon Sep 17 00:00:00 2001 From: jimthematrix Date: Thu, 15 Aug 2024 08:39:38 -0400 Subject: [PATCH 5/7] Update solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts Co-authored-by: Chengxuan Xing Signed-off-by: jimthematrix --- solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts index 9efcb8e..1423337 100644 --- a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts +++ b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts @@ -94,7 +94,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti await smtBob.add(utxo100.hash, utxo100.hash); }); - it("mint to Alice and transfer UTXOs honestly to Bob should succeed", async function () { + it("mint to Alice and transfer UTXOs honestly to Bob should succeed and verifiable by the regulator", async function () { // The authority mints a new UTXO and assigns it to Alice utxo1 = newUTXO(10, Alice); utxo2 = newUTXO(20, Alice); From 23aa921448c1f07e0a3ed5fc5d120ca2f21d599c Mon Sep 17 00:00:00 2001 From: jimthematrix Date: Thu, 15 Aug 2024 08:39:53 -0400 Subject: [PATCH 6/7] Update zkp/js/test/anon_enc_nullifier_non_repudiation.js Co-authored-by: Chengxuan Xing Signed-off-by: jimthematrix --- zkp/js/test/anon_enc_nullifier_non_repudiation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zkp/js/test/anon_enc_nullifier_non_repudiation.js b/zkp/js/test/anon_enc_nullifier_non_repudiation.js index 25820a3..4c1858b 100644 --- a/zkp/js/test/anon_enc_nullifier_non_repudiation.js +++ b/zkp/js/test/anon_enc_nullifier_non_repudiation.js @@ -60,7 +60,7 @@ describe('main circuit tests for Zeto fungible tokens with encryption for non-re smtBob = new Merkletree(storage2, true, SMT_HEIGHT); }); - it('should succeed for valid witness and produce an encypted value', async () => { + it('should succeed for valid witness, produce an encypted value and regulator is able to decrypt', async () => { const inputValues = [32, 40]; const outputValues = [20, 52]; From dc0f7299b96579673f6378d0c17b767e58d90173 Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Thu, 15 Aug 2024 08:59:08 -0400 Subject: [PATCH 7/7] Use separate parameters for encrypted values for receiver vs. authority Signed-off-by: Jim Zhang --- ...eto_anon_enc_nullifier_non_repudiation.sol | 68 ++++++++++++------- solidity/test/lib/utils.ts | 10 +++ ...zeto_anon_enc_nullifier_non_repudiation.ts | 8 ++- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol b/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol index f2d7e81..feb9c97 100644 --- a/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +++ b/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol @@ -37,6 +37,15 @@ contract Zeto_AnonEncNullifierNonRepudiation is ZetoNullifier, ZetoFungibleWithdrawWithNullifiers { + event UTXOTransferNonRepudiation( + uint256[] inputs, + uint256[] outputs, + uint256 encryptionNonce, + uint256[] encryptedValuesForReceiver, + uint256[] encryptedValuesForAuthority, + address indexed submitter + ); + Groth16Verifier_AnonEncNullifierNonRepudiation verifier; // the authority public key that must be used to // encrypt the secrets of every transaction @@ -70,17 +79,21 @@ contract Zeto_AnonEncNullifierNonRepudiation is * @param nullifiers Array of nullifiers that are secretly bound to UTXOs to be spent by the transaction. * @param outputs Array of new UTXOs to generate, for future transactions to spend. * @param root The root hash of the Sparse Merkle Tree that contains the nullifiers. + * @param encryptionNonce The nonce used to derive the shared secret for encryption by the receiver. + * @param encryptedValuesForReceiver Array of encrypted values, salts and public keys for the receiver UTXO + * @param encryptedValuesForAuthority Array of encrypted values, salts and public keys for the input UTXOs and output UTXOs. * @param proof A zero knowledge proof that the submitter is authorized to spend the inputs, and * that the outputs are valid in terms of obeying mass conservation rules. * - * Emits a {UTXOTransferWithEncryptedValues} event. + * Emits a {UTXOTransferNonRepudiation} event. */ function transfer( uint256[2] memory nullifiers, uint256[2] memory outputs, uint256 root, uint256 encryptionNonce, - uint256[16] memory encryptedValues, + uint256[2] memory encryptedValuesForReceiver, + uint256[14] memory encryptedValuesForAuthority, Commonlib.Proof calldata proof ) public returns (bool) { require( @@ -90,22 +103,22 @@ contract Zeto_AnonEncNullifierNonRepudiation is // construct the public inputs uint256[26] memory publicInputs; - publicInputs[0] = encryptedValues[0]; // encrypted value for the receiver UTXO - publicInputs[1] = encryptedValues[1]; // encrypted salt for the receiver UTXO - publicInputs[2] = encryptedValues[2]; // encrypted input owner public key[0] - publicInputs[3] = encryptedValues[3]; // encrypted input owner public key[1] - publicInputs[4] = encryptedValues[4]; // encrypted input value[0] - publicInputs[5] = encryptedValues[5]; // encrypted input salt[0] - publicInputs[6] = encryptedValues[6]; // encrypted input value[1] - publicInputs[7] = encryptedValues[7]; // encrypted input salt[1] - publicInputs[8] = encryptedValues[8]; // encrypted first output owner public key[0] - publicInputs[9] = encryptedValues[9]; // encrypted first output owner public key[1] - publicInputs[10] = encryptedValues[10]; // encrypted second output owner public key[0] - publicInputs[11] = encryptedValues[11]; // encrypted second output owner public key[1] - publicInputs[12] = encryptedValues[12]; // encrypted output value[0] - publicInputs[13] = encryptedValues[13]; // encrypted output salt[0] - publicInputs[14] = encryptedValues[14]; // encrypted output value[1] - publicInputs[15] = encryptedValues[15]; // encrypted output salt[1] + publicInputs[0] = encryptedValuesForReceiver[0]; // encrypted value for the receiver UTXO + publicInputs[1] = encryptedValuesForReceiver[1]; // encrypted salt for the receiver UTXO + publicInputs[2] = encryptedValuesForAuthority[0]; // encrypted input owner public key[0] + publicInputs[3] = encryptedValuesForAuthority[1]; // encrypted input owner public key[1] + publicInputs[4] = encryptedValuesForAuthority[2]; // encrypted input value[0] + publicInputs[5] = encryptedValuesForAuthority[3]; // encrypted input salt[0] + publicInputs[6] = encryptedValuesForAuthority[4]; // encrypted input value[1] + publicInputs[7] = encryptedValuesForAuthority[5]; // encrypted input salt[1] + publicInputs[8] = encryptedValuesForAuthority[6]; // encrypted first output owner public key[0] + publicInputs[9] = encryptedValuesForAuthority[7]; // encrypted first output owner public key[1] + publicInputs[10] = encryptedValuesForAuthority[8]; // encrypted second output owner public key[0] + publicInputs[11] = encryptedValuesForAuthority[9]; // encrypted second output owner public key[1] + publicInputs[12] = encryptedValuesForAuthority[10]; // encrypted output value[0] + publicInputs[13] = encryptedValuesForAuthority[11]; // encrypted output salt[0] + publicInputs[14] = encryptedValuesForAuthority[12]; // encrypted output value[1] + publicInputs[15] = encryptedValuesForAuthority[13]; // encrypted output salt[1] publicInputs[16] = nullifiers[0]; publicInputs[17] = nullifiers[1]; publicInputs[18] = root; @@ -128,22 +141,29 @@ contract Zeto_AnonEncNullifierNonRepudiation is uint256[] memory nullifierArray = new uint256[](nullifiers.length); uint256[] memory outputArray = new uint256[](outputs.length); - uint256[] memory encryptedValuesArray = new uint256[]( - encryptedValues.length + uint256[] memory encryptedValuesReceiverArray = new uint256[]( + encryptedValuesForReceiver.length + ); + uint256[] memory encryptedValuesAuthorityArray = new uint256[]( + encryptedValuesForAuthority.length ); for (uint256 i = 0; i < nullifiers.length; ++i) { nullifierArray[i] = nullifiers[i]; outputArray[i] = outputs[i]; } - for (uint256 i = 0; i < encryptedValues.length; ++i) { - encryptedValuesArray[i] = encryptedValues[i]; + for (uint256 i = 0; i < encryptedValuesForReceiver.length; ++i) { + encryptedValuesReceiverArray[i] = encryptedValuesForReceiver[i]; + } + for (uint256 i = 0; i < encryptedValuesForAuthority.length; ++i) { + encryptedValuesAuthorityArray[i] = encryptedValuesForAuthority[i]; } - emit UTXOTransferWithEncryptedValues( + emit UTXOTransferNonRepudiation( nullifierArray, outputArray, encryptionNonce, - encryptedValuesArray, + encryptedValuesReceiverArray, + encryptedValuesAuthorityArray, msg.sender ); return true; diff --git a/solidity/test/lib/utils.ts b/solidity/test/lib/utils.ts index 9f1c96c..2be8e1a 100644 --- a/solidity/test/lib/utils.ts +++ b/solidity/test/lib/utils.ts @@ -103,6 +103,16 @@ export function parseUTXOEvents(zetoTokenContract: any, result: ContractTransact submitter: event?.args.submitter }; returnValues.push(transfer); + } else if (event?.name === 'UTXOTransferNonRepudiation') { + const transfer = { + inputs: event?.args.inputs, + outputs: event?.args.outputs, + encryptedValuesForReceiver: event?.args.encryptedValuesForReceiver, + encryptedValuesForAuthority: event?.args.encryptedValuesForAuthority, + encryptionNonce: event?.args.encryptionNonce, + submitter: event?.args.submitter + }; + returnValues.push(transfer); } else if (event?.name === 'UTXOMint') { const mint = { outputs: event?.args.outputs, diff --git a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts index 1423337..12ca851 100644 --- a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts +++ b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts @@ -148,7 +148,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti // Bob uses the encrypted values in the event to decrypt and recover the UTXO value and salt const sharedKey1 = genEcdhSharedKey(Bob.babyJubPrivateKey, Alice.babyJubPublicKey); - const plainText1 = poseidonDecrypt(events[0].encryptedValues.slice(0, 2), sharedKey1, events[0].encryptionNonce); + const plainText1 = poseidonDecrypt(events[0].encryptedValuesForReceiver, sharedKey1, events[0].encryptionNonce); expect(plainText1).to.deep.equal([ 25n, result2.plainTextSalt, @@ -156,7 +156,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti // The regulator uses the encrypted values in the event to decrypt and recover the UTXO value and salt const sharedKey2 = genEcdhSharedKey(Authority.babyJubPrivateKey, Alice.babyJubPublicKey); - const plainText2 = poseidonDecrypt(events[0].encryptedValues.slice(2, 16), sharedKey2, events[0].encryptionNonce); + const plainText2 = poseidonDecrypt(events[0].encryptedValuesForAuthority, sharedKey2, events[0].encryptionNonce); expect(plainText2).to.deep.equal([ Alice.babyJubPublicKey[0], Alice.babyJubPublicKey[1], @@ -399,7 +399,9 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti encodedProof: any ) { const startTx = Date.now(); - const tx = await zeto.connect(signer.signer).transfer(nullifiers, outputCommitments, root, encryptionNonce, encryptedValues, encodedProof); + const encryptedValuesForReceiver = encryptedValues.slice(0, 2); + const encryptedValuesForRegulator = encryptedValues.slice(2, 16); + const tx = await zeto.connect(signer.signer).transfer(nullifiers, outputCommitments, root, encryptionNonce, encryptedValuesForReceiver, encryptedValuesForRegulator, encodedProof); const results: ContractTransactionReceipt | null = await tx.wait(); console.log(`Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${results?.gasUsed}`); return results;