No dia 16 de fevereiro de 2023 eu fiz um workshop na Universidade de Waterloo sobre o desenvolvimento de contratos inteligentes em Near usando Rust. Gostei de montá-lo e achei que seria divertido também apresentar o conteúdo aqui como uma série de postagens no blog. Neste primeiro post, farei uma analogia para conectar o desenvolvimento da blockchain a um padrão usado em aplicativos Web normais, apresentarei o exemplo do contrato inteligente que usaremos ao longo desta série e discutirei alguns princípios gerais do desenvolvimento do contrato inteligente que são exclusivos para blockchain em relação a outros domínios de programação.
Um modelo mental para criar um aplicativo distribuído (dapp).
O objetivo desta seção é fazer uma analogia entre o desenvolvimento em cima de uma blockchain (aplicativos apoiados pela tecnologia blockchain são frequentemente chamados de “dapps” no meio) e uma tecnologia mais comum para aplicativos da web que você pode ter encontrado antes. Essa analogia pode ser útil ao pensar em como os usuários interagem com contratos inteligentes.
A ideia é que os dapps são muito semelhantes aos aplicativos da web baseados em uma “arquitetura sem servidor“. O termo “sem servidor” é um pouco enganador porque é claro que os servidores ainda estão envolvidos, mas a razão para o nome é que o hardware subjacente (ou seja, servidor) que executa o código é abstraído do desenvolvedor. Isso tem vantagens sobre outras infraestruturas de computação na nuvem em termos de custo e escalabilidade, porque você só paga pelos recursos que usa exatamente, em vez de pagar para executar uma VM (Máquina virtual) que pode ficar ociosa se o tráfego for baixo ou pode não responder se houver muito tráfego. Cada vez que um usuário interage com o aplicativo da web, uma nova instância da “função sem servidor” é invocada no back-end para atender à solicitação do usuário sem que o desenvolvedor precise pensar exatamente em qual hardware essa função está sendo executada.
Os Dapps abstraem o hardware de maneira semelhante. Um contrato inteligente é implantado na blockchain e executado nos nós (servidores) que formam a rede peer-to-peer dessa blockchain. Quando um usuário interage com o dapp, ele faz uma chamada para a blockchain (uma transação) para executar o contrato inteligente. Cada transação cria uma nova instância do contrato inteligente (no sentido de que não há um estado na memória que persista entre as transações), assim como nas funções sem servidor.
Abaixo está uma imagem tirada diretamente do site da Amazon Web Services (AWS) para Lambda (sua versão de uma oferta de computação sem servidor).
É fácil modificar esta imagem para ver como o fluxo de trabalho em um dapp é semelhante.
Outra semelhança entre computação sem servidor e contratos inteligentes é o fato de que cada transação tem um custo. No caso da AWS, a conta AWS do desenvolvedor é cobrada pelos recursos consumidos, enquanto que no caso da blockchain, quem assinou a transação é cobrado pela sua execução.
Com essa analogia como ponto de referência, vamos discutir o exemplo de desenvolvimento dapp que usaremos ao longo desta série.
Nosso exemplo: aplicativo de bate-papo baseado em blockchain.
O exemplo que usaremos ao longo desta série é um aplicativo de bate-papo baseado na blockchain. Este não é um exemplo do mundo real no sentido de que, (na minha opinião), não existe um bom argumento para se usar uma blockchain pública de bate-papo para fazer negócios. O simples fato de que todas as mensagens sejam completamente públicas e incluídas de forma irreversível em um registro permanente é uma clara desvantagem, em vez de um recurso. No entanto, a razão para escolher este exemplo é que ele ilustra vários conceitos importantes no desenvolvimento da dapp enquanto é logicamente fácil de seguir para qualquer um que tenha usado algo como Facebook Messenger, Telegram ou Signal.
O código para este exemplo está disponível no meu GitHub. O README desse repositório fornece algumas instruções para configurar um ambiente de desenvolvimento para interagir com o código e algumas ideias básicas de como usar o contrato. Esta série de postagens será um mergulho muito mais profundo no código e em como ele funciona.
Para fundamentar a discussão dos princípios do desenvolvimento de contratos inteligentes, aqui está uma visão geral de como funciona o contrato de bate-papo.
- Cada indivíduo que deseja participar da rede de bate-papo implanta sua própria versão do contrato inteligente.
- Cada instância do contrato mantém uma lista de contas com as que ele está relacionado (contatos, solicitações de contato pendentes, etc.). Além de armazenar as mensagens recebidas (e alguns metadados sobre essas mensagens).
- Para enviar uma mensagem para outra pessoa, primeiro você deve tê-la como “contato”. Isso funciona como seria de esperar: Alice envia a Bob uma solicitação de contato, se Bob aceitar, Alice e Bob tornam-se contatos um do outro, caso contrário, eles não são contatos.
- Cada instância do contrato possui um “proprietário” que pode enviar mensagens e enviar/aceitar solicitações de contato.
Princípios do desenvolvimento de contratos inteligentes.
Há três conceitos relacionados que quero enfatizar que são importantes para o desenvolvimento de contratos inteligentes, mas podem não aparecer no típico desenvolvimento de software. Eles são:
- Uma mentalidade adversária,
- Economía,
- Confirmar invariantes antes de fazer uma chamada entre contratos.
Uma mentalidade adversária
A primeira coisa importante a lembrar ao implantar em uma blockchain pública é que qualquer pessoa no mundo inteiro pode interagir com seu código. Se houver alguma ação sensível que seu contrato inteligente possa realizar (por exemplo, ao enviar mensagens no contrato de bate-papo, você certamente não gostaria que alguém se passasse por você), você deveria poder verificar, sem problemas, a autorização para que apenas contas autorizadas possam executar com êxito a ação (e é por isso que nosso contrato de bate-papo tem a característica “dono”). Se você tiver algum método que receba entradas (entrada de dados), deverá validá-lo antes de prosseguir para qualquer lógica de negócios, porque qualquer usuário aleatório pode enviar qualquer entrada que desejar. De fato, a ideia de uma mentalidade adversária vai ainda mais longe; um usuário pode não apenas enviar uma “entrada lixo”, mas também criar cuidadosamente uma entrada para acionar uma vulnerabilidade em seu código. A única maneira de evitar que isso aconteça é não ter tais vulnerabilidades em primeiro lugar.
Da mesma forma, a lógica do contrato inteligente geralmente depende de algum protocolo para coordenar diferentes componentes juntos (por exemplo, o protocolo para adicionar contatos em nosso contrato de bate-papo). Um usuário pode intervir neste protocolo? O que acontece se eles não atuarem corretamente? Estas são perguntas que você deve responder ao desenvolver um contrato inteligente porque os hackers irão tentar explorar seu contrato.
Encurtando o assunto, você deve sempre assumir que qualquer entrada externa é altamente “byzantine” e verificar explicitamente o contrário antes de prosseguir. Você deve praticar a observação de quais suposições está fazendo e sempre pensar “como eu poderia quebrar essa suposição?” sempre que você perceber que está fazendo uma.
Economía
A economia de um aplicativo da Web típico é bastante simples. Você precisa gerar rentabilidade suficiente para cobrir o custo de hospedagem de qualquer servidor que contenha o código e os dados que seu aplicativo usa. A renda que você precisa gerar pode vir de várias fontes, mas as mais comuns são os rendimentos de anúncios e assinaturas pagas de usuários.
Para a blockchain a situação é um pouco mais complicada porque cada transação precisa ser paga de forma independente. Os produtos blockchain mais recentes procuram simplificar essa história, por exemplo Aurora+ fornece algo como uma “assinatura blockchain” que permite uma série de transações gratuitamente. Mas até que isso se torne padrão no espaço blockchain, ainda é importante responder à pergunta “quem está pagando por isso?
Muitas vezes é o usuário quem paga cada transação porque o pagamento está vinculado à conta de assinatura (ou seja, o pagamento está vinculado à identidade/autorização). Um modelo alternativo é usar “meta-transações” (transações dentro de transações) para que o pagamento seja feito pelo “assinante externo” enquanto a autorização é baseada no “assinante interno”. Por exemplo, é assim que Aurora+ funciona. Infelizmente, dado que esta não é a forma padrão como as transações blockchain operam, requer um trabalho extra por parte do desenvolvedor para que isso aconteça.
Para o nosso exemplo de aplicativo de bate-papo, seguiremos o caminho de menor resistência e cada usuário terá que pagar pelos custos incorridos com seu uso. Depois de tomar essa decisão, precisamos revisar quais possíveis custos podem haver e garantir que eles sejam cobertos adequadamente. Por exemplo, no Near, o pagamento do armazenamento é feito por “storage staking“. Essencialmente, isso significa que cada conta tem parte de seu saldo bloqueado, dependendo de quanto armazenamento está usando. Isso é relevante em nosso contrato de bate-papo porque armazena mensagens recebidas de outros usuários, portanto, precisamos garantir que esses outros usuários estejam cobrindo o custo de armazenamento, anexando um depósito suficiente à sua mensagem. Da mesma forma, as solicitações de contato criam uma entrada no armazenamento, portanto, também devem vir com um depósito. Se não aplicássemos esses requisitos de depósito, os usuários poderiam efetivamente roubar dinheiro uns dos outros enviando muitas mensagens e bloqueando todo o saldo da vítima (observe como isso está relacionado à mentalidade adversária acima).
Em resumo, ao projetar um dapp é sempre importante pensar em quais custos estarão envolvidos e como eles serão pagos, seja adicionando verificadores para depósitos ou usando meta-transações.
Assegure as invariantes antes de fazer chamadas entre contratos
Este último ponto é sutil. Em um aplicativo típico, todo o código está vinculado ao mesmo binário. Quando você chama uma função em uma biblioteca, isso geralmente não aciona nenhuma comunicação, mas apenas adiciona um novo quadro no stack e executa algum código de outra parte do binário. Em uma configuração de blockchain, as coisas são um pouco diferentes.
Fazer uma chamada para outro contrato é mais como fazer uma chamada para um processo totalmente diferente do que chamar uma biblioteca. Mais uma vez, devemos aplicar uma mentalidade de adversário e perceber que não temos ideia do que esse outro processo pode estar fazendo; na verdade, pode estar tentando fazer algo propositalmente malicioso. Um vetor de ataque comum é fazer o outro processo chamar de volta nosso contrato e explorá-lo porque nosso contrato não esperava uma nova chamada enquanto esperava por uma resposta à chamada iniciada. Isto é conhecido como “ataque de reentrância” e foi a fonte de um dos hacks mais famosos do Ethereum, que deu como resultado a criação do “Ethereum Classic” . (o Ethereum Classic rejeitou o “hard fork” sendo a Resposta da Fundação Ethereum ao hack).
Em Near este problema é ainda mais pronunciado porque existe a questão adicional da atomicidade. Na Ethereum Virtual Machine (EVM), cada transação é “atômica” no sentido de que todas as ações resultantes da transação são confirmadas no estado da blockchain ou nenhuma delas é (toda a transação é “revertida”). Isso significa que um ataque de reentrância pode ser frustrado usando uma reversão; tudo o que aconteceu será desfeito, mantendo o contrato seguro. Esse padrão está incluído no exemplo Mutex na documentaçao oficial da Solidity. No entanto, no tempo de execução do Near, a execução dos contratos é independente uma da outra; eles não são atômicos. Portanto, se uma transação fizer com que o contrato A chame o contrato B e B encontrar um erro, as alterações de estado que ocorreram no contrato A permanecerão.
Isso tem sido um monte de história e teoria, mas qual é a lição prática? O ponto é que você deve garantir que seu contrato esteja em “bom estado” quando fizer uma chamada para outro contrato. Ou seja, se houver invariantes nas quais a lógica do seu contrato se baseia, elas devem estar corretas no momento em que a chamada é feita. Como um simples exemplo, suponha que temos um contrato com uma função de transferência. A invariante a ser mantida é que os tokens não são criados ou destruídos em uma transferência. Se por algum motivo fosse necessário fazer uma chamada para outro contrato durante a transferência, seria incorreto debitar uma conta e depois fazer a chamada sem creditar a outra primeiro. Isso ocorre porque a invariante sobre os tokens não serem destruídos seria quebrada quando a chamada externa fosse feita e isso poderia ser explorável. Um exemplo nesse sentido também está incluído na Documentação do Near.
Resumo
Para finalizar, nesta postagem do blog, estamos apresentando uma nova série de postagens que apresentam uma introdução ao desenvolvimento de contratos inteligentes no Near usando Rust. Aqui, discutimos o exemplo de contrato de bate-papo que usaremos ao longo da série, bem como alguns princípios gerais a serem lembrados ao desenvolver aplicativos baseados em blockchain. No próximo post vamos nos aprofundar mais no código para discutir os detalhes técnicos de como o contrato é implementado. Isso servirá como um exemplo do Rust SDK de Near, ilustrando conceitos que se aplicarão a todos os tipos de contratos do mundo real que você queira escrever.
Se você está gostando desta série de postagens no blog, por favor entre en contato conosco na consultoria Type-Driven Type-Driven consulting. Temos o prazer de fornecer serviços de desenvolvimento de software para dapps, bem como materiais de treinamento para seus próprios engenheiros.
—————–
2211 words