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.
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.
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).
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.
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.
E por último compara o resultado da operação anterior, Z, com 0xA8.
0D. PUSH1 0xA8 // [A8, Z]
0E. EQ // [A8 == Z ? 1: 0]
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
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'
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++;
}
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]
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]
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
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 nó 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'
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
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 ]
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'
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.
Oldest comments (0)