Skip to content

Cairo

Note: The structs and APIs in Juvix are used for writing circuits, including compliance circuit and resource logic circuits. Meanwhile, the Elixir structs and APIs are utilized outside of the circuit to build the Resource Machine.

Primitive choices

Baisc finite field and curve

The basic data type in Cairo is felt252, which is a large prime number, currently equal to \(P = 2^{251} + 17 * 2^{192}+1\).

We're using the native STARK curve based on the felt252 in Cairo RM.

Hash Funtion

The built-in Poseidon hash and Pedersen hash of Cairo VM are used to construct the Cairo RM.

Poseidon Hash

The Poseidon hash is widely used to construct other primitives like Merkle trees, commitments, nullifiers, etc.

Poseidon Hash in Juvix

poseidonHash1 (x : Field) : Field

poseidonHash2 (x y : Field) : Field

poseidonHashList (list : List Field) : Field

Poseidon Hash in Elixir

@spec poseidon_single(list(byte())) :: list(byte())

@spec poseidon(list(byte()), list(byte())) :: list(byte())

@spec poseidon_many(list(list(byte()))) :: list(byte())

Pedersen Hash

The Pedersen hash is used to calculate the kind of resource in the compliance circuit by passing resource.logic and resource.label.

pedersenHashToCurve (x y : Field) : Ec.Point

Commitment

@spec commitment(ShieldedResource.t()) :: binary()
@doc "A commitment to the given resource."
def commitment(resource = %ShieldedResource{}) do
psi =
    Cairo.poseidon_many([
    Constants.prf_expand_personalization_felt(),
    Constants.felt_zero(),
    resource.rseed,
    resource.nonce
    ])

rcm =
    Cairo.poseidon_many([
    Constants.prf_expand_personalization_felt(),
    Constants.felt_one(),
    resource.rseed,
    resource.nonce
    ])

Cairo.poseidon_many([
    resource.logic,
    resource.label,
    resource.data,
    resource.npk,
    resource.nonce,
    psi,
    resource.quantity,
    resource.eph,
    rcm
])
end

Nullifier

@spec nullifier(ShieldedResource.t()) :: binary()
@doc """
The nullifier of the given resource.
"""
def nullifier(resource = %ShieldedResource{}) do
psi =
    Cairo.poseidon_many([
    Constants.prf_expand_personalization_felt(),
    Constants.felt_zero(),
    resource.rseed,
    resource.nonce
    ])

Cairo.poseidon_many([
    resource.npk,
    resource.nonce,
    psi,
    commitment(resource)
])

Nullifier key commitment

@spec get_nk_commitment(binary()) :: binary()
@doc """
Generate the nullifier key commitment from the nulliffier key.
"""
def get_nk_commitment(nk) do
Cairo.poseidon(
    nk,
    Constants.felt_zero()
)
|> :binary.list_to_bin()
end

Binding signature for the delta proof

@spec cairo_binding_sig_sign(list(list(byte())), list(list(byte()))) ::
        list(byte())
def cairo_binding_sig_sign(_private_key_segments, _messages)

@spec cairo_binding_sig_verify(
        list(list(byte())),
        list(list(byte())),
        list(byte())
    ) :: boolean()
def cairo_binding_sig_verify(_pub_key_segments, _messages, _signature),

Verifiable Encryption

@spec encrypt(list(list(byte())), list(byte()), list(byte()), list(byte())) ::
        list(byte())
def encrypt(messages, pk, sk, nonce)

@spec decrypt(list(list(byte())), list(byte())) :: list(byte())
def decrypt(cihper, sk)

The encrypt in Juvix circuit can be found here.

Encoding choices

The basic types in Anoma Shielded RM(Elixir) have only one canonical representation, which is binary(). The basic felt252 in Anoma RM(Elixir) is a binary() type with a length of 32 bytes or 256 bits.

ShieldedResource

ShieldedResource in Anoma RM(Elixir)

typedstruct enforce: true do
    # resource logic
    field(:logic, <<_::256>>, default: <<0::256>>)
    # fungibility label
    field(:label, <<_::256>>, default: <<0::256>>)
    # quantity
    field(:quantity, <<_::256>>, default: <<0::256>>)
    # arbitrary data
    field(:data, <<_::256>>, default: <<0::256>>)
    # ephemerality flag
    field(:eph, bool(), default: false)
    # resource nonce
    field(:nonce, <<_::256>>, default: <<0::256>>)
    # commitment to nullifier key
    field(:nk_commitment, <<_::256>>, default: <<0::256>>)
    # random seed
    field(:rseed, <<_::256>>, default: <<0::256>>)
end

ShieldedResource in circuits(Juvix)

type Resource :=
  mkResource {
    logic : Field;
    label : Field;
    quantity : Field;
    data : Field;
    eph : Bool;
    nonce : Field;
    np_commitment : Field;
    rseed : Field
  };

ProofRecord

The ProofRecord does not explicitly contain a verifying key since it is already embedded in the proof by the STARK proving system. We provide an additional API to obtain the hash of the verifying key for out-of-circuit verification purposes.

typedstruct enforce: true do
    field(:proof, binary(), default: <<>>)
    field(:public_inputs, binary(), default: <<>>)
end

@spec get_cairo_program_hash(ProofRecord.t()) :: binary()
def get_cairo_program_hash(proof_record)

ComplianceWitness(compliance private inputs)

typedstruct enforce: true do
    # Input resource
    field(:input_resource, Resource.t())
    # Input resource merkle path
    field(:merkle_proof, CommitmentTree.Proof.t())
    # Nullifier key of the input resource
    field(:input_nf_key, <<_::256>>, default: <<0::256>>)
    # Ephemeral root
    field(:eph_root, <<_::256>>, default: <<0::256>>)
    # Output resource
    field(:output_resource, Resource.t())
    # Random value in delta proof(binding signature)
    field(:rcv, <<_::256>>, default: <<0::256>>)
end

@spec to_json_string(ComplianceWitness.t()) :: binary()
@doc """
Generate the compliance input json
"""
def to_json_string(input) do

ComplianceInstance(compliance public inputs)

typedstruct enforce: true do
    # Input Resource nullifier
    field(:nullifier, <<_::256>>, default: <<0::256>>)
    # Output Resource commitment
    field(:output_cm, <<_::256>>, default: <<0::256>>)
    # Resource commitment Merkle tree root
    field(:root, <<_::256>>, default: <<0::256>>)
    # Resource delta
    field(:delta_x, <<_::256>>, default: <<0::256>>)
    field(:delta_y, <<_::256>>, default: <<0::256>>)
    # Input Resource logic
    field(:input_logic, <<_::256>>, default: <<0::256>>)
    # Output Resource logic
    field(:output_logic, <<_::256>>, default: <<0::256>>)
end

LogicInstance(resource logic public inputs)

typedstruct enforce: true do
    # nullifier of input resource or commitment of output resource
    field(:tag, <<_::256>>, default: <<0::256>>)
    # The merkle root of resources in current action(execution context)
    field(:root, <<_::256>>, default: <<0::256>>)
    # Ciphertext
    field(:cipher, list(<<_::256>>), default: [])
    # Custom public inputs
    field(:app_data, list(<<_::256>>), default: [])
end

ShieldedAction

The roots, commitments, and nullifiers are all integral components of the ProofRecord object, thus they are not explicitly listed in the ShieldedAction. Nevertheless, we do provide APIs to retrieve them instead.

typedstruct enforce: true do
    field(:logic_proofs, list(ProofRecord.t()), default: [])
    field(:compliance_proofs, list(ProofRecord.t()), default: [])
end

@spec verify(ShieldedAction.t()) :: boolean()
@doc """
verify all the compliance and resource logic proofs, and other correspondence checks
"""
def verify(action)

ShieldedTransaction

typedstruct enforce: true do
    field(:roots, list(<<_::256>>), default: [])
    field(:commitments, list(<<_::256>>), default: [])
    field(:nullifiers, list(<<_::256>>), default: [])
    field(:actions, list(ShieldedAction.t()), default: [])

    # When the tx is not finalized(balanced), the delta is the collection of private keys
    # to generate the delta proof(binding signature).
    # The delta should be the binding signature once the tx is finalized.
    field(:delta, binary(), default: <<>>)
end

@spec verify(ShieldedTransaction.t()) :: boolean()
@doc """
verify all the actions and delta proof
"""
def verify(transaction)

@spec resource_existence_check(ShieldedTransaction.t(), any()) :: boolean()
@doc "check the existence of merkle roots"
def resource_existence_check(transaction, storage)

@spec nullifier_check(ShieldedTransaction.t(), any()) :: boolean()
@doc "check the non-existence of nullifiers"
def nullifier_check(transaction, storage)

Other decisions

Nullifiers and commitments correspondence check between compliances and resource logics

To support varying numbers of resources in the Action and prevent information leakage in shielded RM, we are constructing a compact Merkle tree within the resource logic to verify nullifier and commitment correspondence. The resource merkle path in the logic circuit proves the existence of resources in the current execution context, while the out-of-circuit root check occurs in Action.verify().

typedstruct enforce: true do
    # The resource merkle tree
    field(:tree, CommitmentTree.t())
    # The merkle root of resources in ation
    field(:root, <<_::256>>)
    # The tree leaves: help find the target index
    field(:leaves, list(<<_::256>>))
end

@spec construct(CommitmentTree.Spec.t(), list(binary())) :: ResourceTree.t()
@doc """
construct the tree from leaves
"""
def construct(spec, leaves)

@spec prove(ResourceTree.t(), binary()) :: list() | nil
@doc """
generate the merkle path for the leaf
"""
def prove(tree, leaf)

Some juvix circuits in practice

Compliance circuit

A trivial resource logic circuit

Encryption circuit(should be an recommended function using in resource logics)