Los enteros con signo en Solidity permiten el uso de números negativos en un contrato inteligente. Este artículo documenta cómo se utilizan a nivel de la EVM. Se asume cierta familiaridad básica con la EVM y los números binarios.
Explicación del complemento a dos
Solidity y la EVM utilizan la representación de complemento a dos para los enteros con signo
Al igual que con todos los tipos de datos, Solidity sigue utilizando palabras de 32 bytes para representar enteros con signo. No hay ningún indicador semántico del tipo en la EVM, al igual que no hay ningún indicador de que un espacio (slot) de 32 bytes sea en realidad un booleano, una dirección o un número de 160 bits. El valor es “tratado” como negativo durante el tiempo de compilación.
Porque puedes obtener el valor máximo de un entero con “type(int256).max” o con el campo .min para obtener el mínimo. El indicador de si un número es positivo o negativo requiere un bit adicional, por lo que solo puede almacenar números hasta un bit menos que la versión sin signo (unsigned).
El complemento a uno significa que un uint256 se convierte en un uint255, con el bit más a la izquierda indicando si es positivo o negativo. Si la EVM utilizara el complemento a uno, esto significaría que type(int256).max == absoluteValue(type(int256.min)), pero este no es el caso. La magnitud máxima de un número negativo en complemento a dos es uno mayor que la magnitud máxima del número positivo. Por ejemplo, el número positivo máximo para int8 es 127, pero el número negativo de magnitud máxima para int8 es -128.
Patrones y ejemplos de la aritmética de complemento a dos.
En lugar de entrar en un montón de demostraciones matemáticas, utilicemos algunos ejemplos reales (esto no pretende ser una demostración por medio de ejemplos de la aritmética de complemento a dos, hay mucha literatura sobre el complemento a dos para el lector interesado).
Vamos a usar un int8 para que los ejemplos sean más legibles. Lo siguiente está en binario, no en hexadecimal.
int8(0) == 0000 0000
type(int8).max == 0111 1111
type(int8).min == 1000 000
Es instructivo ver las representaciones para +1 y -1:
int8(1) == 0000 0001
int8(-1) == 1111 1111
Contemos hacia abajo en complemento a dos para que se haga evidente un patrón:
int8(-2) == 1111 1110
int8(-3) == 1111 1101
int8(-4) == 1111 1100
int8(-5) == 1111 1011
Puedes pensar de forma aproximada en los números negativos en complemento a dos como una “cuenta regresiva”.
Aquí está la característica interesante del complemento a dos. -2 + -2 debería ser igual a -4, y sumar en complemento a dos y permitir el desbordamiento (overflow) hace que esto sea posible. Aquí se suma -2 a sí mismo en Python utilizando la representación de complemento a dos:
>>> (int(b'11111110', 2) + int(b'11111110', 2) ) % 256
252
>>> bin(252)
'0b11111100'
Esto coincide con el patrón esperado arriba.
¿Qué pasa si sumamos +4 a -2? Deberíamos obtener +2. Veámoslo en acción:
>>> # -2 + 4
>>> (int(b'11111110', 2) + int(b'00000100', 2)) % 256
>>> 2
Esto solo funciona si ambos números están en representación de complemento a dos. Solidity no permite sumar enteros sin signo con enteros con signo, ya que resulta ambiguo cuál es la intención.
El complemento a dos también funciona con la multiplicación. El resultado esperado de -2 y -2 es +4, y se anima al lector a copiar el código anterior para verificarlo.
Esto no funciona para todas las operaciones aritméticas.
El complemento a dos no requiere cambios para la suma, la resta, la multiplicación, ni siquiera para un desplazamiento de bits a la izquierda (<<). Estos corresponden a los códigos de operación de la EVM ADD, SUB, MUL y SHL. Discutiremos por qué el desplazamiento a la izquierda todavía funciona en el complemento a dos en una sección posterior de este tutorial.
Sin embargo, la multiplicación, el módulo, el desplazamiento a la derecha y la conversión (casting) a un entero con signo más grande no se pueden realizar utilizando los métodos con signo y requieren sus propios códigos de operación. De manera similar, los operadores de comparación tradicionales no funcionarán, ya que los números negativos “parecen” ser más grandes que los positivos.
Códigos de operación de Ethereum para la aritmética con signo
sdiv
Costo de gas: 5
SDIV, o división con signo, sirve para dividir números con signo. Este código de operación se utiliza en segundo plano en código como el siguiente.
function divide(int256 a, int256 b) public pure returns (int256 quotient)
{
quotient = a / b;
}
smod
Costo de gas: 5
Dado que la aritmética de complemento a dos necesita su propio código de operación para div, no es una sorpresa que ocurra lo mismo para obtener el módulo (resto).
function divide(int256 a, int256 b) public pure returns (int256 remainder)
{
remainder = a % b;
}
slt y sgt
Costo de gas: 3
Para comparar la magnitud de los números con signo, primero necesitamos determinar si es positivo o negativo, y luego comparar la magnitud. Estos códigos de operación hacen esa operación en un solo paso.
Al igual que sus contrapartes sin signo, es más eficiente en gas evitar los operadores >= y <= siempre que sea posible y usar en su lugar los operadores de desigualdad estricta.
sar - desplazamiento aritmético a la derecha con signo
Costo de gas: 3
SAR es un código de operación muy raramente utilizado, pero aparecerá en el resultado de la compilación de este código de Solidity. Ten en cuenta que x es un entero e y es un entero sin signo.
contract SarExample {
function main(int256 x, uint256 y) public pure returns (int256 res) {
res = x >> y;
}
}
¿Cómo le damos sentido a esto? En términos de números ordinarios sin signo, desplazar los bits a la derecha una posición tiene el efecto de dividir por dos, desplazar dos posiciones tiene el efecto de dividir por cuatro, etc.
uint256 x = 8 >> 2; // x = 2
uint256 y = 4 >> 1; // y = 2
Si haces esto con un entero con signo, este fenómeno se conserva.
int256 x = -8 >> 2; // x = -2
int256 y = -4 >> 1; // y = -2
¿Por qué no hay un código de operación SAL (desplazamiento aritmético a la izquierda con signo)? ¿Qué debería ocurrir en el siguiente ejemplo?
int256 x = -8 << 2; // x = -32
int256 y = -4 << 1; // y = -8
Multiplicamos por 4 y 2, respectivamente. En complemento a dos, el desplazamiento a la izquierda preserva el número como se espera.
A nivel interno, se utilizó un código de operación regular SHL (desplazamiento a la izquierda). No hay necesidad de un caso especial para el desplazamiento aritmético a la izquierda. Esto puede parecer poco intuitivo, ya que el número se hace más grande a medida que los bits más a la derecha se rellenan con ceros. Pero recuerda, el valor negativo máximo en complemento a dos es cuando el bit más a la izquierda es uno y todos los demás bits son cero.
signextend evm
Costo de gas: 5
Un entero con signo menor de 256 bits tendrá ceros a la izquierda. Sin embargo, los números negativos en complemento a dos siempre comienzan con el bit más a la izquierda en uno. Por lo tanto, si un entero en complemento a dos se convierte a un tipo más grande, el valor cambiará de negativo a positivo ya que los bits más a la izquierda serán cero. Signextend maneja esta transición de forma fluida.
signextend solidity
No puedes usar signextend directamente en Solidity, pero se utiliza en segundo plano cuando un entero más pequeño se convierte a uno más grande. El siguiente código contiene el código de operación signextend en su bytecode compilado para convertir el int8 a un int256.
contract SignExtendExample {
function main(int8 x) public pure returns (int256 res) {
res = x;
}
}
Debería ser evidente en este punto que los enteros más grandes no pueden ser convertidos a enteros más pequeños.
Aprende más
Aprende más temas avanzados en nuestro curso de formación en solidity para expertos.
Publicado originalmente el 11 de abril de 2023