Anonymity Revokers

Anonymity Revoker (AR in short) is a role that helps protecting the Shielder from bad actors. The main idea is that an AR would deanonymize all actions withing the Shielder of a recognized bad actor. The typical scenario we have in mind here is as follows:

  1. Illicit funds are detected on chain that have been used to interact with the Shielder. For instance the funds come from a well known hack.

  2. A specific Shielder transaction, say a deposit, is determined to be using illicit funds.

  3. A governance process decides whether to deanonymize the user behind this deposit. And if it decides YES, then a request to deanonymize is sent to the AR.

  4. The AR reveals the transaction, and as a consequence (because of how the solution is built technically) this allows everyone to see the details of all other transactions performed by the illicit user, as if they were not using the Shielder at all.

Anonymity Revoker Key

The AR holds a private key AR_sk for asymmetric encryption and the corresponding public key AR_pk is a known parameter of the Shielder. The encryption scheme in use must be snark-friendly. Indeed, each transaction will need to include proofs of statements of the form

Enc(AR_pk, m) = c

where m is some private input and c is public input.

User Transactions

For a ZK-ID id we define by key(id) a procedure key: Scalar -> Scalar that produces a symmetric encryption key out of the id. The key map should be one-way. The simplest example would be to use key(id) = hash(id) but there might be other constraints that might force use to use a different key derivation.

Whenever a user makes a transaction tx in the shielder (that is otherwise be fully anonymous) it includes two additional encrypted pieces of data in it:

  • mac = (r, hash(r, key(id))) : (Scalar, Scalar) -- the HMAC "signature" of the user under a random nonce. This is to be able to identify the user's transactions among all transaction knowing key(id).

  • e_key = Enc(AR_pk, key(id)) -- this is the encrypted key of the user

  • e_op = SymEnc(key(id), op_priv) -- this is an encryption of the private part of the operation op_priv: OpPriv the user is performing on its account, encrypted using a symmetric (snark-friendly) encryption scheme (see SNARK-friendly Symmetric Encryption) using key key(id)

Modification to Transactions

Note that adding the encryptions to the transaction requires some changes to what we introduced in Notes and Accounts -- for completeness we repeat the parts that change including the necessary modifications. The main idea though is simple: in the update_note transaction we need to put constraints checking the correctness of encryption and forming the mac.

relation R_update_note

inputs:
    - op_pub: OpPub // the public part of  the operation to be performed
    - h_note_new: Scalar,
    - merkle_root: Scalar,
    - nullifier_old: Scalar,
    - mac: (Scalar, Scalar),
    - e_key: Ciphertext,
    - e_op: Scalar^n
    
witnesses:
    - note_new, note_old: Note,
    - trapdoor_new, trapdor_old: Scalar
    - nullifier_new: Scalar,
    - proof: MerkleProof
    - op_priv: OpPriv
    - id: Scalar
        
constraints:
    1. h_note_new = hash(note_new)
    2. note_new = Note { id, trapdoor_new, nullifier_new, h_acc_new }
    3. h_note_old = hash(note_old)
    4. note_old = Note { id, trapdoor_old, nullifier_old, h_acc_old }
    5. verify_merkle_proof(merkle_root, h_note_old, proof)
    6. op = combine(op_pub, op_priv)
    7. R_update_account(op, h_acc_old, h_acc_new)
    8. k = key(id)
    9. mac = (r, hash(r, k))
    10. e_key = Enc(AR_pk, k)
    11. e_op = SymEnc(k, op_priv)
transaction update_note

inputs:
    - op_pub: OpPub,
    - proof: ZkProof,
    - nullifier_old: Scalar,
    - merkle_root: Scalar,
    - h_note_new: Scalar,
    - mac: (Scalar, Scalar),
    - e_key: Ciphertext,
    - e_op: Scalar^n
    
execution:
    - shielder.public_exec(op_pub)
    - assert: merkle_root is the current or historical root of shielder.notes
    - assert: nullifier_old not in shielder.nullifier_set
    - v = ZK-Verifier(R_update_note) // initialize verifier for the relation R_update_note
    - assert: v.verify(proof; (op_pub, id, h_note_new, merkle_root, nullifier_old))
    - shielder.notes.add_leaf(h_note_new)
    - shielder.nullifier_set.add(nullifier_old)

Revoking Anonymity

In case the anonymity revocation procedure is triggered on a transaction tx. The AR publicly decrypts the e_key field of the transaction tx revealing the key which the user uses to encrypt all the operations related to its account. Now, by scanning all the shielder transactions from start, each user can decrypt all transactions from the user who created tx . The way it is done is by inspecting the mac attached to each transaction, if mac = (m_0, m_1) then by checking the condition m_1 == hash(m_0, key) it is possible to verify if the transaction came from the deanonymized user. Using this idea, one can find

  • the transaction that created the first note of this user

  • one can find each operation op that was applied to this users' account, this is by combining op_pub that is public and op_priv that is encrypted using key

Using the above one can recover the complete history of this users' account and in particular recover the current state, and see all the new transactions of the user in the plain.

Last updated