Skip to main content

Identity Protocol smart contracts reference

Rarimo builds upon the basic Iden3 smart contracts and adapts them for cross-chain usage.

note

Currently, identity smart contracts are available only for EVM-compatible chains.

Identity protocol uses two types of smart contracts:

  • State Contracts hold iden3 identity states(sparse Merkle trees) that are required for on-chain ZKP verifications;
  • Verifiers are responsible for verifying zero-knowledge proofs on-chain. They fetch identity states for the State Contracts to check that the credential used to generate the proof wasn't revoked by the issuer;

State contracts

Rarimo uses two types of state contracts:

  • StateV2 holds the complete history of identity states. A single instance is deployed to the Rarimo chain and acts as a single source of truth;
  • LightweightState implements the state contract interface but doesn't hold the entire history. The identity states are updated on demand and are secured by a threshold signature from Rarimo's oracles. One instance of this contract is deployed to every supported target chain;

StateV2

Deployments

ChainChainIDAddress
Rarimo Mainnet2014110x5ac96945a771d417B155Cb07A3D7E4b8e2F33FdE
Rarimo Mainnet-Beta2014110x753a8678c85d5fb70A97CFaE37c84CE2fD67EDE8

Interface

function transitState(
uint256 id_,
uint256 oldState_,
uint256 newState_,
bool isOldStateGenesis_,
uint256[2] memory a_,
uint256[2][2] memory b_,
uint256[2] memory c_
) external

event StateTransited(
uint256 gistRoot, // global identity state tree root after the transition.
uint256 indexed id, // identity which called the function
uint256 state, // new state of the identity
uint256 timestamp, // block timestamp of this block
uint256 blockNumber // height of this block
);
  • transitState - the primary function of the contract that performs state transitions. It receives the identity, two states (old and new), a boolean whether it is genesis or not, and ZK Proof, which proves that the state transition from old to new state of this identity is valid and fair;
  • StateTransited - Emited after every state transition. Rarimo's oracles use it to ingest the state transitions;

See State.sol for the full implementation.

LightweightState

Deployments

ChainChainIDAddress
Ethereum10xF3e2491627b9eF3816A4143010B39B2B67F33E55 (Etherscan)
Polygon1370xf9bA419ad9c82451d31d89917db61253a1e46B3C (Polygonscan)
Sepolia111551110x4EaEdFbD2156f5D77b11c4fE073032c4D90Dd315 (Etherscan)
Goerli50x0F08e8EA245E63F2090Bf3fF3772402Da9c047ee (Etherscan)

Interface

contract LightweightState is ILightweightState, UUPSSignableUpgradeable, Signers {
address public override sourceStateContract;
string public override sourceChainName;
bytes32 public override identitiesStatesRoot;

/* ... */

function signedTransitState(
bytes32 newIdentitiesStatesRoot_,
GistRootData calldata gistData_,
bytes calldata proof_ // TSS from Rarimo's oracles
) external override {
/* ... */
}
}

  • sourceStateContract - address of the contract on the source chain (Rarimo in our case)
  • sourceChainName - the chain where the source contract is deployed (Rarimo)
  • identitiesStatesRoot - another tree root (made specifically for the Lightweight contract) that stores the issuer ID, its state, and timestamp of state change (gist doesn't have timestamps).
  • signedTransitState() - state transition function that requires TSS instead of ZKP. It verifies the signature and changes the roots to the ones provided. After that, the user can submit a ZKP using the new root on the destination chain.

See LightweightState.sol for the full implementation.

Verifier contracts

The primary purpose of verifier contracts is to verify ZK proof and then execute business logic specific to a specific DApp. Rarimo has two types of verifier contracts: BaseVerifier and IdentityVerifier.

BaseVerifier

BaseVerifier is an abstract contract you can inherit and add custom business logic.

It implements some of the core functions:

abstract contract BaseVerifier is IBaseVerifier, OwnableUpgradeable, UUPSUpgradeable {
/* ... */

function _transitState(TransitStateParams calldata transitStateParams_) internal {
// ...
}

function _verify(
string memory queryId_,
ProveIdentityParams calldata proveIdentityParams_
) internal view virtual {
// ...
}

/* ... */

}

This contract uses the zkpQueriesStorage contract to store query IDs and the lightweightState contract's address.

  • _transitState(TransitStateParams calldata transitStateParams_) - calls the lightweightState contract from zkpQueriesStorage and transfers the state.
  • _verify(string memory queryId_, ProveIdentityParams calldata proveIdentityParams_) - verifies the proof, using the QueryValidator for stated queryId_

Using both functions, you can implement your custom business logic, and only verified identity can use it. It is also possible to restrict certain issuers, i.e., credentials from them can't be proven because they are not on the allowed list.

See BaseVerifier.sol for the full implementation.

IdentityVerifier

IdentityVerifier inherits BaseVerifier and binds the address to identity after the verification.

contract IdentityVerifier is IIdentityVerifier, BaseVerifier {
/* ... */

mapping(address => uint256) public override addressToIdentityId;

mapping(uint256 => IdentityProofInfo) internal _identitiesProofInfo;

function _proveIdentity(ProveIdentityParams calldata proveIdentityParams_) internal {
// ...
emit IdentityProved(identityId_, msg.sender);
}
}
  • addressToIdentityId - mapping of address to identity identifier;
  • _identitiesProofInfo - mapping of identity to the struct that stores address and boolean (whether the identity is proven or not);
  • _proveIdentity(ProveIdentityParams calldata proveIdentityParams_) - verifies the proof and stores the info in mentioned mappings and emits an IdentityProved event that contains the user's ID and address;

See IdentityVerifier.sol for the full implementation.

Other contracts

ZKPQueriesStorage

It is a utility contract that stores ZK-related information, such as lightweight state contract address, available queries, and their ZK-validator addresses.

contract ZKPQueriesStorage is IZKPQueriesStorage, OwnableUpgradeable, UUPSUpgradeable {
/* ... */

ILightweightState public override lightweightState;

mapping(string => QueryInfo) internal _queriesInfo;

StringSet.Set internal _supportedQueryIds;

function setZKPQuery(
string memory queryId_,
QueryInfo memory queryInfo_
) external override onlyOwner {
require(
address(queryInfo_.queryValidator) != address(0),
"ZKPQueriesStorage: Zero queryValidator address."
);

queryInfo_.circuitQuery.queryHash = getQueryHash(queryInfo_.circuitQuery);

_queriesInfo[queryId_] = queryInfo_;

_supportedQueryIds.add(queryId_);

emit ZKPQuerySet(queryId_, address(queryInfo_.queryValidator), queryInfo_.circuitQuery);
}

function removeZKPQuery(string memory queryId_) external override onlyOwner {
require(isQueryExists(queryId_), "ZKPQueriesStorage: ZKP Query does not exist.");

_supportedQueryIds.remove(queryId_);

delete _queriesInfo[queryId_];

emit ZKPQueryRemoved(queryId_);
}

/* ... */
}
  • lightweightState - stores the address of the lightweight state contract as the ILightweightState interface.
  • _queriesInfo - stores query information, namely circuit query and queryValidator address.
  • _supportedQueryIds - stores the array of available queries, concretely their IDs.
  • setZKPQuery(queryId_, queryInfo_) - adds a new query, stores its information, and emits the event ZKPQuerySet.
  • removeZKPQuery(string memory queryId_) - removes a query and its information and emits the event ZKPQueryRemoved.

See ZKPQueriesStorage.sol for the full implementation.

QueryValidator

It is responsible for ZKP verification (via calling the verifier contract). The contract takes the basic ideas and concepts from Iden3, but in addition to checking the ZKP, it verifies the Merkle proof that the issuer state is in the tree. It also allows accepting proof for a recent state (not the last one) during a certain threshold.

abstract contract QueryValidator is IQueryValidator, OwnableUpgradeable, UUPSUpgradeable {

ILightweightState public override lightweightState;
address public override verifier;

uint256 public override identitesStatesUpdateTime;

/* ... */

function verify(
ILightweightState.StatesMerkleData calldata statesMerkleData_,
uint256[] calldata inputs_,
uint256[2] calldata a_,
uint256[2][2] calldata b_,
uint256[2] calldata c_,
uint256 queryHash_
) external view virtual override returns (bool) {
require(verifier.verifyProof(inputs_, a_, b_, c_), "QueryValidator: proof is not valid");

ValidationParams memory validationParams_ = _getInputValidationParameters(inputs_);

require(
validationParams_.queryHash == queryHash_,
"QueryValidator: query hash does not match the requested one"
);

require(
validationParams_.issuerClaimAuthState == validationParams_.issuerClaimNonRevState,
"QueryValidator: only actual states must be used"
);
require(
validationParams_.issuerId == statesMerkleData_.issuerId &&
validationParams_.issuerClaimNonRevState == statesMerkleData_.issuerState,
"QueryValidator: invalid issuer data in the states merkle data struct"
);

_checkGistRoot(validationParams_.gistRoot);
_verifyStatesMerkleData(statesMerkleData_);

return true;
}

function _checkGistRoot(uint256 gistRoot_) internal view {
ILightweightState.GistRootData memory rootData_ = lightweightState.geGISTRootData(
gistRoot_
);

require(
rootData_.root == gistRoot_,
"QueryValidator: gist root state isn't in state contract"
);
}

function _verifyStatesMerkleData(
ILightweightState.StatesMerkleData calldata statesMerkleData_
) internal view {
(bool isRootExists_, bytes32 computedRoot_) = lightweightState.verifyStatesMerkleData(
statesMerkleData_
);

if (!isRootExists_) {
require(
GenesisUtils.isGenesisState(
statesMerkleData_.issuerId,
statesMerkleData_.issuerState
),
"QueryValidator: issuer state isn't in state contract and not genesis"
);
require(
statesMerkleData_.createdAtTimestamp == 0,
"QueryValidator: it isn't possible to have a state creation time at genesis state"
);
} else if (computedRoot_ != lightweightState.identitiesStatesRoot()) {
ILightweightState.IdentitiesStatesRootData
memory _identitiesStatesRootData = lightweightState.getIdentitiesStatesRootData(
computedRoot_
);

require(
_identitiesStatesRootData.setTimestamp + identitesStatesUpdateTime >
block.timestamp,
"QueryValidator: identites states update time has expired"
);
}
}

}
  • lightweightState stores the address of the Lightweight State contract via the interface.
  • verifier - address of the verifier contract, which verifies the ZKP.
  • identitesStatesUpdateTime - the period during which proofs are valid for recent states (set to one hour currently). It means that the user can provide proof for not the latest state, and it will still be valid.
  • verify verifies the ZKP (via verifier call) and Merkle proof with additional checks on ZKP, such as checking that the query hash is correct.

See QueryValidator.sol for the full implementation.

PoseidonFacade

PoseidonFacade is an Iden3 library that implements a ZK-friendly Poseidon hash function in solidity. Solidity implementation allows hashing up to 6 uint256 elements.

More information about that can be found at poseidon_gencontract.js.

library PoseidonFacade {

/* ... */

function poseidon1(uint256[1] calldata el) public pure returns (uint256) {
return PoseidonUnit1L.poseidon(el);
}

function poseidon2(uint256[2] calldata el) public pure returns (uint256) {
return PoseidonUnit2L.poseidon(el);
}

function poseidon3(uint256[3] calldata el) public pure returns (uint256) {
return PoseidonUnit3L.poseidon(el);
}

function poseidon4(uint256[4] calldata el) public pure returns (uint256) {
return PoseidonUnit4L.poseidon(el);
}

function poseidon5(uint256[5] calldata el) public pure returns (uint256) {
return PoseidonUnit5L.poseidon(el);
}

function poseidon6(uint256[6] calldata el) public pure returns (uint256) {
return PoseidonUnit6L.poseidon(el);
}

function poseidonSponge(uint256[] calldata el) public pure returns (uint256) {
return SpongePoseidon.hash(el);
}
}

It provides several functions for a variable number of elements to be hashed (poseidon1, poseidon2, ...) and poseidonSponge, which hashes the array in chunks of 6 elements.

See Poseidon.sol for the full implementation.