Miden é uma solução de implementação ZKVM totalmente baseada em tecnologia.
Em sua camada básica, a prova stark é gerada com base na biblioteca ZKP e a prova será verificada. A parte pontilhada na Figura 1 a seguir é a principal função implementada pelo Miden. Consiste principalmente em três componentes.
1. Um conjunto de compiladores de sintaxe léxica, que é o analisador léxico e o analisador de sintaxe na Figura 1. Eles podem programar as instruções de montagem definidas por Miden em codeblock ou o opcode e o valor op contidos no bloco.
2. Um conjunto de executores de instruções, que é o executor da Figura 1. Ele executará o codeblock e o opcode e op value contidos no bloco de acordo com as regras definidas. E os resultados executados serão o trace de execução usado para gerar a prova.
3. Um conjunto de AIR (Abstract Intermediate Representation), satisfazendo os requisitos da prova total, ou seja, o AIR da Figura 1. Conduz restrições ao processo de execução da Máquina Virtual Miden.
Figura 1. Componentes do projeto Miden
As restrições do AIR se dividem em pilha e decodificador: A Figura 2 mostra as restrições da pilha, distribuídas com uma pilha com profundidade superior de 8 na inicialização.
O max_depth é incrementado conforme necessário caso a distribuição inicial possa ser excedida durante a execução, dependendo da necessidade do programa, mas não deve ultrapassar a profundidade máxima de 16, caso contrário, um erro é reportado.
Figura 2: Coluna de restrição de pilha
Como pode ser visto abaixo, a Figura 3 mostra as restrições do decodificador. Entre essas restrições, op_counter, op_SPONGE, CF_OP_bits, LD_OP_BITS e hd_OP_bits são comprimentos de coluna fixos.
O op_sponge é usado para restringir a ordem e a exatidão das instruções. O cf_op_bits restringe o flow_ops de 3 bits. Os ld_op_bits e hd_op_bits restringem os 5 bits baixos e os 2 bits altos de user_ops, respectivamente.
Os ld_op_bits e hd_op_bits se combinam para formar um user_op executável que também é usado como um seletor para cada etapa das restrições de estado da pilha!
Figura 3: Coluna de restrição do decodificador
Esta seção mostra a lógica Miden simples para ilustrar a execução da máquina virtual e a geração do rastreamento de execução de stark.
O seguinte segmento de código 1 é o segmento a ser executado: Sua lógica de execução é empurrar 3 e 5 na pilha. Em seguida, leia a bandeira da fita.
Verifique se o sinalizador é 1 ou 0. Se for 1, execute a "lógica if.true" para pegar os dois números 3 e 5 que são colocados na pilha, some-os, resultando em 8, e empurre 8 no pilha. Se for 0, execute a "lógica else" para pegar os números 3 e 5 e multiplicá-los, resultando em 15 e, em seguida, empurre 15 na pilha.
begin push.3 push.5 read if.true add else mul end end
O código de instrução final analisado pelo analisador léxico e pelo analisador de sintaxe de Miden é mostrado no seguinte segmento de código 2:
begin noop noop noop noop noop noop noop push(3) noop noop noop noop noop noop noop push(5) read noop noop noop noop noop noop noop noop noop noop noop noop noop, if assert add noop noop noop noop noop noop noop noop noop noop noop noop noop else not assert mul noop noop noop noop noop noop noop noop noop noop noop noop end]
A Figura 4 mostra o processo da máquina virtual executando o segmento de código 2.
A parte do meio é o fluxograma do executor executando opcodes. As linhas pontilhadas à esquerda referem-se ao rastreamento do decodificador gerado na execução do código. As linhas pontilhadas à direita referem-se ao rastreamento de pilha gerado na execução do código.
Na figura, o executor conduz as execuções bloco a bloco de acordo com o bloco de código. Neste exemplo, um bloco span é executado primeiro.
Em seguida, a estrutura if-else-end na etapa 32 é executada para inserir o bloco switch e pressionar o hash de esponja gerado pela última execução do bloco span anterior no ctx_stack. Depois que o bloco switch for executado, coloque-o na esponja na etapa 49.
Figura 4: a figura de mudança de estado da VM executando o opcode
Nota: Este documento descreve a versão mais recente da ramificação principal para projetos Miden a partir de 2022-05-27. Até agora, a próxima ramificação de Miden fez muito redesenho de instruções e o AIR implementou apenas algumas restrições.
Nesta seção, mostraremos as condições de restrição das principais instruções de operação do usuário, entre as quais, old_stack_x refere-se ao valor armazenado na posição x da pilha antes da execução da instrução, new_stack_x refere-se ao valor armazenado na posição x do pilha após a execução da instrução, “–>” refere-se à cópia do valor do lado esquerdo da pilha para a direita e “==” refere-se à restrição da equação. As restrições da pilha são relativamente simples e nenhuma explicação é necessária.
Restrição:
new_stack_0 == (condition * x) + ((1 - condition) * y) old_stack_0 == x old_stack_1 == y old_stack_2 == condition old_stack_3 --> new_stack_1 old_stack_n. --> new_stack_n-2 aux_0 == condition.is_binary == true
Se a condição for 1, x estará no topo da pilha. Se a condição for 0, y está no topo da pilha.
Restrição:
old_stack_0 + old_stack_1 == new_stack_0 old_stack_2 --> new_stack_1 old_stack_n. --> new_stack_n-1
Restrição:
old_stack_0 * old_stack_1 == new_stack_0 old_stack_2 --> new_stack_1 old_stack_n. --> new_stack_n-1
Restrição:
old_stack_0 * new_stack_0 == 1 old_stack_1 --> new_stack_1 old_stack_n. --> new_stack_n
Restrição:
old_stack_0 + new_stack_0 == 0 old_stack_1 --> new_stack_1 old_stack_n. --> new_stack_n
Restrição:
aux_0: make sure old_stack_0 is binary 1-old_stack_0 == new_stack_0 old_stack_1 --> new_stack_1 old_stack_n. --> new_stack_n
Restrição:
aux_0: make sure old_stack_0 is binary aux_1: make sure old_stack_1 is binary old_stack_0 * old_stack_1 == new_stack_0 old_stack_2 --> new_stack_1 old_stack_n. --> new_stack_n-1
Restrição:
aux_0: make sure old_stack_0 is binary aux_1: make sure old_stack_1 is binary 1- (1-old_stack_0) *(1- old_stack_1) == new_stack_0 old_stack_2 --> new_stack_1 old_stack_n. --> new_stack_n-1
Função de limite de hash que satisfaça o protocolo de função de hash
Ocupando 6 registros.
Restrição:
hash(old_stack0,old_stack1, old_stack2, old_stack3, old_stack3, old_stack5) == (new_stack0,new_stack1, new_stack2, new_stack3, new_stack3, new_stack5) old_stack_6 --> new_stack_6 old_stack_n. --> new_stack_n
Restrição:
aux_0 == new_stack_0 * diff == 0 new_stack_0 == 1- (old_stack_1-old_stack_2)*old_stack_0 old_stack_0 == inv(old_stack_1-old_stack_2) old_stack_3 --> new_stack_1 old_stack_n. --> new_stack_n-2
Compare de acordo com o ciclo de comprimento de bit dos dois números a serem comparados, como:
R: [0,1,0,1]
B: [0,0,1,1]
As instruções de comparação precisam ser executadas quatro vezes.
Restrição:
// layout of first 8 registers // [pow, bit_a, bit_b, not_set, gt, lt, acc_b, acc_a] // x and y bits are binary new_stack_1 == x_big_idx(lg lt flag) (type:binary) new_stack_2 == y_big_idx(lg lt flag) (type:binary) bit_gt = x_big_idx*(1-y_big_idx) bit_lt = y_big_idx*(1-x_big_idx) // comparison trackers were updated correctly new_stack_3 = not_set_idx new_stack_4(gt) == old_stack_4(gt_idx) + bit_gt * not_set_idx new_stack_5(lt) == old_stack_5(lt_idx) + bit_lt * not_set_idx // binary representation accumulators were updated correctly old_stack_0 = POW2_IDX old_stack_6 = Y_ACC_IDX old_stack_7 = X_ACC_IDX new_stack_6 == old_stack_6 + x_big_idx * old_stack_0(power_of_two); new_stack_7 == old_stack_7 + y_big_idx * old_stack_0(power_of_two); // when GT or LT register is set to 1, not_set flag is cleared not_set_check = (1-old_stack_5(lt_idx))*(1-old_stack_4(gt_idx)) not_set_check == new_stack_3(not_set_idx) // power of 2 register was updated correctly new_stack[POW2_IDX] * two == old_stack[POW2_IDX] old_stack_8 --> new_stack_8 old_stack_n --> new_stack_n
new_stack_0 == old_stack_0 ... new_stack_n-1 == old_stack_n-1 old_stack_0 --> new_stack_n old_stack_k --> new_stack_n+k
new_stack_0 == old_stack_1 new_stack_1 == old_stack_0 old_stack_2 --> new_stack_2 old_stack_n --> new_stack_n
new_stack_0 == old_stack_3 new_stack_1 == old_stack_0 new_stack_2 == old_stack_1 new_stack_3 == old_stack_2 old_stack_4 --> new_stack_4 old_stack_n --> new_stack_n
Nesta seção, mostraremos as condições de restrição da instrução principal da operação Flow.
Para as restrições de cf_op_bits, ld_op_bits e hd_op_bits
Restrição 1: cada bit só pode ser 0 ou 1.
Restrição 2: quando op_counter não é 0, ld_ops e hd_ops não podem ser ambos 0.
Restrição 3: quando cf_op_bits for hacc, o estado op_counter aumentará em 1.
Restrição 4: As instruções BEGIN, LOOP, BREAK e WRAP precisam ser alinhadas de acordo com 16.
Restrição 5: As instruções TEND e FEND precisam ser alinhadas de acordo com 16.
Restrição 6: A instrução PUSH precisa ser alinhada de acordo com 8.
cf_op_bits == binary ld_op_bits == binary hd_op_bits == binary // ld_ops and hd_ops can be all 0s at the first step, but cannot be all 0s at any other step op_counter * binary_not(ld_bit_prod) * binary_not(hd_bit_prod) == 0 if op_counter != 0 (1-ld_bit_prod)*(1- hd_bit_prod) == 0 if is_hacc != true op_counter = current.op_counter // BEGIN, LOOP, BREAK, and WRAP are allowed only on one less than multiple of 16 // TEND and FEND is allowed only on multiples of 16 // PUSH is allowed only on multiples of 8
O Hacc, assim como o flowOps, sempre causará a mudança de estado da esponja ao conduzir esta instrução, e por isso é necessário realizar as restrições.
Restrição:
mds(sbox(old_sponge) + user_opcode(7bits) + op_value(push_flag=1)) == sbox(inv_mds(new_sponge))
Como a restrição do verdadeiro final da ramificação de if, ela é dividida em duas partes:
Restrição 1: é a restrição do estado esponja.
O valor no topo da pilha é igual a new_sponge_0. A esponja após a última execução da ramificação verdadeira de if é igual a new_sponge_1. New_sponge_3 é igual a 0.
Restrição 2: é a restrição de ctx_stack. O valor no topo da pilha é igual a new_sponge_0. Qualquer outro elemento na pilha se moverá uma posição para o topo da pilha.
Restrição 3: é a restrição de loop_stack. O estado de loop_stack é imutável.
Restrição:
parent_hash = current.ctx_stack()[0] block_hash = current.op_sponge()[0] new_sponge = next.op_sponge() parent_hash == new_sponge[0] block_hash == new_sponge[1] new_sponge[3] == 0 // make parent hash was popped from context stack ctx_stack_start = OP_SPONGE_WIDTH + 1 // 1 is for loop image constraint ctx_stack_end = ctx_stack_start + ctx_stack_current.len() ctx_result = &mut result[ctx_stack_start..ctx_stack_end] ctx_stack_current_0 = ctx_stack_next_1 ctx_stack_current_1 = ctx_stack_next_2 ... ctx_stack_current_n = ctx_stack_next_n+1 // loop_stack copy copy range: ctx_stack_end to. ctx_stack_end + loop_stack_current.len() loop_stack_current_0 = loop_stack_next_0 ... loop_stack_current_n = loop_stack_next_n
Como a restrição do fim do ramo falso de if, ela é dividida em duas partes:
Restrição 1: é a restrição do estado esponja. O valor no topo da pilha é igual a new_sponge_0.
A esponja após a última execução da ramificação verdadeira de if é igual a new_sponge_2. new_sponge_3 é igual a 0.
Restrição 2: é a restrição de ctx_stack. O valor no topo da pilha é igual a new_sponge_0. Qualquer outro elemento na pilha se moverá uma posição para o topo da pilha.
Restrição 3: é a restrição de loop_stack. O estado de loop_stack permanece inalterado.
Restrição:
parent_hash = current.ctx_stack()[0] block_hash = current.op_sponge()[0] new_sponge = next.op_sponge() parent_hash == new_sponge[0] block_hash == new_sponge[2] new_sponge[3] == 0 // make parent hash was popped from context stack ctx_stack_start = OP_SPONGE_WIDTH + 1 // 1 is for loop image constraint ctx_stack_end = ctx_stack_start + ctx_stack_current.len() ctx_result = &mut result[ctx_stack_start..ctx_stack_end] ctx_stack_current_0 = ctx_stack_next_1 ctx_stack_current_1 = ctx_stack_next_2 ... ctx_stack_current_n = ctx_stack_next_n+1 // loop_stack copy copy range: ctx_stack_end to. ctx_stack_end + loop_stack_current.len() loop_stack_current_0 = loop_stack_next_0 ... loop_stack_current_n = loop_stack_next_n