ZK-ID and Registrars

Each user of the Shielder must generate a ZK-ID: a uniformly random element id: Scalar -- this is a secret the user must not reveal to anyone because it would allow to deanonymize all actions of the user in the Shielder.

There is (optionally) a special party -- a Registrar -- whose role is to register users in the shielder so that they are allowed to create accounts. The Registrar maintains an offchain database of users that are verified. There could be one Registrar, multiple or none, depending on the setup.

Each registrar holds a ECDSA "registrar key" whose public counterpart is well known (stored for instance in the shielder smart contract to verify signatures issued by the registrar). This key can be rotated, but the simplified design below does not take that into account.

Signing Up with a Registrar

A user with ZK-ID id registers offchain with a Registrar by going through an onboarding process (depending on the specific Registrar) and providing the Registrar with Com(id) -- a commitment to its id. In the simplest case the commitment can be just c = Com(id, r) = hash(id, r) where r is a random salt generated by the user (which the user needs to store). The Registrar then marks the fact or registration with a particular Com(id) in its internal database.

A registration, by default, is valid for a certain, limited amount of time. After the time passes the user needs to perform a refresh procedure with the Registrar, which might involve repeating some checks, depending on the specific Registrar.

Certifying registration

The user can receive a certificate of registration (with a particular expiration date) from a Registrar using the following procedure:

  1. The user holding id contacts the Registrar with whom it previously registered.

  2. The registrar holds c=Com(id, r) -- the commitment to the id created by the user, but it doesn't know the randomness r

  3. Denote by date the expiration timestamp of the registration.

  4. The Registrar generates a certificate for the user using the following steps:

    • Generate randomness r' (optionally this randomness might be contributed by the user instead)

    • Compute reg_payload = hash(c, date, r')

    • Compute the ECDSA signature s of payload using the "registrar key"

  5. The registrar provides the user with s, date, r' . Apart from that, the user knows c because they have generated it in the first place.

  6. The user can then use the "certificate" in the form of the signature s along with the reg_payload (whose content is hidden using r') to certify on-chain that they hold a registered id with a particular expiration date. In practice the payload is used as a public input to a particular zk-relation, and the signature s is verified in the plain.

Creating New Notes

After a user has registered its ZK-ID with a registrar it is allowed to create its note (initialize empty account) in the Shielder.

relation R_new_note

inputs:
    - reg_payload: Scalar, // payload from the Registrar
    - h_note: Scalar,
    - nullifier_create: Scalar,
    
witnesses:
    - note: Note,
    - trapdoor, nullifier: Scalar,
    - id: Scalar,
    - r: Scalar,
    - r': Scalar,
    - date: Scalar

constraints:
    1. reg_payload = hash(hash(id, r), date, r') 
    2. h_note = hash(note)
    3. note = Note { id, trapdoor, nullifier, h_acc }
    4. acc = Account::new(date)
    5. h_acc = Account::hash(acc) 
    6. nullifier_create = hash(id, NULL) // NULL is a special field element

Given the above we are ready to describe the new_note transaction

transaction new_note

inputs:
    - proof_new: ZkProof,
    - h_note: Scalar,
    - proof_id: ZkProof,
    - reg_payload: Scalar,
    - s: EcdsaSignature,
    - root: Scalar, // should be a root of the tree in SC_Registrar
    - nullifier_create: Scalar,
    
    
execution:
    - assert: the signature s under reg_payload verifies with Registrar's key
    - v_new = ZK-Verifier(R_new_note) // initialize verifier for the relation R_new_note
    - assert: v_new.verify(proof_new; (reg_payload, h_note, nullifier_create))
    - assert: root is current or historical Merkle root in SC_Registrar
    - assert: nullifier_create is not in shielder.nullifier_create_set
    - v_id = ZK-Verifier(R_verify_identity)
    - shielder.notes.add_leaf(h_note)
    - shielder.nullifier_create_set.add(nullifier_create)

Once a note is initially created the user will keep updating it (spending it and creating a new one) and the information of the user's id is persisted within the note. In the above we have also introduced nullifier_create_set -- a new storage item in shielder that allows us to make sure each id has created just one note.

In Anonymity Revokers we describe how the information about id in the note can be used to add a security mechanism to deanonymize bad actors.

ZK-ID Expiration and Refreshing

With zk ids, there is one additional check that must be introduced in the R_update_note relation in Notes and Accounts, namely that the id has not expired (current_timestamp < date in the note). In case the note has expired, the user is not allowed to transact within the shielder anymore. It has two options then:

  1. Refresh the id with the Registrar. This happens by sending a refresh_id transaction that allows to bump the expiration date based on a new certificate from the Registrar.

  2. If the user doesn't want that, or the Registrar refuses to refresh the id, then user is allowed to withdraw all the assets, but only withdrawal is possible. Such withdrawal will also cause mandatory deanonymization using the anonymity revoking mechanism, as described in Anonymity Revokers.

Last updated