
Blockchain e contratos inteligentes ainda são um tópico importante entre desenvolvedores e técnicos. Há muita pesquisa e discussão sobre o futuro deles, para onde tudo se move e para onde levará. Nós da Waves Platform temos nossa própria visão sobre o que devem ser os contratos inteligentes e, neste artigo, mostrarei como os fizemos, quais problemas encontramos e por que eles não são como contratos inteligentes de outros projetos de blockchain (antes de tudo Ethereum).
Este artigo também é um guia para quem deseja entender como os contratos inteligentes funcionam na rede Waves, tenta escrever seu próprio contrato e se familiarizar com as ferramentas que os desenvolvedores já têm à sua disposição.
Como chegamos a essa vida?
Muitas vezes nos perguntam quando é que conseguimos contratos inteligentes, porque os desenvolvedores gostaram da facilidade de trabalhar com a rede, da velocidade da rede (graças ao Waves NG ) e do baixo nível de comissões. No entanto, contratos inteligentes oferecem muito mais espaço para a imaginação.
Os contratos inteligentes se tornaram muito populares nos últimos anos devido à disseminação da blockchain. Aqueles que encontraram a tecnologia blockchain em seu trabalho, ao mencionar contratos inteligentes, geralmente pensam no Ethereum e no Solidity. Mas existem muitas plataformas blockchain com contratos inteligentes, e a maioria delas simplesmente repetiu o que a Ethereum fez (máquina virtual + sua própria linguagem de contrato). Uma lista interessante com diferentes idiomas e abordagens está neste repositório .
O que é um contrato inteligente?
Em um sentido amplo, um contrato inteligente é um protocolo projetado para suportar, verificar e fazer cumprir os termos de uma transação ou a execução de contratos entre as partes. A idéia foi proposta pela primeira vez em 1996 por Nick Szabo, mas os contratos inteligentes se tornaram populares apenas nos últimos anos.
Do ponto de vista técnico (o que mais nos interessa), um contrato inteligente é um algoritmo (código) que não é executado em nenhum servidor ou computador, mas em muitos (ou todos) nós da rede blockchain, ou seja, descentralizado.
Como isso funciona?
O primeiro protótipo de um contrato inteligente na blockchain é considerado corretamente o Bitcoin Script - incompleto por Turing, uma linguagem baseada em pilha na rede Bitcoin. Não há conceito de conta no Bitcoin; em vez disso, existem entradas e saídas. No Bitcoin, ao fazer uma transação (criando uma saída), é necessário consultar a transação de recebimento (entrada). Se você estiver interessado nos detalhes técnicos do dispositivo Bitcoin, recomendo que você leia esta série de artigos . Como não há contas no Bitcoin, o Bitcoin Script determina em que casos uma ou outra saída pode ser gasta.
O Ethereum oferece muito mais recursos, como Fornece Solidity, uma linguagem completa de Turing que é executada em uma máquina virtual dentro de cada nó. Com grande poder, vem grande responsabilidade, e com uma ampla gama de possibilidades - um número bastante grande de restrições, sobre as quais falaremos mais adiante.
Contratos inteligentes Waves
Como escrevi acima, muitas vezes nos perguntam sobre contratos inteligentes, mas não queríamos "gostar no ar" ou "como em qualquer nome de bloco", e há muitas razões para isso . Portanto, analisamos os casos existentes para contratos e como podemos ajudar a resolver problemas reais com a ajuda deles.
Após analisar os cenários de uso, descobrimos que existem 2 grandes categorias de tarefas que geralmente são resolvidas usando contratos inteligentes:
- Tarefas simples e diretas, como multisig, swaps atômicos ou compromisso.
- dApps, aplicativos descentralizados completos com lógica de usuário. Para ser mais preciso, este é um back-end para aplicativos descentralizados. Os exemplos mais impressionantes são Cryptokitties ou Bancor.
Há também um terceiro tipo mais popular de contrato - tokens. Na rede Ethereum, por exemplo, a grande maioria dos contratos de trabalho são tokens padrão ERC20. No Waves, para criar tokens, não é necessário fazer contratos inteligentes, pois eles fazem parte do próprio blockchain e, para emitir um token (com a capacidade de negociá-lo imediatamente em uma troca descentralizada (DEX)), basta enviar uma transação do tipo emissão (transação de emissão).
Para os dois tipos de tarefas acima (por simplicidade, chamaremos de casos simples e complexos), os requisitos para a linguagem, contratos e conceitos são muito diferentes. Sim, podemos dizer que ter uma linguagem completa de Turing pode resolver problemas simples e complexos, mas há uma condição importante: a linguagem deve ajudar a evitar erros. Esse requisito também é importante para idiomas comuns e, para idiomas inteligentes de contrato, é especialmente importante, porque as operações estão relacionadas financeiramente e os contratos geralmente são imutáveis, e não há como corrigir um erro rápida e facilmente.
Considerando os tipos de tarefas descritos acima, decidimos avançar gradualmente e fornecer uma ferramenta para resolver problemas simples como o primeiro passo e fornecer uma linguagem que possa implementar facilmente qualquer lógica do usuário no próximo passo. Como resultado, o sistema acabou sendo muito mais poderoso do que imaginávamos no início da jornada.
Vamos tornar as contas inteligentes
Gradualmente, chegamos ao conceito de contas inteligentes, projetadas para resolver tarefas principalmente simples. A ideia deles é muito semelhante ao Bitcoin Script: regras adicionais podem ser adicionadas à conta que determinam a validade da transação de saída. Os principais requisitos para contas inteligentes foram:
- Segurança máxima. Quase todo mês você encontra notícias de que outra vulnerabilidade foi encontrada nos contratos do modelo Ethereum. Queríamos evitar isso.
- Não há necessidade de gás, para que a comissão seja fixa. Para fazer isso, o script deve ser executado em um tempo previsível e ter limites de tamanho bastante rígidos.
Antes de prosseguir com os detalhes técnicos da implementação e redação de contratos, descrevemos algumas características da blockchain Waves que serão importantes para um entendimento mais aprofundado:
- A blockchain Waves atualmente possui 13 tipos diferentes de transações.

- Na blockchain Waves, não entradas e saídas (como no Bitcoin), mas contas (como, por exemplo, no Nxt). Uma transação é realizada em nome de uma conta específica.
- Por padrão, a correção de uma transação é determinada pelo estado atual da blockchain e pela validade da assinatura em nome da qual a transação é enviada. A representação JSON da transação parece bastante simples:

Como já temos tipos diferentes de transações na blockchain, decidimos que não faríamos uma entidade separada como uma conta inteligente, mas adicionaríamos uma nova transação que transforma uma conta regular em uma conta inteligente. Qualquer conta pode se tornar uma conta inteligente com regras de validação de transação alteradas; para isso, a conta deve simplesmente enviar uma transação do tipo SetScriptTransaction
, que contém o contrato compilado.
No caso de uma conta inteligente, o contrato é uma regra de validação para cada transação de saída.
E o gás?
Uma das principais tarefas que estabelecemos para nós mesmos é livrar-nos do gás para operações simples. Isso não significa que não haverá comissão. É necessário para que os mineradores tenham interesse em executar scripts. Abordamos a questão do lado prático e decidimos realizar testes de desempenho e calcular a velocidade de várias operações. Para isso, foram desenvolvidos benchmarks usando JMH. Os resultados podem ser vistos aqui . As limitações resultantes são:
- O script deve executar mais rapidamente do que 20 operações de verificação de assinatura, o que significa que as verificações de uma conta inteligente não serão 20 vezes mais lentas do que as de uma conta normal. O tamanho do script não deve exceder 8 KB.
- Para tornar lucrativo para os mineradores cumprir contratos inteligentes, estabelecemos uma comissão adicional mínima para contas inteligentes no valor de 0,004 WAVES. A comissão mínima na rede Waves para uma transação é 0,001 WAVES, no caso de uma conta inteligente - 0,005 WAVES.
Idioma para contratos inteligentes
Uma das tarefas mais difíceis foi a criação de sua própria linguagem de contratos inteligentes. Pegar qualquer linguagem completa de turing existente e adaptar (aparar) para nossas tarefas parece estar disparando de um canhão para pardais: além disso, dependendo da base de código de outra pessoa em um projeto de blockchain é extremamente arriscado .
Vamos tentar imaginar qual deve ser a linguagem ideal para contratos inteligentes. Na minha opinião, qualquer linguagem de programação deve forçar a escrever código "correto" e seguro, ou seja, idealmente, deve haver um caminho certo. Sim, se você quiser, pode escrever código completamente ilegível e sem suporte em qualquer idioma, mas isso deve ser mais difícil do que escrevê-lo corretamente (olá PHP e JavaScript). Ao mesmo tempo, a linguagem deve ser conveniente para o desenvolvimento. Como o idioma é executado em todos os nós da rede, é necessário que seja o mais eficiente possível - a execução lenta pode economizar bastante recursos. Eu também gostaria de ter um sistema de tipos poderoso na linguagem, de preferência algébrico, porque ajuda a descrever o contrato da forma mais clara possível e aproximar-me do sonho de "Código é lei". Se formalizarmos um pouco mais nossos requisitos, obteremos os seguintes parâmetros de idioma:
- Ser digitado estrita e estaticamente. A digitação forte elimina automaticamente muitos erros potenciais do programador.
- Tenha um sistema de tipos poderoso para dificultar o tiro no pé.
- Seja preguiçoso para não perder preciosos ciclos do processador.
- Possui funções específicas na biblioteca padrão para trabalhar com blockchain, por exemplo, hashes. Ao mesmo tempo, a biblioteca de idiomas padrão não deve estar sobrecarregada, porque sempre deve haver um caminho certo.
- Não há exceções em tempo de execução.
Em nossa linguagem RIDE, tentamos levar esses recursos importantes em consideração e, como desenvolvemos muito no Scala e como programação funcional, a linguagem em alguns aspectos é semelhante ao Scala e ao F #.
Os maiores problemas na implementação na prática surgiram com o último requisito, porque se você não tiver exceções no idioma, por exemplo, a operação de adição precisará retornar uma Opção , que precisará ser verificada quanto ao estouro, o que definitivamente será inconveniente para os desenvolvedores. As exceções eram um compromisso, mas sem a capacidade de capturá-las - se houvesse uma exceção, a transação era inválida. Outro problema foi transferir para a linguagem todos os modelos de dados que temos na blockchain. Eu já descrevi que no Waves existem 13 tipos diferentes de transações que devem ser suportadas no idioma e ter acesso a todos os seus campos.
Informações completas sobre operações disponíveis e tipos de dados no RIDE estão na página de descrição do idioma . Entre os recursos interessantes da linguagem, também podemos destacar o fato de que a linguagem é baseada em expressões, ou seja, tudo é expressão, bem como a presença de correspondência de padrões, que permite descrever convenientemente as condições para diferentes tipos de transações:
match tx { case t:TransferTransaction => t.recepient case t:MassTransferTransaction => t.transfers case _ => throw }
Qualquer pessoa interessada em aprender como funciona o trabalho com o código RIDE vale a pena conferir o white paper , que descreve todas as etapas do trabalho com um contrato: análise, compilação, desserialização, cálculo da complexidade e execução do script. Os dois primeiros estágios - a análise e a compilação são realizadas fora da cadeia, apenas o contrato compilado na base64 entra no blockchain. A desserialização, o cálculo da complexidade e a execução são feitos on-chain e várias vezes em diferentes estágios:
- Quando você recebe uma transação e a adiciona à UTX, caso contrário, haverá uma situação em que a transação será aceita pelo nó blockchain, por exemplo, através da API REST, mas nunca entrará no bloco.
- Quando um bloco é formado, o nó de mineração valida transações e o script é necessário.
- Após o recebimento, por nós não mineradores, de um bloco e a validação das transações incluídas nele.
Cada otimização no trabalho com contratos se torna valiosa, porque é realizada várias vezes em muitos nós da rede. Agora, os nós Waves são executados silenciosamente em máquinas virtuais por US $ 15 na DigitalOcean, apesar do aumento nas cargas de trabalho após o lançamento de contas inteligentes.
Qual é o resultado?
Agora vamos ver o que conseguimos no Waves. Escreveremos nosso primeiro contrato, seja um contrato multisig padrão 2-de-3. Para escrever um contrato, você pode usar o IDE online (ajuste para o idioma - um tópico para um artigo separado). Crie um novo contrato vazio (Novo → Contrato Vazio).
Primeiro, anunciaremos as chaves públicas de Alice, Bob e Cooper, que controlarão a conta. Você precisará de duas das três assinaturas:
let alicePubKey = base58'B1Yz7fH1bJ2gVDjyJnuyKNTdMFARkKEpV' let bobPubKey = base58'7hghYeWtiekfebgAcuCg9ai2NXbRreNzc' let cooperPubKey = base58'BVqYXrapgJP9atQccdBPAgJPwHDKkh6A8'
A documentação descreve a função sigVerify
, que permite verificar a assinatura da transação:

Os argumentos para a função são o corpo da transação, a assinatura verificada e a chave pública. Um objeto tx
está disponível no contrato no escopo global, no qual as informações da transação são armazenadas. Este objeto possui um campo tx.bodyBytes
que contém os bytes da transação que está sendo enviada. Há também uma variedade de tx.proofs
, que armazena assinaturas, que podem ter até 8. Vale a pena notar que, de fato, você pode enviar não apenas assinaturas para tx.proofs
, mas qualquer outra informação que possa ser usada pelo contrato.
Podemos garantir que todas as assinaturas sejam apresentadas e estejam na ordem correta usando três linhas simples:
let aliceSigned = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey )) then 1 else 0 let bobSigned = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey )) then 1 else 0 let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey )) then 1 else 0
Bem, o último passo será verificar se pelo menos 2 assinaturas foram enviadas.
aliceSigned + bobSigned + cooperSigned >= 2
Todo o contrato 2 de 3 com várias assinaturas se parece com o seguinte:
# let alicePubKey = base58'B1Yz7fH1bJ2gVDjyJnuyKNTdMFARkKEpV' let bobPubKey = base58'7hghYeWtiekfebgAcuCg9ai2NXbRreNzc' let cooperPubKey = base58'BVqYXrapgJP9atQccdBPAgJPwHDKkh6A8' # let aliceSigned = if(sigVerify(tx.bodyBytes, tx.proofs[0], alicePubKey )) then 1 else 0 let bobSigned = if(sigVerify(tx.bodyBytes, tx.proofs[1], bobPubKey )) then 1 else 0 let cooperSigned = if(sigVerify(tx.bodyBytes, tx.proofs[2], cooperPubKey )) then 1 else 0 # , 2 aliceSigned + bobSigned + cooperSigned >= 2
Observe: não há palavras-chave como return
no código, porque a última linha executada é considerada o resultado do script, e é por isso que sempre deve retornar true
ou false
Em comparação, o contrato comum de várias assinaturas da Ethereum parece muito mais complicado . Mesmo variações relativamente simples são assim:
pragma solidity ^0.4.22; contract SimpleMultiSig { uint public nonce; // (only) mutable state uint public threshold; // immutable state mapping (address => bool) isOwner; // immutable state address[] public ownersArr; // immutable state // Note that owners_ must be strictly increasing, in order to prevent duplicates constructor(uint threshold_, address[] owners_) public { require(owners_.length <= 10 && threshold_ <= owners_.length && threshold_ >= 0); address lastAdd = address(0); for (uint i = 0; i < owners_.length; i++) { require(owners_[i] > lastAdd); isOwner[owners_[i]] = true; lastAdd = owners_[i]; } ownersArr = owners_; threshold = threshold_; } // Note that address recovered from signatures must be strictly increasing, in order to prevent duplicates function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) public { require(sigR.length == threshold); require(sigR.length == sigS.length && sigR.length == sigV.length); // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191 bytes32 txHash = keccak256(byte(0x19), byte(0), this, destination, value, data, nonce); address lastAdd = address(0); // cannot have address(0) as an owner for (uint i = 0; i < threshold; i++) { address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]); require(recovered > lastAdd && isOwner[recovered]); lastAdd = recovered; } // If we make it here all signatures are accounted for. // The address.call() syntax is no longer recommended, see: // https://github.com/ethereum/solidity/issues/2884 nonce = nonce + 1; bool success = false; assembly { success := call(gas, destination, value, add(data, 0x20), mload(data), 0, 0) } require(success); } function () payable public {} }
O IDE possui um console interno que permite compilar imediatamente um contrato, implantá-lo, criar transações e ver o resultado da execução. E se você quiser trabalhar seriamente com contratos, recomendo procurar nas bibliotecas diferentes idiomas e no plug - in do Visual Studio Code .
Se suas mãos estiverem com coceira, no final do artigo existem os links mais importantes para iniciar o mergulho.
O sistema é mais poderoso que o idioma
A blockchain Waves possui tipos de dados especiais para armazenamento de dados - transações de dados . Eles funcionam como armazenamento de valor-chave associado a uma conta, ou seja, de certa forma, esse é o estado da conta.

A data da transação pode conter cadeias, números, valores booleanos e matrizes de bytes de até 32 KB por chave. Um exemplo de trabalho com transações de dados, que permite enviar uma transação apenas se o armazenamento de valores-chave da conta já contiver o número 42 na chave de key
:
let keyName = "key" match (tx) { case tx:DataTransaction => let x = extract(getInteger(tx.sender, keyName)) x == 42 case _ => false }
Graças à transação de dados, as contas inteligentes se tornam uma ferramenta extremamente poderosa que permite trabalhar com oráculos, gerenciar estado e descrever convenientemente o comportamento.

Este artigo descreve como você pode implementar NFT (tokens não fungíveis) usando transações de dados e um contrato inteligente que controla o estado. Como resultado, o estilo da conta conterá entradas como:
+------------+-----------------------------------------------+ | Token Name | Owner Publc Key | +------------+-----------------------------------------------+ | "Token #1" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | | "Token #2" | "3tNLxyJnyxLzDkMkqiZmUjRqXe1UuwFeSyQ14GRYnGL" | | "Token #3" | "3wH7rENpbS78uohErXHq77yKzQwRyKBYhzCR9nKU17q" | | "Token #4" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | | "Token #5" | "6iQaHazE9NVAJfAjMpHifDXMfr1euWcy8fmW6rNcdhr" | +------------+-----------------------------------------------+
O contrato da NFT parece extremamente simples:
match tx { case dt: DataTransaction => let oldOwner = extract(getString(dt.sender, dt.data[0].key)) let newOwner = getBinary(dt.data, 0) size(dt.data) == 1 && sigVerify(dt.bodyBytes, dt.proofs[0], fromBase58String(oldOwner)) case _ => false }
O que vem a seguir?
O desenvolvimento adicional de contratos inteligentes da Waves é o Ride4DApps , que permitirá a chamada de contratos em outras contas, e um idioma (ou sistema) completo de Turing que permitirá resolver todos os tipos de tarefas, acionar outras tarefas etc.
Outra direção interessante para o desenvolvimento de contratos inteligentes no ecossistema Waves é o Smart Assets, que trabalha com um princípio semelhante - contratos incompletos de Turing relacionados ao token. O contrato controla as condições sob as quais as transações de token podem ser concluídas. Por exemplo, com a ajuda deles, será possível congelar tokens até uma certa altura de blockchain ou proibir a negociação de token p2p. Você pode ler mais sobre ativos inteligentes no blog .
Bem, no final, darei mais uma lista do que será necessário para começar a trabalhar com contratos inteligentes na rede Waves.
- A documentação
- IDE com console
- White paper para os mais curiosos