WEB3DEV

Cover image for Resolvendo Quebra-Cabeças more-evm-puzzles de Maneira Diferente? - Parte II
Paulo Gio
Paulo Gio

Posted on • Atualizado em

Resolvendo Quebra-Cabeças more-evm-puzzles de Maneira Diferente? - Parte II

Este post é a segunda parte de uma pequena série dedicada a me motivar enquanto aprendo como a EVM funciona. Vá para a terceira parte.

https://miro.medium.com/v2/resize:fit:1100/format:webp/0*Cxzxesx_0BAab4D1.jpg

Ethereum Virtuelle Machine — cryptoast.fr

Se você chegou aqui pela publicação anterior, bem-vindo de volta! E se você acabou aqui por acaso, embora não haja necessidade de consultar o artigo anterior antes de continuar, provavelmente perderá algum contexto.

Agora, vamos voltar ao nosso próximo e último quebra-cabeça.

Quebra-cabeça 9

Link para o playground do quebra-cabeça.

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*jaU9Xz14mUQ9Wwi690h2wg.png

Com uma olhada rápida, podemos ver a instrução SHA3 aparecendo pela primeira vez nesta série.

Agora vamos dividir isso tudo! 🍴

O código começa armazenando msg.value na memória.

00. CALLVALUE // [CallValue]
02. PUSH1 0x00 // [0, CallValue]
03. MSTORE // [] MEM{CallValue}
Armazena uma representação de 32 bytes do que retorna CallValue, no início da memória (posição 0).
Enter fullscreen mode Exit fullscreen mode

Em seguida, coloca na pilha o resultado da aplicação da instrução SHA3 aos primeiros 32 bytes da memória.

05. PUSH1 0x20 // [20] MEM{CallValue}
07. PUSH1 0x00 // [0, 20] MEM{CallValue}
08. SHA3 // [Keccak256(CallValue)]
Reúne os primeiros 32 bytes da memória e aplica o algoritmo de hash keccak256, empurrando o resultado na pilha.
Enter fullscreen mode Exit fullscreen mode

Desloca para a direita 0xf8 bits (248 bits / 31 bytes), da representação de 256 bits (32 bytes) para a função hash, deixando para trás um único byte.

Basicamente, obtém o primeiro byte do hash.

0A. PUSH1 0xF8 // [F8, Keccak256(CallValue)]
0B. SHR // [(Keccak256(CallValue) >> F8)]
Vamos abreviar (Keccak256(CallValue) >> F8) como Z.
Enter fullscreen mode Exit fullscreen mode

E por último compara o resultado da operação anterior, Z, com 0xA8.

0D. PUSH1 0xA8 // [A8, Z]
0E. EQ // [A8 == Z ? 1: 0]
Enter fullscreen mode Exit fullscreen mode

Se for igual, então nós conseguimos! 🎊 Caso contrário, reverta! ❌

10. PUSH1 0x16 // [16, (A8 == Z ? 1: 0)]
11. JUMPI // []
12. REVERT
13. REVERT
14. REVERT
15. REVERT
16. JUMPDEST
17. STOP
Enter fullscreen mode Exit fullscreen mode

Solução

Excelente! Já sabemos o que fazer! 👌

Temos que encontrar um hash para algum valor ainda desconhecido (aquele que damos dentro de msg.value neste caso) e obter apenas seu primeiro byte, que seria igual a 0xa8.

>>> hex(0xa8?????????????...???????????????????? >> 0xf8)
'0xa8'
Enter fullscreen mode Exit fullscreen mode

Como o keccak256 é um algoritmo de hash criptográfico, a única maneira de chegar ao valor desejado seria pela força bruta 💪.

Em pseudocódigo ficaria assim:

i = 0;
while((keccak256(i) >> 0xF8) != 0xA8) {
   i++;
}
Enter fullscreen mode Exit fullscreen mode

Para deixar isso mais desafiador, decidi resolver usando o código EVM também 😏.

Começamos definindo um contador, que, de maneira análoga, representaria a variável i em nosso código, e imediatamente em seguida definimos um destino de salto onde o corpo do loop deve começar.

01. PUSH1 0x00 // [i] -> starts as i = 0;
02. JUMPDEST // [i]
Enter fullscreen mode Exit fullscreen mode

E agora, se você prestar atenção, perceberá que o código a seguir é bastante semelhante à lógica do quebra-cabeça, o que faz sentido.

03. DUP1 // [i, i]
05. PUSH1 0x00 // [0, i, i]
06. MSTORE // [i] MEM{i}
08. PUSH1 0x20 // [20, i] MEM{i}
0A. PUSH1 0x00 // [0, 20, i] MEM{i}
0B. SHA3 // [keccak256(i), i]
0D. PUSH1 0xF8 // [F8, keccak256(i), i]
0E. SHR // [(keccak256(i) >> 0xF8), i]
10. PUSH1 0xA8 // [A8, (keccak256(i) >> 0xF8), i]
11. EQ // [((A8 == (keccak256(i) >> 0xF8)) ? 1: 0), i]
Enter fullscreen mode Exit fullscreen mode

A única diferença é que, em vez de reverter quando a comparação falha, incrementamos em um a cópia do contador que deixamos na pilha e repetimos até não falhar! 👏

13. PUSH1 0x1B // [0x1B, (A8 == (keccak256(i) >> 0xF8)) ? 1: 0), i]
14. JUMPI // [i]
16. PUSH1 0x01 // [1, i]
17. ADD // [i = (i + 1)] => [i]
19. PUSH1 0x02 // [2, i]
1A. JUMP // [i]
1B. JUMPDEST
1C. STOP
Enter fullscreen mode Exit fullscreen mode

Com o link para a solução no playground, você pode ir e deixá-la correr até terminar, o que levará à pilha com sua resposta: 0x2F (47)

Para ser honesto…

Para ser completamente honesto, meu caro leitor, tentei usar força bruta em javascript e python no início, mas falhei miseravelmente 😢. Então, frustrado, decidi resolvê-lo na mesma linguagem em que seria avaliado.

Como o que me fez falhar foi algo bem idiota, mas me fez olhar mais profundamente no funcionamento da instrução SHA3, eu criei uma subseção dedicada a ela abaixo.

Por que diabos não consigo fazer isso funcionar?

Sabendo a verdadeira resposta, tentei entender porque não conseguia replicar meus próprios resultados 🔍.

Comecei executando um do Hardhat para poder usar ETH. A primeira, e óbvia, coisa que tentei foi obter o keccak256 da resposta conhecida: 0x2f.

> ethers.utils.keccak256(0x2f)
'0xfba9715e477e68952d3f1df7a185b3708aadad50ec10cc793973864023868527'
> ethers.utils.keccak256("0x2f")
'0xfba9715e477e68952d3f1df7a185b3708aadad50ec10cc793973864023868527'> ethers.utils.keccak256(47)
'0xfba9715e477e68952d3f1df7a185b3708aadad50ec10cc793973864023868527'
Enter fullscreen mode Exit fullscreen mode

Isso ☝️ está obviamente errado, pois o hash correto deve começar com 0xa8 como já sabemos.

Então resolvi testar algumas coisas:

> (47 === 0x2f)
true
> (0x2f === 0x0000000000000000000000002f)
true
> (47 === 0x0000002f)
true
Enter fullscreen mode Exit fullscreen mode

Ok, então executar o keccak256 de 0x000…0002f retornaria o mesmo que 0x2f. Então, o que diabos essa função está fazendo?

Segundo a documentação oficial:

ethers.utils.keccak256( aBytesLike ) ⇒ string< DataHexString< 32 > >

Retorna o resumo KECCAK256 aBytesLike.

Ótimo, mas isso está codificando os dados de alguma forma? Verificando o código-fonte, você descobrirá que ele retorna uma string no seguinte formato:

'0x' + sha3.keccak_256(arrayify(dados))

Aqui é quando eu encontro o método arrayify e tudo começa a fazer sentido.

> ethers.utils.arrayify(0x00....2f)
Uint8Array(1) [ 47 ]
Enter fullscreen mode Exit fullscreen mode

Ele transforma a entrada em um array de Uint8s. A instrução SHA3 opera com 32 bytes de memória, o que pode ser considerado um array de 32 Uint8s.

Vamos tentar enviar como o valor ficaria na memória como uma string:

> ethers.utils.arrayify("0x000000000...0000000000000000002f")
Uint8Array(32) [
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 47
]> ethers.utils.keccak256("0x000000000...0000000000000000002f")
'0xa813484aef6fb598f9f753daf162068ff39ccea4075cb95e1a30f86995b5b7ee'
Enter fullscreen mode Exit fullscreen mode

E aí está! Meu erro foi supor que havia algum tipo de expansão de algum tipo, para chegar aos 32 bytes necessários se eu estivesse enviando menos, mas não foi o caso 👎.

Quando percebi isso já era tarde demais, o desafio já estava resolvido, e resolvi compartilhar mesmo assim 😄.

FOFOCA: Alguém me disse que isso era um bug em um contrato inteligente que custou uma fortuna a uma empresa, e se você souber de qual estou falando, deixe-me um link nos comentários abaixo.

Vou deixar você com alguns outros desafios meus no próximo post, se outras coisas não aparecerem no caminho.

がんばれ! 🌸

Vá para a terceira parte desta série ➡️

Artigo original publicado por ᴍatías Λereal Λeón. Traduzido por Paulinho Giovannini.

Top comments (0)