WEB3DEV

Cover image for Economizando gás ao implementar circuitos em contratos inteligentes
Felipe Gueller
Felipe Gueller

Posted on

Economizando gás ao implementar circuitos em contratos inteligentes

Imagem de um quadro iluminando a paisagem atrás

Ou como a interação com circuitos ilumina as otimizações de contratos inteligentes

Tenho trabalhado na extensão de um projeto pessoal Hackathon, oBlock Qualified, e me deparei com uma estratégia interessante de economia de gás: tudo tem a ver com a forma como um contrato inteligente interage com o mesmo circuito de conhecimento zero.

Primeiro, precisamos de um pouco de contexto. O Block Qualified foi uma ideia simples que tive para incorporar uma prova de conhecimento zero em um projeto Hackathon, e trata-se de fazer exames diretamente na cadeia. Ele permite que uma parte, vamos chamar de emissor da credencial, defina um teste, basicamente um exame avaliando algum tipo de conhecimento, na blockchain. Qualquer pessoa pode tentar resolver este teste, provando que possui os conhecimentos/qualificações necessários. E tudo isso é feito usando provas de conhecimento zero para que os solucionadores nunca revelem suas soluções, apenas seu conhecimento sobre elas.

Não vamos nos concentrar em como os exames são definidos como circuitos de conhecimento zero. Esse é o tema para outro artigo. Por enquanto, vamos pensar nos requisitos de alto nível de um aplicativo como este. O que um DApp (Aplicativo Descentralizado) para resolver testes precisa? Pode ser algo como:

  • Permitir que os usuários resolvam um teste
  • Habilitar uma maneira para os usuários publicarem sua solução na blockchain
  • Impedir que usuários mal-intencionados trapaceiam

A maneira como esses requisitos são atendidos é usando provas de conhecimento zero. O usuário ou solucionador não publica suas soluções diretamente na blockchain, pois isso as tornaria públicas - qualquer um poderia trapacear! Em vez disso, eles fornecem uma prova de conhecimento da solução. Não abordaremos como os solucionadores fornecem suas soluções, mas, por enquanto, você pode consultar como os circuitos correspondentes são definidos.

Mas e se eu simplesmente copiar a transação de resolução de outra pessoa e reivindicá-la como minha? Se os circuitos de conhecimento zero servem apenas para fornecer uma prova de conhecimento, temos um problema aí! Para resolver isso, podemos usar algo semelhante a um sal criptográfico. A implementação real se parece com o seguinte:

pragma circom 2.0.0;

template Test() {
  [...]
  signal input salt;
  [...]
  // Adicione sinais ocultos para garantir que a adulteração do sal invalide a prova de sarcasmo
  signal saltSquare;
  saltSquare <== salt * salt;
}
Enter fullscreen mode Exit fullscreen mode

Ao introduzir esse sal em nosso circuito e usá-lo para formar um sinal oculto, garantimos que a mesma solução com dois sais diferentes resultará em duas provas diferentes. Agora só precisamos garantir que cada sal seja usado apenas uma vez. Podemos fazer isso definindo-o como uma entrada pública do circuito e anulando-o dentro do contrato inteligente. Isso pode parecer algo como:

pragma solidity ^0.8.7;

contract TestCreator {
    mapping (uint256 => bool) public usedSalts;

    function solveTest( uint256 testId, uint256 salt, solutionProof proof ) external {
        require(!usedSalts[salt], "Salt was already used");

        require(verifier(proof, salt), "Invalid proof");

        usedSalts[salt] = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma vez que uma prova é verificada, seu sal correspondente é anulado. Se alguém tentar copiar esta transação, ela reverterá: o sal já foi usado. Então ninguém será capaz de trapacear!

Esta foi a implementação original: ela permite que os usuários resolvam os testes e evita que os trapaceiros obtenham credenciais. Mas existe uma maneira mais barata de fazer isso e envolve pensar no papel desse sal. Sinta-se livre para parar aqui para pensar sobre o que isso pode ser.

Agora, que papel esse sal está desempenhando, exatamente? A ideia, como vimos, é que a mesma solução, usando dois sais diferentes, nos leva a duas provas diferentes. Nós rastreamos estes sais para verificar essencialmente se uma prova é uma prova nova ou se foi reutilizada.

Com essa construção, nós não estabelecemos nenhum outro requisito sobre o valor desse sal, além do fato de que ele não deve ter sido usado antes. Com esses sais sendo um uint256 (ou seja, um número entre 0 e 2 ** 256–1), podemos simplesmente gerar um número inteiro aleatório, verificar se ele não foi usado antes e gerar nossas provas de solução com ele. Bastante simples.

(Na realidade, é um pouco mais complexo, pois o valor do sal deve pertencer ao campo escalar SNARK, com um limite superior de ~2**254, mas a ideia básica ainda permanece).

Mas não ter restrições na forma como esse sal é gerado está nos custando gás. Como exatamente pode ser isso?

Bem, vamos pensar no sal novamente. Precisamos garantir que será um valor diferente para cada prova, para evitar trapaças. No momento, estamos transferindo esse fardo para o usuário (ou o DApp que eles usariam para interagir com o protocolo), e o contrato inteligente apenas verifica se eles são obrigados. A solução consiste em fazer o contrário: o contrato inteligente gera de forma determinística o sal a ser usado e depois verifica se o usuário obrigou.

Portanto, precisamos gerar um sal de forma determinística e garantir que seja diferente para cada transação. Poderíamos fazer isso com uma função hash, mas se pensarmos bem, não precisamos que seja diferente para cada transação, mas sim para cada solucionador. O que queremos, afinal, é evitar que outros usuários trapaceiem. E assim segue, por que não simplesmente usar o endereço? Isso pode parecer algo assim:

pragma solidity ^0.8.7;

contract TestCreator {
    function solveTest( uint256 testId, address recipient, solutionProof proof ) external {
        uint salt = uint(uint160(recipient));

        require(verifier(proof, salt), "Invalid proof");
    }
}
Enter fullscreen mode Exit fullscreen mode

Podemos ter certeza de que dois solucionadores diferentes nunca terão o mesmo sal e, portanto, a trapaça se torna inviável. O usuário simplesmente tem que gerar suas provas usando seu endereço (convertido em um número inteiro) como sal.

Simplesmente usando o endereço como um sal dentro do contrato inteligente, economizamos no custo SSTORE de 20.000 gás para valores diferentes de zero onde anteriormente havia um zero. A redução real do custo de gás da implementação completa é de quase 40.000 gás, tudo a partir de uma simples mudança de perspectiva em relação à forma como o contrato inteligente interage com o sistema de comprovação.

Uma consequência inesperada: retransmissão de transações

Você deve ter notado que o último trecho de código introduziu um novo parâmetro na função solveTest: o endereço do destinatário. Podemos obter a mesma lógica removendo esse parâmetro e usando msg.sender quando necessário. Mas como o endereço do destinatário da credencial já está incorporado na prova (pelo uso do endereço como sal), podemos facilmente suportar a retransmissão de transações.

Os usuários podem simplesmente resolver um teste, gerar a prova correspondente usando seu endereço como sal e enviar essa prova para um retransmissor de transação. Como o destinatário está incorporado na própria prova, o retransmissor não pode adulterá-lo sem invalidar a prova. Isso significa que a prova que o usuário gerou só pode ser usada para conceder a credencial a si mesmo - trapacear não é possível, mesmo quando você tem a transação de resolução completa à sua frente!

Portanto, não apenas economizamos quase 40.000 gás com essa implementação, mas também podemos configurar retransmissores de transação para que os usuários nem precisem se preocupar com os custos de gás!

Este artigo é uma tradução de deenz feita por Felipe Gueller. Você pode encontrar o artigo original aqui.

Latest comments (0)