diff --git a/.gitignore b/.gitignore index e43b0f9..2608ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.vscode \ No newline at end of file diff --git a/go-sdk/integration-test/e2e_test.go b/go-sdk/integration-test/e2e_test.go index f8fb626..31ee5f7 100644 --- a/go-sdk/integration-test/e2e_test.go +++ b/go-sdk/integration-test/e2e_test.go @@ -301,13 +301,11 @@ func (s *E2ETestSuite) TestZeto_3_SuccessfulProving() { assert.NoError(s.T(), err) err = mt.AddLeaf(n2) assert.NoError(s.T(), err) - proof1, _, err := mt.GenerateProof(input1, nil) + proofs, _, err := mt.GenerateProofs([]*big.Int{input1, input2}, nil) assert.NoError(s.T(), err) - circomProof1, err := proof1.ToCircomVerifierProof(input1, input1, mt.Root(), MAX_HEIGHT) + circomProof1, err := proofs[0].ToCircomVerifierProof(input1, input1, mt.Root(), MAX_HEIGHT) assert.NoError(s.T(), err) - proof2, _, err := mt.GenerateProof(input2, nil) - assert.NoError(s.T(), err) - circomProof2, err := proof2.ToCircomVerifierProof(input2, input2, mt.Root(), MAX_HEIGHT) + circomProof2, err := proofs[1].ToCircomVerifierProof(input2, input2, mt.Root(), MAX_HEIGHT) assert.NoError(s.T(), err) salt3 := crypto.NewSalt() @@ -387,13 +385,11 @@ func (s *E2ETestSuite) TestZeto_4_SuccessfulProving() { assert.NoError(s.T(), err) err = mt.AddLeaf(n2) assert.NoError(s.T(), err) - proof1, _, err := mt.GenerateProof(input1, nil) - assert.NoError(s.T(), err) - circomProof1, err := proof1.ToCircomVerifierProof(input1, input1, mt.Root(), MAX_HEIGHT) + proofs, _, err := mt.GenerateProofs([]*big.Int{input1, input2}, nil) assert.NoError(s.T(), err) - proof2, _, err := mt.GenerateProof(input2, nil) + circomProof1, err := proofs[0].ToCircomVerifierProof(input1, input1, mt.Root(), MAX_HEIGHT) assert.NoError(s.T(), err) - circomProof2, err := proof2.ToCircomVerifierProof(input2, input2, mt.Root(), MAX_HEIGHT) + circomProof2, err := proofs[1].ToCircomVerifierProof(input2, input2, mt.Root(), MAX_HEIGHT) assert.NoError(s.T(), err) salt3 := crypto.NewSalt() @@ -527,9 +523,9 @@ func (s *E2ETestSuite) TestZeto_6_SuccessfulProving() { assert.NoError(s.T(), err) err = mt.AddLeaf(n1) assert.NoError(s.T(), err) - proof1, _, err := mt.GenerateProof(input1, nil) + proofs, _, err := mt.GenerateProofs([]*big.Int{input1}, nil) assert.NoError(s.T(), err) - circomProof1, err := proof1.ToCircomVerifierProof(input1, input1, mt.Root(), MAX_HEIGHT) + circomProof1, err := proofs[0].ToCircomVerifierProof(input1, input1, mt.Root(), MAX_HEIGHT) assert.NoError(s.T(), err) proof1Siblings := make([]*big.Int, len(circomProof1.Siblings)-1) for i, s := range circomProof1.Siblings[0 : len(circomProof1.Siblings)-1] { diff --git a/go-sdk/internal/sparse-merkle-tree/smt/merkletree.go b/go-sdk/internal/sparse-merkle-tree/smt/merkletree.go index e1734e8..4ca7e6a 100644 --- a/go-sdk/internal/sparse-merkle-tree/smt/merkletree.go +++ b/go-sdk/internal/sparse-merkle-tree/smt/merkletree.go @@ -118,17 +118,32 @@ func (mt *sparseMerkleTree) GetNode(key core.NodeIndex) (core.Node, error) { return mt.getNode(key) } -// GenerateProof generates the proof of existence (or non-existence) of a leaf node -// for a Merkle Tree given the root. It uses the node's index to represent the node. -// If the rootKey is nil, the current merkletree root is used -func (mt *sparseMerkleTree) GenerateProof(k *big.Int, rootKey core.NodeIndex) (core.Proof, *big.Int, error) { +// GenerateProofs generates a list of proofs of existence (or non-existence) of the provided +// leaf nodes that are represented by their indexes. An optional Merkle tree root can be provided. +// If rootKey is not provided, the current Merkle tree root is used +func (mt *sparseMerkleTree) GenerateProofs(keys []*big.Int, rootKey core.NodeIndex) ([]core.Proof, []*big.Int, error) { mt.RLock() defer mt.RUnlock() + merkleProofs := make([]core.Proof, len(keys)) + foundValues := make([]*big.Int, len(keys)) + for i, key := range keys { + proof, value, err := mt.generateProof(key, rootKey) + if err != nil { + return nil, nil, err + } + merkleProofs[i] = proof + foundValues[i] = value + } + + return merkleProofs, foundValues, nil +} + +func (mt *sparseMerkleTree) generateProof(key *big.Int, rootKey core.NodeIndex) (core.Proof, *big.Int, error) { p := &proof{} var siblingKey core.NodeIndex - kHash, err := node.NewNodeIndexFromBigInt(k) + kHash, err := node.NewNodeIndexFromBigInt(key) if err != nil { return nil, nil, err } @@ -177,7 +192,7 @@ func (mt *sparseMerkleTree) GenerateProof(k *big.Int, rootKey core.NodeIndex) (c p.siblings = append(p.siblings, siblingKey) } } - return nil, nil, ErrKeyNotFound + return nil, nil, ErrReachedMaxLevel } // must be called from inside a read lock diff --git a/go-sdk/internal/sparse-merkle-tree/smt/proof.go b/go-sdk/internal/sparse-merkle-tree/smt/proof.go index 4ec7b5a..243d872 100644 --- a/go-sdk/internal/sparse-merkle-tree/smt/proof.go +++ b/go-sdk/internal/sparse-merkle-tree/smt/proof.go @@ -53,12 +53,10 @@ func (p *proof) ExistingNode() core.Node { } func (p *proof) MarkNonEmptySibling(level uint) { - desiredLength := (level + 7) / 8 - if desiredLength == 0 { - desiredLength = 1 - } - if len(p.nonEmptySiblings) <= int(desiredLength) { - newBytes := make([]byte, desiredLength) + desiredByteLength := level/8 + 1 + if len(p.nonEmptySiblings) <= int(desiredByteLength) { + // the bitmap is not big enough, resize it + newBytes := make([]byte, desiredByteLength) if len(p.nonEmptySiblings) == 0 { p.nonEmptySiblings = newBytes } else { @@ -186,10 +184,12 @@ func calculateRootFromProof(proof *proof, leafNode core.Node) (core.NodeIndex, e // isBitOnBigEndian tests whether the bit n in bitmap is 1, in Big Endian. func isBitOnBigEndian(bitmap []byte, n uint) bool { - return bitmap[uint(len(bitmap))-n/8-1]&(1<<(n%8)) != 0 + byteIdxToCheck := n / 8 + return bitmap[byteIdxToCheck]&(1<<(n%8)) != 0 } // setBitBigEndian sets the bit n in the bitmap to 1, in Big Endian. func setBitBigEndian(bitmap []byte, n uint) { - bitmap[uint(len(bitmap))-n/8-1] |= 1 << (n % 8) + byteIdxToSet := n / 8 + bitmap[byteIdxToSet] |= 1 << (n % 8) } diff --git a/go-sdk/internal/sparse-merkle-tree/smt/proof_test.go b/go-sdk/internal/sparse-merkle-tree/smt/proof_test.go new file mode 100644 index 0000000..2efbc03 --- /dev/null +++ b/go-sdk/internal/sparse-merkle-tree/smt/proof_test.go @@ -0,0 +1,68 @@ +// 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. + +package smt + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMarkNonEmptySibling(t *testing.T) { + p := &proof{} + for i := 0; i < 256; i++ { + p.MarkNonEmptySibling(uint(i)) + } + expected := make([]byte, 32) + for i := 0; i < 32; i++ { + expected[i] = 0xff + } + assert.Equal(t, p.nonEmptySiblings, expected) +} + +func TestIsBitOnBigEndian(t *testing.T) { + p := &proof{} + expected := make([]byte, 32) + for i := 0; i < 32; i++ { + expected[i] = 0xff + } + p.nonEmptySiblings = expected + for i := 0; i < 256; i++ { + assert.True(t, isBitOnBigEndian(p.nonEmptySiblings, uint(i))) + } +} + +func TestMarkAndCheck(t *testing.T) { + p := &proof{} + p.MarkNonEmptySibling(0) + p.MarkNonEmptySibling(10) + p.MarkNonEmptySibling(136) + assert.True(t, p.IsNonEmptySibling(0)) + assert.False(t, p.IsNonEmptySibling(1)) + assert.False(t, p.IsNonEmptySibling(2)) + assert.False(t, p.IsNonEmptySibling(3)) + assert.False(t, p.IsNonEmptySibling(4)) + assert.False(t, p.IsNonEmptySibling(5)) + assert.False(t, p.IsNonEmptySibling(6)) + assert.False(t, p.IsNonEmptySibling(7)) + assert.False(t, p.IsNonEmptySibling(8)) + assert.False(t, p.IsNonEmptySibling(9)) + assert.True(t, p.IsNonEmptySibling(10)) + assert.False(t, p.IsNonEmptySibling(55)) + assert.True(t, p.IsNonEmptySibling(136)) + assert.False(t, p.IsNonEmptySibling(137)) +} 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 9f71858..1e505dc 100644 --- a/go-sdk/internal/sparse-merkle-tree/smt/smt_test.go +++ b/go-sdk/internal/sparse-merkle-tree/smt/smt_test.go @@ -205,22 +205,20 @@ func (s *MerkleTreeTestSuite) TestGenerateProof() { assert.NoError(s.T(), err) target1 := node1.Index().BigInt() - proof1, foundValue1, err := mt.GenerateProof(target1, mt.Root()) - assert.NoError(s.T(), err) - assert.Equal(s.T(), target1, foundValue1) - assert.True(s.T(), proof1.(*proof).existence) - valid := VerifyProof(mt.Root(), proof1, node1) - assert.True(s.T(), valid) utxo3 := node.NewFungible(big.NewInt(10), alice.PublicKey, big.NewInt(12347)) node3, err := node.NewLeafNode(utxo3) assert.NoError(s.T(), err) target2 := node3.Index().BigInt() - proof2, _, err := mt.GenerateProof(target2, mt.Root()) + proofs, foundValues, err := mt.GenerateProofs([]*big.Int{target1, target2}, mt.Root()) assert.NoError(s.T(), err) - assert.False(s.T(), proof2.(*proof).existence) + assert.Equal(s.T(), target1, foundValues[0]) + assert.True(s.T(), proofs[0].(*proof).existence) + valid := VerifyProof(mt.Root(), proofs[0], node1) + assert.True(s.T(), valid) + assert.False(s.T(), proofs[1].(*proof).existence) - proof3, err := proof1.ToCircomVerifierProof(target1, foundValue1, mt.Root(), levels) + proof3, err := proofs[0].ToCircomVerifierProof(target1, foundValues[0], mt.Root(), levels) assert.NoError(s.T(), err) assert.False(s.T(), proof3.IsOld0) } @@ -254,11 +252,11 @@ func (s *MerkleTreeTestSuite) TestVerifyProof() { target := n.Index().BigInt() root := mt.Root() - p, _, err := mt.GenerateProof(target, root) + p, _, err := mt.GenerateProofs([]*big.Int{target}, root) assert.NoError(s.T(), err) - assert.True(s.T(), p.(*proof).existence) + assert.True(s.T(), p[0].(*proof).existence) - valid := VerifyProof(root, p, n) + valid := VerifyProof(root, p[0], n) assert.True(s.T(), valid) }() diff --git a/go-sdk/pkg/sparse-merkle-tree/core/merkletree.go b/go-sdk/pkg/sparse-merkle-tree/core/merkletree.go index de5447a..316b110 100644 --- a/go-sdk/pkg/sparse-merkle-tree/core/merkletree.go +++ b/go-sdk/pkg/sparse-merkle-tree/core/merkletree.go @@ -36,9 +36,9 @@ type SparseMerkleTree interface { // Root returns the root hash of the tree Root() NodeIndex // AddLeaf adds a key-value pair to the tree - AddLeaf(Node) error + AddLeaf(leaf Node) error // GetNode returns the node at the given reference hash - GetNode(NodeIndex) (Node, error) + GetNode(node NodeIndex) (Node, error) // GetnerateProof generates a proof of existence (or non-existence) of a leaf node - GenerateProof(*big.Int, NodeIndex) (Proof, *big.Int, error) + GenerateProofs(nodeIndexes []*big.Int, root NodeIndex) ([]Proof, []*big.Int, error) } diff --git a/solidity/contracts/lib/verifier_anon_enc.sol b/solidity/contracts/lib/verifier_anon_enc.sol index a9d676e..ba456da 100644 --- a/solidity/contracts/lib/verifier_anon_enc.sol +++ b/solidity/contracts/lib/verifier_anon_enc.sol @@ -46,17 +46,17 @@ contract Groth16Verifier_AnonEnc { uint256 constant IC0x = 5696326670703652601376328914723856805804139150397636629981154943267586835846; uint256 constant IC0y = 17913431813079674015620165360736989993822627917830194405007674349883405519566; - uint256 constant IC1x = 15825765194464726182776026234639522157004618110842020817264286413596445235307; - uint256 constant IC1y = 2401477487480347699703028792091325200698394178743640467196981936283298710021; + uint256 constant IC1x = 2102562587253616254650248571898720579563063454685611900201869023012028011038; + uint256 constant IC1y = 858837120372047227699859025595943051604219338012183710955972141361344096680; - uint256 constant IC2x = 17823254154132200329306690888156067227498822342519393685861534093309766001383; - uint256 constant IC2y = 14804040920166770014088667887230353137747938842993092323563528559936821334324; + uint256 constant IC2x = 19871902121561448541783335513612822391415363559792595451397804594141318386828; + uint256 constant IC2y = 14203964380144242038882743638980469366132880555873818345914201687170773944754; - uint256 constant IC3x = 18619661077507789630281262029605339062675871751807780618791872378570042056116; - uint256 constant IC3y = 18692831968495678168274986594838227336308377551834418943816657968243321416157; + uint256 constant IC3x = 4534338244167504974683945726615729215678954323916000129150608047181600075942; + uint256 constant IC3y = 9004530693581096650487103914238080672423858350236817229254519649589395343912; - uint256 constant IC4x = 8481249797936633465645328566302395583826148290507195864467073391607936154307; - uint256 constant IC4y = 4631061052012679777402506942756063974564041904906197227863591152456358430540; + uint256 constant IC4x = 16926200767829183396766074136228821955738540059328214039731068817771880630127; + uint256 constant IC4y = 15535238050385142389806452090946716626530242126040333805866336792975972380578; uint256 constant IC5x = 269115022971501175992618085182824077406065858697651888560831707201556157978; uint256 constant IC5y = 19699073094724988754117299114476621695804537148277402334737306097425629024180; diff --git a/solidity/contracts/lib/verifier_anon_enc_nullifier.sol b/solidity/contracts/lib/verifier_anon_enc_nullifier.sol index 18647db..cfbe4f3 100644 --- a/solidity/contracts/lib/verifier_anon_enc_nullifier.sol +++ b/solidity/contracts/lib/verifier_anon_enc_nullifier.sol @@ -46,17 +46,17 @@ contract Groth16Verifier_AnonEncNullifier { uint256 constant IC0x = 19712575420668268335634440622054263372704657131549679423053748840610444649065; uint256 constant IC0y = 21552018618280422667464008645151951952543569291144282502207358485399703168568; - uint256 constant IC1x = 8879640517323472103773105774693203238739849734532829129136892239475022333668; - uint256 constant IC1y = 8763769862439095053692831300380880413523736171679197931386220812298104316000; + uint256 constant IC1x = 21152648367462137565411101477610760889415992049938974004172633386627195379833; + uint256 constant IC1y = 17526913059347331839566720566403027766484314611214702153099182458740108823; - uint256 constant IC2x = 938844563415264744387226778430871504680270942210685580230120080929277918399; - uint256 constant IC2y = 9128906501228717785843913573881993155178978243801248472013755207523145161265; + uint256 constant IC2x = 5554249810657633166990745736699774333938659965451342592982728994998805456417; + uint256 constant IC2y = 9730676046857216859008721613182313555756289801254042508531677626774346999622; - uint256 constant IC3x = 15866014959526954538183423799071681422553639776348384721828133108069370471390; - uint256 constant IC3y = 17150051530085128616371619156987380853738195686559145384648523741607899076676; + uint256 constant IC3x = 16411770669872316544841615427270736472060848758458781353444450000872952405569; + uint256 constant IC3y = 3883712029134278704236321509155602434293779347341551692030084302663589933567; - uint256 constant IC4x = 16628587721511911382464102713531591331106282985537150732913149203131020489047; - uint256 constant IC4y = 15415751731187982732941102993178329101138735638465538195218064540808443483176; + uint256 constant IC4x = 7689756896440471053876099704125828625789315755353261183292592808881136591701; + uint256 constant IC4y = 8154637307804119872460314948142196702392444351248969101164117581937044926170; uint256 constant IC5x = 12299330364713292827284970278663419727424821736280679334696618553775046366194; uint256 constant IC5y = 21075649501958706881669057145213727696785798949405955661717827102964559861134; diff --git a/solidity/contracts/lib/verifier_anon_enc_nullifier_kyc.sol b/solidity/contracts/lib/verifier_anon_enc_nullifier_kyc.sol new file mode 100644 index 0000000..377364a --- /dev/null +++ b/solidity/contracts/lib/verifier_anon_enc_nullifier_kyc.sol @@ -0,0 +1,254 @@ +// 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_AnonEncNullifierKyc { + // 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 = 9571444203847882263349163823295111750012388583457810565732023354927681835330; + uint256 constant IC0y = 8269430673392355800760839451470053559913816443341643262118006327992857911204; + + uint256 constant IC1x = 4171425333877223863056817685674841454723358686631303251580406544847360463144; + uint256 constant IC1y = 15508434706912610115961511481609592716639093445421134335652880195025374357944; + + uint256 constant IC2x = 2805234997490797748511701790284534458659062187129667755544299868178828988756; + uint256 constant IC2y = 12737871927823892823240455203969207068418359360382939881586564793007265918634; + + uint256 constant IC3x = 10037927645989308427370729946962854922194022862178974033089345641900642443287; + uint256 constant IC3y = 1869974150112738935511161628734764742128849823652210548167483049626273686915; + + uint256 constant IC4x = 20687486203577926209406522416577489369623505298901447042889614462294165837937; + uint256 constant IC4y = 2451926520013626473243440086333573559066473857550647156934680886755185047100; + + uint256 constant IC5x = 17506157096475522989244923493066068666613854999605413994180693698287006754601; + uint256 constant IC5y = 18337141315691397667266548067044460253572013083720894042089481649614687489349; + + uint256 constant IC6x = 13169264733918854243504094459072017908892434194925230289069972473474865423260; + uint256 constant IC6y = 12873033270668213434072395062604551402080299183418425353301844285688316634747; + + uint256 constant IC7x = 4139352737455682815820399999502807530902270791132588001997383898128565726560; + uint256 constant IC7y = 18130358037765445238866373406538395199727660880929816773059128417420908294103; + + uint256 constant IC8x = 10242723724534050633824073816542552050545643197784462676335615048377039832420; + uint256 constant IC8y = 1522077533586266907104192963207500158509684879768383246564448114116860886045; + + uint256 constant IC9x = 9331953561001587156072982617181494822635260561293948857418310817015123003664; + uint256 constant IC9y = 21434180133029315517714324102175079176171466970274711902142297829796086888419; + + uint256 constant IC10x = 9864640468360420795111662135353906469273928885319595720979255522131895987062; + uint256 constant IC10y = 4347845420812651539643028407865491584531559916191524645768616247857747093924; + + uint256 constant IC11x = 12816855581744969923196393315369134257252339197623955937790101684782832727528; + uint256 constant IC11y = 10852386397854977410896573210092846697497925902496902459833247615681245355742; + + uint256 constant IC12x = 14713198324057425181537280290297404746877953868974288063529731081142062871032; + uint256 constant IC12y = 12697331297870828686780559216672799476293214005074082773567772130976271883336; + + uint256 constant IC13x = 15748313782243366848688530768530734630700786383695534160287365098087100629526; + uint256 constant IC13y = 19624787309979166735737404819294173148608360816934232838701010568600344101162; + + + // 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[13] calldata _pubSignals) public view returns (bool) { + assembly { + function checkField(v) { + if iszero(lt(v, r)) { + 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))) + + + // -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))) + + + // Validate all evaluations + let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) + + mstore(0, isValid) + return(0, 0x20) + } + } + } diff --git a/solidity/contracts/lib/zeto_nullifier.sol b/solidity/contracts/lib/zeto_nullifier.sol index c442e0e..6912f4c 100644 --- a/solidity/contracts/lib/zeto_nullifier.sol +++ b/solidity/contracts/lib/zeto_nullifier.sol @@ -113,6 +113,9 @@ abstract contract ZetoNullifier is ZetoCommon { function _mint(uint256[] memory utxos) internal virtual { for (uint256 i = 0; i < utxos.length; ++i) { uint256 utxo = utxos[i]; + if (utxo == 0) { + continue; + } uint256 nodeHash = _getLeafNodeHash(utxo); SmtLib.Node memory node = _commitmentsTree.getNode(nodeHash); diff --git a/solidity/contracts/zeto_anon_enc_nullifier_kyc.sol b/solidity/contracts/zeto_anon_enc_nullifier_kyc.sol new file mode 100644 index 0000000..02a6993 --- /dev/null +++ b/solidity/contracts/zeto_anon_enc_nullifier_kyc.sol @@ -0,0 +1,171 @@ +// 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_AnonEncNullifierKyc} from "./lib/verifier_anon_enc_nullifier_kyc.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 {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.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_AnonEncNullifierKyc is + ZetoNullifier, + ZetoFungibleWithdrawWithNullifiers, + Registry, + UUPSUpgradeable +{ + Groth16Verifier_AnonEncNullifierKyc verifier; + + function initialize( + address initialOwner, + Groth16Verifier_AnonEncNullifierKyc _verifier, + Groth16Verifier_CheckHashesValue _depositVerifier, + Groth16Verifier_CheckNullifierValue _withdrawVerifier + ) public initializer { + __Registry_init(); + __ZetoNullifier_init(initialOwner); + __ZetoFungibleWithdrawWithNullifiers_init( + _depositVerifier, + _withdrawVerifier + ); + verifier = _verifier; + } + + function _authorizeUpgrade(address) internal override onlyOwner {} + + function register(uint256[2] memory publicKey) public onlyOwner { + _register(publicKey); + } + /** + * @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[4] memory encryptedValues, + Commonlib.Proof calldata proof + ) public returns (bool) { + require( + validateTransactionProposal(nullifiers, outputs, root), + "Invalid transaction proposal" + ); + + // construct the public inputs + uint256[13] 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]; // parity bit for the cipher text + publicInputs[3] = encryptedValues[3]; // parity bit for the cipher text + publicInputs[4] = nullifiers[0]; + publicInputs[5] = nullifiers[1]; + publicInputs[6] = root; + publicInputs[7] = (nullifiers[0] == 0) ? 0 : 1; // if the first nullifier is empty, disable its MT proof verification + publicInputs[8] = (nullifiers[1] == 0) ? 0 : 1; // if the second nullifier is empty, disable its MT proof verification + publicInputs[9] = getIdentitiesRoot(); + publicInputs[10] = outputs[0]; + publicInputs[11] = outputs[1]; + publicInputs[12] = 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]; + } + for (uint256 i = 0; i < encryptedValues.length; ++i) { + encryptedValuesArray[i] = encryptedValues[i]; + } + + emit UTXOTransferWithEncryptedValues( + nullifierArray, + outputArray, + encryptionNonce, + encryptedValuesArray, + msg.sender + ); + return true; + } + + // in the current design, no KYC check is performed for deposit & withdraw functions + // this is to reduce gas fee for deposit and withdraw function + // users that doesn't pass KYC check will not be able to participate in transfers + // because the transfer circuit requires the input and output owners to be in the KYC list + // Therefore, token circulation from & to parties that are not in the KYC list is prevented + 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 { + validateTransactionProposal(nullifiers, [output, 0], root); + _withdrawWithNullifiers(amount, nullifiers, output, root, proof); + processInputsAndOutputs(nullifiers, [output, 0]); + } + + function mint(uint256[] memory utxos) public onlyOwner { + _mint(utxos); + } +} diff --git a/solidity/ignition/modules/zeto_anon_enc_nullifier_kyc.ts b/solidity/ignition/modules/zeto_anon_enc_nullifier_kyc.ts new file mode 100644 index 0000000..1dc86b6 --- /dev/null +++ b/solidity/ignition/modules/zeto_anon_enc_nullifier_kyc.ts @@ -0,0 +1,48 @@ +// 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_AnonEncNullifierKyc', + (m) => { + const verifier = m.contract('Groth16Verifier_AnonEncNullifierKyc', []); + return { verifier }; + } +); + +export default buildModule('Zeto_AnonEncNullifierKyc', (m) => { + const { smtLib, poseidon2, poseidon3 } = m.useModule(SmtLibModule); + const { verifier } = m.useModule(VerifierModule); + const { verifier: depositVerifier } = m.useModule(DepositVerifierModule); + const { verifier: withdrawVerifier } = m.useModule( + WithdrawNullifierVerifierModule + ); + + return { + depositVerifier, + withdrawVerifier, + verifier, + smtLib, + poseidon2, + poseidon3, + }; +}); diff --git a/solidity/scripts/tokens.json b/solidity/scripts/tokens.json index 420f153..b016cbe 100644 --- a/solidity/scripts/tokens.json +++ b/solidity/scripts/tokens.json @@ -2,6 +2,7 @@ "Zeto_Anon": "fungible", "Zeto_AnonEnc": "fungible", "Zeto_AnonEncNullifier": "fungible", + "Zeto_AnonEncNullifierKyc": "fungible", "Zeto_AnonNullifier": "fungible", "Zeto_AnonNullifierKyc": "fungible", "Zeto_AnonEncNullifierNonRepudiation": "fungible", diff --git a/solidity/scripts/tokens/Zeto_AnonEncNullifierKyc.ts b/solidity/scripts/tokens/Zeto_AnonEncNullifierKyc.ts new file mode 100644 index 0000000..3942746 --- /dev/null +++ b/solidity/scripts/tokens/Zeto_AnonEncNullifierKyc.ts @@ -0,0 +1,45 @@ +// 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 zetoModule from '../../ignition/modules/zeto_anon_enc_nullifier_kyc'; + +export async function deployDependencies() { + const [deployer] = await ethers.getSigners(); + + const { + depositVerifier, + withdrawVerifier, + verifier, + smtLib, + poseidon2, + poseidon3, + } = await ignition.deploy(zetoModule); + return { + deployer, + args: [ + await deployer.getAddress(), + verifier.target, + depositVerifier.target, + withdrawVerifier.target, + ], + libraries: { + SmtLib: smtLib.target, + PoseidonUnit2L: poseidon2.target, + PoseidonUnit3L: poseidon3.target, + }, + }; +} diff --git a/solidity/test/zeto_anon_enc_nullifier.ts b/solidity/test/zeto_anon_enc_nullifier.ts index 7a9072b..41e3c42 100644 --- a/solidity/test/zeto_anon_enc_nullifier.ts +++ b/solidity/test/zeto_anon_enc_nullifier.ts @@ -134,7 +134,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti // check the private transfer activity is not exposed in the ERC20 contract const afterTransferBalance = await erc20.balanceOf(Alice.ethAddress); expect(afterTransferBalance).to.equal(startingBalance); - + // Alice locally tracks the UTXOs inside the Sparse Merkle Tree await smtAlice.add(_utxo3.hash, _utxo3.hash); await smtAlice.add(utxo4.hash, utxo4.hash); @@ -326,6 +326,13 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); }).timeout(600000); + + it("repeated mint calls with single UTXO should not fail", async function () { + const utxo5 = newUTXO(10, Alice); + await expect(doMint(zeto, deployer, [utxo5, ZERO_UTXO])).fulfilled; + const utxo6 = newUTXO(20, Alice); + await expect(doMint(zeto, deployer, [utxo6, ZERO_UTXO])).fulfilled; + }); }); async function doTransfer(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], root: BigInt, merkleProofs: BigInt[][], owners: User[]) { diff --git a/solidity/test/zeto_anon_enc_nullifier_kyc.ts b/solidity/test/zeto_anon_enc_nullifier_kyc.ts new file mode 100644 index 0000000..d28e5f8 --- /dev/null +++ b/solidity/test/zeto_anon_enc_nullifier_kyc.ts @@ -0,0 +1,966 @@ +// 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, network } from 'hardhat'; +import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; +import { expect } from 'chai'; +import { + loadCircuit, + poseidonDecrypt, + encodeProof, + newEncryptionNonce, + kycHash, +} from 'zeto-js'; +import { groth16 } from 'snarkjs'; +import { genEcdhSharedKey, stringifyBigInts } from 'maci-crypto'; +import { Merkletree, InMemoryDB, str2Bytes } from '@iden3/js-merkletree'; +import { + UTXO, + User, + newUser, + newUTXO, + newNullifier, + doMint, + ZERO_UTXO, + parseUTXOEvents, + parseRegistryEvents, +} from './lib/utils'; +import { + loadProvingKeys, + prepareDepositProof, + prepareNullifierWithdrawProof, +} from './utils'; +import { deployZeto } from './lib/deploy'; + +describe('Zeto based fungible token with anonymity using nullifiers and encryption with KYC', function () { + let deployer: Signer; + let Alice: User; + let Bob: User; + let Charlie: User; + let unregistered: User; + let erc20: any; + let zeto: any; + let utxo100: UTXO; + let utxo1: UTXO; + let utxo2: UTXO; + let utxo3: UTXO; + let _utxo3: UTXO; + let utxo4: UTXO; + let utxo6: UTXO; + let utxo7: UTXO; + let withdrawChangeUTXO: UTXO; + let circuit: any, provingKey: any; + let smtAlice: Merkletree; + let smtBob: Merkletree; + let smtKyc: Merkletree; + + before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } + let [d, a, b, c, e] = await ethers.getSigners(); + deployer = d; + Alice = await newUser(a); + Bob = await newUser(b); + Charlie = await newUser(c); + unregistered = await newUser(e); + + ({ deployer, zeto, erc20 } = await deployZeto('Zeto_AnonEncNullifierKyc')); + + const tx2 = await zeto.connect(deployer).register(Alice.babyJubPublicKey); + const result1 = await tx2.wait(); + const tx3 = await zeto.connect(deployer).register(Bob.babyJubPublicKey); + const result2 = await tx3.wait(); + const tx4 = await zeto.connect(deployer).register(Charlie.babyJubPublicKey); + const result3 = await tx4.wait(); + + circuit = await loadCircuit('anon_enc_nullifier_kyc'); + ({ provingKeyFile: provingKey } = loadProvingKeys( + 'anon_enc_nullifier_kyc' + )); + + const storage1 = new InMemoryDB(str2Bytes('alice')); + smtAlice = new Merkletree(storage1, true, 64); + + const storage2 = new InMemoryDB(str2Bytes('bob')); + smtBob = new Merkletree(storage2, true, 64); + + const storage3 = new InMemoryDB(str2Bytes('kyc')); + smtKyc = new Merkletree(storage3, true, 10); + + const publicKey1 = parseRegistryEvents(zeto, result1); + await smtKyc.add(kycHash(publicKey1), kycHash(publicKey1)); + const publicKey2 = parseRegistryEvents(zeto, result2); + await smtKyc.add(kycHash(publicKey2), kycHash(publicKey2)); + const publicKey3 = parseRegistryEvents(zeto, result3); + await smtKyc.add(kycHash(publicKey3), kycHash(publicKey3)); + }); + + 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 startingBalance = await erc20.balanceOf(Alice.ethAddress); + const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100); + await tx.wait(); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.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 () { + const startingBalance = await erc20.balanceOf(Alice.ethAddress); + // 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]); + + // check the private mint activity is not exposed in the ERC20 contract + const afterMintBalance = await erc20.balanceOf(Alice.ethAddress); + expect(afterMintBalance).to.equal(startingBalance); + + // 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 utxosRoot = await smtAlice.root(); + let onchainRoot = await zeto.getRoot(); + expect(utxosRoot.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 + _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, + utxosRoot + ); + const proof2 = await smtAlice.generateCircomVerifierProof( + utxo2.hash, + utxosRoot + ); + const utxoMerkleProofs = [ + proof1.siblings.map((s) => s.bigInt()), + proof2.siblings.map((s) => s.bigInt()), + ]; + + // Alice generates inclusion proofs for the identities in the transaction + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof( + kycHash(Alice.babyJubPublicKey), + identitiesRoot + ); + const proof4 = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const identityMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Alice) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) + proof3.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (Alice) + ]; + + // Alice transfers her UTXOs to Bob + const result2 = await doTransfer( + Alice, + [utxo1, utxo2], + [nullifier1, nullifier2], + [_utxo3, utxo4], + utxosRoot.bigInt(), + utxoMerkleProofs, + identitiesRoot.bigInt(), + identityMerkleProofs, + [Bob, Alice] + ); + + // check the private transfer activity is not exposed in the ERC20 contract + const afterTransferBalance = await erc20.balanceOf(Alice.ethAddress); + expect(afterTransferBalance).to.equal(startingBalance); + + // Alice locally tracks the UTXOs inside the Sparse Merkle Tree + await smtAlice.add(_utxo3.hash, _utxo3.hash); + await smtAlice.add(utxo4.hash, utxo4.hash); + utxosRoot = await smtAlice.root(); + onchainRoot = await zeto.getRoot(); + expect(utxosRoot.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 sharedKey = genEcdhSharedKey( + Bob.babyJubPrivateKey, + Alice.babyJubPublicKey + ); + const plainText = poseidonDecrypt( + events[0].encryptedValues, + sharedKey, + events[0].encryptionNonce, + 2 + ); + expect(plainText).to.deep.equal([25n, result2.plainTextSalt]); + + // Bob uses the decrypted values to construct the UTXO received from the transaction + utxo3 = newUTXO(Number(plainText[0]), Bob, plainText[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 utxosRoot = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof( + utxo3.hash, + utxosRoot + ); + const proof2 = await smtBob.generateCircomVerifierProof(0n, utxosRoot); + const utxosMerkleProofs = [ + proof1.siblings.map((s) => s.bigInt()), + proof2.siblings.map((s) => s.bigInt()), + ]; + + // Bob proposes the output UTXOs + utxo6 = newUTXO(10, Charlie); + utxo7 = newUTXO(15, Bob); + // Bob generates inclusion proofs for the identities in the transaction + // using a shortcut with a shared SMT for all identities, but obviously + // Bob would need his own SMT instance in a real-world scenario + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const proof4 = await smtKyc.generateCircomVerifierProof( + kycHash(Charlie.babyJubPublicKey), + identitiesRoot + ); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Charlie) + proof3.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (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], + utxosRoot.bigInt(), + utxosMerkleProofs, + identitiesRoot.bigInt(), + identitiesMerkleProofs, + [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 () { + const startingBalance = await erc20.balanceOf(Alice.ethAddress); + + // 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 + withdrawChangeUTXO = newUTXO(20, Alice); + + const { nullifiers, outputCommitments, encodedProof } = + await prepareNullifierWithdrawProof( + Alice, + [utxo100, ZERO_UTXO], + [nullifier1, ZERO_UTXO], + withdrawChangeUTXO, + 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 tracks the UTXO inside the SMT + await smtAlice.add(withdrawChangeUTXO.hash, withdrawChangeUTXO.hash); + // Bob also locally tracks the UTXOs inside the SMT + await smtBob.add(withdrawChangeUTXO.hash, withdrawChangeUTXO.hash); + + // Alice checks her ERC20 balance + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(80); + }); + + describe('unregistered user cases', function () { + let storage3; + let smtUnregistered: Merkletree; + let unregisteredUtxo100: UTXO; + + before(() => { + storage3 = new InMemoryDB(str2Bytes('unregistered')); + smtUnregistered = new Merkletree(storage3, true, 64); + }); + + it('deposit by an unregistered user should succeed', async function () { + const tx = await erc20 + .connect(deployer) + .mint(unregistered.ethAddress, 100); + await tx.wait(); + const tx1 = await erc20 + .connect(unregistered.signer) + .approve(zeto.target, 100); + await tx1.wait(); + + unregisteredUtxo100 = newUTXO(100, unregistered); + const { outputCommitments, encodedProof } = await prepareDepositProof( + unregistered, + unregisteredUtxo100 + ); + const tx2 = await zeto + .connect(unregistered.signer) + .deposit(100, outputCommitments[0], encodedProof); + await tx2.wait(); + + // Alice tracks the UTXO inside the SMT + await smtAlice.add(unregisteredUtxo100.hash, unregisteredUtxo100.hash); + // Bob also locally tracks the UTXOs inside the SMT + await smtBob.add(unregisteredUtxo100.hash, unregisteredUtxo100.hash); + }); + + it('transfer from an unregistered user should fail', async function () { + // catch up the local SMT for the unregistered user + await smtUnregistered.add(utxo100.hash, utxo100.hash); + await smtUnregistered.add(utxo1.hash, utxo1.hash); + await smtUnregistered.add(utxo2.hash, utxo2.hash); + await smtUnregistered.add(_utxo3.hash, _utxo3.hash); + await smtUnregistered.add(utxo4.hash, utxo4.hash); + await smtUnregistered.add(utxo6.hash, utxo6.hash); + await smtUnregistered.add(utxo7.hash, utxo7.hash); + await smtUnregistered.add( + withdrawChangeUTXO.hash, + withdrawChangeUTXO.hash + ); + await smtUnregistered.add( + unregisteredUtxo100.hash, + unregisteredUtxo100.hash + ); + const utxosRoot = await smtUnregistered.root(); + + const nullifier = newNullifier(unregisteredUtxo100, unregistered); + const output1 = newUTXO(100, Bob); + const output2 = newUTXO(0, unregistered); + const proof = await smtUnregistered.generateCircomVerifierProof( + unregisteredUtxo100.hash, + utxosRoot + ); + const merkleProofs = [ + proof.siblings.map((s) => s.bigInt()), + proof.siblings.map((s) => s.bigInt()), + ]; + + // add the unregistered user to the local KYC SMT, but not to the onchain SMT + await smtKyc.add( + kycHash(unregistered.babyJubPublicKey), + kycHash(unregistered.babyJubPublicKey) + ); + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof( + kycHash(unregistered.babyJubPublicKey), + identitiesRoot + ); + const proof4 = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (unregistered) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) + proof3.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (unregistered) + ]; + await expect( + doTransfer( + unregistered, + [unregisteredUtxo100, ZERO_UTXO], + [nullifier, ZERO_UTXO], + [output1, output2], + utxosRoot.bigInt(), + merkleProofs, + identitiesRoot.bigInt(), + identitiesMerkleProofs, + [Bob, unregistered] + ) + ).rejectedWith('Invalid proof'); + }); + + it('the unregistered user can still withdraw their UTXOs to ERC20 tokens', async function () { + const startingBalance = await erc20.balanceOf(unregistered.ethAddress); + // unregistered user generates the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(unregisteredUtxo100, unregistered); + + // unregistered user generates inclusion proofs for the UTXOs to be spent + let root = await smtUnregistered.root(); + const proof1 = await smtUnregistered.generateCircomVerifierProof( + unregisteredUtxo100.hash, + root + ); + const proof2 = await smtUnregistered.generateCircomVerifierProof( + 0n, + root + ); + const merkleProofs = [ + proof1.siblings.map((s) => s.bigInt()), + proof2.siblings.map((s) => s.bigInt()), + ]; + + // unregistered user proposes the output ERC20 tokens + const unregisteredWithdrawChangeUTXO = newUTXO(0, unregistered); + + const { nullifiers, outputCommitments, encodedProof } = + await prepareNullifierWithdrawProof( + unregistered, + [unregisteredUtxo100, ZERO_UTXO], + [nullifier1, ZERO_UTXO], + unregisteredWithdrawChangeUTXO, + root.bigInt(), + merkleProofs + ); + + // unregistered user withdraws her UTXOs to ERC20 tokens + const tx = await zeto + .connect(unregistered.signer) + .withdraw( + 100, + nullifiers, + outputCommitments[0], + root.bigInt(), + encodedProof + ); + await tx.wait(); + + // Alice tracks the UTXO inside the SMT + await smtAlice.add( + unregisteredWithdrawChangeUTXO.hash, + unregisteredWithdrawChangeUTXO.hash + ); + // Bob also locally tracks the UTXOs inside the SMT + await smtBob.add( + unregisteredWithdrawChangeUTXO.hash, + unregisteredWithdrawChangeUTXO.hash + ); + + // unregistered user checks her ERC20 balance + const endingBalance = await erc20.balanceOf(unregistered.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(100); + }); + }); + + describe('failure cases', function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } + + it('Alice attempting to withdraw spent UTXOs should fail', 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 + withdrawChangeUTXO = newUTXO(90, Alice); + + const { nullifiers, outputCommitments, encodedProof } = + await prepareNullifierWithdrawProof( + Alice, + [utxo100, ZERO_UTXO], + [nullifier1, ZERO_UTXO], + withdrawChangeUTXO, + root.bigInt(), + merkleProofs + ); + + // Alice withdraws her UTXOs to ERC20 tokens + await expect( + zeto + .connect(Alice.signer) + .withdraw( + 10, + nullifiers, + outputCommitments[0], + root.bigInt(), + encodedProof + ) + ).rejectedWith('UTXOAlreadySpent'); + }); + + 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()), + ]; + + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof( + kycHash(Alice.babyJubPublicKey), + identitiesRoot + ); + const proof4 = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Alice) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) + proof3.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (Alice) + ]; + + await expect( + doTransfer( + Alice, + [utxo1, utxo2], + [nullifier1, nullifier2], + [_utxo1, _utxo2], + root.bigInt(), + merkleProofs, + identitiesRoot.bigInt(), + identitiesMerkleProofs, + [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()), + ]; + + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const proof4 = await smtKyc.generateCircomVerifierProof( + kycHash(Alice.babyJubPublicKey), + identitiesRoot + ); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (Alice) + ]; + + await expect( + doTransfer( + Bob, + [utxo7, _utxo1], + [nullifier1, nullifier2], + [utxo1, utxo2], + root.bigInt(), + merkleProofs, + identitiesRoot.bigInt(), + identitiesMerkleProofs, + [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()), + ]; + + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const proof4 = await smtKyc.generateCircomVerifierProof( + kycHash(Alice.babyJubPublicKey), + identitiesRoot + ); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice) + proof3.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (Bob) + ]; + + await expect( + doTransfer( + Bob, + [utxo7, utxo7], + [nullifier1, nullifier2], + [_utxo1, _utxo2], + root.bigInt(), + merkleProofs, + identitiesRoot.bigInt(), + identitiesMerkleProofs, + [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); + const utxo7 = newUTXO(15, Bob); + + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof( + kycHash(Alice.babyJubPublicKey), + identitiesRoot + ); + const proof4 = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const proof5 = await smtKyc.generateCircomVerifierProof( + kycHash(Charlie.babyJubPublicKey), + identitiesRoot + ); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Alice) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) + proof5.siblings.map((s) => s.bigInt()), // identity proof for the 2nd owner of the output UTXO (Charlie) + ]; + + await expect( + doTransfer( + Alice, + [nonExisting1, nonExisting2], + [nullifier1, nullifier2], + [utxo7, _utxo1], + root.bigInt(), + merkleProofs, + identitiesRoot.bigInt(), + identitiesMerkleProofs, + [Bob, Charlie] + ) + ).rejectedWith('UTXORootNotFound'); + }).timeout(600000); + }); + + async function doTransfer( + signer: User, + inputs: UTXO[], + _nullifiers: UTXO[], + outputs: UTXO[], + utxosRoot: BigInt, + utxosMerkleProof: BigInt[][], + identitiesRoot: BigInt, + identitiesMerkleProof: BigInt[][], + owners: User[] + ) { + let nullifiers: [BigNumberish, BigNumberish]; + let outputCommitments: [BigNumberish, BigNumberish]; + let encryptedValues: BigNumberish[]; + let encryptionNonce: BigNumberish; + let encodedProof: any; + const result = await prepareProof( + signer, + inputs, + _nullifiers, + outputs, + utxosRoot, + utxosMerkleProof, + identitiesRoot, + identitiesMerkleProof, + 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, + utxosRoot, + 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[], + utxosRoot: BigInt, + utxosMerkleProof: BigInt[][], + identitiesRoot: BigInt, + identitiesMerkleProof: 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 = newEncryptionNonce() as BigNumberish; + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + const startWitnessCalculation = Date.now(); + const inputObj = { + nullifiers, + inputCommitments, + inputValues, + inputSalts, + inputOwnerPrivateKey: signer.formattedPrivateKey, + utxosRoot, + enabled: [nullifiers[0] !== 0n ? 1 : 0, nullifiers[1] !== 0n ? 1 : 0], + utxosMerkleProof, + identitiesRoot, + identitiesMerkleProof, + outputCommitments, + outputValues, + outputSalts: outputs.map((output) => output.salt || 0n), + outputOwnerPublicKeys, + ...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 encryptedValues = publicSignals.slice(0, 4); + return { + inputCommitments, + outputCommitments, + encryptedValues, + encryptionNonce, + encodedProof, + }; + } + + async function sendTx( + signer: User, + nullifiers: [BigNumberish, BigNumberish], + outputCommitments: [BigNumberish, BigNumberish], + root: BigNumberish, + encryptedValues: 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; + } +}); diff --git a/solidity/test/zeto_anon_nullifier.ts b/solidity/test/zeto_anon_nullifier.ts index 9ec9f65..158bb43 100644 --- a/solidity/test/zeto_anon_nullifier.ts +++ b/solidity/test/zeto_anon_nullifier.ts @@ -327,6 +327,13 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); }).timeout(600000); + + it("repeated mint calls with single UTXO should not fail", async function () { + const utxo5 = newUTXO(10, Alice); + await expect(doMint(zeto, deployer, [utxo5, ZERO_UTXO])).fulfilled; + const utxo6 = newUTXO(20, Alice); + await expect(doMint(zeto, deployer, [utxo6, ZERO_UTXO])).fulfilled; + }); }); async function doTransfer(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], root: BigInt, merkleProofs: BigInt[][], owners: User[]) { diff --git a/solidity/test/zeto_anon_nullifier_kyc.ts b/solidity/test/zeto_anon_nullifier_kyc.ts index 272e415..fc4d09c 100644 --- a/solidity/test/zeto_anon_nullifier_kyc.ts +++ b/solidity/test/zeto_anon_nullifier_kyc.ts @@ -496,6 +496,13 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); }).timeout(600000); + + it("repeated mint calls with single UTXO should not fail", async function () { + const utxo5 = newUTXO(10, Alice); + await expect(doMint(zeto, deployer, [utxo5, ZERO_UTXO])).fulfilled; + const utxo6 = newUTXO(20, Alice); + await expect(doMint(zeto, deployer, [utxo6, ZERO_UTXO])).fulfilled; + }); }); async function doTransfer(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], utxosRoot: BigInt, utxosMerkleProofs: BigInt[][], identitiesRoot: BigInt, identitiesMerkleProof: BigInt[][], owners: User[]) { diff --git a/zkp/circuits/anon_enc.circom b/zkp/circuits/anon_enc.circom index e31db25..db9d8af 100644 --- a/zkp/circuits/anon_enc.circom +++ b/zkp/circuits/anon_enc.circom @@ -93,10 +93,10 @@ template Zeto(nInputs, nOutputs) { encrypt.plainText[1] <== outputSalts[0]; encrypt.key <== sharedSecret; encrypt.nonce <== encryptionNonce; - encrypt.cipherText[0] --> cipherText[0]; - encrypt.cipherText[1] --> cipherText[1]; - encrypt.cipherText[2] --> cipherText[2]; - encrypt.cipherText[3] --> cipherText[3]; + encrypt.cipherText[0] ==> cipherText[0]; + encrypt.cipherText[1] ==> cipherText[1]; + encrypt.cipherText[2] ==> cipherText[2]; + encrypt.cipherText[3] ==> cipherText[3]; } component main { public [ inputCommitments, outputCommitments, encryptionNonce ] } = Zeto(2, 2); \ No newline at end of file diff --git a/zkp/circuits/anon_enc_nullifier.circom b/zkp/circuits/anon_enc_nullifier.circom index d8f479a..8360a3b 100644 --- a/zkp/circuits/anon_enc_nullifier.circom +++ b/zkp/circuits/anon_enc_nullifier.circom @@ -118,10 +118,10 @@ template Zeto(nInputs, nOutputs, nSMTLevels) { encrypt.plainText[1] <== outputSalts[0]; encrypt.key <== sharedSecret; encrypt.nonce <== encryptionNonce; - encrypt.cipherText[0] --> cipherText[0]; - encrypt.cipherText[1] --> cipherText[1]; - encrypt.cipherText[2] --> cipherText[2]; - encrypt.cipherText[3] --> cipherText[3]; + encrypt.cipherText[0] ==> cipherText[0]; + encrypt.cipherText[1] ==> cipherText[1]; + encrypt.cipherText[2] ==> cipherText[2]; + encrypt.cipherText[3] ==> cipherText[3]; } component main { public [ nullifiers, outputCommitments, encryptionNonce, root, enabled ] } = Zeto(2, 2, 64); \ No newline at end of file diff --git a/zkp/circuits/anon_enc_nullifier_kyc.circom b/zkp/circuits/anon_enc_nullifier_kyc.circom new file mode 100644 index 0000000..8a93670 --- /dev/null +++ b/zkp/circuits/anon_enc_nullifier_kyc.circom @@ -0,0 +1,156 @@ +// 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 +// - check the owner public keys for inputs and outputs are included in the identities merkle tree +template Zeto(nInputs, nOutputs, nUTXOSMTLevels, nIdentitiesSMTLevels) { + 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 utxosRoot; + signal input utxosMerkleProof[nInputs][nUTXOSMTLevels]; + signal input enabled[nInputs]; + signal input identitiesRoot; + signal input identitiesMerkleProof[nOutputs + 1][nIdentitiesSMTLevels]; + signal input outputCommitments[nOutputs]; + signal input outputValues[nOutputs]; + signal input outputOwnerPublicKeys[nOutputs][2]; + signal input outputSalts[nOutputs]; + signal input encryptionNonce; + + // the output for a 2-element input (value and salt) encryption is a 4-element array + signal output cipherText[4]; + + // 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 checkUTXOSMTProof = CheckSMTProof(nInputs, nUTXOSMTLevels); + checkUTXOSMTProof.root <== utxosRoot; + checkUTXOSMTProof.merkleProof <== utxosMerkleProof; + checkUTXOSMTProof.enabled <== enabled; + checkUTXOSMTProof.leafNodeIndexes <== inputCommitments; + + // Then, we need to check that the owner public keys + // for the inputs and outputs are included in the identities + // Sparse Merkle Tree with the root `identitiesRoot`. + var ownerPublicKeyHashes[nOutputs + 1]; + component hash1 = Poseidon(2); + hash1.inputs[0] <== inputOwnerPublicKey[0]; + hash1.inputs[1] <== inputOwnerPublicKey[1]; + ownerPublicKeyHashes[0] = hash1.out; + + component hashes[nOutputs]; + var identitiesMTPCheckEnabled[nOutputs + 1]; + identitiesMTPCheckEnabled[0] = 1; + for (var i = 0; i < nOutputs; i++) { + hashes[i] = Poseidon(2); + hashes[i].inputs[0] <== outputOwnerPublicKeys[i][0]; + hashes[i].inputs[1] <== outputOwnerPublicKeys[i][1]; + ownerPublicKeyHashes[i+1] = hashes[i].out; + identitiesMTPCheckEnabled[i+1] = 1; + } + + component checkIdentitiesSMTProof = CheckSMTProof(nOutputs + 1, nIdentitiesSMTLevels); + checkIdentitiesSMTProof.root <== identitiesRoot; + checkIdentitiesSMTProof.merkleProof <== identitiesMerkleProof; + checkIdentitiesSMTProof.enabled <== identitiesMTPCheckEnabled; + checkIdentitiesSMTProof.leafNodeIndexes <== ownerPublicKeyHashes; + + // generate shared secret + var sharedSecret[2]; + component ecdh = Ecdh(); + ecdh.privKey <== inputOwnerPrivateKey; + // our circuit requires that the output UTXO for the receiver must be the first in the array + ecdh.pubKey[0] <== outputOwnerPublicKeys[0][0]; + ecdh.pubKey[1] <== outputOwnerPublicKeys[0][1]; + sharedSecret[0] = ecdh.sharedKey[0]; + sharedSecret[1] = ecdh.sharedKey[1]; + + // encrypt the value for the receiver + component encrypt = SymmetricEncrypt(2); + // our circuit requires that the output UTXO for the receiver must be the first in the array + encrypt.plainText[0] <== outputValues[0]; + encrypt.plainText[1] <== outputSalts[0]; + encrypt.key <== sharedSecret; + encrypt.nonce <== encryptionNonce; + encrypt.cipherText[0] ==> cipherText[0]; + encrypt.cipherText[1] ==> cipherText[1]; + encrypt.cipherText[2] ==> cipherText[2]; + encrypt.cipherText[3] ==> cipherText[3]; +} + +component main { public [ nullifiers, outputCommitments, encryptionNonce, utxosRoot, identitiesRoot, enabled ] } = Zeto(2, 2, 64, 10); \ No newline at end of file diff --git a/zkp/circuits/gen-config.json b/zkp/circuits/gen-config.json index 9f8e36a..6e22d48 100644 --- a/zkp/circuits/gen-config.json +++ b/zkp/circuits/gen-config.json @@ -23,6 +23,10 @@ "ptau": "powersOfTau28_hez_final_16", "skipSolidityGenaration": false }, + "anon_enc_nullifier_kyc": { + "ptau": "powersOfTau28_hez_final_16", + "skipSolidityGenaration": false + }, "nf_anon": { "ptau": "powersOfTau28_hez_final_11", "skipSolidityGenaration": false diff --git a/zkp/js/integration-test/anon_enc_nullifier_kyc.js b/zkp/js/integration-test/anon_enc_nullifier_kyc.js new file mode 100644 index 0000000..8338f81 --- /dev/null +++ b/zkp/js/integration-test/anon_enc_nullifier_kyc.js @@ -0,0 +1,212 @@ +// 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 { + genKeypair, + formatPrivKeyForBabyJub, + stringifyBigInts, +} = require('maci-crypto'); +const { + Merkletree, + InMemoryDB, + str2Bytes, + ZERO_HASH, +} = require('@iden3/js-merkletree'); +const { + Poseidon, + newSalt, + loadCircuit, + newEncryptionNonce, + kycHash, +} = require('../index.js'); +const { loadProvingKeys } = require('./utils.js'); + +const SMT_HEIGHT_UTXO = 64; +const SMT_HEIGHT_IDENTITY = 10; +const poseidonHash = Poseidon.poseidon4; +const poseidonHash2 = Poseidon.poseidon2; +const poseidonHash3 = Poseidon.poseidon3; + +describe('main circuit tests for Zeto fungible tokens with encryption and anonymity using nullifiers with KYC', () => { + let circuit, provingKeyFile, verificationKey, smtAlice, smtKYC, smtBob; + + const Alice = {}; + const Bob = {}; + let senderPrivateKey; + + before(async () => { + circuit = await loadCircuit('anon_enc_nullifier_kyc'); + ({ provingKeyFile, verificationKey } = loadProvingKeys( + 'anon_enc_nullifier_kyc' + )); + + 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; + + // initialize the local storage for Alice to manage her UTXOs in the Spart Merkle Tree + const storage1 = new InMemoryDB(str2Bytes('alice')); + smtAlice = new Merkletree(storage1, true, SMT_HEIGHT_UTXO); + + // initialize the local storage for Bob to manage his UTXOs in the Spart Merkle Tree + const storage2 = new InMemoryDB(str2Bytes('bob')); + smtBob = new Merkletree(storage2, true, SMT_HEIGHT_UTXO); + + // initialize the local storage for the sender to manage identities in the Spart Merkle Tree + const storage3 = new InMemoryDB(str2Bytes('kyc')); + smtKYC = new Merkletree(storage3, true, SMT_HEIGHT_IDENTITY); + + // calculate the identity hash for Alice + const identity1 = poseidonHash2(Alice.pubKey); + await smtKYC.add(identity1, identity1); + + // calculate the identity hash for Bob + const identity2 = poseidonHash2(Bob.pubKey); + await smtKYC.add(identity2, identity2); + }); + + it('should generate a valid proof that can be verified successfully', async () => { + const inputValues = [32, 40]; + const outputValues = [20, 52]; + + // 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 + ); + const utxosRoot = proof1.root.bigInt(); + + // 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]; + + // generate the merkle proof for the transacting identities + const proof3 = await smtKYC.generateCircomVerifierProof( + kycHash(Alice.pubKey), + ZERO_HASH + ); + const proof4 = await smtKYC.generateCircomVerifierProof( + kycHash(Bob.pubKey), + ZERO_HASH + ); + const identitiesRoot = proof3.root.bigInt(); + + const encryptionNonce = newEncryptionNonce(); + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + const startTime = Date.now(); + const witness = await circuit.calculateWTNSBin( + { + nullifiers, + inputCommitments, + inputValues, + inputSalts: [salt1, salt2], + inputOwnerPrivateKey: senderPrivateKey, + utxosRoot, + utxosMerkleProof: [ + proof1.siblings.map((s) => s.bigInt()), + proof2.siblings.map((s) => s.bigInt()), + ], + enabled: [1, 1], + identitiesRoot, + identitiesMerkleProof: [ + proof3.siblings.map((s) => s.bigInt()), + proof4.siblings.map((s) => s.bigInt()), + proof3.siblings.map((s) => s.bigInt()), + ], + outputCommitments, + outputValues, + outputSalts: [salt3, salt4], + outputOwnerPublicKeys: [Bob.pubKey, Alice.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('utxo root', proof1.root.bigInt()); + // console.log('identitiesRoot', proof3.root.bigInt()); + // console.log('encryptionNonce', encryptionNonce); + // console.log('publicSignals', publicSignals); + expect(success, true); + }).timeout(600000); +}); diff --git a/zkp/js/test/anon_enc_nullifier_kyc.js b/zkp/js/test/anon_enc_nullifier_kyc.js new file mode 100644 index 0000000..26b134b --- /dev/null +++ b/zkp/js/test/anon_enc_nullifier_kyc.js @@ -0,0 +1,340 @@ +// 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, + newEncryptionNonce, +} = require('../index.js'); + +const SMT_HEIGHT_UTXO = 64; +const SMT_HEIGHT_IDENTITY = 10; +const poseidonHash = Poseidon.poseidon4; +const poseidonHash2 = Poseidon.poseidon2; +const poseidonHash3 = Poseidon.poseidon3; + +describe('main circuit tests for Zeto fungible tokens with encryption and anonymity using nullifiers with KYC', () => { + let circuit, smtAlice, smtKYC, smtBob; + + const Alice = {}; + const Bob = {}; + let senderPrivateKey; + + before(async function () { + this.timeout(60000); + + circuit = await wasm_tester( + join(__dirname, '../../circuits/anon_enc_nullifier_kyc.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; + + // initialize the local storage for Alice to manage her UTXOs in the Spart Merkle Tree + const storage1 = new InMemoryDB(str2Bytes('alice')); + smtAlice = new Merkletree(storage1, true, SMT_HEIGHT_UTXO); + + // initialize the local storage for Bob to manage his UTXOs in the Spart Merkle Tree + const storage2 = new InMemoryDB(str2Bytes('bob')); + smtBob = new Merkletree(storage2, true, SMT_HEIGHT_UTXO); + + // initialize the local storage for the sender to manage identities in the Spart Merkle Tree + const storage3 = new InMemoryDB(str2Bytes('kyc')); + smtKYC = new Merkletree(storage3, true, SMT_HEIGHT_IDENTITY); + + // calculate the identity hash for Alice + const identity1 = poseidonHash2(Alice.pubKey); + await smtKYC.add(identity1, identity1); + + // calculate the identity hash for Bob + const identity2 = poseidonHash2(Bob.pubKey); + await smtKYC.add(identity2, identity2); + }); + + 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 + ); + const utxosRoot = proof1.root.bigInt(); + + // 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 = newEncryptionNonce(); + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + // generate the merkle proof for the transacting identities + const proof3 = await smtKYC.generateCircomVerifierProof( + poseidonHash2(Alice.pubKey), + ZERO_HASH + ); + const proof4 = await smtKYC.generateCircomVerifierProof( + poseidonHash2(Bob.pubKey), + ZERO_HASH + ); + const identitiesRoot = proof3.root.bigInt(); + + const witness = await circuit.calculateWitness( + { + nullifiers, + inputCommitments, + inputValues, + inputSalts: [salt1, salt2], + inputOwnerPrivateKey: senderPrivateKey, + utxosRoot, + utxosMerkleProof: [ + proof1.siblings.map((s) => s.bigInt()), + proof2.siblings.map((s) => s.bigInt()), + ], + enabled: [1, 1], + identitiesRoot, + identitiesMerkleProof: [ + proof3.siblings.map((s) => s.bigInt()), + proof4.siblings.map((s) => s.bigInt()), + proof3.siblings.map((s) => s.bigInt()), + ], + outputCommitments, + outputValues, + outputSalts: [salt3, salt4], + outputOwnerPublicKeys: [Bob.pubKey, Alice.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('utxosRoot', proof1.root.bigInt()); + // console.log('outputValues', outputValues); + // console.log('outputSalt', salt3); + // console.log('outputOwnerPublicKeys', [Bob.pubKey, Alice.pubKey]); + // console.log('identitiesRoot', proof3.root.bigInt()); + // console.log('encryptionNonce', encryptionNonce); + + expect(witness[5]).to.equal(BigInt(nullifiers[0])); + expect(witness[6]).to.equal(BigInt(nullifiers[1])); + expect(witness[7]).to.equal(proof1.root.bigInt()); + expect(witness[10]).to.equal(proof3.root.bigInt()); + + // take the output from the proof circuit and attempt to decrypt + // as the receiver + const cipherText = witness.slice(1, 5); // first 4 elements are the cipher text for the first encryption output + const recoveredKey = genEcdhSharedKey(Bob.privKey, Alice.pubKey); + const plainText = poseidonDecrypt( + cipherText, + recoveredKey, + encryptionNonce, + 2 + ); + expect(plainText).to.deep.equal([20n, salt3]); + }); + + it('should fail if not using the right identities merkle proofs', 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 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 + ); + const utxosRoot = proof1.root.bigInt(); + + // 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]; + // generate the merkle proof for the transacting identities + const proof3 = await smtKYC.generateCircomVerifierProof( + poseidonHash2(Alice.pubKey), + ZERO_HASH + ); + const proof4 = await smtKYC.generateCircomVerifierProof( + poseidonHash2(Bob.pubKey), + ZERO_HASH + ); + const identitiesRoot = proof3.root.bigInt(); + + const encryptionNonce = genRandomSalt(); + const encryptInputs = stringifyBigInts({ + encryptionNonce, + }); + + let err; + try { + await circuit.calculateWitness( + { + nullifiers, + inputCommitments, + inputValues, + inputSalts: [salt1, salt2], + inputOwnerPrivateKey: senderPrivateKey, + utxosRoot, + utxosMerkleProof: [ + proof1.siblings.map((s) => s.bigInt()), + proof2.siblings.map((s) => s.bigInt()), + ], + enabled: [1, 1], + identitiesRoot, + identitiesMerkleProof: [ + proof3.siblings.map((s) => s.bigInt()), + proof4.siblings.map((s) => s.bigInt()), + [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], // invalid MTP + ], + outputCommitments, + outputValues, + outputSalts: [salt3, salt4], + outputOwnerPublicKeys: [Bob.pubKey, Alice.pubKey], + ...encryptInputs, + }, + true + ); + } catch (e) { + err = e; + } + // console.log(err); + expect(err).to.match(/Error in template Zeto_266 line: 131/); + expect(err).to.match(/Error in template CheckSMTProof_253 line: 46/); + }); +}); diff --git a/zkp/js/test/anon_nullifier_kyc.js b/zkp/js/test/anon_nullifier_kyc.js index c866970..f5b7143 100644 --- a/zkp/js/test/anon_nullifier_kyc.js +++ b/zkp/js/test/anon_nullifier_kyc.js @@ -183,7 +183,7 @@ describe('main circuit tests for Zeto fungible tokens with anonymity, KYC, using let error; try { - const witness = await circuit.calculateWitness( + await circuit.calculateWitness( { nullifiers, inputCommitments, @@ -194,7 +194,11 @@ describe('main circuit tests for Zeto fungible tokens with anonymity, KYC, using utxosMerkleProof: [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())], enabled: [1, 1], identitiesRoot, - identitiesMerkleProof: [proof3.siblings.map((s) => s.bigInt()), proof4.siblings.map((s) => s.bigInt()), proof4.siblings.map((s) => s.bigInt())], + identitiesMerkleProof: [ + proof3.siblings.map((s) => s.bigInt()), + proof4.siblings.map((s) => s.bigInt()), + [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], // invalid MTP + ], outputCommitments, outputValues, outputSalts: [salt3, salt4],