Cairo is a practically-efficient Turing-complete STARK-friendly CPU architecture. In this article, we introduce the CPU architecture of Cairo in terms of instruction structure and state transition, and provide some examples of instruction.
The word natively supported by Cairo CPU is a field element, where the field is some fixed finite field of characteristic
The state transition function represents a general state transition unit (because it contains the processing logic of all instruction types), and a calculation is usually decomposed into multiple continuously executed instructions. Therefore, we need to:
If the state before and after the instruction execution is consistent, the update of the state must be executed according to the following logic:
#Context: m(.).
#Input state: (pc, ap, and fp).
#Output state: (next_pc, next_ap, and next_fp).
#Compute op0.
If op0_reg == 0: // Judge the basic register of op0 according to the flag value, 0 is ap,1 is fp, and obtain the value of op0.
op0 = m(ap + off_{op0})
else:
op0 = m(fp + off_{op0})
#Compute op1 and instruction_size.
switch op1_src: // Obtain the value of op1
case 0:
instruction_size = 1 // op1 = m[[ap/fp + off_op0] +off_op1], with two-level imbedding at most.
op1 = m(op0 + off_{op1})
case 1:
instruction_size = 2 // There exist(s) immediate number(s). The instruction size is 2 words.
op1 = m(pc + off_{op1})//
#If off_{op1} = 1, we have op1 = immediate_value. // For example, [ap] = "12345678", op1 = "12345678"
case 2:
instruction_size = 1 // op1 = [fp + off_op1]
op1 = m(fp + off_{op1})
case 4:
instruction_size = 1 // op1 = [ap +off_op1]
op1 = m(ap + off_{op1})
default:
Undefined Behavior
#Compute res.
if pc_update == 4: // jnz
if res_logic == 0 && opcode == 0 && ap_update != 1: // Assign && jump && advanced ap
res = Unused // Under conditional jump, the values of res_logic, opcode and ap_update flags can only be as shown above.The res variable will not be used on this occasion.
else:
Undefined Behavior
else if pc_update = 0, 1 or 2:
switch res_logic: // The computational logic of res is:
case 0: res = op1
case 1: res = op0 + op1
case 2: res = op0 * op1
default: Undefined Behavior
else: Undefined Behavior
# Compute dst.
if dst_reg == 0:
dst = m(ap + offdst)
else:
dst = m(fp + offdst)
# Compute the new value of pc.
switch pc_update:
case 0: # The common case: [ap] = 1
next_pc = pc + instruction_size
case 1: # Absolute jump: jmp abs 123
next_pc = res
case 2: # Relative jump: jmp rel [ap - 1]
next_pc = pc + res
case 4: # Conditional relative jump (jnz): jmp rel [ap - 1] if [fp - 7] = 0
next_pc =
if dst == 0: pc + instruction_size // If dst = 0, then pc conducts ordinary updates; otherwise, it is similar to Case 2.
else: pc + op1 // Under this circumstance, res is Unused, so pc directly conducts + op1, instead of + res.
default: Undefined Behavior
# Compute new value of ap and fp based on the opcode.
if opcode == 1:
# "Call" instruction.
assert op0 == pc + instruction_size
assert dst == fp
# Update fp.
next_fp = ap + 2 // [ap] saves the current fp value, [ap + 1] saves the pc after the call instruction; when the ret instruction is executed, it is convenient to reset fp to [ap], and then continue to execute subsequent instructions.
# Update ap.
switch ap_update:
case 0: next_ap = ap + 2 // [ap] saves the value of fp, and [ap+1] saves the instruction address after the call instruction; thus, ap increases by 2.
default: Undefined Behavior
else if opcode is one of 0, 2, 4:
# Update ap.
switch ap_update:
case 0: next_ap = ap // [fp + 1] = 5
case 1: next_ap = ap + res // advanced ap [ap] += 123
case 2: next_ap = ap + 1 // normal [fp + 1] = [ap]; ap++
default: Undefined Behavior
switch opcode: // Within the same function, the fp value remains unchanged.
case 0:
next_fp = fp
case 2:
# "ret" instruction.
next_fp = dst // The ret instruction goes with the call instruction, and assert dst == fp.
case 4:
# "assert equal" instruction.
assert res = dst
next_fp = fp
else: Undefined Behavior
As shown in Figure 1, one instruction consists of the following elements:
structure instruction :=
(off_dst : bitvec 16)
(off_op0 : bitvec 16)
(off_op1 : bitvec 16)
(flags : bitvec 15)
Where:
But in AIR, we convert them into the following form:
So the value range of
Therefore, for the coding of an instruction, we have:
Note: Instead of allocating 15 virtual columns with a length of NN to the 15-bit flags of the instruction, instead, we use one virtual column
Therefore, equation (1) can be written as:
Because the finite field's characteristic
Therefore, for the instruction itself, the following constraints should be met:
Instruction:
Bit:
Last value is zero:
Offset are in range: virtual column
The assert equal instruction can be expressed in the following syntax:
<left_handle_op> = <right_handle_op>
It ensures that both sides of the formula are equal; otherwise the execution of the program will be rejected.
The left side of the equation often comes from
Note2: Division and subtraction can be expressed as multiplication and addition with different operand orders, respectively.
An
Figure 4 shows some examples of "assert equal" instructions and the flag values for each instruction:
Figure 4. Examples of assert equal instructions
Interpret instruction [fp + 1] = 5:
The
Cairo supports relative jump (its operand represents the offset from the current
The syntax of the instruction is as follows:
#Unconditional jumps.
jmp abs <address>
jmp rel <offset>
#Conditional jumps.
jmp rel <offset> if <op> !
Figure 5 shows some examples of the
Figure 5 Examples of jump instructions
Interpret instruction jmp rel [ap +1] + [fp - 7]:
The
The syntax of the instructions are as follows:
call abs <address>
call rel <offset>
ret
Figure 6 shows some examples of
Figure 6 Examples of call instructions and ret instructions
Interpret instruction call abs [fp + 4]:
The instruction ap += <op> increments the value of
Figure 7 shows some examples of the advancing
Figure 7 Examples of advancing ap instructions
Interpret instruction ap += 123: