2021-10-17 13:01:49 +02:00
|
|
|
include "../node_modules/circomlib/circuits/poseidon.circom";
|
2021-08-25 00:23:16 +02:00
|
|
|
include "./merkleProof.circom"
|
2021-10-17 13:05:01 +02:00
|
|
|
include "./keypair.circom"
|
2020-04-08 11:41:12 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
Utxo structure:
|
|
|
|
{
|
|
|
|
amount,
|
|
|
|
blinding, // random number
|
|
|
|
pubkey,
|
|
|
|
}
|
|
|
|
|
|
|
|
commitment = hash(amount, blinding, pubKey)
|
|
|
|
nullifier = hash(commitment, privKey, merklePath)
|
|
|
|
*/
|
|
|
|
|
2021-06-15 13:25:06 +02:00
|
|
|
// Universal JoinSplit transaction with nIns inputs and 2 outputs
|
2021-06-09 12:58:57 +02:00
|
|
|
template Transaction(levels, nIns, nOuts, zeroLeaf) {
|
2020-04-08 11:41:12 +02:00
|
|
|
signal input root;
|
2021-08-13 19:07:53 +02:00
|
|
|
// extAmount = external amount used for deposits and withdrawals
|
2020-04-08 11:41:12 +02:00
|
|
|
// correct extAmount range is enforced on the smart contract
|
2021-08-19 09:58:29 +02:00
|
|
|
// publicAmount = extAmount - fee
|
2021-08-13 19:07:53 +02:00
|
|
|
signal input publicAmount;
|
2021-06-07 12:12:15 +02:00
|
|
|
signal input extDataHash;
|
2020-04-08 11:41:12 +02:00
|
|
|
|
2021-06-09 12:58:57 +02:00
|
|
|
// data for transaction inputs
|
2021-06-16 13:01:29 +02:00
|
|
|
signal input inputNullifier[nIns];
|
2021-06-09 12:58:57 +02:00
|
|
|
signal private input inAmount[nIns];
|
|
|
|
signal private input inBlinding[nIns];
|
|
|
|
signal private input inPrivateKey[nIns];
|
|
|
|
signal private input inPathIndices[nIns];
|
|
|
|
signal private input inPathElements[nIns][levels];
|
|
|
|
|
|
|
|
// data for transaction outputs
|
2021-06-16 13:01:29 +02:00
|
|
|
signal input outputCommitment[nOuts];
|
2021-06-09 12:58:57 +02:00
|
|
|
signal private input outAmount[nOuts];
|
|
|
|
signal private input outBlinding[nOuts];
|
|
|
|
signal private input outPubkey[nOuts];
|
2020-04-08 11:41:12 +02:00
|
|
|
|
2021-06-09 12:58:57 +02:00
|
|
|
component inKeypair[nIns];
|
|
|
|
component inUtxoHasher[nIns];
|
|
|
|
component nullifierHasher[nIns];
|
|
|
|
component tree[nIns];
|
|
|
|
component checkRoot[nIns];
|
|
|
|
var sumIns = 0;
|
2020-04-09 11:04:06 +02:00
|
|
|
|
2020-04-08 11:41:12 +02:00
|
|
|
// verify correctness of transaction inputs
|
2021-06-09 12:58:57 +02:00
|
|
|
for (var tx = 0; tx < nIns; tx++) {
|
2021-06-08 20:50:34 +02:00
|
|
|
inKeypair[tx] = Keypair();
|
|
|
|
inKeypair[tx].privateKey <== inPrivateKey[tx];
|
|
|
|
|
2021-10-17 13:01:49 +02:00
|
|
|
inUtxoHasher[tx] = Poseidon(3);
|
|
|
|
inUtxoHasher[tx].inputs[0] <== inAmount[tx];
|
|
|
|
inUtxoHasher[tx].inputs[1] <== inBlinding[tx];
|
|
|
|
inUtxoHasher[tx].inputs[2] <== inKeypair[tx].publicKey;
|
2020-04-08 11:41:12 +02:00
|
|
|
|
2021-10-17 13:01:49 +02:00
|
|
|
nullifierHasher[tx] = Poseidon(3);
|
|
|
|
nullifierHasher[tx].inputs[0] <== inUtxoHasher[tx].out;
|
|
|
|
nullifierHasher[tx].inputs[1] <== inPathIndices[tx];
|
|
|
|
nullifierHasher[tx].inputs[2] <== inPrivateKey[tx];
|
|
|
|
nullifierHasher[tx].out === inputNullifier[tx];
|
2020-04-08 11:41:12 +02:00
|
|
|
|
2021-08-25 00:23:16 +02:00
|
|
|
tree[tx] = MerkleProof(levels);
|
2021-10-17 13:01:49 +02:00
|
|
|
tree[tx].leaf <== inUtxoHasher[tx].out;
|
2020-04-08 11:41:12 +02:00
|
|
|
tree[tx].pathIndices <== inPathIndices[tx];
|
|
|
|
for (var i = 0; i < levels; i++) {
|
|
|
|
tree[tx].pathElements[i] <== inPathElements[tx][i];
|
|
|
|
}
|
2020-04-09 11:04:06 +02:00
|
|
|
|
2020-04-08 11:41:12 +02:00
|
|
|
// check merkle proof only if amount is non-zero
|
|
|
|
checkRoot[tx] = ForceEqualIfEnabled();
|
|
|
|
checkRoot[tx].in[0] <== root;
|
|
|
|
checkRoot[tx].in[1] <== tree[tx].root;
|
|
|
|
checkRoot[tx].enabled <== inAmount[tx];
|
|
|
|
|
2021-08-25 12:20:48 +02:00
|
|
|
// We don't need to range check input amounts, since all inputs are valid UTXOs that
|
|
|
|
// were already checked as outputs in the previous transaction (or zero amount UTXOs that don't
|
|
|
|
// need to be checked either).
|
2021-06-09 12:58:57 +02:00
|
|
|
|
|
|
|
sumIns += inAmount[tx];
|
2020-04-08 11:41:12 +02:00
|
|
|
}
|
|
|
|
|
2021-06-09 12:58:57 +02:00
|
|
|
component outUtxoHasher[nOuts];
|
|
|
|
component outAmountCheck[nOuts];
|
|
|
|
var sumOuts = 0;
|
|
|
|
|
2020-04-08 11:41:12 +02:00
|
|
|
// verify correctness of transaction outputs
|
2021-06-09 12:58:57 +02:00
|
|
|
for (var tx = 0; tx < nOuts; tx++) {
|
2021-10-17 13:01:49 +02:00
|
|
|
outUtxoHasher[tx] = Poseidon(3);
|
|
|
|
outUtxoHasher[tx].inputs[0] <== outAmount[tx];
|
|
|
|
outUtxoHasher[tx].inputs[1] <== outBlinding[tx];
|
|
|
|
outUtxoHasher[tx].inputs[2] <== outPubkey[tx];
|
|
|
|
outUtxoHasher[tx].out === outputCommitment[tx];
|
2020-04-08 11:41:12 +02:00
|
|
|
|
|
|
|
// Check that amount fits into 248 bits to prevent overflow
|
|
|
|
outAmountCheck[tx] = Num2Bits(248);
|
|
|
|
outAmountCheck[tx].in <== outAmount[tx];
|
2021-06-09 12:58:57 +02:00
|
|
|
|
|
|
|
sumOuts += outAmount[tx];
|
2020-04-08 11:41:12 +02:00
|
|
|
}
|
|
|
|
|
2021-06-15 13:25:06 +02:00
|
|
|
// check that there are no same nullifiers among all inputs
|
|
|
|
component sameNullifiers[nIns * (nIns - 1) / 2];
|
|
|
|
var index = 0;
|
|
|
|
for (var i = 0; i < nIns - 1; i++) {
|
|
|
|
for (var j = i + 1; j < nIns; j++) {
|
|
|
|
sameNullifiers[index] = IsEqual();
|
|
|
|
sameNullifiers[index].in[0] <== inputNullifier[i];
|
|
|
|
sameNullifiers[index].in[1] <== inputNullifier[j];
|
|
|
|
sameNullifiers[index].out === 0;
|
|
|
|
index++;
|
|
|
|
}
|
|
|
|
}
|
2020-04-08 11:41:12 +02:00
|
|
|
|
|
|
|
// verify amount invariant
|
2021-08-16 21:17:07 +02:00
|
|
|
sumIns + publicAmount === sumOuts;
|
2020-04-08 11:41:12 +02:00
|
|
|
|
2021-09-26 18:14:05 +02:00
|
|
|
// optional safety constraint to make sure extDataHash cannot be changed
|
2021-08-19 18:20:02 +02:00
|
|
|
signal extDataSquare <== extDataHash * extDataHash;
|
2020-04-08 11:41:12 +02:00
|
|
|
}
|