WEB3DEV

Cover image for Matemática em Solidity (Parte 1: Números)
Yan Luiz
Yan Luiz

Posted on • Atualizado em

Matemática em Solidity (Parte 1: Números)

Esse artigo é uma tradução de Mikhail Vladimirov feita por Rodrigo Faria e você pode encontrar o original aqui.

Este artigo abre uma série de artigos sobre como lidar com matemática em Solidity. O primeiro tópico a ser discutido é: Números.

Introdução

O Ethereum é uma blockchain programável, cuja funcionalidade pode ser estendida pela publicação de pedaços de código executável, conhecidos como contratos inteligentes, na própria blockchain. Isso distingue o Ethereum da primeira geração de blockchains, em que novas funcionalidades exigem que o software cliente seja modificado, os nós sejam atualizados e toda a blockchain seja bifurcada (forked).

Um contrato inteligente é um pedaço de código executável publicado na cadeia e que possui um endereço de blockchain exclusivo atribuído a ele. O contrato inteligente controla todos os ativos pertencentes ao seu endereço e pode atuar em nome desse endereço ao interagir com outros contratos inteligentes. Cada contrato inteligente tem armazenamento persistente que é usado para preservar o estado do contrato inteligente entre as invocações.

Solidity é a principal linguagem de programação para o desenvolvimento de contratos inteligentes no Ethereum, bem como em várias outras plataformas blockchain que usam a EVM (Ethereum Virtual Machine - Máquina Virtual Ethereum).

A programação sempre foi sobre matemática, blockchain sempre foi sobre finanças e finanças sobre matemática desde os tempos antigos (ou talvez a matemática fosse sobre finanças). Sendo a principal linguagem de programação para a blockchain Ethereum, o Solidity tem que fazer matemática adequadamente.

Nesta série, discutimos vários aspectos de como o Solidity faz contas e como os desenvolvedores fazem contas em Solidity. O primeiro tópico a ser discutido é: números.

Tipos Numéricos em Solidity

Em comparação com as linguagens de programação convencionais, o Solidity tem muitos tipos numéricos: nominalmente, 5.248. Sim, de acordo com a documentação, existem 32 tipos de signed integers (inteiros positivos e negativos), 32 unsigned integers (apenas inteiros positivos), 2592 signed fixed-point (ponto-fixo positivos e negativos) e 2592 unsigned fixed-point (apenas ponto-fixo positivos). JavaScript tem apenas dois tipos numéricos. O Python 2 costumava ter quatro, mas no Python 3 o tipo “long” foi descartado, então agora existem apenas três. Java tem sete e C++ tem algo em torno de quatorze.

Com tantos tipos numéricos, o Solidity deveria ter um tipo adequado para todos, certo? Não tão rápido. Vejamos esses tipos numéricos um pouco mais de perto.

Começaremos com a seguinte pergunta:

Por que precisamos de vários tipos numéricos?

Spoiler: nós não precisamos.

Não há tipos numéricos em matemática pura. Um número pode ser inteiro ou não inteiro, racional ou irracional, positivo ou negativo, real ou imaginário etc., mas essas são apenas propriedades, o número pode ou não ter, e um único número pode ter várias dessas propriedades ao mesmo tempo.

Muitas linguagens de programação de alto nível têm um único tipo numérico. JavaScript tinha apenas “número” até que o “BigInt” foi introduzido em 2019.

A menos que faça coisas hardcore de baixo nível, os desenvolvedores realmente não precisam de vários tipos numéricos, eles só precisam de números puros com alcance e precisão arbitrários. No entanto, esses números não são suportados nativamente pelo hardware e são um pouco caros para emular em software.

É por isso que linguagens de programação de baixo nível e linguagens voltadas para alto desempenho geralmente têm vários tipos numéricos, como signed/unsigned, 8/16/32/64/128 bits de largura, inteiro/ponto flutuante etc. Esses tipos são suportados nativamente por hardware e são amplamente utilizados em formatos de arquivo, protocolos de rede etc., portanto, o código de baixo nível se beneficia deles.

No entanto, por motivos de desempenho, esses tipos geralmente herdam toda a semântica estranha das instruções subjacentes da CPU, como over- e underflow silenciosos, intervalo assimétrico, frações binárias, problemas de ordenação de bytes etc. Isso os torna dolorosos nas lógicas de negócio de alto nível. O uso direto geralmente parece inseguro, e o uso seguro geralmente se torna complicado e ilegível.

Então, a próxima pergunta é:

Por que Solidity tem tantos tipos numéricos?

Spoiler: ele não possui.

A EVM suporta nativamente dois tipos de dados: palavra de 256 bits e byte de 8 bits. Elementos de pilha, chaves e valores de armazenamento, ponteiros de instrução e memória, tipos de data e hora, saldos, hashes de transação e bloco, endereços etc. são palavras de 256 bits. Memória, código de byte, dados de chamada e dados de retorno consistem em bytes. A maioria dos opcodes da EVM lida com palavras, incluindo todas as operações matemáticas. Algumas das operações matemáticas tratam palavras como inteiros signed, algumas como inteiros unsigned, enquanto outras operações funcionam da mesma maneira, independentemente de os argumentos serem signed ou unsigned.

Portanto, a EVM oferece suporte nativo a dois tipos numéricos: inteiro de 256 bits signed e inteiro de 256 bits unsigned. Esses tipos são conhecidos em Solidity como int e uint respectivamente.

Além desses dois tipos (e seus aliases int256 e uint256), o Solidity tem 62 tipos inteiros int<N>e uint<N>, onde <N> pode ser qualquer múltiplo de 8, de 8 a 248, ou seja, 8, 16, … , 248. Nos níveis da EVM, todos esses tipos são apoiados pelas mesmas palavras de 256 bits, mas o resultado de cada operação é truncado para N bits. Eles podem ser úteis para casos específicos, quando uma largura de bit específica é necessária, mas para cálculos gerais esses tipos são apenas menos poderosos e menos eficientes (truncar após cada operação não é de graça) versões de int e uint.

Finalmente, o Solidity tem 5.184 tipos de ponto fixo fixedNxM e ufixedNxM onde N é múltiplo de 8 de 8 a 256 e N é um número inteiro de 0 a 80 inclusive. Esses tipos devem implementar aritmética de ponto fixo decimal de vários intervalos e precisões, mas a partir de agora (Solidity 0.6.2) a documentação diz que:

Os números de ponto fixo ainda não são totalmente suportados pelo Solidity. Eles podem ser declarados, mas não podem ser atribuídos a ou de.

Portanto, números de ponto fixo, bem como números fracionários em geral, não são suportados atualmente.

Então, a próxima pergunta é:

E se precisarmos de números fracionários ou inteiros maiores que 256 bits?

Spoiler: você terá que imitá-los.

Pode-se dizer que 256 bits devem ser suficientes para qualquer um. No entanto, uma vez que a maioria dos números no Ethereum tem 256 bits de largura, mesmo a simples soma de dois números pode ter até 257 bits, e o produto de dois números pode ter até 512 bits.

A maneira comum de emular números inteiros de largura fixa ou variável, que são maiores do que os tipos suportados nativamente pela linguagem de programação, é representá-los como sequências de comprimento fixo ou variável de números inteiros mais curtos e suportados nativamente. Assim, um mapa de bits de inteiros "largos" é a concatenação de mapa de bits de inteiros mais curtos.

Em Solidity, inteiros largos podem ser representados como arrays fixos ou dinâmicos cujos elementos são bytes ou uint .

Para frações a situação é um pouco mais complicada, pois existem diferentes "sabores" deles, cada um com suas próprias vantagens e desvantagens.

As mais básicas são as frações simples: apenas um inteiro, chamado “numerador”, dividido por outro inteiro, chamado “denominador”. Em Solidity, a fração simples pode ser representada como um par de dois inteiros, ou como um único inteiro, cujo mapa de bits é a concatenação das imagens de bits do numerador e do denominador. Neste último caso, o numerador e o denominador devem ter a mesma largura.

Outro formato popular para frações são os números de ponto fixo. O número de ponto fixo é basicamente uma fração simples cujo denominador é uma constante predefinida, geralmente potência de 2 ou 10. O primeiro caso é conhecido como ponto fixo “binário”, enquanto o segundo é conhecido como ponto fixo “decimal”. Desde que o denominador seja predefinido, não há necessidade de especificar explicitamente, portanto, apenas o numerador precisa ser especificado. Em Solidity, os números de ponto fixo são geralmente representados como um único numerador inteiro, enquanto os denominadores comumente usados ​​são 10¹⁸, 10²⁷, 2⁶⁴ e 2¹²⁸.

Ainda outro formato bem conhecido para números fracionários é o ponto flutuante. Basicamente, o número de ponto flutuante pode ser descrito da seguinte forma:

Image description

onde m (mantissa) e e (expoente) são inteiros, e B (base) é uma constante inteira predefinida, geralmente 2 ou 10. O caso quando B=2 é conhecido como “binário” de ponto flutuante, e o caso em que B=10 é conhecido como ponto flutuante “decimal”.

O IEEE-754 padroniza vários formatos comuns de ponto flutuante, incluindo cinco formatos binários conhecidos como precisão “half”, “single”, “double”, “quadruple” e “octuple”. Cada um desses formatos empacota ambos, mantissa e expoente, em uma única sequência de 16, 32, 64, 128 ou 256 bits, respectivamente. Em Solidity, esses formatos padrão podem ser representados por tipos binários bytes2, bytes4, bytes8, bytes16e bytes32. Alternativamente, mantissa e expoente podem ser representados separadamente como um par de inteiros.

E a pergunta do arquivo para esta seção:

Temos que implementar tudo isso por nós mesmos?

Spoiler: não temos.

A boa notícia é que existem bibliotecas Solidity para vários formatos de números, como: fixidity (ponto fixo decimal com número arbitrário de decimais), DSMath (ponto fixo decimal com 18 ou 27 decimais), BANKEX Library (óctuplo IEEE-754 ponto flutuante de precisão), Bibliotecas ABDK (ponto fixo binário e ponto flutuante de precisão quádrupla) etc.

A má notícia é que bibliotecas diferentes usam formatos diferentes, então é muito difícil combiná-los. As raízes desse problema serão discutidas na próxima seção.

Literais Numéricos em Solidity

Na seção anterior discutimos como os números são representados em tempo de execução. Aqui veremos como eles são representados no momento do desenvolvimento, ou seja, no próprio código.

Comparado às linguagens convencionais, o Solidity possui uma sintaxe bastante rica para literais numéricos. Em primeiro lugar, os bons e antigos inteiros decimais são suportados, como 42. Como em outras linguagens semelhantes a C, existem literais inteiros hexadecimais, como 0xDeedBeef. Até agora tudo bem.

Em Solidity, os literais podem ter sufixo de unidade, como 6 ether ou 3 days. Uma unidade, é basicamente um fator pelo qual o literal é multiplicado. Aqui ether é 10¹⁸ e days é 86.400 (24 horas × 60 minutos × 60 segundos).

Além disso, o Solidity suporta notação científica para literais inteiros, como 2.99792458e8. Isso é bastante incomum, pois as linguagens convencionais suportam notação científica apenas para literais fracionários.

Mas provavelmente a característica mais original de toda a linguagem Solidity é seu suporte para expressões literais racionais. Praticamente todo compilador maduro é capaz de avaliar expressões constantes em tempo de compilação, então x = 2 + 2 não gera opcode de add, mas é equivalente a x = 4. O Solidity também é capaz de fazer isso, mas, na verdade, vai muito além disso.

Nas linguagens convencionais, a avaliação em tempo de compilação da expressão constante é apenas uma otimização, portanto, a expressão constante é avaliada em tempo de compilação exatamente da mesma maneira que seria avaliada em tempo de execução. Isso torna possível substituir qualquer parte dessa expressão por uma constante ou variável nomeada mantendo o mesmo valor e obter exatamente o mesmo resultado. No entanto, para Solidity este não é o caso.

Em tempo de execução, a divisão do Solidity arredonda direcionando a zero, e outras operações aritméticas direcionam para overflow, enquanto em tempo de compilação, as expressões são avaliadas usando frações simples com numerador e denominador grandes e arbitrários. Assim, em tempo de execução, expressão ((7 / 11 + 3 / 13) * 22 + 1) * 39 seria avaliada em 39, enquanto em tempo de compilação a mesma expressão é avaliada em 705. A diferença é porque em tempo de execução , 7/11 e 3/13 são arredondados para zero, mas em tempo de compilação, toda a expressão é avaliada em frações simples sem nenhum arredondamento.

Ainda mais interessante, a seguinte expressão é válida em Solidity: 7523 /48124631 * 6397, enquanto isso não é válido: 7523 / 48125631 * 6397. A diferença é que o primeiro é avaliado como número inteiro, enquanto o último é avaliado como não inteiro. Lembre-se que Solidity não suporta frações em tempo de execução, então todos os literais precisam ser inteiros.

Embora números fracionários e inteiros grandes possam ser representados em Solidity em tempo de execução, conforme descrito nas seções anteriores, não há uma maneira conveniente de representá-los no código. Isso faz com que qualquer código que realize operações com esses números seja bastante enigmático.

Desde que o Solidity não tenha um formato padrão de ponto fixo nem de ponto flutuante, cada biblioteca usa o seu próprio, o que torna as bibliotecas incompatíveis entre si.

Conclusão

Os contratos inteligentes fizeram contas desde o início da blockchain Ethereum. De porcentagens simples a complicadas avaliações de derivativos. No entanto, Solidity, a principal linguagem para o desenvolvimento de contratos inteligentes, não vai muito além, em termos de matemática, expondo apenas o que os opcodes EVM são capazes de fazer.

As bibliotecas tentam cobrir o que falta na linguagem básica, mas sofrem com a ausência de formatos numéricos padronizados.

A linguagem principal tem alguns recursos incomparáveis, mas ao mesmo tempo não tem suporte para coisas básicas e obrigatórias.

Nos próximos artigos, discutiremos como lidar com tudo isso, e o próximo tópico a ser discutido será: overflow.

Outros artigos desta série:

· Parte 2: Overflow
· Parte 3: Porcentagens e Proporções
· Parte 4: Juros Compostos
· Parte 5: Expoente e Logaritmo

Oldest comments (0)