Circuit for updates

Tx witness data structure

From EVM

  • pub_input:
    • from
    • to
    • value
    • fee
    • good until block

Rolling current state before update

  • from_leaf, to_leaf:
    • pubkey
    • nonce
    • balance
    • merkle_path: Fr[TREE_HEIGHT]

Modified EdDSA signature: meddsa_sign(pub_input || nonce || good_until_block)

  • sig

Rolling current state after update

  • merkle_root_updated

Circuit input

Public input:

  • final_hash (see rolling_hash_cur calculation in circuit)

Witness:

  • merkle_root_old
  • merkle_root_new
  • total_fees
  • block number
  • tx_witness

Pseudocode


# Initialize variables
merkle_root_cur := merkle_root_old
total_fees_cur := 0
block_number := N
rolling_hash_cur := sha256(total_fees|| block_number || xor(merkle_root_old, merkle_root_new)) // should fit into 512 bit for input, 256 bits of output

for tx in tx_witness:

    # Add pub input to the rolling tx hash
    rolling_hash_cur = sha256(tx.pub_input || rolling_hash_cur)
    
    # Ensure from and to leaves are rooted in the current Merkle tree state
    enforce_leaves_in_merkle_tree(merkle_root_cur, [tx.from_leaf, tx.from_leaf])

    # Verify signature
    enforce( meddsa_verify(tx.pub_input || tx.from_leaf.nonce, sig) )

    # Check balance
    value_uncompressed := uncompress(tx.pub_input.value)
    enforce( value_uncompressed > 0 ) # unless enforced by uncompress()
    enforce( tx.from_leaf.balance >= value_uncompressed )
    # Overflow check unneccessary if token supply is bounded

    # Check fee
    fee_uncompressed := uncompress(tx.pub_input.fee)
    tx.from_leaf.balance = safe_sub(tx.from_leaf.balance, fee_uncompressed)

    # Update leaves (intermediate witness variables required for the next loop cycle)
    tx.from_leaf.nonce += 1
    tx.from_leaf.balance -= value_uncompressed
    tx.to_leaf.balance += value_uncompressed
    
    # Update total sum of fees (this should take only one constraint per proof)
    total_fees_cur += fee_uncompressed

    # Check post-update state
    enforce( tx.to_leaf.balance >= 0 ) # this overflow check unnecessary if total token supply is limited
    merkle_root_cur = tx.merkle_root_updated
    enforce_leaves_in_merkle_tree(merkle_root_cur, [tx.from_leaf, tx.from_leaf])
    enforce( tx.good_until_block <= block_number)
    
# OPTION save on constraints by placing bytes 
# from more than one transaction into the rolling hash 
# at once. It also saves on sha256 calls in smart-contract

for i in (0, num_txes / (512/pub_input_length)):
    pub_input_bytes = []
    for j in (0, 512/pub_input_length):
        pub_input_bytes += tx[i*(512/pub_input_length) + j].pub_input
    rolling_hash_batched = sha256(pub_input_bytes || rolling_hash_batched)

# Last Merkle state must equal input parameter
enforce( merkle_root_cur == merkle_root_new )

# Final check verifies validity of all input parameters
enforce( truncated(rolling_hash_cur) == final_hash )

Helper gadgets

enforce_leaves_in_merkle_tree(merkle_root_cur, leaves)

Non-trivial part here will be to find the intersection point of two merkle paths and properly update this node on recomputation of the root

for leaf in leaves:
    leaf_hash := pedersen_hash(leaf.pubkey || leaf.balance || leaf.nonce)
    enforce( merkle_root_cur == merkle_root(leaf_hash, leaf.merkle_path) )

uncompress(val)

return ( 10 ^ (val & (2 ^ EXPONENT_BITS - 1) ) ) * (val >> EXPONENT_BITS)

merkle_root(), sha256_truncated(), safe_sub()

Trivial.

Select a repo