From a1c1676a475b9777baecce1a5bc707212401425b Mon Sep 17 00:00:00 2001 From: Wojciech Zmuda Date: Mon, 26 Aug 2024 23:15:27 +0200 Subject: [PATCH] Add WorldID Identity Manager for EIP-4844 Implement WorldID Identity Manager in its 3rd implementation. The new implementation contains one main method - registerIdentities. It's the EIP-4844 equivalent of the existing registerIdentities from the V1 implementation. The new registerIdentities calls verifyProof that is also different from its classic counterpart. The new verifyProof comes from the EIP-4844-ready InsertionTreeVerifier164844 contract, that was generated from the semaphore-mtb's InsertionCircuit modified to accommodae EIP-4844 features. Since the new verifyProof function has a different prototype than the previous one, the ITreeVerifier interface has been updated with the new prototype. This change results in lots of boilerplate changes as all contracts implementing this interface must contain a dummy implementation of the new function, even if it does not make practical sense. Add tests for the new feature. Tests are modelled by the existing registration tests. The new contract is basically a copy of the old one, with the main difference of calling the new registerIdentities implementation. Signed-off-by: Wojciech Zmuda --- src/WorldIDIdentityManagerImplV3.sol | 233 ++++++ src/interfaces/ITreeVerifier.sol | 18 + src/test/DeletionTreeVerifier16.sol | 10 + src/test/InsertionTreeVerifier16.sol | 10 + src/test/InsertionTreeVerifier164844.sol | 723 ++++++++++++++++++ .../WorldIDIdentityManagerCalculation.t.sol | 4 +- .../WorldIDIdentityManagerConstruction.t.sol | 8 +- .../WorldIDIdentityManagerDataQuery.t.sol | 6 +- ...WorldIDIdentityManagerGettersSetters.t.sol | 16 +- ...rldIDIdentityManagerIdentityDeletion.t.sol | 4 +- ...DIdentityManagerIdentityRegistration.t.sol | 14 +- ...ntityManagerIdentityRegistration4844.t.sol | 650 ++++++++++++++++ ...WorldIDIdentityManagerInitialization.t.sol | 26 +- ...IDIdentityManagerOwnershipManagement.t.sol | 4 +- .../WorldIDIdentityManagerTest.sol | 128 +++- .../WorldIDIdentityManagerUpgrade.t.sol | 2 +- src/test/mock/SequencerVerifier.sol | 9 + src/test/mock/SimpleVerifier.sol | 14 + .../mock/WorldIDIdentityManagerImplMock.sol | 3 +- src/utils/UnimplementedTreeVerifier.sol | 14 + 20 files changed, 1841 insertions(+), 55 deletions(-) create mode 100644 src/WorldIDIdentityManagerImplV3.sol create mode 100644 src/test/InsertionTreeVerifier164844.sol create mode 100644 src/test/identity-manager/WorldIDIdentityManagerIdentityRegistration4844.t.sol diff --git a/src/WorldIDIdentityManagerImplV3.sol b/src/WorldIDIdentityManagerImplV3.sol new file mode 100644 index 0000000..907365a --- /dev/null +++ b/src/WorldIDIdentityManagerImplV3.sol @@ -0,0 +1,233 @@ +pragma solidity ^0.8.24; + +import "./WorldIDIdentityManagerImplV2.sol"; + +/// @title WorldID Identity Manager Implementation Version 3 +/// @author Worldcoin +/// @notice An implementation of a batch-based identity manager for the WorldID protocol. +/// @dev The manager is based on the principle of verifying externally-created Zero Knowledge Proofs +/// to perform the insertion using EIP-4844. +/// @dev This is the implementation delegated to by a proxy. +contract WorldIDIdentityManagerImplV3 is WorldIDIdentityManagerImplV2 { + /////////////////////////////////////////////////////////////////////////////// + /// A NOTE ON IMPLEMENTATION CONTRACTS /// + /////////////////////////////////////////////////////////////////////////////// + + // This contract is designed explicitly to operate from behind a proxy contract. As a result, + // there are a few important implementation considerations: + // + // - All updates made after deploying a given version of the implementation should inherit from + // the latest version of the implementation. This contract inherits from its previous implementation + // WorldIDIdentityManagerImplV2. This prevents storage clashes. + // - All functions that are less access-restricted than `private` should be marked `virtual` in + // order to enable the fixing of bugs in the existing interface. + // - Any function that reads from or modifies state (i.e. is not marked `pure`) must be + // annotated with the `onlyProxy` and `onlyInitialized` modifiers. This ensures that it can + // only be called when it has access to the data in the proxy, otherwise results are likely to + // be nonsensical. + // - This contract deals with important data for the WorldID system. Ensure that all newly-added + // functionality is carefully access controlled using `onlyOwner`, or a more granular access + // mechanism. + // - Do not assign any contract-level variables at the definition site unless they are + // `constant`. + // + // Additionally, the following notes apply: + // + // - Initialisation and ownership management are not protected behind `onlyProxy` intentionally. + // This ensures that the contract can safely be disposed of after it is no longer used. + // - Carefully consider what data recovery options are presented as new functionality is added. + // Care must be taken to ensure that a migration plan can exist for cases where upgrades + // cannot recover from an issue or vulnerability. + + /////////////////////////////////////////////////////////////////////////////// + /// !!!!! DATA: DO NOT REORDER !!!!! /// + /////////////////////////////////////////////////////////////////////////////// + + // To ensure compatibility between upgrades, it is exceedingly important that no reordering of + // these variables takes place. If reordering happens, a storage clash will occur (effectively a + // memory safety error). + + /// @notice The table of verifiers for verifying batch identity insertions following the EIP-4844 scheme. + VerifierLookupTable internal batchInsertion4844Verifiers; + + /// @notice Thrown when the WorldIDIdentityManagerImplV3 contract is initialized + event WorldIDIdentityManagerImplV3Initialized(); + + /// @notice Initializes the V3 implementation contract. + /// @param _batchInsertion4844Verifiers The table of verifiers for verifying batch identity insertions. + /// @dev Must be called exactly once + /// @dev This is marked `reinitializer()` to allow for updated initialisation steps when working + /// with upgrades based upon this contract. Be aware that there are only 256 (zero-indexed) + /// initialisations allowed, so decide carefully when to use them. Many cases can safely be + /// replaced by use of setters. + /// @dev This function is explicitly not virtual as it does not make sense to override even when + /// upgrading. Create a separate initializer function instead. + /// + /// + /// @custom:reverts InvalidVerifierLUT if `_batchInsertion4844Verifiers` is set to the zero address + function initializeV3(VerifierLookupTable _batchInsertion4844Verifiers) public reinitializer(3) { + if (address(_batchInsertion4844Verifiers) == address(0)) { + revert InvalidVerifierLUT(); + } + + batchInsertion4844Verifiers = _batchInsertion4844Verifiers; + + emit WorldIDIdentityManagerImplV3Initialized(); + } + + /////////////////////////////////////////////////////////////////// + /// IDENTITY MANAGEMENT /// + /////////////////////////////////////////////////////////////////// + + /// @notice Parameters for registerIdentities function. + /// @dev This struct holds the parameters for registering identities and verifying the insertion proof + /// using KZG proofs as described in EIP-4844. + struct RegisterIdentities4844Params { + + /// @notice The proof that given the conditions, insertion into the tree results in `postRoot`. + /// Elements 0 and 1 are the `x` and `y` coordinates for `ar` respectively. Elements 2 and 3 are the + /// `x` coordinate for `bs`, and elements 4 and 5 are the `y` coordinate for `bs`. Elements 6 and 7 + /// are the `x` and `y` coordinates for `krs`. + uint256[8] insertionProof; + + /// @notice The Pedersen commitments from the proof. + uint256[2] commitments; + + /// @notice The proof of knowledge for the Pedersen commitments. + uint256[2] commitmentPok; + + /// @notice KZG commitment for the polynomial extrapolated from the identities. + uint128[3] kzgCommitment; + + /// @notice KZG proof associated with the commitment. + uint128[3] kzgProof; + + /// @notice Expected evaluation of the polynomial at a certain point equal to kzgChallenge. + uint256 expectedEvaluation; + + /// @notice The value for the root of the tree before the `identityCommitments` have been inserted. + /// Must be an element of the field `Kr`. (already in reduced form) + uint256 preRoot; + + /// @notice The root obtained after inserting all of `identityCommitments` into the tree described + /// by `preRoot`. Must be an element of the field `Kr`. (already in reduced form) + uint256 postRoot; + + /// @notice Challenge value used in the KZG proof to evaluate the polynomial. + uint256 kzgChallenge; + + /// @notice Hash of the input data calculated as described in the comment + /// to `calculateIdentityRegistrationInputHash()`. + bytes32 inputHash; + + /// @notice Number of identities being registered in this batch. + uint32 batchSize; + + /// @notice The position in the tree at which the insertions were made. + uint32 startIndex; + } + + /// @notice Registers identities into the WorldID system following the EIP-4844 scheme. + /// @dev Can only be called by the identity operator. + /// @dev Registration is performed off-chain and verified on-chain via insertion proof + /// and KZG proof. This saves gas and time over inserting identities one at a time. + /// + /// @param params parameters for the process defined by the `RegisterIdentities4844Params` structure. + /// @custom:reverts Unauthorized If the message sender is not authorised to add identities. + /// @custom:reverts NotLatestRoot If the provided `params.preRoot` is not the latest root. + /// @custom:reverts ProofValidationFailure If `params.insertionProof` cannot be verified using the + /// provided inputs. + /// @custom:reverts VerifierLookupTable.NoSuchVerifier If the batch sizes doesn't match a known + /// verifier. + function registerIdentities( + RegisterIdentities4844Params calldata params + ) public virtual onlyProxy onlyInitialized onlyIdentityOperator { + if (params.preRoot != _latestRoot) { + revert NotLatestRoot(params.preRoot, _latestRoot); + } + + // No matter what, the inputs can result in a hash that is not an element of the scalar + // field in which we're operating. We reduce it into the field before handing it to the + // verifier. All other elements that are passed as calldata are reduced in the circuit. + uint256 reducedElement = uint256(params.inputHash) % SNARK_SCALAR_FIELD; + uint256 evaluationReduced = params.expectedEvaluation % SNARK_SCALAR_FIELD; + + // We need to look up the correct verifier before we can verify. + ITreeVerifier insertionVerifier = + batchInsertion4844Verifiers.getVerifierFor(params.batchSize); + + // TODO are we using always this one slot or should it be configurable or calculated? + bytes32 kzgCommitmentHash = blobhash(0); + + // With that, we can properly try and verify. + try insertionVerifier.verifyProof( + params.insertionProof, + params.commitments, + params.commitmentPok, + [reducedElement, + evaluationReduced, + uint256(kzgCommitmentHash), + uint256(params.startIndex), + params.preRoot, + params.postRoot] + ) { + // If it did verify, we need to update the contract's state. We set the currently valid + // root to the root after the insertions. + _latestRoot = params.postRoot; + + // We also need to add the previous root to the history, and set the timestamp at + // which it was expired. + rootHistory[params.preRoot] = uint128(block.timestamp); + + bool success = evaluatePoint( + kzgCommitmentHash, + bytes32(params.kzgChallenge), + bytes32(params.expectedEvaluation), + params.kzgCommitment, + params.kzgProof + ); + require(success, "Call to point evaluation precompiled contract failed"); + + emit TreeChanged(params.preRoot, TreeChange.Insertion, params.postRoot); + } catch Error(string memory errString) { + /// This is not the revert we're looking for. + revert(errString); + } catch { + // If we reach here we know it's the internal error, as the tree verifier only uses + // `require`s otherwise, which will be re-thrown above. + revert ProofValidationFailure(); + } + } + + /////////////////////////////////////////////////////////////////////////////// + /// UTILITY FUNCTIONS /// + /////////////////////////////////////////////////////////////////////////////// + + address constant PRECOMPILE_POINT_EVALUATION = address(0x0a); + + /// @notice Call the point evaluation precompiled contract. + /// Verify p(z) = y given commitment that corresponds to the polynomial p(x) and a KZG proof. + /// Also verify that the provided commitment matches the provided versioned_hash. + /// @param versioned_hash Reference to a blob in the execution layer (obtained from the data storage or execution environment). + /// @param x x-coordinate at which the blob is being evaluated. + /// @param y y-coordinate at which the blob is being evaluated. + /// @param commitment Commitment to the blob being evaluated (obtained from the KZG commitment scheme). + /// @param kzgProof Proof associated with the commitment (obtained from the KZG proof generation). + /// @return True on success, false otherwise. + function evaluatePoint( + bytes32 versioned_hash, + bytes32 x, + bytes32 y, + uint128[3] calldata commitment, + uint128[3] calldata kzgProof + ) public view returns (bool) { + bytes memory input = abi.encodePacked( + versioned_hash, + x, y, + commitment[0], commitment[1], commitment[2], + kzgProof[0], kzgProof[1], kzgProof[2] + ); + (bool success, bytes memory output) = PRECOMPILE_POINT_EVALUATION.staticcall(input); + return success; + } +} diff --git a/src/interfaces/ITreeVerifier.sol b/src/interfaces/ITreeVerifier.sol index 543fc87..10aa409 100644 --- a/src/interfaces/ITreeVerifier.sol +++ b/src/interfaces/ITreeVerifier.sol @@ -15,4 +15,22 @@ interface ITreeVerifier { /// @param input the public input field elements in the scalar field Fr. /// Elements must be reduced. function verifyProof(uint256[8] calldata proof, uint256[1] calldata input) external; + + /// Verify an uncompressed Groth16 proof. + /// @notice Reverts with InvalidProof if the proof is invalid or + /// with PublicInputNotInField the public input is not reduced. + /// @notice There is no return value. If the function does not revert, the + /// proof was successfully verified. + /// @param proof the points (A, B, C) in EIP-197 format matching the output + /// of compressProof. + /// @param commitments the Pedersen commitments from the proof. + /// @param commitmentPok the proof of knowledge for the Pedersen commitments. + /// @param input the public input field elements in the scalar field Fr. + /// Elements must be reduced. + function verifyProof( + uint256[8] calldata proof, + uint256[2] calldata commitments, + uint256[2] calldata commitmentPok, + uint256[6] calldata input + ) external; } diff --git a/src/test/DeletionTreeVerifier16.sol b/src/test/DeletionTreeVerifier16.sol index 0cbb5f5..b6634e4 100644 --- a/src/test/DeletionTreeVerifier16.sol +++ b/src/test/DeletionTreeVerifier16.sol @@ -564,4 +564,14 @@ contract Verifier is ITreeVerifier { revert ProofInvalid(); } } + + /// Dummy implementation to satisfy ITreeVerifier interface. + function verifyProof( + uint256[8] calldata, + uint256[2] calldata, + uint256[2] calldata, + uint256[6] calldata + ) public pure { + revert ProofInvalid(); + } } diff --git a/src/test/InsertionTreeVerifier16.sol b/src/test/InsertionTreeVerifier16.sol index 3fa3d32..6f89c44 100644 --- a/src/test/InsertionTreeVerifier16.sol +++ b/src/test/InsertionTreeVerifier16.sol @@ -564,4 +564,14 @@ contract Verifier is ITreeVerifier { revert ProofInvalid(); } } + + /// Dummy implementation to satisfy ITreeVerifier interface. + function verifyProof( + uint256[8] calldata, + uint256[2] calldata, + uint256[2] calldata, + uint256[6] calldata + ) public pure { + revert ProofInvalid(); + } } diff --git a/src/test/InsertionTreeVerifier164844.sol b/src/test/InsertionTreeVerifier164844.sol new file mode 100644 index 0000000..2534902 --- /dev/null +++ b/src/test/InsertionTreeVerifier164844.sol @@ -0,0 +1,723 @@ +import {ITreeVerifier} from "src/interfaces/ITreeVerifier.sol"; + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @title Groth16 verifier template. +/// @author Remco Bloemen +/// @notice Supports verifying Groth16 proofs. Proofs can be in uncompressed +/// (256 bytes) and compressed (128 bytes) format. A view function is provided +/// to compress proofs. +/// @notice See for further explanation. +contract Verifier is ITreeVerifier { + + /// Some of the provided public input values are larger than the field modulus. + /// @dev Public input elements are not automatically reduced, as this is can be + /// a dangerous source of bugs. + error PublicInputNotInField(); + + /// The proof is invalid. + /// @dev This can mean that provided Groth16 proof points are not on their + /// curves, that pairing equation fails, or that the proof is not for the + /// provided public input. + error ProofInvalid(); + /// The commitment is invalid + /// @dev This can mean that provided commitment points and/or proof of knowledge are not on their + /// curves, that pairing equation fails, or that the commitment and/or proof of knowledge is not for the + /// commitment key. + error CommitmentInvalid(); + + // Addresses of precompiles + uint256 constant PRECOMPILE_MODEXP = 0x05; + uint256 constant PRECOMPILE_ADD = 0x06; + uint256 constant PRECOMPILE_MUL = 0x07; + uint256 constant PRECOMPILE_VERIFY = 0x08; + + // Base field Fp order P and scalar field Fr order R. + // For BN254 these are computed as follows: + // t = 4965661367192848881 + // P = 36⋅t⁴ + 36⋅t³ + 24⋅t² + 6⋅t + 1 + // R = 36⋅t⁴ + 36⋅t³ + 18⋅t² + 6⋅t + 1 + uint256 constant P = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47; + uint256 constant R = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001; + + // Extension field Fp2 = Fp[i] / (i² + 1) + // Note: This is the complex extension field of Fp with i² = -1. + // Values in Fp2 are represented as a pair of Fp elements (a₀, a₁) as a₀ + a₁⋅i. + // Note: The order of Fp2 elements is *opposite* that of the pairing contract, which + // expects Fp2 elements in order (a₁, a₀). This is also the order in which + // Fp2 elements are encoded in the public interface as this became convention. + + // Constants in Fp + uint256 constant FRACTION_1_2_FP = 0x183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea4; + uint256 constant FRACTION_27_82_FP = 0x2b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5; + uint256 constant FRACTION_3_82_FP = 0x2fcd3ac2a640a154eb23960892a85a68f031ca0c8344b23a577dcf1052b9e775; + + // Exponents for inversions and square roots mod P + uint256 constant EXP_INVERSE_FP = 0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD45; // P - 2 + uint256 constant EXP_SQRT_FP = 0xC19139CB84C680A6E14116DA060561765E05AA45A1C72A34F082305B61F3F52; // (P + 1) / 4; + + // Groth16 alpha point in G1 + uint256 constant ALPHA_X = 2708969884581619714583557951222918593768772820936646583136789853819173846869; + uint256 constant ALPHA_Y = 8043584586634431593445517970972074344995663485134139288455040086659571635007; + + // Groth16 beta point in G2 in powers of i + uint256 constant BETA_NEG_X_0 = 2959634726718323159960582986494250462813569884099052977631775603535594317697; + uint256 constant BETA_NEG_X_1 = 2567045386415897874314697628037970624972832810910119870003888717933699604901; + uint256 constant BETA_NEG_Y_0 = 16901523272247727115591169122896696499546950846485806085170956609925505472585; + uint256 constant BETA_NEG_Y_1 = 9215676527870136774869096108544312861626222401455601740260979586958004850529; + + // Groth16 gamma point in G2 in powers of i + uint256 constant GAMMA_NEG_X_0 = 8904268816337492794416634636731345512132126205040334900538636341318421538665; + uint256 constant GAMMA_NEG_X_1 = 19004930723547195287383134699314663710813331368262617951429852355608517342331; + uint256 constant GAMMA_NEG_Y_0 = 7842710818119939895862574378653362734227090432895633776678943219170947197152; + uint256 constant GAMMA_NEG_Y_1 = 2119828549076590392190534591515249836229477358991111181013402454206155157118; + + // Groth16 delta point in G2 in powers of i + uint256 constant DELTA_NEG_X_0 = 16751268373860897104759671698719630006610347517780893954841247694987729581925; + uint256 constant DELTA_NEG_X_1 = 17074355565731257728214063587600772000822732044793320082675686505906845346452; + uint256 constant DELTA_NEG_Y_0 = 19276066093240754710159494686794885924197536752080653828868593863903058741434; + uint256 constant DELTA_NEG_Y_1 = 10391802489342383226451710090356140861208165419529434978751035344608888508609; + // Pedersen G point in G2 in powers of i + uint256 constant PEDERSEN_G_X_0 = 191329193964481065495212234619899074886870006108961472779204277869671687115; + uint256 constant PEDERSEN_G_X_1 = 18448119812880134607636953435856909803133499313412999813309765036275639759714; + uint256 constant PEDERSEN_G_Y_0 = 7320805055268657491420712525633197949310593554647878873210287658169673900549; + uint256 constant PEDERSEN_G_Y_1 = 3560501050363463728186450810474818536571923785681141032866912257045688789262; + + // Pedersen GRootSigmaNeg point in G2 in powers of i + uint256 constant PEDERSEN_GROOTSIGMANEG_X_0 = 211898546590335769030549987216927375237199629913882184437950823448881903970; + uint256 constant PEDERSEN_GROOTSIGMANEG_X_1 = 11880079357380769452360200537618953332011342321508956754029570189743729673109; + uint256 constant PEDERSEN_GROOTSIGMANEG_Y_0 = 20343036341560553279453586916303753798495271995175822900148534636526224647164; + uint256 constant PEDERSEN_GROOTSIGMANEG_Y_1 = 18398984926832230363518321101521122731640509584485783610091427005978221352504; + + // Constant and public input points + uint256 constant CONSTANT_X = 19358550662348637607112384570170513296928047638941946872811625925166768621689; + uint256 constant CONSTANT_Y = 6626464468086658346890654816494620493177727644183109536624044536458730824315; + uint256 constant PUB_0_X = 21039846169407040170900369597064405833466068013105470546221436302050929487572; + uint256 constant PUB_0_Y = 2611968432498880732075319619253202283148240652063087246949510105674797190280; + uint256 constant PUB_1_X = 17167433712229735771674828746728526346122363071518232159739058701502221378434; + uint256 constant PUB_1_Y = 1739891145391694203708089066027069569757327500962996441495332028277069755267; + uint256 constant PUB_2_X = 17798576048770024481159849810854079136640512359488534233899537955087709779447; + uint256 constant PUB_2_Y = 3109649566724390234731095093811315543854286970944206387661935696332701907948; + uint256 constant PUB_3_X = 7890320639901919646211933804150885252994659227692929006872667869290059075281; + uint256 constant PUB_3_Y = 11396290418620464030035551260992220541169365820463159918373688231853778829859; + uint256 constant PUB_4_X = 16030714242136825467561218858637637879071023746882506204693189470117499845846; + uint256 constant PUB_4_Y = 14919105472258793272718206026880812470549694675722205962400380939241926000512; + uint256 constant PUB_5_X = 15864446105389560446153403630146334838539883248526951175596504258425435272439; + uint256 constant PUB_5_Y = 3764670878071624467628034771504294179451467482124027272932022992371176433497; + uint256 constant PUB_6_X = 13240007634426972631439790531670899863212947213748180981433638304827247605238; + uint256 constant PUB_6_Y = 9107574043090153668746153226362613394226970755908178311480271382776655025033; + + /// Negation in Fp. + /// @notice Returns a number x such that a + x = 0 in Fp. + /// @notice The input does not need to be reduced. + /// @param a the base + /// @return x the result + function negate(uint256 a) internal pure returns (uint256 x) { + unchecked { + x = (P - (a % P)) % P; // Modulo is cheaper than branching + } + } + + /// Exponentiation in Fp. + /// @notice Returns a number x such that a ^ e = x in Fp. + /// @notice The input does not need to be reduced. + /// @param a the base + /// @param e the exponent + /// @return x the result + function exp(uint256 a, uint256 e) internal view returns (uint256 x) { + bool success; + assembly ("memory-safe") { + let f := mload(0x40) + mstore(f, 0x20) + mstore(add(f, 0x20), 0x20) + mstore(add(f, 0x40), 0x20) + mstore(add(f, 0x60), a) + mstore(add(f, 0x80), e) + mstore(add(f, 0xa0), P) + success := staticcall(gas(), PRECOMPILE_MODEXP, f, 0xc0, f, 0x20) + x := mload(f) + } + if (!success) { + // Exponentiation failed. + // Should not happen. + revert ProofInvalid(); + } + } + + /// Invertsion in Fp. + /// @notice Returns a number x such that a * x = 1 in Fp. + /// @notice The input does not need to be reduced. + /// @notice Reverts with ProofInvalid() if the inverse does not exist + /// @param a the input + /// @return x the solution + function invert_Fp(uint256 a) internal view returns (uint256 x) { + x = exp(a, EXP_INVERSE_FP); + if (mulmod(a, x, P) != 1) { + // Inverse does not exist. + // Can only happen during G2 point decompression. + revert ProofInvalid(); + } + } + + /// Square root in Fp. + /// @notice Returns a number x such that x * x = a in Fp. + /// @notice Will revert with InvalidProof() if the input is not a square + /// or not reduced. + /// @param a the square + /// @return x the solution + function sqrt_Fp(uint256 a) internal view returns (uint256 x) { + x = exp(a, EXP_SQRT_FP); + if (mulmod(x, x, P) != a) { + // Square root does not exist or a is not reduced. + // Happens when G1 point is not on curve. + revert ProofInvalid(); + } + } + + /// Square test in Fp. + /// @notice Returns wheter a number x exists such that x * x = a in Fp. + /// @notice Will revert with InvalidProof() if the input is not a square + /// or not reduced. + /// @param a the square + /// @return x the solution + function isSquare_Fp(uint256 a) internal view returns (bool) { + uint256 x = exp(a, EXP_SQRT_FP); + return mulmod(x, x, P) == a; + } + + /// Square root in Fp2. + /// @notice Fp2 is the complex extension Fp[i]/(i^2 + 1). The input is + /// a0 + a1 ⋅ i and the result is x0 + x1 ⋅ i. + /// @notice Will revert with InvalidProof() if + /// * the input is not a square, + /// * the hint is incorrect, or + /// * the input coefficents are not reduced. + /// @param a0 The real part of the input. + /// @param a1 The imaginary part of the input. + /// @param hint A hint which of two possible signs to pick in the equation. + /// @return x0 The real part of the square root. + /// @return x1 The imaginary part of the square root. + function sqrt_Fp2(uint256 a0, uint256 a1, bool hint) internal view returns (uint256 x0, uint256 x1) { + // If this square root reverts there is no solution in Fp2. + uint256 d = sqrt_Fp(addmod(mulmod(a0, a0, P), mulmod(a1, a1, P), P)); + if (hint) { + d = negate(d); + } + // If this square root reverts there is no solution in Fp2. + x0 = sqrt_Fp(mulmod(addmod(a0, d, P), FRACTION_1_2_FP, P)); + x1 = mulmod(a1, invert_Fp(mulmod(x0, 2, P)), P); + + // Check result to make sure we found a root. + // Note: this also fails if a0 or a1 is not reduced. + if (a0 != addmod(mulmod(x0, x0, P), negate(mulmod(x1, x1, P)), P) + || a1 != mulmod(2, mulmod(x0, x1, P), P)) { + revert ProofInvalid(); + } + } + + /// Compress a G1 point. + /// @notice Reverts with InvalidProof if the coordinates are not reduced + /// or if the point is not on the curve. + /// @notice The point at infinity is encoded as (0,0) and compressed to 0. + /// @param x The X coordinate in Fp. + /// @param y The Y coordinate in Fp. + /// @return c The compresed point (x with one signal bit). + function compress_g1(uint256 x, uint256 y) internal view returns (uint256 c) { + if (x >= P || y >= P) { + // G1 point not in field. + revert ProofInvalid(); + } + if (x == 0 && y == 0) { + // Point at infinity + return 0; + } + + // Note: sqrt_Fp reverts if there is no solution, i.e. the x coordinate is invalid. + uint256 y_pos = sqrt_Fp(addmod(mulmod(mulmod(x, x, P), x, P), 3, P)); + if (y == y_pos) { + return (x << 1) | 0; + } else if (y == negate(y_pos)) { + return (x << 1) | 1; + } else { + // G1 point not on curve. + revert ProofInvalid(); + } + } + + /// Decompress a G1 point. + /// @notice Reverts with InvalidProof if the input does not represent a valid point. + /// @notice The point at infinity is encoded as (0,0) and compressed to 0. + /// @param c The compresed point (x with one signal bit). + /// @return x The X coordinate in Fp. + /// @return y The Y coordinate in Fp. + function decompress_g1(uint256 c) internal view returns (uint256 x, uint256 y) { + // Note that X = 0 is not on the curve since 0³ + 3 = 3 is not a square. + // so we can use it to represent the point at infinity. + if (c == 0) { + // Point at infinity as encoded in EIP196 and EIP197. + return (0, 0); + } + bool negate_point = c & 1 == 1; + x = c >> 1; + if (x >= P) { + // G1 x coordinate not in field. + revert ProofInvalid(); + } + + // Note: (x³ + 3) is irreducible in Fp, so it can not be zero and therefore + // y can not be zero. + // Note: sqrt_Fp reverts if there is no solution, i.e. the point is not on the curve. + y = sqrt_Fp(addmod(mulmod(mulmod(x, x, P), x, P), 3, P)); + if (negate_point) { + y = negate(y); + } + } + + /// Compress a G2 point. + /// @notice Reverts with InvalidProof if the coefficients are not reduced + /// or if the point is not on the curve. + /// @notice The G2 curve is defined over the complex extension Fp[i]/(i^2 + 1) + /// with coordinates (x0 + x1 ⋅ i, y0 + y1 ⋅ i). + /// @notice The point at infinity is encoded as (0,0,0,0) and compressed to (0,0). + /// @param x0 The real part of the X coordinate. + /// @param x1 The imaginary poart of the X coordinate. + /// @param y0 The real part of the Y coordinate. + /// @param y1 The imaginary part of the Y coordinate. + /// @return c0 The first half of the compresed point (x0 with two signal bits). + /// @return c1 The second half of the compressed point (x1 unmodified). + function compress_g2(uint256 x0, uint256 x1, uint256 y0, uint256 y1) + internal view returns (uint256 c0, uint256 c1) { + if (x0 >= P || x1 >= P || y0 >= P || y1 >= P) { + // G2 point not in field. + revert ProofInvalid(); + } + if ((x0 | x1 | y0 | y1) == 0) { + // Point at infinity + return (0, 0); + } + + // Compute y^2 + // Note: shadowing variables and scoping to avoid stack-to-deep. + uint256 y0_pos; + uint256 y1_pos; + { + uint256 n3ab = mulmod(mulmod(x0, x1, P), P-3, P); + uint256 a_3 = mulmod(mulmod(x0, x0, P), x0, P); + uint256 b_3 = mulmod(mulmod(x1, x1, P), x1, P); + y0_pos = addmod(FRACTION_27_82_FP, addmod(a_3, mulmod(n3ab, x1, P), P), P); + y1_pos = negate(addmod(FRACTION_3_82_FP, addmod(b_3, mulmod(n3ab, x0, P), P), P)); + } + + // Determine hint bit + // If this sqrt fails the x coordinate is not on the curve. + bool hint; + { + uint256 d = sqrt_Fp(addmod(mulmod(y0_pos, y0_pos, P), mulmod(y1_pos, y1_pos, P), P)); + hint = !isSquare_Fp(mulmod(addmod(y0_pos, d, P), FRACTION_1_2_FP, P)); + } + + // Recover y + (y0_pos, y1_pos) = sqrt_Fp2(y0_pos, y1_pos, hint); + if (y0 == y0_pos && y1 == y1_pos) { + c0 = (x0 << 2) | (hint ? 2 : 0) | 0; + c1 = x1; + } else if (y0 == negate(y0_pos) && y1 == negate(y1_pos)) { + c0 = (x0 << 2) | (hint ? 2 : 0) | 1; + c1 = x1; + } else { + // G1 point not on curve. + revert ProofInvalid(); + } + } + + /// Decompress a G2 point. + /// @notice Reverts with InvalidProof if the input does not represent a valid point. + /// @notice The G2 curve is defined over the complex extension Fp[i]/(i^2 + 1) + /// with coordinates (x0 + x1 ⋅ i, y0 + y1 ⋅ i). + /// @notice The point at infinity is encoded as (0,0,0,0) and compressed to (0,0). + /// @param c0 The first half of the compresed point (x0 with two signal bits). + /// @param c1 The second half of the compressed point (x1 unmodified). + /// @return x0 The real part of the X coordinate. + /// @return x1 The imaginary poart of the X coordinate. + /// @return y0 The real part of the Y coordinate. + /// @return y1 The imaginary part of the Y coordinate. + function decompress_g2(uint256 c0, uint256 c1) + internal view returns (uint256 x0, uint256 x1, uint256 y0, uint256 y1) { + // Note that X = (0, 0) is not on the curve since 0³ + 3/(9 + i) is not a square. + // so we can use it to represent the point at infinity. + if (c0 == 0 && c1 == 0) { + // Point at infinity as encoded in EIP197. + return (0, 0, 0, 0); + } + bool negate_point = c0 & 1 == 1; + bool hint = c0 & 2 == 2; + x0 = c0 >> 2; + x1 = c1; + if (x0 >= P || x1 >= P) { + // G2 x0 or x1 coefficient not in field. + revert ProofInvalid(); + } + + uint256 n3ab = mulmod(mulmod(x0, x1, P), P-3, P); + uint256 a_3 = mulmod(mulmod(x0, x0, P), x0, P); + uint256 b_3 = mulmod(mulmod(x1, x1, P), x1, P); + + y0 = addmod(FRACTION_27_82_FP, addmod(a_3, mulmod(n3ab, x1, P), P), P); + y1 = negate(addmod(FRACTION_3_82_FP, addmod(b_3, mulmod(n3ab, x0, P), P), P)); + + // Note: sqrt_Fp2 reverts if there is no solution, i.e. the point is not on the curve. + // Note: (X³ + 3/(9 + i)) is irreducible in Fp2, so y can not be zero. + // But y0 or y1 may still independently be zero. + (y0, y1) = sqrt_Fp2(y0, y1, hint); + if (negate_point) { + y0 = negate(y0); + y1 = negate(y1); + } + } + + /// Compute the public input linear combination. + /// @notice Reverts with PublicInputNotInField if the input is not in the field. + /// @notice Computes the multi-scalar-multiplication of the public input + /// elements and the verification key including the constant term. + /// @param input The public inputs. These are elements of the scalar field Fr. + /// @param publicCommitments public inputs generated from pedersen commitments. + /// @param commitments The Pedersen commitments from the proof. + /// @return x The X coordinate of the resulting G1 point. + /// @return y The Y coordinate of the resulting G1 point. + function publicInputMSM( + uint256[6] calldata input, + uint256[1] memory publicCommitments, + uint256[2] memory commitments + ) + internal view returns (uint256 x, uint256 y) { + // Note: The ECMUL precompile does not reject unreduced values, so we check this. + // Note: Unrolling this loop does not cost much extra in code-size, the bulk of the + // code-size is in the PUB_ constants. + // ECMUL has input (x, y, scalar) and output (x', y'). + // ECADD has input (x1, y1, x2, y2) and output (x', y'). + // We reduce commitments(if any) with constants as the first point argument to ECADD. + // We call them such that ecmul output is already in the second point + // argument to ECADD so we can have a tight loop. + bool success = true; + assembly ("memory-safe") { + let f := mload(0x40) + let g := add(f, 0x40) + let s + mstore(f, CONSTANT_X) + mstore(add(f, 0x20), CONSTANT_Y) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, commitments, 64, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_0_X) + mstore(add(g, 0x20), PUB_0_Y) + s := calldataload(input) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_1_X) + mstore(add(g, 0x20), PUB_1_Y) + s := calldataload(add(input, 32)) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_2_X) + mstore(add(g, 0x20), PUB_2_Y) + s := calldataload(add(input, 64)) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_3_X) + mstore(add(g, 0x20), PUB_3_Y) + s := calldataload(add(input, 96)) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_4_X) + mstore(add(g, 0x20), PUB_4_Y) + s := calldataload(add(input, 128)) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_5_X) + mstore(add(g, 0x20), PUB_5_Y) + s := calldataload(add(input, 160)) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_6_X) + mstore(add(g, 0x20), PUB_6_Y) + s := mload(publicCommitments) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + + x := mload(f) + y := mload(add(f, 0x20)) + } + if (!success) { + // Either Public input not in field, or verification key invalid. + // We assume the contract is correctly generated, so the verification key is valid. + revert PublicInputNotInField(); + } + } + + /// Compress a proof. + /// @notice Will revert with InvalidProof if the curve points are invalid, + /// but does not verify the proof itself. + /// @param proof The uncompressed Groth16 proof. Elements are in the same order as for + /// verifyProof. I.e. Groth16 points (A, B, C) encoded as in EIP-197. + /// @param commitments Pedersen commitments from the proof. + /// @param commitmentPok proof of knowledge for the Pedersen commitments. + /// @return compressed The compressed proof. Elements are in the same order as for + /// verifyCompressedProof. I.e. points (A, B, C) in compressed format. + /// @return compressedCommitments compressed Pedersen commitments from the proof. + /// @return compressedCommitmentPok compressed proof of knowledge for the Pedersen commitments. + function compressProof( + uint256[8] calldata proof, + uint256[2] calldata commitments, + uint256[2] calldata commitmentPok + ) + public view returns ( + uint256[4] memory compressed, + uint256[1] memory compressedCommitments, + uint256 compressedCommitmentPok + ) { + compressed[0] = compress_g1(proof[0], proof[1]); + (compressed[2], compressed[1]) = compress_g2(proof[3], proof[2], proof[5], proof[4]); + compressed[3] = compress_g1(proof[6], proof[7]); + compressedCommitments[0] = compress_g1(commitments[0], commitments[1]); + compressedCommitmentPok = compress_g1(commitmentPok[0], commitmentPok[1]); + } + + /// Verify a Groth16 proof with compressed points. + /// @notice Reverts with InvalidProof if the proof is invalid or + /// with PublicInputNotInField the public input is not reduced. + /// @notice There is no return value. If the function does not revert, the + /// proof was successfully verified. + /// @param compressedProof the points (A, B, C) in compressed format + /// matching the output of compressProof. + /// @param compressedCommitments compressed Pedersen commitments from the proof. + /// @param compressedCommitmentPok compressed proof of knowledge for the Pedersen commitments. + /// @param input the public input field elements in the scalar field Fr. + /// Elements must be reduced. + function verifyCompressedProof( + uint256[4] calldata compressedProof, + uint256[1] calldata compressedCommitments, + uint256 compressedCommitmentPok, + uint256[6] calldata input + ) public view { + uint256[1] memory publicCommitments; + uint256[2] memory commitments; + uint256[24] memory pairings; + { + (commitments[0], commitments[1]) = decompress_g1(compressedCommitments[0]); + (uint256 Px, uint256 Py) = decompress_g1(compressedCommitmentPok); + + uint256[] memory publicAndCommitmentCommitted; + + publicCommitments[0] = uint256( + sha256( + abi.encodePacked( + commitments[0], + commitments[1], + publicAndCommitmentCommitted + ) + ) + ) % R; + // Commitments + pairings[ 0] = commitments[0]; + pairings[ 1] = commitments[1]; + pairings[ 2] = PEDERSEN_G_X_1; + pairings[ 3] = PEDERSEN_G_X_0; + pairings[ 4] = PEDERSEN_G_Y_1; + pairings[ 5] = PEDERSEN_G_Y_0; + pairings[ 6] = Px; + pairings[ 7] = Py; + pairings[ 8] = PEDERSEN_GROOTSIGMANEG_X_1; + pairings[ 9] = PEDERSEN_GROOTSIGMANEG_X_0; + pairings[10] = PEDERSEN_GROOTSIGMANEG_Y_1; + pairings[11] = PEDERSEN_GROOTSIGMANEG_Y_0; + + // Verify pedersen commitments + bool success; + assembly ("memory-safe") { + let f := mload(0x40) + + success := staticcall(gas(), PRECOMPILE_VERIFY, pairings, 0x180, f, 0x20) + success := and(success, mload(f)) + } + if (!success) { + revert CommitmentInvalid(); + } + } + + { + (uint256 Ax, uint256 Ay) = decompress_g1(compressedProof[0]); + (uint256 Bx0, uint256 Bx1, uint256 By0, uint256 By1) = decompress_g2(compressedProof[2], compressedProof[1]); + (uint256 Cx, uint256 Cy) = decompress_g1(compressedProof[3]); + (uint256 Lx, uint256 Ly) = publicInputMSM( + input, + publicCommitments, + commitments + ); + + // Verify the pairing + // Note: The precompile expects the F2 coefficients in big-endian order. + // Note: The pairing precompile rejects unreduced values, so we won't check that here. + // e(A, B) + pairings[ 0] = Ax; + pairings[ 1] = Ay; + pairings[ 2] = Bx1; + pairings[ 3] = Bx0; + pairings[ 4] = By1; + pairings[ 5] = By0; + // e(C, -δ) + pairings[ 6] = Cx; + pairings[ 7] = Cy; + pairings[ 8] = DELTA_NEG_X_1; + pairings[ 9] = DELTA_NEG_X_0; + pairings[10] = DELTA_NEG_Y_1; + pairings[11] = DELTA_NEG_Y_0; + // e(α, -β) + pairings[12] = ALPHA_X; + pairings[13] = ALPHA_Y; + pairings[14] = BETA_NEG_X_1; + pairings[15] = BETA_NEG_X_0; + pairings[16] = BETA_NEG_Y_1; + pairings[17] = BETA_NEG_Y_0; + // e(L_pub, -γ) + pairings[18] = Lx; + pairings[19] = Ly; + pairings[20] = GAMMA_NEG_X_1; + pairings[21] = GAMMA_NEG_X_0; + pairings[22] = GAMMA_NEG_Y_1; + pairings[23] = GAMMA_NEG_Y_0; + + // Check pairing equation. + bool success; + uint256[1] memory output; + assembly ("memory-safe") { + success := staticcall(gas(), PRECOMPILE_VERIFY, pairings, 0x300, output, 0x20) + } + if (!success || output[0] != 1) { + // Either proof or verification key invalid. + // We assume the contract is correctly generated, so the verification key is valid. + revert ProofInvalid(); + } + } + } + + /// Verify an uncompressed Groth16 proof. + /// @notice Reverts with InvalidProof if the proof is invalid or + /// with PublicInputNotInField the public input is not reduced. + /// @notice There is no return value. If the function does not revert, the + /// proof was successfully verified. + /// @param proof the points (A, B, C) in EIP-197 format matching the output + /// of compressProof. + /// @param commitments the Pedersen commitments from the proof. + /// @param commitmentPok the proof of knowledge for the Pedersen commitments. + /// @param input the public input field elements in the scalar field Fr. + /// Elements must be reduced. + function verifyProof( + uint256[8] calldata proof, + uint256[2] calldata commitments, + uint256[2] calldata commitmentPok, + uint256[6] calldata input + ) public view { + // HashToField + uint256[1] memory publicCommitments; + uint256[] memory publicAndCommitmentCommitted; + + publicCommitments[0] = uint256( + sha256( + abi.encodePacked( + commitments[0], + commitments[1], + publicAndCommitmentCommitted + ) + ) + ) % R; + + // Verify pedersen commitments + bool success; + assembly ("memory-safe") { + let f := mload(0x40) + + calldatacopy(f, commitments, 0x40) // Copy Commitments + mstore(add(f, 0x40), PEDERSEN_G_X_1) + mstore(add(f, 0x60), PEDERSEN_G_X_0) + mstore(add(f, 0x80), PEDERSEN_G_Y_1) + mstore(add(f, 0xa0), PEDERSEN_G_Y_0) + calldatacopy(add(f, 0xc0), commitmentPok, 0x40) + mstore(add(f, 0x100), PEDERSEN_GROOTSIGMANEG_X_1) + mstore(add(f, 0x120), PEDERSEN_GROOTSIGMANEG_X_0) + mstore(add(f, 0x140), PEDERSEN_GROOTSIGMANEG_Y_1) + mstore(add(f, 0x160), PEDERSEN_GROOTSIGMANEG_Y_0) + + success := staticcall(gas(), PRECOMPILE_VERIFY, f, 0x180, f, 0x20) + success := and(success, mload(f)) + } + if (!success) { + revert CommitmentInvalid(); + } + + (uint256 x, uint256 y) = publicInputMSM( + input, + publicCommitments, + commitments + ); + + // Note: The precompile expects the F2 coefficients in big-endian order. + // Note: The pairing precompile rejects unreduced values, so we won't check that here. + assembly ("memory-safe") { + let f := mload(0x40) // Free memory pointer. + + // Copy points (A, B, C) to memory. They are already in correct encoding. + // This is pairing e(A, B) and G1 of e(C, -δ). + calldatacopy(f, proof, 0x100) + + // Complete e(C, -δ) and write e(α, -β), e(L_pub, -γ) to memory. + // OPT: This could be better done using a single codecopy, but + // Solidity (unlike standalone Yul) doesn't provide a way to + // to do this. + mstore(add(f, 0x100), DELTA_NEG_X_1) + mstore(add(f, 0x120), DELTA_NEG_X_0) + mstore(add(f, 0x140), DELTA_NEG_Y_1) + mstore(add(f, 0x160), DELTA_NEG_Y_0) + mstore(add(f, 0x180), ALPHA_X) + mstore(add(f, 0x1a0), ALPHA_Y) + mstore(add(f, 0x1c0), BETA_NEG_X_1) + mstore(add(f, 0x1e0), BETA_NEG_X_0) + mstore(add(f, 0x200), BETA_NEG_Y_1) + mstore(add(f, 0x220), BETA_NEG_Y_0) + mstore(add(f, 0x240), x) + mstore(add(f, 0x260), y) + mstore(add(f, 0x280), GAMMA_NEG_X_1) + mstore(add(f, 0x2a0), GAMMA_NEG_X_0) + mstore(add(f, 0x2c0), GAMMA_NEG_Y_1) + mstore(add(f, 0x2e0), GAMMA_NEG_Y_0) + + // Check pairing equation. + success := staticcall(gas(), PRECOMPILE_VERIFY, f, 0x300, f, 0x20) + // Also check returned value (both are either 1 or 0). + success := and(success, mload(f)) + } + if (!success) { + // Either proof or verification key invalid. + // We assume the contract is correctly generated, so the verification key is valid. + revert ProofInvalid(); + } + } + + function verifyProof(uint256[8] calldata, uint256[1] calldata) public pure { + revert ProofInvalid(); + } +} diff --git a/src/test/identity-manager/WorldIDIdentityManagerCalculation.t.sol b/src/test/identity-manager/WorldIDIdentityManagerCalculation.t.sol index f9f4c1f..f8637d1 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerCalculation.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerCalculation.t.sol @@ -32,7 +32,7 @@ contract WorldIDIdentityManagerCalculation is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.calculateIdentityRegistrationInputHash( + managerImplV2.calculateIdentityRegistrationInputHash( startIndex, insertionPreRoot, insertionPostRoot, identityCommitments ); } @@ -57,7 +57,7 @@ contract WorldIDIdentityManagerCalculation is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.calculateIdentityDeletionInputHash( + managerImplV2.calculateIdentityDeletionInputHash( packedDeletionIndices, deletionPreRoot, deletionPostRoot, deletionBatchSize ); } diff --git a/src/test/identity-manager/WorldIDIdentityManagerConstruction.t.sol b/src/test/identity-manager/WorldIDIdentityManagerConstruction.t.sol index 385a57c..2b20e7a 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerConstruction.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerConstruction.t.sol @@ -45,18 +45,18 @@ contract WorldIDIdentityManagerConstruction is WorldIDIdentityManagerTest { ); // Test - identityManager = new IdentityManager(address(managerImpl), callData); + identityManager = new IdentityManager(address(managerImplV2), callData); identityManagerAddress = address(identityManager); // creates Manager Impl V2, which will be used for tests - managerImpl = new ManagerImpl(); - managerImplAddress = address(managerImpl); + managerImplV2 = new ManagerImpl(); + managerImplV2Address = address(managerImplV2); bytes memory initCallV2 = abi.encodeCall(ManagerImpl.initializeV2, (defaultDeletionVerifiers)); bytes memory upgradeCall = abi.encodeCall( - UUPSUpgradeable.upgradeToAndCall, (address(managerImplAddress), initCallV2) + UUPSUpgradeable.upgradeToAndCall, (address(managerImplV2Address), initCallV2) ); // Test diff --git a/src/test/identity-manager/WorldIDIdentityManagerDataQuery.t.sol b/src/test/identity-manager/WorldIDIdentityManagerDataQuery.t.sol index 2df1cb1..a6c1af4 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerDataQuery.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerDataQuery.t.sol @@ -128,7 +128,7 @@ contract WorldIDIdentityManagerDataQuery is WorldIDIdentityManagerTest { // Setup vm.assume(badRoot != initialRoot); bytes memory callData = abi.encodeCall(ManagerImplV1.queryRoot, badRoot); - bytes memory returnData = abi.encode(managerImpl.NO_SUCH_ROOT()); + bytes memory returnData = abi.encode(managerImplV2.NO_SUCH_ROOT()); // Test assertCallSucceedsOn(identityManagerAddress, callData, returnData); @@ -140,7 +140,7 @@ contract WorldIDIdentityManagerDataQuery is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.queryRoot(initialRoot); + managerImplV2.queryRoot(initialRoot); } /// @notice Checks that it is possible to get the latest root from the contract. @@ -167,7 +167,7 @@ contract WorldIDIdentityManagerDataQuery is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.latestRoot(); + managerImplV2.latestRoot(); } /// @notice Checks that it is possible to get the tree depth the contract was initialized with. diff --git a/src/test/identity-manager/WorldIDIdentityManagerGettersSetters.t.sol b/src/test/identity-manager/WorldIDIdentityManagerGettersSetters.t.sol index 921e387..5269859 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerGettersSetters.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerGettersSetters.t.sol @@ -43,7 +43,7 @@ contract WorldIDIdentityManagerGettersSetters is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.getRegisterIdentitiesVerifierLookupTableAddress(); + managerImplV2.getRegisterIdentitiesVerifierLookupTableAddress(); } /// @notice Checks that it is possible to set the lookup table currently being used to verify @@ -93,7 +93,7 @@ contract WorldIDIdentityManagerGettersSetters is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.setRegisterIdentitiesVerifierLookupTable(insertionVerifiers); + managerImplV2.setRegisterIdentitiesVerifierLookupTable(insertionVerifiers); } /// @notice Checks that it is possible to get the address of the contract currently being used @@ -115,7 +115,7 @@ contract WorldIDIdentityManagerGettersSetters is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.getDeleteIdentitiesVerifierLookupTableAddress(); + managerImplV2.getDeleteIdentitiesVerifierLookupTableAddress(); } /// @notice Checks that it is possible to set the lookup table currently being used to verify @@ -162,7 +162,7 @@ contract WorldIDIdentityManagerGettersSetters is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.setDeleteIdentitiesVerifierLookupTable(deletionVerifiers); + managerImplV2.setDeleteIdentitiesVerifierLookupTable(deletionVerifiers); } /// @notice Ensures that we can get the address of the semaphore verifier. @@ -181,7 +181,7 @@ contract WorldIDIdentityManagerGettersSetters is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.getSemaphoreVerifierAddress(); + managerImplV2.getSemaphoreVerifierAddress(); } /// @notice Checks that it is possible to set the contract currently being used to verify @@ -224,7 +224,7 @@ contract WorldIDIdentityManagerGettersSetters is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.setSemaphoreVerifier(newVerifier); + managerImplV2.setSemaphoreVerifier(newVerifier); } /// @notice Ensures that it's possible to get the root history expiry time. @@ -243,7 +243,7 @@ contract WorldIDIdentityManagerGettersSetters is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.getRootHistoryExpiry(); + managerImplV2.getRootHistoryExpiry(); } /// @notice Ensures that it is possible to set the root history expiry time. @@ -290,6 +290,6 @@ contract WorldIDIdentityManagerGettersSetters is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.setRootHistoryExpiry(newExpiry); + managerImplV2.setRootHistoryExpiry(newExpiry); } } diff --git a/src/test/identity-manager/WorldIDIdentityManagerIdentityDeletion.t.sol b/src/test/identity-manager/WorldIDIdentityManagerIdentityDeletion.t.sol index fcdd41c..3528e5a 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerIdentityDeletion.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerIdentityDeletion.t.sol @@ -318,12 +318,12 @@ contract WorldIDIdentityManagerIdentityDeletion is WorldIDIdentityManagerTest { /// @notice Tests that identities can only be deleted through the proxy. function testCannotDelteIdentitiesIfNotViaProxy() public { // Setup - address expectedOwner = managerImpl.owner(); + address expectedOwner = managerImplV2.owner(); vm.expectRevert("Function must be called through delegatecall"); vm.prank(expectedOwner); // Test - managerImpl.deleteIdentities( + managerImplV2.deleteIdentities( deletionProof, packedDeletionIndices, initialRoot, deletionPostRoot ); } diff --git a/src/test/identity-manager/WorldIDIdentityManagerIdentityRegistration.t.sol b/src/test/identity-manager/WorldIDIdentityManagerIdentityRegistration.t.sol index 48e2288..ec4c73c 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerIdentityRegistration.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerIdentityRegistration.t.sol @@ -9,11 +9,13 @@ import {ITreeVerifier} from "../../interfaces/ITreeVerifier.sol"; import {SimpleVerifier, SimpleVerify} from "../mock/SimpleVerifier.sol"; import {TypeConverter as TC} from "../utils/TypeConverter.sol"; import {Verifier as TreeVerifier} from "src/test/InsertionTreeVerifier16.sol"; +import {Verifier as TreeVerifier4844} from "src/test/InsertionTreeVerifier164844.sol"; import {VerifierLookupTable} from "../../data/VerifierLookupTable.sol"; import {WorldIDIdentityManager as IdentityManager} from "../../WorldIDIdentityManager.sol"; import {WorldIDIdentityManagerImplV2 as ManagerImpl} from "../../WorldIDIdentityManagerImplV2.sol"; import {WorldIDIdentityManagerImplV1 as ManagerImplV1} from "../../WorldIDIdentityManagerImplV1.sol"; +import {WorldIDIdentityManagerImplV3 as ManagerImplV3} from "../../WorldIDIdentityManagerImplV3.sol"; /// @title World ID Identity Manager Identity Registration Tests /// @notice Contains tests for the WorldID identity manager. @@ -319,8 +321,8 @@ contract WorldIDIdentityManagerIdentityRegistration is WorldIDIdentityManagerTes function testCannotRegisterIdentitiesIfPostRootIncorrect(uint256 newPostRoot) public { // Setup vm.assume(newPostRoot != insertionPostRoot && newPostRoot < SNARK_SCALAR_FIELD); - managerImpl = new ManagerImpl(); - managerImplAddress = address(managerImpl); + managerImplV2 = new ManagerImpl(); + managerImplV2Address = address(managerImplV2); ITreeVerifier actualVerifier = new TreeVerifier(); ( VerifierLookupTable insertVerifiers, @@ -334,13 +336,13 @@ contract WorldIDIdentityManagerIdentityRegistration is WorldIDIdentityManagerTes (treeDepth, insertionPreRoot, insertVerifiers, updateVerifiers, semaphoreVerifier) ); - identityManager = new IdentityManager(managerImplAddress, callData); + identityManager = new IdentityManager(managerImplV2Address, callData); identityManagerAddress = address(identityManager); // Init V2 bytes memory initCallV2 = abi.encodeCall(ManagerImpl.initializeV2, (deletionVerifiers)); bytes memory upgradeCall = abi.encodeCall( - UUPSUpgradeable.upgradeToAndCall, (address(managerImplAddress), initCallV2) + UUPSUpgradeable.upgradeToAndCall, (address(managerImplV2Address), initCallV2) ); assertCallSucceedsOn(identityManagerAddress, upgradeCall, new bytes(0x0)); @@ -451,12 +453,12 @@ contract WorldIDIdentityManagerIdentityRegistration is WorldIDIdentityManagerTes /// @notice Tests that identities can only be registered through the proxy. function testCannotRegisterIdentitiesIfNotViaProxy() public { // Setup - address expectedOwner = managerImpl.owner(); + address expectedOwner = managerImplV2.owner(); vm.expectRevert("Function must be called through delegatecall"); vm.prank(expectedOwner); // Test - managerImpl.registerIdentities( + managerImplV2.registerIdentities( insertionProof, initialRoot, startIndex, identityCommitments, insertionPostRoot ); } diff --git a/src/test/identity-manager/WorldIDIdentityManagerIdentityRegistration4844.t.sol b/src/test/identity-manager/WorldIDIdentityManagerIdentityRegistration4844.t.sol new file mode 100644 index 0000000..9a1e0bc --- /dev/null +++ b/src/test/identity-manager/WorldIDIdentityManagerIdentityRegistration4844.t.sol @@ -0,0 +1,650 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {UUPSUpgradeable} from "contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {WorldIDIdentityManagerTest} from "./WorldIDIdentityManagerTest.sol"; + +import {ITreeVerifier} from "../../interfaces/ITreeVerifier.sol"; +import {SimpleVerifier, SimpleVerify} from "../mock/SimpleVerifier.sol"; +import {TypeConverter as TC} from "../utils/TypeConverter.sol"; +import {Verifier as TreeVerifier} from "src/test/InsertionTreeVerifier164844.sol"; +import {VerifierLookupTable} from "../../data/VerifierLookupTable.sol"; + +import {WorldIDIdentityManager as IdentityManager} from "../../WorldIDIdentityManager.sol"; +import {WorldIDIdentityManagerImplV2 as ManagerImpl} from "../../WorldIDIdentityManagerImplV2.sol"; +import {WorldIDIdentityManagerImplV1 as ManagerImplV1} from "../../WorldIDIdentityManagerImplV1.sol"; +import {WorldIDIdentityManagerImplV3 as ManagerImplV3} from "../../WorldIDIdentityManagerImplV3.sol"; + +/// @title World ID Identity Manager Identity Registration Tests for EIP-4844 protocol. +/// @notice Contains tests for the WorldID identity manager. +/// @author Worldcoin +/// @dev This test suite tests both the proxy and the functionality of the underlying implementation +/// so as to test everything in the context of how it will be deployed. +contract WorldIDIdentityManagerIdentityRegistration4844 is WorldIDIdentityManagerTest { + /// Taken from SimpleVerifier.sol + event VerifiedProof(uint256 batchSize); + + /// Taken from WorldIDIdentityManagerImplV1.sol + event TreeChanged( + uint256 indexed insertionPreRoot, + ManagerImplV1.TreeChange indexed kind, + uint256 indexed insertionPostRoot + ); + + /// @notice Checks that the proof validates properly with the correct inputs. + function testRegisterIdentitiesWithCorrectInputsFromKnown() public { + // Setup + ITreeVerifier actualVerifier = new TreeVerifier(); + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([40])); + insertVerifiers.addVerifier(identityCommitmentsSize, actualVerifier); + makeNewIdentityManager( + treeDepth, + insertionPreRoot, + insertVerifiers, + deletionVerifiers, + updateVerifiers, + semaphoreVerifier + ); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: insertionProof4844, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: insertionPreRoot, + postRoot: insertionPostRoot4844, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identityCommitmentsSize), + startIndex: startIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + + bytes memory registerCallData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory latestRootCallData = abi.encodeCall(ManagerImplV1.latestRoot, ()); + bytes memory queryRootCallData = + abi.encodeCall(ManagerImplV1.queryRoot, (insertionPostRoot4844)); + + // Test + assertCallSucceedsOn(identityManagerAddress, registerCallData); + assertCallSucceedsOn( + identityManagerAddress, latestRootCallData, abi.encode(insertionPostRoot4844) + ); + assertCallSucceedsOn( + identityManagerAddress, + queryRootCallData, + abi.encode(ManagerImplV1.RootInfo(insertionPostRoot4844, 0, true)) + ); + } + + /// @notice Checks that the proof validates properly with correct inputs. + function testRegisterIdentitiesWithCorrectInputs( + uint128[8] memory prf, + uint32 newStartIndex, + uint128 newPreRoot, + uint128 newPostRoot, + uint128[] memory identities, + address identityOperator + ) public { + // Setup + vm.assume(SimpleVerify.isValidInput(uint256(prf[0]))); + vm.assume(newPreRoot != newPostRoot); + vm.assume(identities.length <= 1000); + vm.assume(identityOperator != nullAddress && identityOperator != thisAddress); + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([identities.length])); + makeNewIdentityManager( + treeDepth, + newPreRoot, + insertVerifiers, + deletionVerifiers, + updateVerifiers, + semaphoreVerifier + ); + + (uint256[] memory preparedIdents, uint256[8] memory actualProof) = + prepareInsertIdentitiesTestCase(identities, prf); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: actualProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: newPreRoot, + postRoot: newPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(preparedIdents.length), + startIndex: newStartIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory callData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory setupCallData = + abi.encodeCall(ManagerImplV1.setIdentityOperator, identityOperator); + (bool success,) = identityManagerAddress.call(setupCallData); + assert(success); + + // Expect the root to have been sent to the state bridge. + vm.expectEmit(true, true, true, true); + emit TreeChanged(newPreRoot, ManagerImplV1.TreeChange.Insertion, newPostRoot); + vm.prank(identityOperator); + + // Test + assertCallSucceedsOn(identityManagerAddress, callData); + } + + /// @notice Ensures that identity registration selects the correct verifier when registering + /// identities. + function testRegisterIdentitiesSelectsCorrectVerifier( + uint128[8] memory prf, + uint32 newStartIndex, + uint128 newPreRoot, + uint128 newPostRoot, + uint128[] memory identities + ) public { + vm.assume(SimpleVerify.isValidInput(uint256(prf[0]))); + vm.assume(newPreRoot != newPostRoot); + vm.assume(identities.length <= 1000 && identities.length > 0); + uint256 secondIdentsLength = identities.length / 2; + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([identities.length, secondIdentsLength])); + makeNewIdentityManager( + treeDepth, + newPreRoot, + insertVerifiers, + deletionVerifiers, + updateVerifiers, + semaphoreVerifier + ); + (uint256[] memory preparedIdents, uint256[8] memory actualProof) = + prepareInsertIdentitiesTestCase(identities, prf); + uint256[] memory secondIdents = new uint256[](secondIdentsLength); + for (uint256 i = 0; i < secondIdentsLength; ++i) { + secondIdents[i] = preparedIdents[i]; + } + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: actualProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: newPreRoot, + postRoot: newPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(preparedIdents.length), + startIndex: newStartIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory firstCallData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + uint256 secondPostRoot = uint256(newPostRoot) + 1; + params.preRoot = newPostRoot; + params.batchSize = uint32(secondIdentsLength); + params.postRoot = secondPostRoot; + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory secondCallData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + vm.expectEmit(true, true, true, true); + emit VerifiedProof(identities.length); + + // Test + assertCallSucceedsOn(identityManagerAddress, firstCallData); + + vm.expectEmit(true, true, true, true); + emit VerifiedProof(identities.length / 2); + + assertCallSucceedsOn(identityManagerAddress, secondCallData); + } + + /// @notice Ensures that the contract reverts if passed a batch size it doesn't know about. + function testCannotRegisterIdentitiesWithInvalidBatchSize( + uint128[8] memory prf, + uint32 newStartIndex, + uint128 newPreRoot, + uint128 newPostRoot, + uint128[] memory identities + ) public { + vm.assume(SimpleVerify.isValidInput(uint256(prf[0]))); + vm.assume(newPreRoot != newPostRoot); + vm.assume(identities.length > 0); + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([identities.length - 1])); + makeNewIdentityManager( + treeDepth, + newPreRoot, + insertVerifiers, + deletionVerifiers, + updateVerifiers, + semaphoreVerifier + ); + (uint256[] memory preparedIdents, uint256[8] memory actualProof) = + prepareInsertIdentitiesTestCase(identities, prf); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: actualProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: newPreRoot, + postRoot: newPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(preparedIdents.length), + startIndex: newStartIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory callData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory errorData = abi.encodeWithSelector(VerifierLookupTable.NoSuchVerifier.selector); + + // Test + assertCallFailsOn(identityManagerAddress, callData, errorData); + } + + /// @notice Checks that it reverts if the provided proof is incorrect for the public inputs. + function testCannotRegisterIdentitiesWithIncorrectInputs( + uint128[8] memory prf, + uint32 newStartIndex, + uint128 newPreRoot, + uint128 newPostRoot + ) public { + // Setup + vm.assume(!SimpleVerify.isValidInput(uint256(prf[0]))); + ITreeVerifier actualVerifier = new TreeVerifier(); + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([70])); + insertVerifiers.addVerifier(identityCommitments.length, actualVerifier); + makeNewIdentityManager( + treeDepth, + newPreRoot, + insertVerifiers, + deletionVerifiers, + updateVerifiers, + semaphoreVerifier + ); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: insertionProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: newPreRoot, + postRoot: newPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identityCommitments.length), + startIndex: newStartIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory callData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory expectedError = + abi.encodeWithSelector(ManagerImplV1.ProofValidationFailure.selector); + + // Test + assertCallFailsOn(identityManagerAddress, callData, expectedError); + } + + /// @notice Checks that it reverts if the provided start index is incorrect. + function testCannotRegisterIdentitiesIfStartIndexIncorrect(uint32 newStartIndex) public { + // Setup + vm.assume(newStartIndex != startIndex); + ITreeVerifier actualVerifier = new TreeVerifier(); + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([70])); + insertVerifiers.addVerifier(identityCommitmentsSize, actualVerifier); + makeNewIdentityManager( + treeDepth, + insertionPreRoot, + insertVerifiers, + deletionVerifiers, + updateVerifiers, + semaphoreVerifier + ); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: insertionProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: insertionPreRoot, + postRoot: insertionPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identityCommitments.length), + startIndex: newStartIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory registerCallData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory expectedError = + abi.encodeWithSelector(ManagerImplV1.ProofValidationFailure.selector); + + // Test + assertCallFailsOn(identityManagerAddress, registerCallData, expectedError); + } + + /// @notice Checks that it reverts if the provided set of identities is incorrect. + function testCannotRegisterIdentitiesIfIdentitiesIncorrect(uint256 identity) public { + // Setup + uint256 invalidSlot = rotateSlot(); + vm.assume( + identity != identityCommitments[invalidSlot] && identity < SNARK_SCALAR_FIELD + && identity != 0x0 + ); + uint256[] memory identities = cloneArray(identityCommitments); + identities[invalidSlot] = identity; + ITreeVerifier actualVerifier = new TreeVerifier(); + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([70])); + insertVerifiers.addVerifier(identityCommitmentsSize, actualVerifier); + makeNewIdentityManager( + treeDepth, + insertionPreRoot, + insertVerifiers, + deletionVerifiers, + updateVerifiers, + semaphoreVerifier + ); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: insertionProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: insertionPreRoot, + postRoot: insertionPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identities.length), + startIndex: startIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory registerCallData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory expectedError = + abi.encodeWithSelector(ManagerImplV1.ProofValidationFailure.selector); + + // Test + assertCallFailsOn(identityManagerAddress, registerCallData, expectedError); + } + + /// @notice Checks that it reverts if the provided post root is incorrect. + function testCannotRegisterIdentitiesIfPostRootIncorrect(uint256 newPostRoot) public { + // Setup + vm.assume(newPostRoot != insertionPostRoot && newPostRoot < SNARK_SCALAR_FIELD); + managerImplV2 = new ManagerImpl(); + managerImplV2Address = address(managerImplV2); + ITreeVerifier actualVerifier = new TreeVerifier(); + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([70])); + insertVerifiers.addVerifier(identityCommitmentsSize, actualVerifier); + + bytes memory callData = abi.encodeCall( + ManagerImplV1.initialize, + (treeDepth, insertionPreRoot, insertVerifiers, updateVerifiers, semaphoreVerifier) + ); + + identityManager = new IdentityManager(managerImplV2Address, callData); + identityManagerAddress = address(identityManager); + + // Init V2 + bytes memory initCallV2 = abi.encodeCall(ManagerImpl.initializeV2, (deletionVerifiers)); + bytes memory upgradeCall = abi.encodeCall( + UUPSUpgradeable.upgradeToAndCall, (address(managerImplV2Address), initCallV2) + ); + assertCallSucceedsOn(identityManagerAddress, upgradeCall, new bytes(0x0)); + + // Init V3 + managerImplV3 = new ManagerImplV3(); + managerImplV3Address = address(managerImplV3); + bytes memory initCallV3 = abi.encodeCall(managerImplV3.initializeV3, (insertVerifiers)); + bytes memory upgradeCallV3 = abi.encodeCall( + UUPSUpgradeable.upgradeToAndCall, (address(managerImplV3Address), initCallV3) + ); + assertCallSucceedsOn(identityManagerAddress, upgradeCallV3, new bytes(0x0)); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: insertionProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: insertionPreRoot, + postRoot: newPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identityCommitments.length), + startIndex: startIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory registerCallData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory expectedError = + abi.encodeWithSelector(ManagerImplV1.ProofValidationFailure.selector); + + // Test + assertCallFailsOn(identityManagerAddress, registerCallData, expectedError); + } + + /// @notice Tests that it reverts if an attempt is made to register identities as an address + /// that is not the identity operator address. + function testCannotRegisterIdentitiesAsNonIdentityOperator(address nonOperator) public { + // Setup + vm.assume(nonOperator != address(this) && nonOperator != address(0x0)); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: insertionProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: insertionPreRoot, + postRoot: insertionPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identityCommitments.length), + startIndex: startIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory callData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory errorData = + abi.encodeWithSelector(ManagerImplV1.Unauthorized.selector, nonOperator); + vm.prank(nonOperator); + + // Test + assertCallFailsOn(identityManagerAddress, callData, errorData); + } + + /// @notice Tests that it reverts if an attempt is made to register identities with an outdated + /// root. + function testCannotRegisterIdentitiesWithOutdatedRoot( + uint256 currentPreRoot, + uint256 actualRoot + ) public { + // Setup + vm.assume( + currentPreRoot != actualRoot && currentPreRoot < SNARK_SCALAR_FIELD + && actualRoot < SNARK_SCALAR_FIELD + ); + makeNewIdentityManager( + treeDepth, + uint256(currentPreRoot), + defaultInsertVerifiers, + defaultDeletionVerifiers, + defaultUpdateVerifiers, + semaphoreVerifier + ); + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: insertionProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: actualRoot, + postRoot: insertionPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identityCommitments.length), + startIndex: startIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory callData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + bytes memory expectedError = abi.encodeWithSelector( + ManagerImplV1.NotLatestRoot.selector, actualRoot, uint256(currentPreRoot) + ); + + // Test + assertCallFailsOn(identityManagerAddress, callData, expectedError); + } + + /// @notice Tests that runs of zeroes are accepted by the `registerIdentities` function as valid + /// arrays of identity commitments. + function testRegisterIdentitiesWithRunsOfZeroes(uint8 identitiesLength, uint8 zeroPosition) + public + { + // Setup + vm.assume(identitiesLength != 0 && identitiesLength <= 1000); + vm.assume(zeroPosition < identitiesLength && zeroPosition > 0); + uint256[] memory identities = new uint256[](identitiesLength); + ( + VerifierLookupTable insertVerifiers, + VerifierLookupTable deletionVerifiers, + VerifierLookupTable updateVerifiers + ) = makeVerifierLookupTables(TC.makeDynArray([identitiesLength])); + makeNewIdentityManager( + treeDepth, + initialRoot, + insertVerifiers, + deletionVerifiers, + updateVerifiers, + semaphoreVerifier + ); + + for (uint256 i = 0; i < zeroPosition; ++i) { + identities[i] = i + 1; + } + for (uint256 i = zeroPosition; i < identitiesLength; ++i) { + identities[i] = 0x0; + } + + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: [uint256(2), 1, 3, 4, 5, 6, 7, 9], + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: initialRoot, + postRoot: insertionPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identities.length), + startIndex: startIndex + }); + + // Mock blobhash. This is valid for the next call only. + prepareBlobhash(kzgToVersionedHash(kzgCommitment)); + bytes memory callData = abi.encodeCall(ManagerImplV3.registerIdentities, params); + + // Test + assertCallSucceedsOn(identityManagerAddress, callData, new bytes(0)); + } + + /// @notice Tests that identities can only be registered through the proxy. + function testCannotRegisterIdentitiesIfNotViaProxy() public { + // Setup + address expectedOwner = managerImplV3.owner(); + vm.expectRevert("Function must be called through delegatecall"); + vm.prank(expectedOwner); + + // Test + ManagerImplV3.RegisterIdentities4844Params memory params = ManagerImplV3.RegisterIdentities4844Params({ + insertionProof: insertionProof, + commitments: commitments, + commitmentPok: commitmentsPok, + kzgCommitment: kzgCommitment, + kzgProof: kzgProof, + expectedEvaluation: insertionExpectedEvaluation, + preRoot: initialRoot, + postRoot: insertionPostRoot, + kzgChallenge: kzgChallenge, + inputHash: insertionInputHash4844, + batchSize: uint32(identityCommitments.length), + startIndex: startIndex + }); + + managerImplV3.registerIdentities(params); + } +} diff --git a/src/test/identity-manager/WorldIDIdentityManagerInitialization.t.sol b/src/test/identity-manager/WorldIDIdentityManagerInitialization.t.sol index 675ad10..684016e 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerInitialization.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerInitialization.t.sol @@ -24,7 +24,7 @@ contract WorldIDIdentityManagerInitialization is WorldIDIdentityManagerTest { function testInitialisation() public { // Setup delete identityManager; - delete managerImpl; + delete managerImplV2; delete managerImplV1; bytes memory V1CallData = abi.encodeCall( @@ -39,7 +39,7 @@ contract WorldIDIdentityManagerInitialization is WorldIDIdentityManagerTest { ); managerImplV1 = new ManagerImplV1(); - managerImplAddress = address(managerImpl); + managerImplV2Address = address(managerImplV2); vm.expectEmit(true, true, true, true); emit Initialized(1); @@ -48,13 +48,13 @@ contract WorldIDIdentityManagerInitialization is WorldIDIdentityManagerTest { identityManagerAddress = address(identityManager); // creates Manager Impl V2, which will be used for tests - managerImpl = new ManagerImpl(); - managerImplAddress = address(managerImpl); + managerImplV2 = new ManagerImpl(); + managerImplV2Address = address(managerImplV2); bytes memory initCallV2 = abi.encodeCall(ManagerImpl.initializeV2, (defaultDeletionVerifiers)); bytes memory upgradeCall = abi.encodeCall( - UUPSUpgradeable.upgradeToAndCall, (address(managerImplAddress), initCallV2) + UUPSUpgradeable.upgradeToAndCall, (address(managerImplV2Address), initCallV2) ); vm.expectEmit(true, true, true, true); @@ -70,7 +70,7 @@ contract WorldIDIdentityManagerInitialization is WorldIDIdentityManagerTest { function testInitialisation2() public { // Setup delete identityManager; - delete managerImpl; + delete managerImplV2; delete managerImplV1; bytes memory V1CallData = abi.encodeCall( @@ -85,12 +85,12 @@ contract WorldIDIdentityManagerInitialization is WorldIDIdentityManagerTest { ); // creates Manager Impl V2, which will be used for tests - managerImpl = new ManagerImpl(); - managerImplAddress = address(managerImpl); + managerImplV2 = new ManagerImpl(); + managerImplV2Address = address(managerImplV2); vm.expectEmit(true, true, true, true); emit Initialized(1); - identityManager = new IdentityManager(managerImplAddress, V1CallData); + identityManager = new IdentityManager(managerImplV2Address, V1CallData); identityManagerAddress = address(identityManager); bytes memory initCallV2 = @@ -145,10 +145,10 @@ contract WorldIDIdentityManagerInitialization is WorldIDIdentityManagerTest { function testCannotPassUnsupportedTreeDepth() public { // Setup delete identityManager; - delete managerImpl; + delete managerImplV2; - managerImpl = new ManagerImpl(); - managerImplAddress = address(managerImpl); + managerImplV2 = new ManagerImpl(); + managerImplV2Address = address(managerImplV2); uint8 unsupportedDepth = 15; bytes memory callData = abi.encodeCall( @@ -165,6 +165,6 @@ contract WorldIDIdentityManagerInitialization is WorldIDIdentityManagerTest { vm.expectRevert(abi.encodeWithSelector(ManagerImplV1.UnsupportedTreeDepth.selector, 15)); // Test - identityManager = new IdentityManager(managerImplAddress, callData); + identityManager = new IdentityManager(managerImplV2Address, callData); } } diff --git a/src/test/identity-manager/WorldIDIdentityManagerOwnershipManagement.t.sol b/src/test/identity-manager/WorldIDIdentityManagerOwnershipManagement.t.sol index 630fb93..79d9982 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerOwnershipManagement.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerOwnershipManagement.t.sol @@ -137,7 +137,7 @@ contract WorldIDIdentityManagerOwnershipManagement is WorldIDIdentityManagerTest vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.identityOperator(); + managerImplV2.identityOperator(); } /// @notice Ensures that it is possible for the owner to set the address of the identity @@ -180,6 +180,6 @@ contract WorldIDIdentityManagerOwnershipManagement is WorldIDIdentityManagerTest vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.setIdentityOperator(newOperator); + managerImplV2.setIdentityOperator(newOperator); } } diff --git a/src/test/identity-manager/WorldIDIdentityManagerTest.sol b/src/test/identity-manager/WorldIDIdentityManagerTest.sol index fffcf09..083e4a0 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerTest.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerTest.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; +pragma solidity ^0.8.24; import {UUPSUpgradeable} from "contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -18,6 +18,7 @@ import {VerifierLookupTable} from "../../data/VerifierLookupTable.sol"; import {WorldIDIdentityManager as IdentityManager} from "../../WorldIDIdentityManager.sol"; import {WorldIDIdentityManagerImplV1 as ManagerImplV1} from "../../WorldIDIdentityManagerImplV1.sol"; import {WorldIDIdentityManagerImplV2 as ManagerImpl} from "../../WorldIDIdentityManagerImplV2.sol"; +import {WorldIDIdentityManagerImplV3 as ManagerImplV3} from "../../WorldIDIdentityManagerImplV3.sol"; /// @title World ID Identity Manager Test. /// @notice Contains tests for the WorldID identity manager. @@ -30,8 +31,10 @@ contract WorldIDIdentityManagerTest is WorldIDTest { /////////////////////////////////////////////////////////////////////////////// IdentityManager internal identityManager; + // V3 + ManagerImplV3 internal managerImplV3; // V2 - ManagerImpl internal managerImpl; + ManagerImpl internal managerImplV2; // V1 ManagerImplV1 internal managerImplV1; @@ -40,8 +43,10 @@ contract WorldIDIdentityManagerTest is WorldIDTest { uint8 internal treeDepth = 16; address internal identityManagerAddress; + // V3 + address internal managerImplV3Address; // V2 - address internal managerImplAddress; + address internal managerImplV2Address; // V1 address internal managerImplV1Address; @@ -65,6 +70,25 @@ contract WorldIDIdentityManagerTest is WorldIDTest { uint256 identityCommitmentsSize = 3; uint256[8] insertionProof; + /////////////////////////////////////////////////////////////////// + /// 4844 INSERTION /// + /////////////////////////////////////////////////////////////////// + /// @dev generated using `./gnark-mbu gen-test-params --mode insertion --tree-depth 16 --batch-size 3 | ./gnark-mbu prove --mode insertion --keys-file test_insertion.ps` + bytes32 internal constant insertionInputHash4844 = + 0x14a24bedc17b5596c60da74552640bd130d41d96b8c587dcadcf23217399e17b; + uint256 internal constant insertionExpectedEvaluation = + 0x3d5d4a7d6098f2147ed77be69d93179e6179479b8771c2554e5404c06f836408; + uint256 internal constant insertionPostRoot4844 = + 0x0c3f30b0604dae9a378e2bf62826bf5a772e9ad745df6f8c8256dff351fecee8; + uint256 internal constant kzgChallenge = + 0x1b5f5a71803049342dbd89f27e11d537400e959eea99de6e29e262d22adc3468; + + uint256[8] insertionProof4844; + uint256[2] commitments; + uint256[2] commitmentsPok; + uint128[3] kzgCommitment; + uint128[3] kzgProof; + /////////////////////////////////////////////////////////////////// /// DELETION /// /////////////////////////////////////////////////////////////////// @@ -143,6 +167,39 @@ contract WorldIDIdentityManagerTest is WorldIDTest { 0x24830332559eada283d4473b17091b239443e75e9e09f0ebce8e72c235ee665d ]; + insertionProof4844 = [ + 0x18dba02648df62914fe9c6dba182c73480253ed22b383c4ae7ead51152e73300, + 0x0660cf8023a5785e930e0333864ed17c8641a559e7bad817af736f6648a76447, + 0x2662090884185d3f910ce62dadf005a278226d877f41d3c52bd1d6b4a91aa2be, + 0x18a279bd46da024aa71cc7f64c396b3c64a6f13a1cf5fc443ad916ac93478b4d, + 0x0fdd92b46d74766433d3a501da207f8dfb16e4d74fe2a6dcad008f2e656f8842, + 0x2212ff3545056108d1162467172c368a89614ad29469f4d71f02e4ebcb6eb3ac, + 0x041dbe374440a1a1acdef5bc7d204ce3b20e4d6fbd41b41b787896e51ed023e9, + 0x047eddec1fc18e112fe15ce861484e8309f0605260e063c9591a6e0450934c80 + ]; + + commitments = [ + 0x04dd4ea218ac1d6b85f5d8ffb3007ad0c029302d1af96f0830ade252ccba5b98, + 0x18702f80829840758f18e3a9e624a8d049944dcc494bf260f8e0f9047cebf027 + ]; + + commitmentsPok = [ + 0x0ab698df05861ae9048ba5c388857fc32a0db801ab3b8bfc4c9b298819da6a66, + 0x085221b73b2a59518c04f1f6f41a7879637cf9f984ed4e05bd28e6507fb67614 + ]; + + kzgProof = [ + 0x925d42714da54a935f209022d256986a, + 0x2b545ab39f127832297a492ed5875be9, + 0x1a983b57c0639403a38ad7d24e0095bc + ]; + + kzgCommitment = [ + 0xb422b2e3bf75a087b84d8086fd35b8a2, + 0x299a559c92ef938fd63d6e6009b74bb9, + 0x47670537338c47c4472f9be9886b65ac + ]; + // Create the deletion proof term. // output from semaphore-mtb prove in src/test/data/DeletionProof.json /// @dev test_deletion.ps is generated using semaphore-mtb: `./gnark-mbu setup --mode deletion --batch-size 8 --tree-depth 16 --output test_deletion.ps` @@ -198,8 +255,9 @@ contract WorldIDIdentityManagerTest is WorldIDTest { hevm.label(address(this), "Sender"); hevm.label(identityManagerAddress, "IdentityManager"); - hevm.label(managerImplAddress, "ManagerImplementation"); hevm.label(managerImplV1Address, "ManagerImplementationV1"); + hevm.label(managerImplV1Address, "ManagerImplementationV2"); + hevm.label(managerImplV3Address, "ManagerImplementationV3"); } /////////////////////////////////////////////////////////////////////////////// @@ -239,16 +297,28 @@ contract WorldIDIdentityManagerTest is WorldIDTest { identityManagerAddress = address(identityManager); // creates Manager Impl V2, which will be used for tests - managerImpl = new ManagerImpl(); - managerImplAddress = address(managerImpl); + managerImplV2 = new ManagerImpl(); + managerImplV2Address = address(managerImplV2); + + bytes memory initCallV2 = abi.encodeCall(managerImplV2.initializeV2, (deletionVerifiers)); + bytes memory upgradeCallV2 = abi.encodeCall( + UUPSUpgradeable.upgradeToAndCall, (address(managerImplV2Address), initCallV2) + ); + + // Test + assertCallSucceedsOn(identityManagerAddress, upgradeCallV2, new bytes(0x0)); + + // creates Manager Impl V3, which will be used for tests + managerImplV3 = new ManagerImplV3(); + managerImplV3Address = address(managerImplV3); - bytes memory initCallV2 = abi.encodeCall(ManagerImpl.initializeV2, (deletionVerifiers)); - bytes memory upgradeCall = abi.encodeCall( - UUPSUpgradeable.upgradeToAndCall, (address(managerImplAddress), initCallV2) + bytes memory initCallV3 = abi.encodeCall(managerImplV3.initializeV3, (insertVerifiers)); + bytes memory upgradeCallV3 = abi.encodeCall( + UUPSUpgradeable.upgradeToAndCall, (address(managerImplV3Address), initCallV3) ); // Test - assertCallSucceedsOn(identityManagerAddress, upgradeCall, new bytes(0x0)); + assertCallSucceedsOn(identityManagerAddress, upgradeCallV3, new bytes(0x0)); } /// @notice Initialises a new identity manager using the provided information. @@ -327,9 +397,9 @@ contract WorldIDIdentityManagerTest is WorldIDTest { /// @notice Creates a new identity manager without initializing the delegate. /// @dev It is constructed in the globals. function makeUninitIdentityManager() public { - managerImpl = new ManagerImpl(); - managerImplAddress = address(managerImpl); - identityManager = new IdentityManager(managerImplAddress, new bytes(0x0)); + managerImplV2 = new ManagerImpl(); + managerImplV2Address = address(managerImplV2); + identityManager = new IdentityManager(managerImplV2Address, new bytes(0x0)); identityManagerAddress = address(identityManager); } @@ -379,6 +449,38 @@ contract WorldIDIdentityManagerTest is WorldIDTest { actualProof = [uint256(prf[0]), prf[1], prf[2], prf[3], prf[4], prf[5], prf[6], prf[7]]; } + bytes1 constant VERSIONED_HASH_VERSION_KZG = 0x01; + + /// @notice Convert a KZG commitment to a versioned hash as per EIP-4844. + /// Implementation as per https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md#helpers + /// @param commitment KZG commitment split to 3 128-bit words. + /// @return versioned hash in the form of a 32-byte word. + function kzgToVersionedHash(uint128[3] memory commitment) public pure returns (bytes32) { + bytes memory commitmentBytes = abi.encodePacked(commitment[0], commitment[1], commitment[2]); + bytes32 hash = sha256(commitmentBytes); + + bytes memory truncatedHash = new bytes(31); + for (uint i = 0; i < 31; i++) { + truncatedHash[i] = hash[i + 1]; + } + + return bytes32(abi.encodePacked(VERSIONED_HASH_VERSION_KZG, truncatedHash)); + } + + /// @notice Store the given value as a blobhash to be used in tests. + /// The given value will be stored in the 0th blobhash slot and can be retrieved with `blobhash(0)` + /// convenience wrapper of with the `BLOBHASH` opcode. + /// @dev This function is effective only for the next function call, so prepare blobhash as the very last + /// step before the intended usage. + /// @param value Value to be set as contents of the 0th blobhash slot. + function prepareBlobhash(bytes32 value) + public + { + bytes32[] memory blobhashes = new bytes32[](1); + blobhashes[0] = value; + vm.blobhashes(blobhashes); + } + /// @notice Prepares a verifier test case. /// @dev This is useful to make property-based fuzz testing work better by requiring less /// constraints on the generated input. diff --git a/src/test/identity-manager/WorldIDIdentityManagerUpgrade.t.sol b/src/test/identity-manager/WorldIDIdentityManagerUpgrade.t.sol index 012c4d4..fbcf4a8 100644 --- a/src/test/identity-manager/WorldIDIdentityManagerUpgrade.t.sol +++ b/src/test/identity-manager/WorldIDIdentityManagerUpgrade.t.sol @@ -74,6 +74,6 @@ contract WorldIDIdentityManagerUpdate is WorldIDIdentityManagerTest { vm.expectRevert("Function must be called through delegatecall"); // Test - managerImpl.upgradeToAndCall(mockUpgradeAddress, initCall); + managerImplV2.upgradeToAndCall(mockUpgradeAddress, initCall); } } diff --git a/src/test/mock/SequencerVerifier.sol b/src/test/mock/SequencerVerifier.sol index 919dbcc..b668af0 100644 --- a/src/test/mock/SequencerVerifier.sol +++ b/src/test/mock/SequencerVerifier.sol @@ -14,4 +14,13 @@ contract SequencerVerifier is ITreeVerifier { function verifyProof(uint256[8] calldata proof, uint256[1] calldata input) external pure { require(proof[0] % 2 == 0 && proof[1] % SNARK_SCALAR_FIELD == input[0], "Invalid Proof"); } + + function verifyProof( + uint256[8] calldata proof, + uint256[2] calldata, + uint256[2] calldata, + uint256[6] calldata input + ) external pure { + require(proof[0] % 2 == 0 && proof[1] % SNARK_SCALAR_FIELD == input[0], "Invalid Proof"); + } } diff --git a/src/test/mock/SimpleVerifier.sol b/src/test/mock/SimpleVerifier.sol index c2f2cc1..4e89bb5 100644 --- a/src/test/mock/SimpleVerifier.sol +++ b/src/test/mock/SimpleVerifier.sol @@ -23,6 +23,20 @@ contract SimpleVerifier is ITreeVerifier { emit VerifiedProof(batchSize); } } + + function verifyProof( + uint256[8] memory proof, + uint256[2] memory, + uint256[2] memory, + uint256[6] memory input) + external { + bool result = proof[0] % 2 == 0; + + input[0] = 0; + if (result) { + emit VerifiedProof(batchSize); + } + } } library SimpleVerify { diff --git a/src/test/mock/WorldIDIdentityManagerImplMock.sol b/src/test/mock/WorldIDIdentityManagerImplMock.sol index f4a2cd0..af99121 100644 --- a/src/test/mock/WorldIDIdentityManagerImplMock.sol +++ b/src/test/mock/WorldIDIdentityManagerImplMock.sol @@ -13,7 +13,8 @@ contract WorldIDIdentityManagerImplMock is WorldIDIdentityManagerImplV1 { } /// @notice Used to initialize the new things in the upgraded contract. - function initialize(uint32 data) public virtual reinitializer(3) { + /// The reinitializer value is high not to conflict with subsequent implementations. + function initialize(uint32 data) public virtual reinitializer(255) { _someMoreData = data; } diff --git a/src/utils/UnimplementedTreeVerifier.sol b/src/utils/UnimplementedTreeVerifier.sol index b9dc091..749ca03 100644 --- a/src/utils/UnimplementedTreeVerifier.sol +++ b/src/utils/UnimplementedTreeVerifier.sol @@ -23,4 +23,18 @@ contract UnimplementedTreeVerifier is ITreeVerifier { function verifyProof(uint256[8] calldata /*proof*/, uint256[1] calldata /*input*/) external pure { revert UnsupportedOperation(); } + + /// Verify an uncompressed Groth16 proof. + /// @notice Reverts with InvalidProof if the proof is invalid or + /// with PublicInputNotInField the public input is not reduced. + /// @notice There is no return value. If the function does not revert, the + /// proof was successfully verified. + function verifyProof( + uint256[8] calldata, + uint256[2] calldata, + uint256[2] calldata, + uint256[6] calldata + ) public pure { + revert UnsupportedOperation(); + } }