Skip to content

Commit

Permalink
Add WorldID Identity Manager for EIP-4844
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
wzmuda committed Aug 27, 2024
1 parent 64ff319 commit a894eb6
Show file tree
Hide file tree
Showing 20 changed files with 1,840 additions and 55 deletions.
233 changes: 233 additions & 0 deletions src/WorldIDIdentityManagerImplV3.sol
Original file line number Diff line number Diff line change
@@ -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, ) = PRECOMPILE_POINT_EVALUATION.staticcall(input);
return success;
}
}
18 changes: 18 additions & 0 deletions src/interfaces/ITreeVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
10 changes: 10 additions & 0 deletions src/test/DeletionTreeVerifier16.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
10 changes: 10 additions & 0 deletions src/test/InsertionTreeVerifier16.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Loading

0 comments on commit a894eb6

Please sign in to comment.