test: complex integration test cases (#54)

This commit is contained in:
moebius
2025-01-31 11:25:09 +00:00
committed by GitHub
parent 5318a1c597
commit 326246cacb
8 changed files with 756 additions and 132 deletions

View File

@@ -57,7 +57,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: v0.3.0
- name: Use Node.js
uses: actions/setup-node@v4

View File

@@ -1,30 +1,59 @@
#!/usr/bin/env node
import { ethers } from "ethers";
import { generateMerkleProof } from "@privacy-pool-core/sdk";
async function main() {
// Fetch script arguments
const args = process.argv.slice(2);
// Function to temporarily redirect stdout
function withSilentStdout(fn) {
const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write;
// Leaf we want to prove
const leaf = BigInt(args[0]);
// All leaves in tree
const leaves = args.slice(1).map(BigInt);
return async (...args) => {
// Temporarily disable stdout/stderr
process.stdout.write = () => true;
process.stderr.write = () => true;
// Generate merkle proof using the SDK method
const proof = generateMerkleProof(leaves, leaf);
// Fix the issue with index being NaN for depth 0 (LeanIMT issue)
proof.index = Object.is(proof.index, NaN) ? 0 : proof.index;
// Convert proof to ABI-encoded bytes
const abiCoder = new ethers.AbiCoder();
const encodedProof = abiCoder.encode(
["uint256", "uint256", "uint256[]"],
[proof.root, proof.index, proof.siblings],
);
// Write to stdout as hex string
process.stdout.write(encodedProof);
try {
const result = await fn(...args);
// Restore stdout/stderr
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
return result;
} catch (error) {
// Restore stdout/stderr
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
throw error;
}
};
}
main().catch(console.error);
async function main() {
try {
const args = process.argv.slice(2);
const leaf = BigInt(args[0]);
const leaves = args.slice(1).map(BigInt);
// Wrap the generateMerkleProof call with stdout redirection
const silentGenerateProof = withSilentStdout(() =>
generateMerkleProof(leaves, leaf),
);
const proof = await silentGenerateProof();
proof.index = Object.is(proof.index, NaN) ? 0 : proof.index;
const abiCoder = new ethers.AbiCoder();
const encodedProof = abiCoder.encode(
["uint256", "uint256", "uint256[]"],
[proof.root, proof.index, proof.siblings],
);
process.stdout.write(encodedProof);
process.exit(0);
} catch {
// Exit silently on any error
process.exit(1);
}
}
// Catch any uncaught errors and exit silently
main().catch(() => process.exit(1));

View File

@@ -1,26 +1,51 @@
#!/usr/bin/env node
import { PrivacyPoolSDK, Circuits } from "@privacy-pool-core/sdk";
import { encodeAbiParameters } from "viem";
// Function to temporarily redirect stdout
function withSilentStdout(fn) {
const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write;
return async (...args) => {
// Temporarily disable stdout/stderr
process.stdout.write = () => true;
process.stderr.write = () => true;
try {
const result = await fn(...args);
// Restore stdout/stderr
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
return result;
} catch (error) {
// Restore stdout/stderr
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
throw error;
}
};
}
async function main() {
// Get command line arguments
const [value, label, nullifier, secret] = process.argv.slice(2).map(BigInt);
// Initialize SDK with circuits
const circuits = new Circuits();
const privacyPoolSDK = new PrivacyPoolSDK(circuits);
try {
// Generate the commitment proof
const { proof, publicSignals } = await privacyPoolSDK.proveCommitment(
const circuits = new Circuits();
const privacyPoolSDK = new PrivacyPoolSDK(circuits);
// Wrap the proveCommitment call with stdout redirection
const silentProveCommitment = withSilentStdout(
privacyPoolSDK.proveCommitment.bind(privacyPoolSDK),
);
const { proof, publicSignals } = await silentProveCommitment(
value,
label,
nullifier,
secret,
);
// Format the proof to match the Solidity struct
const ragequitProof = {
_pA: [BigInt(proof.pi_a[0]), BigInt(proof.pi_a[1])],
_pB: [
@@ -37,7 +62,6 @@ async function main() {
].map((x) => BigInt(x)),
};
// ABI encode the proof
const encodedProof = encodeAbiParameters(
[
{
@@ -53,13 +77,13 @@ async function main() {
[ragequitProof],
);
// Output the encoded proof directly to stdout
process.stdout.write(encodedProof);
process.exit(0);
} catch (error) {
console.error("Error generating proof:", error);
} catch {
// Exit silently on any error
process.exit(1);
}
}
main().catch(console.error);
// Catch any uncaught errors and exit silently
main().catch(() => process.exit(1));

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env node
import { ethers } from "ethers";
import {
PrivacyPoolSDK,
@@ -16,6 +15,31 @@ function padSiblings(siblings, treeDepth) {
return paddedSiblings;
}
// Function to temporarily redirect stdout
function withSilentStdout(fn) {
const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write;
return async (...args) => {
// Temporarily disable stdout/stderr
process.stdout.write = () => true;
process.stderr.write = () => true;
try {
const result = await fn(...args);
// Restore stdout/stderr
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
return result;
} catch (error) {
// Restore stdout/stderr
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
throw error;
}
};
}
async function main() {
const [
existingValue,
@@ -32,94 +56,97 @@ async function main() {
aspTreeDepth,
] = process.argv.slice(2);
const circuits = new Circuits();
const sdk = new PrivacyPoolSDK(circuits);
try {
const circuits = new Circuits();
const sdk = new PrivacyPoolSDK(circuits);
// Decode the Merkle proofs
const abiCoder = new ethers.AbiCoder();
const stateMerkleProof = abiCoder.decode(
["uint256", "uint256", "uint256[]"],
stateMerkleProofHex,
);
const aspMerkleProof = abiCoder.decode(
["uint256", "uint256", "uint256[]"],
aspMerkleProofHex,
);
const abiCoder = new ethers.AbiCoder();
const stateMerkleProof = abiCoder.decode(
["uint256", "uint256", "uint256[]"],
stateMerkleProofHex,
);
const aspMerkleProof = abiCoder.decode(
["uint256", "uint256", "uint256[]"],
aspMerkleProofHex,
);
const commitment = getCommitment(
existingValue,
label,
existingNullifier,
existingSecret,
);
const commitment = getCommitment(
existingValue,
label,
existingNullifier,
existingSecret,
);
// Pad siblings arrays to required length
const paddedStateSiblings = padSiblings(stateMerkleProof[2], 32);
const paddedAspSiblings = padSiblings(aspMerkleProof[2], 32);
const paddedStateSiblings = padSiblings(stateMerkleProof[2], 32);
const paddedAspSiblings = padSiblings(aspMerkleProof[2], 32);
// Generate withdrawal proof
const { proof, publicSignals } = await sdk.proveWithdrawal(commitment, {
context,
withdrawalAmount: withdrawnValue,
stateMerkleProof: {
root: stateMerkleProof[0],
leaf: commitment.hash,
index: stateMerkleProof[1],
siblings: paddedStateSiblings,
},
aspMerkleProof: {
root: aspMerkleProof[0],
leaf: commitment.hash,
index: aspMerkleProof[1],
siblings: paddedAspSiblings,
},
stateRoot: stateMerkleProof[0],
stateTreeDepth: parseInt(stateTreeDepth),
aspRoot: aspMerkleProof[0],
aspTreeDepth: parseInt(aspTreeDepth),
newSecret,
newNullifier,
});
// Wrap the proveWithdrawal call with stdout redirection
const silentProveWithdrawal = withSilentStdout(
sdk.proveWithdrawal.bind(sdk),
);
// Format the proof to match the Solidity struct
const withdrawalProof = {
_pA: [BigInt(proof.pi_a[0]), BigInt(proof.pi_a[1])],
_pB: [
[BigInt(proof.pi_b[0][1]), BigInt(proof.pi_b[0][0])],
[BigInt(proof.pi_b[1][1]), BigInt(proof.pi_b[1][0])],
],
_pC: [BigInt(proof.pi_c[0]), BigInt(proof.pi_c[1])],
_pubSignals: [
publicSignals[0], // new commitment hash
publicSignals[1], // existing nullifier hash
publicSignals[2], // withdrawn value
publicSignals[3], // state root
publicSignals[4], // state depth
publicSignals[5], // asp root
publicSignals[6], // asp depth
publicSignals[7], // context
].map((x) => BigInt(x)),
};
// ABI encode the proof
const encodedProof = encodeAbiParameters(
[
{
type: "tuple",
components: [
{ name: "_pA", type: "uint256[2]" },
{ name: "_pB", type: "uint256[2][2]" },
{ name: "_pC", type: "uint256[2]" },
{ name: "_pubSignals", type: "uint256[8]" },
],
const { proof, publicSignals } = await silentProveWithdrawal(commitment, {
context,
withdrawalAmount: withdrawnValue,
stateMerkleProof: {
root: stateMerkleProof[0],
leaf: commitment.hash,
index: stateMerkleProof[1],
siblings: paddedStateSiblings,
},
],
[withdrawalProof],
);
aspMerkleProof: {
root: aspMerkleProof[0],
leaf: commitment.hash,
index: aspMerkleProof[1],
siblings: paddedAspSiblings,
},
stateRoot: stateMerkleProof[0],
stateTreeDepth: parseInt(stateTreeDepth),
aspRoot: aspMerkleProof[0],
aspTreeDepth: parseInt(aspTreeDepth),
newSecret,
newNullifier,
});
// Write to stdout as hex string
process.stdout.write(encodedProof);
process.exit(0);
const withdrawalProof = {
_pA: [BigInt(proof.pi_a[0]), BigInt(proof.pi_a[1])],
_pB: [
[BigInt(proof.pi_b[0][1]), BigInt(proof.pi_b[0][0])],
[BigInt(proof.pi_b[1][1]), BigInt(proof.pi_b[1][0])],
],
_pC: [BigInt(proof.pi_c[0]), BigInt(proof.pi_c[1])],
_pubSignals: [
publicSignals[0],
publicSignals[1],
publicSignals[2],
publicSignals[3],
publicSignals[4],
publicSignals[5],
publicSignals[6],
publicSignals[7],
].map((x) => BigInt(x)),
};
const encodedProof = encodeAbiParameters(
[
{
type: "tuple",
components: [
{ name: "_pA", type: "uint256[2]" },
{ name: "_pB", type: "uint256[2][2]" },
{ name: "_pC", type: "uint256[2]" },
{ name: "_pubSignals", type: "uint256[8]" },
],
},
],
[withdrawalProof],
);
process.stdout.write(encodedProof);
process.exit(0);
} catch {
process.exit(1);
}
}
main().catch(console.error);
main().catch(() => process.exit(1));

View File

@@ -27,6 +27,10 @@ import {Constants} from 'test/helper/Constants.sol';
contract IntegrationBase is Test {
using InternalLeanIMT for LeanIMTData;
error WithdrawalProofGenerationFailed();
error RagequitProofGenerationFailed();
error MerkleProofGenerationFailed();
/*///////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/
@@ -274,7 +278,7 @@ contract IntegrationBase is Test {
IPrivacyPool.Withdrawal memory _withdrawal,
WithdrawalParams memory _params,
bool _direct
) private returns (Commitment memory _commitment) {
) internal returns (Commitment memory _commitment) {
// Fetch balances before withdrawal
uint256 _recipientInitialBalance = _balance(_params.recipient, _params.commitment.asset);
uint256 _entrypointInitialBalance = _balance(address(_entrypoint), _params.commitment.asset);
@@ -406,7 +410,7 @@ contract IntegrationBase is Test {
uint256 _label,
uint256 _nullifier,
uint256 _secret
) private returns (ProofLib.RagequitProof memory _proof) {
) internal returns (ProofLib.RagequitProof memory _proof) {
// Generate real proof using the helper script
string[] memory _inputs = new string[](5);
_inputs[0] = vm.toString(_value);
@@ -420,12 +424,16 @@ contract IntegrationBase is Test {
_scriptArgs[1] = 'test/helper/RagequitProofGenerator.mjs';
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
if (_proofData.length == 0) {
revert RagequitProofGenerationFailed();
}
// Decode the ABI-encoded proof directly
_proof = abi.decode(_proofData, (ProofLib.RagequitProof));
}
function _generateWithdrawalProof(WithdrawalProofParams memory _params)
private
internal
returns (ProofLib.WithdrawProof memory _proof)
{
// Generate state merkle proof
@@ -433,6 +441,10 @@ contract IntegrationBase is Test {
// Generate ASP merkle proof
bytes memory _aspMerkleProof = _generateMerkleProof(_aspLeaves, _params.label);
if (_aspMerkleProof.length == 0 || _stateMerkleProof.length == 0) {
revert MerkleProofGenerationFailed();
}
string[] memory _inputs = new string[](12);
_inputs[0] = vm.toString(_params.existingValue);
_inputs[1] = vm.toString(_params.label);
@@ -453,11 +465,14 @@ contract IntegrationBase is Test {
_scriptArgs[1] = 'test/helper/WithdrawalProofGenerator.mjs';
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
// Decode the ABI-encoded proof directly
if (_proofData.length == 0) {
revert WithdrawalProofGenerationFailed();
}
_proof = abi.decode(_proofData, (ProofLib.WithdrawProof));
}
function _generateMerkleProof(uint256[] storage _leaves, uint256 _leaf) private returns (bytes memory _proof) {
function _generateMerkleProof(uint256[] storage _leaves, uint256 _leaf) internal returns (bytes memory _proof) {
uint256 _leavesAmt = _leaves.length;
string[] memory inputs = new string[](_leavesAmt + 1);
inputs[0] = vm.toString(_leaf);
@@ -532,7 +547,7 @@ contract IntegrationBase is Test {
_commitmentHash = PoseidonT4.hash([_amount, _label, _precommitment]);
}
function _genSecretBySeed(string memory _seed) private pure returns (uint256 _secret) {
function _genSecretBySeed(string memory _seed) internal pure returns (uint256 _secret) {
_secret = uint256(keccak256(bytes(_seed))) % Constants.SNARK_SCALAR_FIELD;
}
}

View File

@@ -5,12 +5,16 @@ import {IntegrationBase} from './IntegrationBase.sol';
import {InternalLeanIMT, LeanIMTData} from 'lean-imt/InternalLeanIMT.sol';
import {IPrivacyPool} from 'interfaces/IPrivacyPool.sol';
import {IState} from 'interfaces/IState.sol';
contract IntegrationERC20 is IntegrationBase {
using InternalLeanIMT for LeanIMTData;
Commitment internal _commitment;
/**
* @notice Test that users can make a deposit and fully withdraw its value (after fees) directly, without a relayer
*/
function test_fullDirectWithdrawal() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
@@ -34,6 +38,9 @@ contract IntegrationERC20 is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and fully withdraw its value (after fees) through a relayer
*/
function test_fullRelayedWithdrawal() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
@@ -57,6 +64,9 @@ contract IntegrationERC20 is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and partially withdraw, without a relayer
*/
function test_partialDirectWithdrawal() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
@@ -80,6 +90,9 @@ contract IntegrationERC20 is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and do multiple partial withdrawals without a relayer
*/
function test_multiplePartialDirectWithdrawals() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
@@ -139,6 +152,9 @@ contract IntegrationERC20 is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and do a partial withdrawal through a relayer
*/
function test_partialRelayedWithdrawal() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
@@ -162,6 +178,9 @@ contract IntegrationERC20 is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and do multiple partial withdrawals through a relayer
*/
function test_multiplePartialRelayedWithdrawals() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
@@ -221,6 +240,9 @@ contract IntegrationERC20 is IntegrationBase {
);
}
/**
* @notice Test that users can ragequit a commitment when their label is not in the ASP tree
*/
function test_ragequit() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
@@ -247,6 +269,9 @@ contract IntegrationERC20 is IntegrationBase {
_ragequit(_ALICE, _commitment);
}
/**
* @notice Test that users can get approved by the ASP, make a partial withdrawal, and if removed from the ASP set, they can only ragequit
*/
function test_aspRemoval() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
@@ -288,4 +313,109 @@ contract IntegrationERC20 is IntegrationBase {
// Ragequit
_ragequit(_ALICE, _commitment);
}
/**
* @notice Test that users can't spend a commitment more than once
*/
function test_failWhenCommitmentAlreadySpent() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
DepositParams({depositor: _ALICE, asset: _DAI, amount: 5000 ether, nullifier: 'nullifier_1', secret: 'secret_1'})
);
// Push ASP root with label included
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
// Fully spend child commitment
_selfWithdraw(
WithdrawalParams({
withdrawnAmount: _commitment.value,
newNullifier: 'nullifier_2',
newSecret: 'secret_2',
recipient: _BOB,
commitment: _commitment,
revertReason: NONE
})
);
// Fail to spend same commitment that was just spent
_selfWithdraw(
WithdrawalParams({
withdrawnAmount: _commitment.value,
newNullifier: 'nullifier_2',
newSecret: 'secret_2',
recipient: _BOB,
commitment: _commitment,
revertReason: IState.NullifierAlreadySpent.selector
})
);
}
/**
* @notice Test that commitments with reused nullifiers can not be spent
*/
function test_failWhenReusingNullifier() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
DepositParams({depositor: _ALICE, asset: _DAI, amount: 5000 ether, nullifier: 'nullifier_1', secret: 'secret_1'})
);
// Push ASP root with label included
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
// Bob withdraws some of Alice's commitment
_commitment = _selfWithdraw(
WithdrawalParams({
withdrawnAmount: 2000 ether,
newNullifier: 'nullifier_1', // Reusing nullifier of deposit for new commitment
newSecret: 'secret_2',
recipient: _BOB,
commitment: _commitment,
revertReason: NONE
})
);
// Fail to spend the child commitment
_selfWithdraw(
WithdrawalParams({
withdrawnAmount: 2000 ether,
newNullifier: 'nullifier_3',
newSecret: 'secret_3',
recipient: _BOB,
commitment: _commitment,
revertReason: IState.NullifierAlreadySpent.selector
})
);
}
/**
* @notice Test that spent commitments can not be ragequitted (and spent again)
*/
function test_failWhenTryingToSpendRagequitCommitment() public {
// Alice deposits 5000 DAI
_commitment = _deposit(
DepositParams({depositor: _ALICE, asset: _DAI, amount: 5000 ether, nullifier: 'nullifier_1', secret: 'secret_1'})
);
// Ragequit full amount
_ragequit(_ALICE, _commitment);
// Push ASP root with label included
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
// Fail to withdraw commitment that was already ragequitted
_selfWithdraw(
WithdrawalParams({
withdrawnAmount: _commitment.value,
newNullifier: 'nullifier_2',
newSecret: 'secret_2',
recipient: _BOB,
commitment: _commitment,
revertReason: IState.NullifierAlreadySpent.selector
})
);
}
}

View File

@@ -5,12 +5,16 @@ import {IntegrationBase} from './IntegrationBase.sol';
import {InternalLeanIMT, LeanIMTData} from 'lean-imt/InternalLeanIMT.sol';
import {IPrivacyPool} from 'interfaces/IPrivacyPool.sol';
import {IState} from 'interfaces/IState.sol';
contract IntegrationETH is IntegrationBase {
contract IntegrationNative is IntegrationBase {
using InternalLeanIMT for LeanIMTData;
Commitment internal _commitment;
/**
* @notice Test that users can make a deposit and fully withdraw its value (after fees) directly, without a relayer
*/
function test_fullDirectWithdrawal() public {
// Alice deposits 100 ETH
_commitment = _deposit(
@@ -34,6 +38,9 @@ contract IntegrationETH is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and fully withdraw its value (after fees) through a relayer
*/
function test_fullRelayedWithdrawal() public {
// Alice deposits 100 ETH
_commitment = _deposit(
@@ -44,7 +51,7 @@ contract IntegrationETH is IntegrationBase {
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
// Bob receives the total amount of Alice's commitment
// Bob receives withdraws total amount of Alice's commitment through a relayer
_withdrawThroughRelayer(
WithdrawalParams({
withdrawnAmount: _commitment.value,
@@ -57,6 +64,9 @@ contract IntegrationETH is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and partially withdraw, without a relayer
*/
function test_partialDirectWithdrawal() public {
// Alice deposits 100 ETH
_commitment = _deposit(
@@ -80,6 +90,9 @@ contract IntegrationETH is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and do multiple partial withdrawals without a relayer
*/
function test_multiplePartialDirectWithdrawals() public {
// Alice deposits 100 ETH
_commitment = _deposit(
@@ -151,6 +164,9 @@ contract IntegrationETH is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and do a partial withdrawal through a relayer
*/
function test_partialRelayedWithdrawal() public {
// Alice deposits 100 ETH
_commitment = _deposit(
@@ -161,7 +177,7 @@ contract IntegrationETH is IntegrationBase {
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
// Bob receives the total amount of Alice's commitment
// Bob receives the total amount of Alice's commitment through a relayer
_withdrawThroughRelayer(
WithdrawalParams({
withdrawnAmount: 40 ether,
@@ -174,6 +190,9 @@ contract IntegrationETH is IntegrationBase {
);
}
/**
* @notice Test that users can make a deposit and do multiple partial withdrawals through a relayer
*/
function test_multiplePartialRelayedWithdrawals() public {
// Alice deposits 100 ETH
_commitment = _deposit(
@@ -245,6 +264,9 @@ contract IntegrationETH is IntegrationBase {
);
}
/**
* @notice Test that users can ragequit a commitment when their label is not in the ASP tree
*/
function test_ragequit() public {
// Alice deposits 100 ETH
_commitment = _deposit(
@@ -255,7 +277,7 @@ contract IntegrationETH is IntegrationBase {
vm.prank(_POSTMAN);
_entrypoint.updateRoot(uint256(keccak256('some_root')) % SNARK_SCALAR_FIELD, bytes32('IPFS_HASH'));
// Fail to withdraw
// Fail to withdraw because the label is not included in the latest ASP root
_withdrawThroughRelayer(
WithdrawalParams({
withdrawnAmount: 40 ether,
@@ -271,6 +293,9 @@ contract IntegrationETH is IntegrationBase {
_ragequit(_ALICE, _commitment);
}
/**
* @notice Test that users can get approved by the ASP, make a partial withdrawal, and if removed from the ASP set, they can only ragequit
*/
function test_aspRemoval() public {
// Alice deposits 100 ETH
_commitment = _deposit(
@@ -297,7 +322,7 @@ contract IntegrationETH is IntegrationBase {
vm.prank(_POSTMAN);
_entrypoint.updateRoot(uint256(keccak256('some_root')) % SNARK_SCALAR_FIELD, bytes32('IPFS_HASH'));
// Fail to withdraw
// Fail to withdraw because label is not included in the latest ASP root
_withdrawThroughRelayer(
WithdrawalParams({
withdrawnAmount: 40 ether,
@@ -312,4 +337,109 @@ contract IntegrationETH is IntegrationBase {
// Ragequit
_ragequit(_ALICE, _commitment);
}
/**
* @notice Test that users can't spend a commitment more than once
*/
function test_failWhenCommitmentAlreadySpent() public {
// Alice deposits 100 ETH
_commitment = _deposit(
DepositParams({depositor: _ALICE, asset: _ETH, amount: 100 ether, nullifier: 'nullifier_1', secret: 'secret_1'})
);
// Push ASP root with label included
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
// Fully spend commitment
_selfWithdraw(
WithdrawalParams({
withdrawnAmount: _commitment.value,
newNullifier: 'nullifier_2',
newSecret: 'secret_2',
recipient: _BOB,
commitment: _commitment,
revertReason: NONE
})
);
// Fail to spend same commitment that was just spent
_selfWithdraw(
WithdrawalParams({
withdrawnAmount: _commitment.value,
newNullifier: 'nullifier_2',
newSecret: 'secret_2',
recipient: _BOB,
commitment: _commitment,
revertReason: IState.NullifierAlreadySpent.selector
})
);
}
/**
* @notice Test that commitments with reused nullifiers can not be spent
*/
function test_failWhenReusingNullifier() public {
// Alice deposits 100 ETH
_commitment = _deposit(
DepositParams({depositor: _ALICE, asset: _ETH, amount: 100 ether, nullifier: 'nullifier_1', secret: 'secret_1'})
);
// Push ASP root with label included
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
// Bob partially withdraws Alice's commitment
_commitment = _selfWithdraw(
WithdrawalParams({
withdrawnAmount: 20 ether,
newNullifier: 'nullifier_1', // Reusing nullifier of deposit for new commitment
newSecret: 'secret_2',
recipient: _BOB,
commitment: _commitment,
revertReason: NONE
})
);
// Fail to spend the child commitment because it was generated using an already spent nullifier
_selfWithdraw(
WithdrawalParams({
withdrawnAmount: 20 ether,
newNullifier: 'nullifier_3',
newSecret: 'secret_3',
recipient: _BOB,
commitment: _commitment,
revertReason: IState.NullifierAlreadySpent.selector
})
);
}
/**
* @notice Test that spent commitments can not be ragequitted (and spent again)
*/
function test_failWhenTryingToSpendRagequitCommitment() public {
// Alice deposits 100 ETH
_commitment = _deposit(
DepositParams({depositor: _ALICE, asset: _ETH, amount: 100 ether, nullifier: 'nullifier_1', secret: 'secret_1'})
);
// Ragequit full amount
_ragequit(_ALICE, _commitment);
// Push ASP root with label included
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
// Fail to withdraw commitment that was already ragequitted
_selfWithdraw(
WithdrawalParams({
withdrawnAmount: _commitment.value,
newNullifier: 'nullifier_2',
newSecret: 'secret_2',
recipient: _BOB,
commitment: _commitment,
revertReason: IState.NullifierAlreadySpent.selector
})
);
}
}

View File

@@ -0,0 +1,269 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {IntegrationBase} from './IntegrationBase.sol';
import {ProofLib} from 'contracts/lib/ProofLib.sol';
import {InternalLeanIMT, LeanIMTData} from 'lean-imt/InternalLeanIMT.sol';
import {IPrivacyPool} from 'interfaces/IPrivacyPool.sol';
contract IntegrationProofs is IntegrationBase {
using InternalLeanIMT for LeanIMTData;
Commitment internal _commitment;
IPrivacyPool.Withdrawal internal _withdrawal;
uint256 internal _context;
function setUp() public override {
super.setUp();
// Alice deposits 100 ETH
_commitment = _deposit(
DepositParams({depositor: _ALICE, asset: _ETH, amount: 100 ether, nullifier: 'nullifier_1', secret: 'secret_1'})
);
// Push ASP root with label included
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
_withdrawal =
IPrivacyPool.Withdrawal({processooor: _BOB, scope: _ethPool.SCOPE(), data: abi.encode(_BOB, address(0), 0)});
_context = uint256(keccak256(abi.encode(_withdrawal, _ethPool.SCOPE()))) % SNARK_SCALAR_FIELD;
}
function test_failToGenerateProof_whenCommitmentHashMismatches() public {
// Try to withdraw more value than commitment
vm.expectRevert(MerkleProofGenerationFailed.selector);
_generateWithdrawalProof(
WithdrawalProofParams({
existingCommitment: _commitment.hash - 1, // Mismatching existing commitment hash
withdrawnValue: _commitment.value,
context: _context,
label: _commitment.label,
existingValue: _commitment.value,
existingNullifier: _commitment.nullifier,
existingSecret: _commitment.secret,
newNullifier: _genSecretBySeed('nullifier_2'),
newSecret: _genSecretBySeed('secret_2')
})
);
}
function test_failToGenerateProof_whenInvalidWithdrawnValue() public {
// Try to withdraw more value than commitment
vm.expectRevert(WithdrawalProofGenerationFailed.selector);
_generateWithdrawalProof(
WithdrawalProofParams({
existingCommitment: _commitment.hash,
withdrawnValue: _commitment.value + 1, // Greater withdrawn value that existing value
context: _context,
label: _commitment.label,
existingValue: _commitment.value,
existingNullifier: _commitment.nullifier,
existingSecret: _commitment.secret,
newNullifier: _genSecretBySeed('nullifier_2'),
newSecret: _genSecretBySeed('secret_2')
})
);
}
function test_failToGenerateProof_whenLabelMismatches() public {
// Try to withdraw more value than commitment
vm.expectRevert(MerkleProofGenerationFailed.selector);
_generateWithdrawalProof(
WithdrawalProofParams({
existingCommitment: _commitment.hash,
withdrawnValue: _commitment.value,
context: _context,
label: _commitment.label - 1,
existingValue: _commitment.value,
existingNullifier: _commitment.nullifier,
existingSecret: _commitment.secret,
newNullifier: _genSecretBySeed('nullifier_2'),
newSecret: _genSecretBySeed('secret_2')
})
);
}
function test_failToGenerateProof_whenWithdrawnValueGreaterThanCommitment() public {
// Try to witdhraw with an invalid commitment value
vm.expectRevert(WithdrawalProofGenerationFailed.selector);
_generateWithdrawalProof(
WithdrawalProofParams({
existingCommitment: _commitment.hash,
withdrawnValue: _commitment.value,
context: _context,
label: _commitment.label,
existingValue: _commitment.value + 1, // Greater existing value than actual
existingNullifier: _commitment.nullifier,
existingSecret: _commitment.secret,
newNullifier: _genSecretBySeed('nullifier_2'),
newSecret: _genSecretBySeed('secret_2')
})
);
}
function test_failToGenerateProof_whenExistingNullifierMismatches() public {
// Try to witdhraw with an invalid commitment value
vm.expectRevert(WithdrawalProofGenerationFailed.selector);
_generateWithdrawalProof(
WithdrawalProofParams({
existingCommitment: _commitment.hash,
withdrawnValue: _commitment.value,
context: _context,
label: _commitment.label,
existingValue: _commitment.value,
existingNullifier: _commitment.nullifier - 1, // Different existing nullifier
existingSecret: _commitment.secret,
newNullifier: _genSecretBySeed('nullifier_2'),
newSecret: _genSecretBySeed('secret_2')
})
);
}
function test_failToGenerateProof_whenExistingSecretMismatches() public {
// Try to witdhraw with an invalid commitment value
vm.expectRevert(WithdrawalProofGenerationFailed.selector);
_generateWithdrawalProof(
WithdrawalProofParams({
existingCommitment: _commitment.hash,
withdrawnValue: _commitment.value,
context: _context,
label: _commitment.label,
existingValue: _commitment.value,
existingNullifier: _commitment.nullifier,
existingSecret: _commitment.secret - 1, // Different existing secret
newNullifier: _genSecretBySeed('nullifier_2'),
newSecret: _genSecretBySeed('secret_2')
})
);
}
function test_failToWithdraw_whenPublicSignalMismatch() public {
// Generate a valid proof
ProofLib.WithdrawProof memory _proof = _generateWithdrawalProof(
WithdrawalProofParams({
existingCommitment: _commitment.hash,
withdrawnValue: _commitment.value,
context: _context,
label: _commitment.label,
existingValue: _commitment.value,
existingNullifier: _commitment.nullifier,
existingSecret: _commitment.secret,
newNullifier: _genSecretBySeed('nullifier_2'),
newSecret: _genSecretBySeed('secret_2')
})
);
/*///////////////////////////////////////////////////////////////
NEW COMMITMENT HASH
//////////////////////////////////////////////////////////////*/
// Change the new commitment hash
_proof.pubSignals[0] = _proof.pubSignals[0] + 1;
vm.expectRevert(IPrivacyPool.InvalidProof.selector);
vm.prank(_BOB);
_ethPool.withdraw(_withdrawal, _proof);
// Reset
_proof.pubSignals[0] = _proof.pubSignals[0] - 1;
/*///////////////////////////////////////////////////////////////
EXISTING NULLIFIER HASH
//////////////////////////////////////////////////////////////*/
// Change the existing commitment hash
_proof.pubSignals[1] = _proof.pubSignals[1] + 1;
vm.expectRevert(IPrivacyPool.InvalidProof.selector);
vm.prank(_BOB);
_ethPool.withdraw(_withdrawal, _proof);
// Reset
_proof.pubSignals[1] = _proof.pubSignals[1] - 1;
/*///////////////////////////////////////////////////////////////
WITHDRAWN VALUE
//////////////////////////////////////////////////////////////*/
// Change the withdrawn value
_proof.pubSignals[2] = _proof.pubSignals[2] + 1;
vm.expectRevert(IPrivacyPool.InvalidProof.selector);
vm.prank(_BOB);
_ethPool.withdraw(_withdrawal, _proof);
// Reset
_proof.pubSignals[2] = _proof.pubSignals[2] - 1;
/*///////////////////////////////////////////////////////////////
STATE ROOT
//////////////////////////////////////////////////////////////*/
// Change the state root value
_proof.pubSignals[3] = _proof.pubSignals[3] + 1;
vm.expectRevert(IPrivacyPool.UnknownStateRoot.selector);
vm.prank(_BOB);
_ethPool.withdraw(_withdrawal, _proof);
// Reset
_proof.pubSignals[3] = _proof.pubSignals[3] - 1;
/*///////////////////////////////////////////////////////////////
STATE TREE DEPTH
//////////////////////////////////////////////////////////////*/
// Change the state tree depth value
_proof.pubSignals[4] = _proof.pubSignals[4] + 1;
vm.expectRevert(IPrivacyPool.InvalidProof.selector);
vm.prank(_BOB);
_ethPool.withdraw(_withdrawal, _proof);
// Reset
_proof.pubSignals[4] = _proof.pubSignals[4] - 1;
/*///////////////////////////////////////////////////////////////
ASP ROOT
//////////////////////////////////////////////////////////////*/
// Change the asp root value
_proof.pubSignals[5] = _proof.pubSignals[5] + 1;
vm.expectRevert(IPrivacyPool.IncorrectASPRoot.selector);
vm.prank(_BOB);
_ethPool.withdraw(_withdrawal, _proof);
// Reset
_proof.pubSignals[5] = _proof.pubSignals[5] - 1;
/*///////////////////////////////////////////////////////////////
ASP TREE DEPTH
//////////////////////////////////////////////////////////////*/
// Change the asp tree depth value
_proof.pubSignals[6] = _proof.pubSignals[6] + 1;
vm.expectRevert(IPrivacyPool.InvalidProof.selector);
vm.prank(_BOB);
_ethPool.withdraw(_withdrawal, _proof);
// Reset
_proof.pubSignals[6] = _proof.pubSignals[6] - 1;
/*///////////////////////////////////////////////////////////////
CONTEXT
//////////////////////////////////////////////////////////////*/
// Change the context value
_proof.pubSignals[7] = _proof.pubSignals[7] + 1;
vm.expectRevert(IPrivacyPool.ContextMismatch.selector);
vm.prank(_BOB);
_ethPool.withdraw(_withdrawal, _proof);
}
}