Agradecimentos especiais a Yoav Weiss, Dan Finlay, Martin Koppelmann e às equipes da Arbitrum, Optimism, Polygon, Scroll e SoulWallet pelo feedback e pela revisão.
Nesta postagem sobre as Três Transições, descrevi alguns motivos importantes pelos quais é importante começar a pensar explicitamente sobre o suporte L1 + cross-L2, a segurança da carteira e a privacidade como recursos básicos necessários da pilha do ecossistema, em vez de criar cada um desses itens como complementos que podem ser projetados separadamente por carteiras individuais.
Esta postagem se concentrará mais diretamente nos aspectos técnicos de um subproblema específico: como facilitar a leitura de L1 a partir de L2, L2 a partir de L1 ou uma L2 a partir de outra L2. A solução desse problema é crucial para a implementação de uma arquitetura de separação de ativos/keystore, mas também tem casos de uso valiosos em outras áreas, principalmente a otimização de chamadas confiáveis entre L2s, incluindo casos de uso como a movimentação de ativos entre L1 e L2s.
Quando as L2s se tornarem mais comuns, os usuários terão ativos em várias L2s e, possivelmente, também em L1. Quando as carteiras de contratos inteligentes (multisig, recuperação social ou outras) se tornarem comuns, as chaves necessárias para acessar alguma conta mudarão com o tempo, e as chaves antigas precisarão deixar de ser válidas. Quando essas duas coisas acontecerem, o usuário precisará ter uma maneira de alterar as chaves que têm autoridade para acessar muitas contas que estão em muitos lugares diferentes, sem fazer um número extremamente alto de transações.
Em especial, precisamos de uma maneira de lidar com endereços contrafactuais: endereços que ainda não foram "registrados" de nenhuma forma na cadeia, mas que, no entanto, precisam receber e manter fundos com segurança. Todos nós dependemos de endereços contrafactuais: quando o senhor usa o Ethereum pela primeira vez, consegue gerar um endereço ETH que alguém pode usar para pagá-lo, sem "registrar" o endereço na cadeia (o que exigiria o pagamento de taxas e, portanto, já possuir algum ETH).
Com as EOAs, todos os endereços começam como endereços contrafactuais. Com as carteiras de contratos inteligentes, os endereços contrafatuais ainda são possíveis, em grande parte graças ao CREATE2, que permite que o senhor tenha um endereço ETH que só pode ser preenchido por um contrato inteligente que tenha um código correspondente a um determinado hash.
Algoritmo de cálculo de endereço EIP-1014 (CREATE2).
No entanto, as carteiras de contratos inteligentes apresentam um novo desafio: a possibilidade de alteração das chaves de acesso. O endereço, que é um hash do initcode, só pode conter a chave de verificação inicial da carteira. A chave de verificação atual seria armazenada no armazenamento da carteira, mas esse registro de armazenamento não se propaga magicamente para outros L2s.
Se um usuário tiver muitos endereços em muitos L2s, incluindo endereços que (por serem contrafactuais) o L2 em que ele está não conhece, parece que há apenas uma maneira de permitir que os usuários alterem suas chaves: arquitetura de separação entre ativos e armazenamento de chaves. Cada usuário tem (i) um "contrato de armazenamento de chaves" (em L1 ou em um L2 específico), que armazena a chave de verificação para todas as carteiras, juntamente com as regras para alterar a chave, e (ii) "contratos de carteira" em L1 e em muitos L2s, que fazem a leitura entre cadeias para obter a chave de verificação.
Há duas maneiras de implementar isso:
Para mostrar toda a complexidade, exploraremos o caso mais difícil: quando o armazenamento de chaves estiver em um L2 e a carteira estiver em um L2 diferente. Se o armazenamento de chaves ou a carteira estiver no L1, será necessário apenas metade desse design.
Vamos supor que o repositório de chaves esteja no Linea e a carteira no Kakarot. Uma prova completa das chaves da carteira consiste no seguinte:
Há duas questões primárias de implementação complicadas aqui:
Há cinco opções principais:
Em termos de trabalho de infraestrutura necessário e custo para os usuários, eu os classifico aproximadamente da seguinte forma:
"Agregação" refere-se à ideia de agregar todas as provas fornecidas pelos usuários dentro de cada bloco em uma grande meta-prova que combina todas elas. Isso é possível para os SNARKs e para o KZG, mas não para as ramificações Merkle (o senhor pode combinar um pouco as ramificações Merkle, mas isso economiza apenas log(txs por bloco) / log(número total de keystores), talvez 15-30% na prática, portanto, provavelmente não vale o custo).
A agregação só passa a valer a pena quando o esquema tem um número substancial de usuários, portanto, realisticamente, não há problema em uma implementação da versão 1 deixar a agregação de fora e implementá-la na versão 2.
Este é simples: siga diretamente o diagrama da seção anterior. Mais precisamente, cada "prova" (supondo o caso de dificuldade máxima de provar um L2 em outro L2) conteria:
Infelizmente, as provas de estado do Ethereum são complicadas, mas existem bibliotecas para verificá-las e, se o senhor usar essas bibliotecas, esse mecanismo não será muito complicado de implementar.
O problema maior é o custo. As provas de Merkle são longas e as árvores Patricia são, infelizmente, ~3,9x mais longas do que o necessário (precisamente: uma prova de Merkle ideal em uma árvore com N objetos tem 32 log2(N) bytes e, como as árvores Patricia da Ethereum têm 16 folhas por filho, as provas para essas árvores têm 32 15 log16(N) ~= 125 log2(N) bytes). Em um estado com cerca de 250 milhões (~2²⁸) de contas, isso faz com que cada prova tenha 125 * 28 = 3500 bytes, ou cerca de 56.000 gases, além de custos extras para decodificação e verificação de hashes.
Duas provas juntas acabariam custando cerca de 100.000 a 150.000 gases (sem incluir a verificação de assinatura, se for usada por transação) - significativamente mais do que a base atual de 21.000 gases por transação. Mas a disparidade piora se a prova estiver sendo verificada em L2. A computação dentro de um L2 é barata, porque é feita fora da cadeia e em um ecossistema com muito menos nós do que o L1. Os dados, por outro lado, precisam ser lançados no L1. Portanto, a comparação não é de 21.000 gases contra 150.000 gases; é de 21.000 gases L2 contra 100.000 gases L1.
Podemos calcular o que isso significa observando as comparações entre os custos de gás L1 e os custos de gás L2:
Atualmente, o L1 é cerca de 15 a 25 vezes mais caro que o L2 para envios simples e 20 a 50 vezes mais caro para trocas de tokens. Os envios simples são relativamente pesados em termos de dados, mas as trocas são muito mais pesadas em termos de computação. Portanto, as trocas são uma referência melhor para aproximar o custo da computação L1 em relação à computação L2. Levando tudo isso em conta, se assumirmos uma relação de custo de 30x entre o custo de computação L1 e o custo de computação L2, isso parece implicar que colocar uma prova de Merkle em L2 custará o equivalente a talvez cinquenta transações regulares.
É claro que o uso de uma árvore Merkle binária pode reduzir os custos em cerca de 4 vezes, mas, mesmo assim, o custo, na maioria dos casos, será muito alto e, se estivermos dispostos a fazer o sacrifício de não sermos mais compatíveis com a atual árvore de estado hexário da Ethereum, podemos também buscar opções ainda melhores.
Conceitualmente, o uso de ZK-SNARKs também é fácil de entender: basta substituir as provas de Merkle no diagrama acima por um ZK-SNARK que prove que essas provas de Merkle existem. Um ZK-SNARK custa aproximadamente 400.000 gases de computação e cerca de 400 bytes (compare: 21.000 gases e 100 bytes para uma transação básica, que no futuro poderá ser reduzida para cerca de 25 bytes com a compactação). Portanto, do ponto de vista computacional, um ZK-SNARK custa 19 vezes mais do que uma transação básica hoje e, do ponto de vista dos dados, um ZK-SNARK custa quatro vezes mais do que uma transação básica hoje e 16 vezes mais do que uma transação básica pode custar no futuro.
Esses números são uma grande melhoria em relação às provas de Merkle, mas ainda são bastante caros. Há duas maneiras de melhorar isso: (i) provas KZG para fins especiais ou (ii) agregação, semelhante à agregação ERC-4337, mas usando matemática mais sofisticada. Podemos examinar os dois.
Atenção, esta seção é muito mais matemática do que as outras. Isso ocorre porque estamos indo além das ferramentas de uso geral e construindo algo de uso especial para ser mais barato, de modo que temos que ir muito mais "por baixo do capô". Se o senhor não gosta de matemática profunda, pule direto para a próxima seção.
Primeiro, uma recapitulação de como os compromissos da KZG funcionam:
Algumas das principais propriedades que é importante entender são:
Assim, temos uma estrutura em que podemos continuar adicionando valores ao final de uma lista cada vez maior, embora com um certo limite de tamanho (realisticamente, centenas de milhões poderiam ser viáveis). Em seguida, usamos isso como nossa estrutura de dados para gerenciar (i) um compromisso com a lista de chaves em cada L2, armazenado nesse L2 e espelhado para L1, e (ii) um compromisso com a lista de compromissos de chave L2, armazenado no Ethereum L1 e espelhado para cada L2.
Manter os compromissos atualizados pode se tornar parte da lógica do núcleo L2 ou pode ser implementado sem alterações no protocolo do núcleo L2 por meio de pontes de depósito e retirada.
Portanto, uma prova completa exigiria:
Na verdade, é possível mesclar as duas provas KZG em uma só, de modo que o tamanho total seja de apenas 100 bytes.
Observe uma sutileza: como a lista de chaves é uma lista, e não um mapa de chave/valor como o estado, a lista de chaves terá que atribuir posições sequencialmente. O contrato de compromisso de chave conteria seu próprio registro interno mapeando cada repositório de chaves para um ID e, para cada chave, armazenaria hash (chave, endereço do repositório de chaves) em vez de apenas chave, para comunicar inequivocamente a outros L2s sobre qual repositório de chaves uma determinada entrada está falando.
A vantagem dessa técnica é que ela funciona muito bem em L2. Os dados são de 100 bytes, aproximadamente 4 vezes mais curtos do que um ZK-SNARK e muito mais curtos do que uma prova de Merkle. O custo de computação é, em grande parte, uma verificação de emparelhamento tamanho 2, ou cerca de 119.000 gases. Em L1, os dados são menos importantes do que a computação e, portanto, infelizmente, o KZG é um pouco mais caro do que as provas de Merkle.
As árvores de Verkle envolvem essencialmente o empilhamento de compromissos KZG (ou compromissos IPA, que podem ser mais eficientes e usar criptografia mais simples) uns sobre os outros: para armazenar 2⁴⁸ valores, o senhor pode fazer um compromisso KZG com uma lista de 2²⁴ valores, cada um dos quais é um compromisso KZG com 2²⁴ valores. As árvores Verkle estão sendo <a href="https://notes.ethereum.org/@vbuterin/verkle_tree_eip"> fortemente considerada para a árvore de estados do Ethereum, porque as árvores Verkle podem ser usadas para armazenar mapas de valores-chave e não apenas listas (basicamente, o senhor pode criar uma árvore de tamanho 2²⁵⁶, mas iniciá-la vazia, preenchendo apenas partes específicas da árvore quando realmente precisar preenchê-las).
Como é uma árvore Verkle. Na prática, o senhor pode dar a cada nó uma largura de 256 == 2⁸ para árvores baseadas em IPA, ou 2²⁴ para árvores baseadas em KZG.
As provas nas árvores de Verkle são um pouco mais longas do que na KZG; elas podem ter algumas centenas de bytes. Elas também são difíceis de verificar, especialmente se o senhor tentar agregar muitas provas em uma só.
Na realidade, as árvores de Verkle devem ser consideradas como árvores de Merkle, mas mais viáveis sem SNARKing (devido aos custos menores dos dados) e mais baratas com SNARKing (devido aos custos menores do provador).
A maior vantagem das árvores de Verkle é a possibilidade de harmonizar as estruturas de dados: As provas de Verkle poderiam ser usadas diretamente sobre o estado L1 ou L2, sem estruturas de sobreposição e usando exatamente o mesmo mecanismo para L1 e L2. Quando os computadores quânticos se tornarem um problema, ou quando a comprovação das ramificações de Merkle se tornar eficiente o suficiente, as árvores de Verkle poderão ser substituídas no local por uma árvore de hash binária com uma função de hash adequada ao SNARK.
Se N usuários fizerem N transações (ou, de forma mais realista, N ERC-4337 UserOperations) que precisem provar N reivindicações entre cadeias, podemos economizar muito combustível agregando essas provas: o construtor que combinaria essas transações em um bloco ou pacote que entra em um bloco pode criar uma única prova que comprove todas essas reivindicações simultaneamente.
Isso pode significar:
Em todos os três casos, as provas custariam apenas algumas centenas de milhares de gás cada. O construtor precisaria fazer um desses em cada L2 para os usuários desse L2; portanto, para que a construção seja útil, o esquema como um todo precisa ter uso suficiente para que haja, com frequência, pelo menos algumas transações dentro do mesmo bloco em vários L2s principais.
Se os ZK-SNARKs forem usados, o principal custo marginal será simplesmente a "lógica comercial" de passar números entre contratos, portanto, talvez alguns milhares de gás L2 por usuário. Se forem usadas provas múltiplas KZG, o provador precisaria adicionar 48 gases para cada L2 com armazenamento de chaves usado nesse bloco, de modo que o custo marginal do esquema por usuário adicionaria mais ~800 gases L1 por L2 (não por usuário). Mas esses custos são muito menores do que os custos da não agregação, que inevitavelmente envolvem mais de 10.000 gases L1 e centenas de milhares de gases L2 por usuário. Para árvores Verkle, o senhor pode usar diretamente as multiprovas Verkle, adicionando cerca de 100-200 bytes por usuário, ou pode fazer um ZK-SNARK de uma multiprova Verkle, que tem custos semelhantes aos ZK-SNARKs de ramos Merkle, mas é significativamente mais barato de provar.
Do ponto de vista da implementação, provavelmente é melhor fazer com que os empacotadores agreguem provas entre cadeias por meio do padrão de abstração de conta ERC-4337. O ERC-4337 já tem um mecanismo para que os construtores agreguem partes de UserOperations de forma personalizada. Há até mesmo uma <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi"> implementação disso para a agregação de assinaturas BLS, que poderia reduzir os custos de gás no L2 de 1,5x a 3x, dependendo de quais outras formas de compressão estão incluídas.
Diagrama de <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi"> Postagem de implementação da carteira BLS mostrando o fluxo de trabalho das assinaturas agregadas BLS em uma versão anterior do ERC-4337. O fluxo de trabalho de agregação de provas entre cadeias provavelmente será muito semelhante.
Uma última possibilidade, que só pode ser usada para L2 que lê L1 (e não L1 que lê L2), é modificar os L2s para permitir que eles façam chamadas estáticas para contratos em L1 diretamente.
Isso poderia ser feito com um opcode ou uma pré-compilação, que permite chamadas para L1 em que o usuário fornece o endereço de destino, gás e calldata, e retorna a saída, embora, como essas chamadas são chamadas estáticas, elas não possam realmente alterar qualquer estado de L1. Os L2s já precisam estar cientes do L1 para processar os depósitos, portanto, não há nada fundamental que impeça a implementação de tal coisa; trata-se principalmente de um desafio de implementação técnica (veja: esta RFP do Optimism para dar suporte a chamadas estáticas para o L1).
Observe que, se o armazenamento de chaves estiver em L1 e os L2s integrarem a funcionalidade de chamada estática de L1, não será necessário nenhum tipo de prova! No entanto, se os L2s não integrarem as chamadas estáticas do L1 ou se o armazenamento de chaves estiver no L2 (o que talvez seja necessário, uma vez que o L1 se torne muito caro para os usuários usarem, mesmo que só um pouco), serão necessárias provas.
Todos os esquemas acima exigem que o L2 acesse a raiz do estado L1 recente ou todo o estado L1 recente. Felizmente, todos os L2s já têm alguma funcionalidade para acessar o estado recente do L1. Isso ocorre porque eles precisam dessa funcionalidade para processar as mensagens que chegam do L1 para o L2, principalmente os depósitos.
E, de fato, se um L2 tiver um recurso de depósito, o senhor poderá usar esse L2 no estado em que se encontra para mover as raízes do estado L1 para um contrato no L2: basta que um contrato no L1 chame o opcode BLOCKHASH e passe-o para o L2 como uma mensagem de depósito. O cabeçalho completo do bloco pode ser recebido, e sua raiz de estado extraída, no lado L2. No entanto, seria muito melhor que cada L2 tivesse uma maneira explícita de acessar diretamente o estado L1 recente completo ou as raízes do estado L1 recente.
O principal desafio de otimizar a forma como os L2s recebem as raízes recentes do estado L1 é obter simultaneamente segurança e baixa latência:
Além disso, na direção oposta (L1s lendo L2):
Algumas dessas velocidades para operações de cadeia cruzada sem confiança são inaceitavelmente lentas para muitos casos de uso definidos; para esses casos, o senhor precisa de pontes mais rápidas com modelos de segurança mais imperfeitos. No entanto, para o caso de uso de atualização de chaves de carteira, atrasos maiores são mais aceitáveis: o senhor não está atrasando as transações em horas, está atrasando as alterações de chaves. O senhor terá que manter as chaves antigas por mais tempo. Se o senhor estiver trocando as chaves porque elas são roubadas, terá um período significativo de vulnerabilidade, mas isso pode ser atenuado, por exemplo, se as carteiras tiverem uma função de congelamento.
Em última análise, a melhor solução para minimizar a latência é que os L2s implementem a leitura direta das raízes do estado L1 de forma otimizada, em que cada bloco L2 (ou o registro de computação da raiz do estado) contenha um ponteiro para o bloco L1 mais recente, portanto, se o L1 reverter, o L2 também poderá reverter. Os contratos do Keystore devem ser colocados na rede principal ou em L2s que sejam ZK-rollups e, portanto, possam se comprometer rapidamente com a L1.
Os blocos da cadeia L2 podem ter dependências não apenas dos blocos L2 anteriores, mas também de um bloco L1. Se o L1 for revertido após esse link, o L2 também será revertido. Vale a pena observar que essa também é a forma como uma versão anterior (pré-Dank) do sharding foi concebida para funcionar; veja aqui o código.
Surpreendentemente, não muito. Na verdade, nem precisa ser um rollup: se for um L3 ou um validium, não há problema em manter as carteiras lá, desde que o senhor mantenha os armazenadores de chaves no L1 ou em um rollup ZK. O que o senhor precisa é que a cadeia tenha acesso direto às raízes do estado do Ethereum e um compromisso técnico e social de estar disposto a se reorganizar se o Ethereum se reorganizar e a fazer hard fork se o Ethereum fizer hard fork.
Um problema de pesquisa interessante é identificar até que ponto é possível que uma cadeia tenha essa forma de conexão com várias outras cadeias (por exemplo, a cadeia de Ethereum e Zcash). É possível fazer isso de forma ingênua: sua cadeia poderia concordar em se reorganizar se a Ethereum ou a Zcash se reorganizarem (e fazer um hard fork se a Ethereum ou a Zcash fizerem um hard fork), mas, nesse caso, os operadores de nós e a comunidade em geral teriam o dobro de dependências técnicas e políticas. Portanto, essa técnica poderia ser usada para se conectar a algumas outras cadeias, mas a um custo cada vez maior. Os esquemas baseados em pontes ZK têm propriedades técnicas atraentes, mas têm como principal ponto fraco o fato de não serem resistentes a ataques de 51% ou hard forks. Pode haver soluções mais inteligentes.
Idealmente, também queremos preservar a privacidade. Se o senhor tiver muitas carteiras que são gerenciadas pelo mesmo repositório de chaves, queremos ter certeza disso:
Isso gera alguns problemas:
Com os SNARKs, as soluções são conceitualmente fáceis: as provas ocultam informações por padrão, e o agregador precisa produzir um SNARK recursivo para provar os SNARKs.
O principal desafio dessa abordagem hoje é que a agregação exige que o agregador crie um SNARK recursivo, o que atualmente é bastante lento.
Com o KZG, podemos usar <a href="https://notes.ethereum.org/@vbuterin/non_index_revealing_proof"> this O senhor pode usar o trabalho sobre provas KZG sem revelação de índice (veja também: uma versão mais formalizada desse trabalho no artigo de Caulk) como ponto de partida. A agregação de provas cegas, no entanto, é um problema em aberto que requer mais atenção.
Infelizmente, a leitura direta de L1 de dentro de L2 não preserva a privacidade, embora a implementação da funcionalidade de leitura direta ainda seja muito útil, tanto para minimizar a latência quanto por causa de sua utilidade para outros aplicativos.
Agradecimentos especiais a Yoav Weiss, Dan Finlay, Martin Koppelmann e às equipes da Arbitrum, Optimism, Polygon, Scroll e SoulWallet pelo feedback e pela revisão.
Nesta postagem sobre as Três Transições, descrevi alguns motivos importantes pelos quais é importante começar a pensar explicitamente sobre o suporte L1 + cross-L2, a segurança da carteira e a privacidade como recursos básicos necessários da pilha do ecossistema, em vez de criar cada um desses itens como complementos que podem ser projetados separadamente por carteiras individuais.
Esta postagem se concentrará mais diretamente nos aspectos técnicos de um subproblema específico: como facilitar a leitura de L1 a partir de L2, L2 a partir de L1 ou uma L2 a partir de outra L2. A solução desse problema é crucial para a implementação de uma arquitetura de separação de ativos/keystore, mas também tem casos de uso valiosos em outras áreas, principalmente a otimização de chamadas confiáveis entre L2s, incluindo casos de uso como a movimentação de ativos entre L1 e L2s.
Quando as L2s se tornarem mais comuns, os usuários terão ativos em várias L2s e, possivelmente, também em L1. Quando as carteiras de contratos inteligentes (multisig, recuperação social ou outras) se tornarem comuns, as chaves necessárias para acessar alguma conta mudarão com o tempo, e as chaves antigas precisarão deixar de ser válidas. Quando essas duas coisas acontecerem, o usuário precisará ter uma maneira de alterar as chaves que têm autoridade para acessar muitas contas que estão em muitos lugares diferentes, sem fazer um número extremamente alto de transações.
Em especial, precisamos de uma maneira de lidar com endereços contrafactuais: endereços que ainda não foram "registrados" de nenhuma forma na cadeia, mas que, no entanto, precisam receber e manter fundos com segurança. Todos nós dependemos de endereços contrafactuais: quando o senhor usa o Ethereum pela primeira vez, consegue gerar um endereço ETH que alguém pode usar para pagá-lo, sem "registrar" o endereço na cadeia (o que exigiria o pagamento de taxas e, portanto, já possuir algum ETH).
Com as EOAs, todos os endereços começam como endereços contrafactuais. Com as carteiras de contratos inteligentes, os endereços contrafatuais ainda são possíveis, em grande parte graças ao CREATE2, que permite que o senhor tenha um endereço ETH que só pode ser preenchido por um contrato inteligente que tenha um código correspondente a um determinado hash.
Algoritmo de cálculo de endereço EIP-1014 (CREATE2).
No entanto, as carteiras de contratos inteligentes apresentam um novo desafio: a possibilidade de alteração das chaves de acesso. O endereço, que é um hash do initcode, só pode conter a chave de verificação inicial da carteira. A chave de verificação atual seria armazenada no armazenamento da carteira, mas esse registro de armazenamento não se propaga magicamente para outros L2s.
Se um usuário tiver muitos endereços em muitos L2s, incluindo endereços que (por serem contrafactuais) o L2 em que ele está não conhece, parece que há apenas uma maneira de permitir que os usuários alterem suas chaves: arquitetura de separação entre ativos e armazenamento de chaves. Cada usuário tem (i) um "contrato de armazenamento de chaves" (em L1 ou em um L2 específico), que armazena a chave de verificação para todas as carteiras, juntamente com as regras para alterar a chave, e (ii) "contratos de carteira" em L1 e em muitos L2s, que fazem a leitura entre cadeias para obter a chave de verificação.
Há duas maneiras de implementar isso:
Para mostrar toda a complexidade, exploraremos o caso mais difícil: quando o armazenamento de chaves estiver em um L2 e a carteira estiver em um L2 diferente. Se o armazenamento de chaves ou a carteira estiver no L1, será necessário apenas metade desse design.
Vamos supor que o repositório de chaves esteja no Linea e a carteira no Kakarot. Uma prova completa das chaves da carteira consiste no seguinte:
Há duas questões primárias de implementação complicadas aqui:
Há cinco opções principais:
Em termos de trabalho de infraestrutura necessário e custo para os usuários, eu os classifico aproximadamente da seguinte forma:
"Agregação" refere-se à ideia de agregar todas as provas fornecidas pelos usuários dentro de cada bloco em uma grande meta-prova que combina todas elas. Isso é possível para os SNARKs e para o KZG, mas não para as ramificações Merkle (o senhor pode combinar um pouco as ramificações Merkle, mas isso economiza apenas log(txs por bloco) / log(número total de keystores), talvez 15-30% na prática, portanto, provavelmente não vale o custo).
A agregação só passa a valer a pena quando o esquema tem um número substancial de usuários, portanto, realisticamente, não há problema em uma implementação da versão 1 deixar a agregação de fora e implementá-la na versão 2.
Este é simples: siga diretamente o diagrama da seção anterior. Mais precisamente, cada "prova" (supondo o caso de dificuldade máxima de provar um L2 em outro L2) conteria:
Infelizmente, as provas de estado do Ethereum são complicadas, mas existem bibliotecas para verificá-las e, se o senhor usar essas bibliotecas, esse mecanismo não será muito complicado de implementar.
O problema maior é o custo. As provas de Merkle são longas e as árvores Patricia são, infelizmente, ~3,9x mais longas do que o necessário (precisamente: uma prova de Merkle ideal em uma árvore com N objetos tem 32 log2(N) bytes e, como as árvores Patricia da Ethereum têm 16 folhas por filho, as provas para essas árvores têm 32 15 log16(N) ~= 125 log2(N) bytes). Em um estado com cerca de 250 milhões (~2²⁸) de contas, isso faz com que cada prova tenha 125 * 28 = 3500 bytes, ou cerca de 56.000 gases, além de custos extras para decodificação e verificação de hashes.
Duas provas juntas acabariam custando cerca de 100.000 a 150.000 gases (sem incluir a verificação de assinatura, se for usada por transação) - significativamente mais do que a base atual de 21.000 gases por transação. Mas a disparidade piora se a prova estiver sendo verificada em L2. A computação dentro de um L2 é barata, porque é feita fora da cadeia e em um ecossistema com muito menos nós do que o L1. Os dados, por outro lado, precisam ser lançados no L1. Portanto, a comparação não é de 21.000 gases contra 150.000 gases; é de 21.000 gases L2 contra 100.000 gases L1.
Podemos calcular o que isso significa observando as comparações entre os custos de gás L1 e os custos de gás L2:
Atualmente, o L1 é cerca de 15 a 25 vezes mais caro que o L2 para envios simples e 20 a 50 vezes mais caro para trocas de tokens. Os envios simples são relativamente pesados em termos de dados, mas as trocas são muito mais pesadas em termos de computação. Portanto, as trocas são uma referência melhor para aproximar o custo da computação L1 em relação à computação L2. Levando tudo isso em conta, se assumirmos uma relação de custo de 30x entre o custo de computação L1 e o custo de computação L2, isso parece implicar que colocar uma prova de Merkle em L2 custará o equivalente a talvez cinquenta transações regulares.
É claro que o uso de uma árvore Merkle binária pode reduzir os custos em cerca de 4 vezes, mas, mesmo assim, o custo, na maioria dos casos, será muito alto e, se estivermos dispostos a fazer o sacrifício de não sermos mais compatíveis com a atual árvore de estado hexário da Ethereum, podemos também buscar opções ainda melhores.
Conceitualmente, o uso de ZK-SNARKs também é fácil de entender: basta substituir as provas de Merkle no diagrama acima por um ZK-SNARK que prove que essas provas de Merkle existem. Um ZK-SNARK custa aproximadamente 400.000 gases de computação e cerca de 400 bytes (compare: 21.000 gases e 100 bytes para uma transação básica, que no futuro poderá ser reduzida para cerca de 25 bytes com a compactação). Portanto, do ponto de vista computacional, um ZK-SNARK custa 19 vezes mais do que uma transação básica hoje e, do ponto de vista dos dados, um ZK-SNARK custa quatro vezes mais do que uma transação básica hoje e 16 vezes mais do que uma transação básica pode custar no futuro.
Esses números são uma grande melhoria em relação às provas de Merkle, mas ainda são bastante caros. Há duas maneiras de melhorar isso: (i) provas KZG para fins especiais ou (ii) agregação, semelhante à agregação ERC-4337, mas usando matemática mais sofisticada. Podemos examinar os dois.
Atenção, esta seção é muito mais matemática do que as outras. Isso ocorre porque estamos indo além das ferramentas de uso geral e construindo algo de uso especial para ser mais barato, de modo que temos que ir muito mais "por baixo do capô". Se o senhor não gosta de matemática profunda, pule direto para a próxima seção.
Primeiro, uma recapitulação de como os compromissos da KZG funcionam:
Algumas das principais propriedades que é importante entender são:
Assim, temos uma estrutura em que podemos continuar adicionando valores ao final de uma lista cada vez maior, embora com um certo limite de tamanho (realisticamente, centenas de milhões poderiam ser viáveis). Em seguida, usamos isso como nossa estrutura de dados para gerenciar (i) um compromisso com a lista de chaves em cada L2, armazenado nesse L2 e espelhado para L1, e (ii) um compromisso com a lista de compromissos de chave L2, armazenado no Ethereum L1 e espelhado para cada L2.
Manter os compromissos atualizados pode se tornar parte da lógica do núcleo L2 ou pode ser implementado sem alterações no protocolo do núcleo L2 por meio de pontes de depósito e retirada.
Portanto, uma prova completa exigiria:
Na verdade, é possível mesclar as duas provas KZG em uma só, de modo que o tamanho total seja de apenas 100 bytes.
Observe uma sutileza: como a lista de chaves é uma lista, e não um mapa de chave/valor como o estado, a lista de chaves terá que atribuir posições sequencialmente. O contrato de compromisso de chave conteria seu próprio registro interno mapeando cada repositório de chaves para um ID e, para cada chave, armazenaria hash (chave, endereço do repositório de chaves) em vez de apenas chave, para comunicar inequivocamente a outros L2s sobre qual repositório de chaves uma determinada entrada está falando.
A vantagem dessa técnica é que ela funciona muito bem em L2. Os dados são de 100 bytes, aproximadamente 4 vezes mais curtos do que um ZK-SNARK e muito mais curtos do que uma prova de Merkle. O custo de computação é, em grande parte, uma verificação de emparelhamento tamanho 2, ou cerca de 119.000 gases. Em L1, os dados são menos importantes do que a computação e, portanto, infelizmente, o KZG é um pouco mais caro do que as provas de Merkle.
As árvores de Verkle envolvem essencialmente o empilhamento de compromissos KZG (ou compromissos IPA, que podem ser mais eficientes e usar criptografia mais simples) uns sobre os outros: para armazenar 2⁴⁸ valores, o senhor pode fazer um compromisso KZG com uma lista de 2²⁴ valores, cada um dos quais é um compromisso KZG com 2²⁴ valores. As árvores Verkle estão sendo <a href="https://notes.ethereum.org/@vbuterin/verkle_tree_eip"> fortemente considerada para a árvore de estados do Ethereum, porque as árvores Verkle podem ser usadas para armazenar mapas de valores-chave e não apenas listas (basicamente, o senhor pode criar uma árvore de tamanho 2²⁵⁶, mas iniciá-la vazia, preenchendo apenas partes específicas da árvore quando realmente precisar preenchê-las).
Como é uma árvore Verkle. Na prática, o senhor pode dar a cada nó uma largura de 256 == 2⁸ para árvores baseadas em IPA, ou 2²⁴ para árvores baseadas em KZG.
As provas nas árvores de Verkle são um pouco mais longas do que na KZG; elas podem ter algumas centenas de bytes. Elas também são difíceis de verificar, especialmente se o senhor tentar agregar muitas provas em uma só.
Na realidade, as árvores de Verkle devem ser consideradas como árvores de Merkle, mas mais viáveis sem SNARKing (devido aos custos menores dos dados) e mais baratas com SNARKing (devido aos custos menores do provador).
A maior vantagem das árvores de Verkle é a possibilidade de harmonizar as estruturas de dados: As provas de Verkle poderiam ser usadas diretamente sobre o estado L1 ou L2, sem estruturas de sobreposição e usando exatamente o mesmo mecanismo para L1 e L2. Quando os computadores quânticos se tornarem um problema, ou quando a comprovação das ramificações de Merkle se tornar eficiente o suficiente, as árvores de Verkle poderão ser substituídas no local por uma árvore de hash binária com uma função de hash adequada ao SNARK.
Se N usuários fizerem N transações (ou, de forma mais realista, N ERC-4337 UserOperations) que precisem provar N reivindicações entre cadeias, podemos economizar muito combustível agregando essas provas: o construtor que combinaria essas transações em um bloco ou pacote que entra em um bloco pode criar uma única prova que comprove todas essas reivindicações simultaneamente.
Isso pode significar:
Em todos os três casos, as provas custariam apenas algumas centenas de milhares de gás cada. O construtor precisaria fazer um desses em cada L2 para os usuários desse L2; portanto, para que a construção seja útil, o esquema como um todo precisa ter uso suficiente para que haja, com frequência, pelo menos algumas transações dentro do mesmo bloco em vários L2s principais.
Se os ZK-SNARKs forem usados, o principal custo marginal será simplesmente a "lógica comercial" de passar números entre contratos, portanto, talvez alguns milhares de gás L2 por usuário. Se forem usadas provas múltiplas KZG, o provador precisaria adicionar 48 gases para cada L2 com armazenamento de chaves usado nesse bloco, de modo que o custo marginal do esquema por usuário adicionaria mais ~800 gases L1 por L2 (não por usuário). Mas esses custos são muito menores do que os custos da não agregação, que inevitavelmente envolvem mais de 10.000 gases L1 e centenas de milhares de gases L2 por usuário. Para árvores Verkle, o senhor pode usar diretamente as multiprovas Verkle, adicionando cerca de 100-200 bytes por usuário, ou pode fazer um ZK-SNARK de uma multiprova Verkle, que tem custos semelhantes aos ZK-SNARKs de ramos Merkle, mas é significativamente mais barato de provar.
Do ponto de vista da implementação, provavelmente é melhor fazer com que os empacotadores agreguem provas entre cadeias por meio do padrão de abstração de conta ERC-4337. O ERC-4337 já tem um mecanismo para que os construtores agreguem partes de UserOperations de forma personalizada. Há até mesmo uma <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi"> implementação disso para a agregação de assinaturas BLS, que poderia reduzir os custos de gás no L2 de 1,5x a 3x, dependendo de quais outras formas de compressão estão incluídas.
Diagrama de <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi"> Postagem de implementação da carteira BLS mostrando o fluxo de trabalho das assinaturas agregadas BLS em uma versão anterior do ERC-4337. O fluxo de trabalho de agregação de provas entre cadeias provavelmente será muito semelhante.
Uma última possibilidade, que só pode ser usada para L2 que lê L1 (e não L1 que lê L2), é modificar os L2s para permitir que eles façam chamadas estáticas para contratos em L1 diretamente.
Isso poderia ser feito com um opcode ou uma pré-compilação, que permite chamadas para L1 em que o usuário fornece o endereço de destino, gás e calldata, e retorna a saída, embora, como essas chamadas são chamadas estáticas, elas não possam realmente alterar qualquer estado de L1. Os L2s já precisam estar cientes do L1 para processar os depósitos, portanto, não há nada fundamental que impeça a implementação de tal coisa; trata-se principalmente de um desafio de implementação técnica (veja: esta RFP do Optimism para dar suporte a chamadas estáticas para o L1).
Observe que, se o armazenamento de chaves estiver em L1 e os L2s integrarem a funcionalidade de chamada estática de L1, não será necessário nenhum tipo de prova! No entanto, se os L2s não integrarem as chamadas estáticas do L1 ou se o armazenamento de chaves estiver no L2 (o que talvez seja necessário, uma vez que o L1 se torne muito caro para os usuários usarem, mesmo que só um pouco), serão necessárias provas.
Todos os esquemas acima exigem que o L2 acesse a raiz do estado L1 recente ou todo o estado L1 recente. Felizmente, todos os L2s já têm alguma funcionalidade para acessar o estado recente do L1. Isso ocorre porque eles precisam dessa funcionalidade para processar as mensagens que chegam do L1 para o L2, principalmente os depósitos.
E, de fato, se um L2 tiver um recurso de depósito, o senhor poderá usar esse L2 no estado em que se encontra para mover as raízes do estado L1 para um contrato no L2: basta que um contrato no L1 chame o opcode BLOCKHASH e passe-o para o L2 como uma mensagem de depósito. O cabeçalho completo do bloco pode ser recebido, e sua raiz de estado extraída, no lado L2. No entanto, seria muito melhor que cada L2 tivesse uma maneira explícita de acessar diretamente o estado L1 recente completo ou as raízes do estado L1 recente.
O principal desafio de otimizar a forma como os L2s recebem as raízes recentes do estado L1 é obter simultaneamente segurança e baixa latência:
Além disso, na direção oposta (L1s lendo L2):
Algumas dessas velocidades para operações de cadeia cruzada sem confiança são inaceitavelmente lentas para muitos casos de uso definidos; para esses casos, o senhor precisa de pontes mais rápidas com modelos de segurança mais imperfeitos. No entanto, para o caso de uso de atualização de chaves de carteira, atrasos maiores são mais aceitáveis: o senhor não está atrasando as transações em horas, está atrasando as alterações de chaves. O senhor terá que manter as chaves antigas por mais tempo. Se o senhor estiver trocando as chaves porque elas são roubadas, terá um período significativo de vulnerabilidade, mas isso pode ser atenuado, por exemplo, se as carteiras tiverem uma função de congelamento.
Em última análise, a melhor solução para minimizar a latência é que os L2s implementem a leitura direta das raízes do estado L1 de forma otimizada, em que cada bloco L2 (ou o registro de computação da raiz do estado) contenha um ponteiro para o bloco L1 mais recente, portanto, se o L1 reverter, o L2 também poderá reverter. Os contratos do Keystore devem ser colocados na rede principal ou em L2s que sejam ZK-rollups e, portanto, possam se comprometer rapidamente com a L1.
Os blocos da cadeia L2 podem ter dependências não apenas dos blocos L2 anteriores, mas também de um bloco L1. Se o L1 for revertido após esse link, o L2 também será revertido. Vale a pena observar que essa também é a forma como uma versão anterior (pré-Dank) do sharding foi concebida para funcionar; veja aqui o código.
Surpreendentemente, não muito. Na verdade, nem precisa ser um rollup: se for um L3 ou um validium, não há problema em manter as carteiras lá, desde que o senhor mantenha os armazenadores de chaves no L1 ou em um rollup ZK. O que o senhor precisa é que a cadeia tenha acesso direto às raízes do estado do Ethereum e um compromisso técnico e social de estar disposto a se reorganizar se o Ethereum se reorganizar e a fazer hard fork se o Ethereum fizer hard fork.
Um problema de pesquisa interessante é identificar até que ponto é possível que uma cadeia tenha essa forma de conexão com várias outras cadeias (por exemplo, a cadeia de Ethereum e Zcash). É possível fazer isso de forma ingênua: sua cadeia poderia concordar em se reorganizar se a Ethereum ou a Zcash se reorganizarem (e fazer um hard fork se a Ethereum ou a Zcash fizerem um hard fork), mas, nesse caso, os operadores de nós e a comunidade em geral teriam o dobro de dependências técnicas e políticas. Portanto, essa técnica poderia ser usada para se conectar a algumas outras cadeias, mas a um custo cada vez maior. Os esquemas baseados em pontes ZK têm propriedades técnicas atraentes, mas têm como principal ponto fraco o fato de não serem resistentes a ataques de 51% ou hard forks. Pode haver soluções mais inteligentes.
Idealmente, também queremos preservar a privacidade. Se o senhor tiver muitas carteiras que são gerenciadas pelo mesmo repositório de chaves, queremos ter certeza disso:
Isso gera alguns problemas:
Com os SNARKs, as soluções são conceitualmente fáceis: as provas ocultam informações por padrão, e o agregador precisa produzir um SNARK recursivo para provar os SNARKs.
O principal desafio dessa abordagem hoje é que a agregação exige que o agregador crie um SNARK recursivo, o que atualmente é bastante lento.
Com o KZG, podemos usar <a href="https://notes.ethereum.org/@vbuterin/non_index_revealing_proof"> this O senhor pode usar o trabalho sobre provas KZG sem revelação de índice (veja também: uma versão mais formalizada desse trabalho no artigo de Caulk) como ponto de partida. A agregação de provas cegas, no entanto, é um problema em aberto que requer mais atenção.
Infelizmente, a leitura direta de L1 de dentro de L2 não preserva a privacidade, embora a implementação da funcionalidade de leitura direta ainda seja muito útil, tanto para minimizar a latência quanto por causa de sua utilidade para outros aplicativos.