Aprofundamento da leitura cross-L2 para carteiras e outros casos de uso

AvançadoFeb 29, 2024
Neste artigo, Vitalik aborda diretamente um aspecto técnico específico de um subproblema: como ler mais facilmente de L2 para L1, de L1 para L2 ou de uma L2 para outra L2. A solução desse problema é fundamental para a obtenção da arquitetura de separação de ativos/chaves, mas também tem casos de uso valiosos em outras áreas, principalmente a otimização de chamadas confiáveis entre L2, incluindo casos de uso como a movimentação de ativos entre L1 e L2.
Aprofundamento da leitura cross-L2 para carteiras e outros casos de uso

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.

Leituras prévias recomendadas

Tabela de conteúdo

Qual é o objetivo?

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:

  • Versão light (verificação apenas para atualizar chaves): cada carteira armazena a chave de verificação localmente e contém uma função que pode ser chamada para verificar uma prova entre cadeias do estado atual do armazenamento de chaves e atualizar sua chave de verificação armazenada localmente para corresponder. Quando uma carteira é usada pela primeira vez em um L2 específico, é obrigatório chamar essa função para obter a chave de verificação atual do armazenamento de chaves.
    • Vantagem: usa provas de cadeia cruzada com moderação, portanto, não há problema se as provas de cadeia cruzada forem caras. Todos os fundos só podem ser gastos com as chaves atuais, portanto, ainda é seguro.
    • Desvantagem: Para alterar a chave de verificação, o senhor precisa fazer uma alteração de chave na cadeia tanto no repositório de chaves quanto em todas as carteiras já inicializadas (embora não nas contrafactuais). Isso pode custar muito combustível.
  • Versão pesada (verificação para cada tx): uma prova de cadeia cruzada mostrando a chave atualmente no keystore é necessária para cada transação.
    • Vantagem: menos complexidade sistêmica e a atualização do armazenamento de chaves é barata.
    • Lado negativo: caro por tx, portanto, requer muito mais engenharia para tornar as provas de cadeia cruzada aceitavelmente baratas. Também não é facilmente compatível com o ERC-4337, que atualmente não oferece suporte à leitura entre contratos de objetos mutáveis durante a validação.

Como é uma prova de cadeia cruzada?

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:

  • Uma prova que comprova a raiz do estado atual do Linea, dada a raiz do estado atual do Ethereum que Kakarot conhece
  • Uma prova que comprova as chaves atuais no keystore, dada a raiz do estado atual do Linea

Há duas questões primárias de implementação complicadas aqui:

  1. Que tipo de prova usamos? (São provas de Merkle? O senhor não está se sentindo bem?)
  2. Em primeiro lugar, como o L2 aprende a raiz do estado recente do L1 (Ethereum) (ou, como veremos, potencialmente o estado completo do L1)? E, alternativamente, como o L1 aprende a raiz do estado L2?
    • Em ambos os casos, qual é o tempo de espera entre a ocorrência de algo em um lado e a comprovação desse algo pelo outro lado?

Que tipos de esquemas de prova podemos usar?

Há cinco opções principais:

  • Provas de Merkle
  • ZK-SNARKs de uso geral
  • Provas para fins especiais (por exemplo. com a KZG)
  • Provas de Verkle, que estão entre KZG e ZK-SNARKs, tanto em relação à carga de trabalho quanto ao custo da infraestrutura.
  • Não há provas e depende da leitura direta do estado

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.

Como funcionam as provas de Merkle?

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:

  • Um ramo de Merkle provando a raiz de estado do L2 que contém o armazenamento de chaves, dada a raiz de estado mais recente do Ethereum que o L2 conhece. A raiz de estado do L2 que contém o repositório de chaves é armazenada em um slot de armazenamento conhecido de um endereço conhecido (o contrato em L1 que representa o L2) e, portanto, o caminho pela árvore pode ser codificado.
  • Um ramo de Merkle que comprova as chaves de verificação atuais, dada a raiz de estado do L2 de armazenamento de chaves. Aqui, mais uma vez, a chave de verificação é armazenada em um slot de armazenamento conhecido de um endereço conhecido, de modo que o caminho pode ser codificado.

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.

Como funcionam as provas ZK-SNARK?

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.

Como funcionariam as provas KZG para fins especiais?

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:

  • Podemos representar um conjunto de dados [D_1 ... D_n] com uma prova KZG de um polinômio derivado dos dados: especificamente, o polinômio P em que P(w) = D_1, P(w²) = D_2 ... P(wⁿ) = D_n. w aqui é uma "raiz da unidade", um valor em que wᴺ = 1 para algum tamanho de domínio de avaliação N (tudo isso é feito em um campo finito).
  • Para "comprometer-se" com P, criamos um ponto de curva elíptica com(P) = P₀ G + P₁ S₁ + ... + Pₖ * Sₖ. Aqui:
    • G é o ponto gerador da curva
    • Pᵢ é o coeficiente de grau i do polinômio P
    • Sᵢ é o iº ponto na configuração confiável
  • Para provar que P(z) = a, criamos um polinômio quociente Q = (P - a) / (X - z) e criamos um compromisso com(Q) para ele. Só é possível criar esse polinômio se P(z) for realmente igual a a.
  • Para verificar uma prova, verificamos a equação Q * (X - z) = P - a fazendo uma verificação de curva elíptica na prova com(Q) e o compromisso polinomial com(P): verificamos e(com(Q), com(X - z)) ?= e(com(P) - com(a), com(1))

Algumas das principais propriedades que é importante entender são:

  • Uma prova é apenas o valor com(Q), que é de 48 bytes
  • com(P₁) + com(P₂) = com(P₁ + P₂)
  • Isso também significa que o senhor pode "editar" um valor em um compromisso existente. Suponha que saibamos que D_i é atualmente a, que queiramos defini-lo como b e que o compromisso existente com D seja com(P). Um compromisso com "P, mas com P(wⁱ) = b, e nenhuma outra avaliação foi alterada", então definimos com(new_P) = com(P) + (b-a) * com(Lᵢ), em que Lᵢ é o "polinômio de Lagrange" que é igual a 1 em wⁱ e 0 em outros pontos de wʲ.
  • Para realizar essas atualizações com eficiência, todos os N compromissos com os polinômios de Lagrange (com(Lᵢ)) podem ser pré-calculados e armazenados por cada cliente. Dentro de um contrato na cadeia, pode ser muito difícil armazenar todos os N compromissos, portanto, em vez disso, o senhor poderia assumir um compromisso KZG com o conjunto de valores com(L_i) (ou hash(com(L_i)), de modo que, sempre que alguém precisar atualizar a árvore na cadeia, poderá simplesmente fornecer o com(L_i) apropriado com uma prova de sua correçã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:

  • O último com (lista de chaves) no L2 que mantém o repositório de chaves (48 bytes)
  • Prova KZG de que com(key list) é um valor dentro de com(mirror_list), o compromisso com a lista de todos os compromissos da lista de chaves (48 bytes)
  • KZG prova de sua chave em com(key list) (48 bytes, mais 4 bytes para o índice)

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.

Como funcionam as árvores Verkle?

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.

Agregação

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.

Leitura direta do estado

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.

Como o L2 aprende a raiz do estado recente do Ethereum?

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:

  • Se os L2s implementarem a funcionalidade de "leitura direta de L1" de forma preguiçosa, lendo apenas as raízes de estado finalizadas de L1, o atraso normalmente será de 15 minutos, mas no caso extremo de vazamentos de inatividade (que o senhor precisa tolerar), o atraso pode ser de várias semanas.
  • Os L2s podem ser projetados para ler raízes de estado L1 muito mais recentes, mas como o L1 pode reverter (mesmo com a finalidade de slot único, as reversões podem ocorrer durante vazamentos de inatividade), o L2 também precisaria ser capaz de reverter. Isso é tecnicamente desafiador do ponto de vista da engenharia de software, mas pelo menos o Optimism já tem esse recurso.
  • Se o senhor usar a ponte de depósito para trazer as raízes do estado L1 para o L2, a viabilidade econômica simples poderá exigir um longo período de tempo entre as atualizações do depósito: se o custo total de um depósito for de 100.000 gas, e presumirmos que a ETH está em US$ 1.800, e as taxas estão em 200 gwei, e as raízes L1 são trazidas para o L2 uma vez por dia, isso representaria um custo de US$ 36 por L2 por dia, ou US$ 13.148 por L2 por ano para manter o sistema. Com um atraso de uma hora, esse valor sobe para US$ 315.569 por L2 por ano. Na melhor das hipóteses, um fluxo constante de usuários ricos e impacientes cobre as taxas de atualização e mantém o sistema atualizado para todos os outros. Na pior das hipóteses, algum ator altruísta teria que pagar por isso.
  • Os "oráculos" (pelo menos, o tipo de tecnologia que algumas pessoas definem como "oráculos") não são uma solução aceitável aqui: o gerenciamento de chaves de carteira é uma funcionalidade de baixo nível muito crítica para a segurança e, portanto, deve depender de, no máximo, algumas peças de infraestrutura de baixo nível muito simples e criptograficamente confiável.

Além disso, na direção oposta (L1s lendo L2):

  • Em rollups otimistas, as raízes estaduais levam uma semana para chegar a L1 devido ao atraso na prova de fraude. Nos rollups ZK, leva algumas horas por enquanto, devido a uma combinação de tempos de prova e limites econômicos, embora a tecnologia futura vá reduzir isso.
  • As pré-confirmações (de sequenciadores, atestadores, etc.) não são uma solução aceitável para a leitura de L1 em L2. O gerenciamento de carteiras é uma funcionalidade de baixo nível muito crítica em termos de segurança e, portanto, o nível de segurança da comunicação L2 -> L1 deve ser absoluto: não deve ser possível nem mesmo forçar uma falsa raiz de estado L1 assumindo o conjunto de validadores L2. As únicas raízes de estado em que o L1 deve confiar são as raízes de estado que foram aceitas como finais pelo contrato de retenção de raiz de estado do L2 no L1.

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.

De quanta conexão com a Ethereum outra cadeia precisa para manter carteiras cujos armazenamentos de chaves estão enraizados na Ethereum ou em uma L2?

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.

Preservação da privacidade

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:

  • Não é de conhecimento público que essas carteiras estão todas conectadas umas às outras.
  • Os guardiões da recuperação social não ficam sabendo quais são os endereços que estão protegendo.

Isso gera alguns problemas:

  • Não podemos usar diretamente as provas de Merkle, pois elas não preservam a privacidade.
  • Se usarmos KZG ou SNARKs, a prova precisará fornecer uma versão cega da chave de verificação, sem revelar o local da chave de verificação.
  • Se usarmos a agregação, o agregador não deverá saber o local em texto simples; em vez disso, o agregador deverá receber provas cegas e ter uma maneira de agregá-las.
  • Não podemos usar a "versão light" (usar provas de cadeia cruzada somente para atualizar chaves), porque ela cria um vazamento de privacidade: se muitas carteiras forem atualizadas ao mesmo tempo devido a um procedimento de atualização, o momento vaza a informação de que essas carteiras provavelmente estão relacionadas. Portanto, temos que usar a "versão pesada" (provas de cadeia cruzada para cada transação).

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.

Resumo

  • Para ter carteiras de recuperação social entre cadeias, o fluxo de trabalho mais realista é uma carteira que mantém um repositório de chaves em um local e carteiras em vários locais, onde a carteira lê o repositório de chaves (i) para atualizar sua visão local da chave de verificação ou (ii) durante o processo de verificação de cada transação.
  • Um ingrediente fundamental para tornar isso possível são as provas de cadeia cruzada. Precisamos otimizar essas provas com afinco. As melhores opções parecem ser os ZK-SNARKs, aguardando as provas de Verkle, ou uma solução KZG personalizada.
  • A longo prazo, os protocolos de agregação em que os empacotadores geram provas agregadas como parte da criação de um pacote de todas as UserOperations que foram enviadas pelos usuários serão necessários para minimizar os custos. Isso provavelmente deve ser integrado ao ecossistema ERC-4337, embora provavelmente sejam necessárias alterações no ERC-4337.
  • Os L2s devem ser otimizados para minimizar a latência da leitura do estado L1 (ou pelo menos a raiz do estado) de dentro do L2. Os L2s que leem diretamente o estado L1 são ideais e podem economizar espaço de prova.
  • As carteiras não podem estar apenas em L2s; o senhor também pode colocar carteiras em sistemas com níveis mais baixos de conexão com a Ethereum (L3s, ou até mesmo cadeias separadas que só concordam em incluir as raízes de estado da Ethereum e reorg ou hard fork quando a Ethereum se reorg ou hardfork).
  • No entanto, os armazenamentos de chaves devem estar em L1 ou no ZK-rollup L2 de alta segurança. Estar em L1 economiza muita complexidade, embora, a longo prazo, até mesmo isso possa ser muito caro, daí a necessidade de keystores em L2.
  • A preservação da privacidade exigirá trabalho adicional e tornará algumas opções mais difíceis. No entanto, provavelmente deveríamos avançar para soluções que preservem a privacidade de qualquer forma e, pelo menos, garantir que tudo o que propusermos seja compatível com a preservação da privacidade.

Isenção de responsabilidade:

  1. Este artigo foi reimpresso de[vitalik], Todos os direitos autorais pertencem ao autor original[Vitalik Buterin]. Se houver alguma objeção a essa reimpressão, entre em contato com a equipe do Gate Learn, que tratará do assunto imediatamente.
  2. Isenção de responsabilidade: Os pontos de vista e opiniões expressos neste artigo são de responsabilidade exclusiva do autor e não constituem consultoria de investimento.
  3. As traduções do artigo para outros idiomas são feitas pela equipe do Gate Learn. A menos que mencionado, é proibido copiar, distribuir ou plagiar os artigos traduzidos.

Aprofundamento da leitura cross-L2 para carteiras e outros casos de uso

AvançadoFeb 29, 2024
Neste artigo, Vitalik aborda diretamente um aspecto técnico específico de um subproblema: como ler mais facilmente de L2 para L1, de L1 para L2 ou de uma L2 para outra L2. A solução desse problema é fundamental para a obtenção da arquitetura de separação de ativos/chaves, mas também tem casos de uso valiosos em outras áreas, principalmente a otimização de chamadas confiáveis entre L2, incluindo casos de uso como a movimentação de ativos entre L1 e L2.
Aprofundamento da leitura cross-L2 para carteiras e outros casos de uso

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.

Leituras prévias recomendadas

Tabela de conteúdo

Qual é o objetivo?

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:

  • Versão light (verificação apenas para atualizar chaves): cada carteira armazena a chave de verificação localmente e contém uma função que pode ser chamada para verificar uma prova entre cadeias do estado atual do armazenamento de chaves e atualizar sua chave de verificação armazenada localmente para corresponder. Quando uma carteira é usada pela primeira vez em um L2 específico, é obrigatório chamar essa função para obter a chave de verificação atual do armazenamento de chaves.
    • Vantagem: usa provas de cadeia cruzada com moderação, portanto, não há problema se as provas de cadeia cruzada forem caras. Todos os fundos só podem ser gastos com as chaves atuais, portanto, ainda é seguro.
    • Desvantagem: Para alterar a chave de verificação, o senhor precisa fazer uma alteração de chave na cadeia tanto no repositório de chaves quanto em todas as carteiras já inicializadas (embora não nas contrafactuais). Isso pode custar muito combustível.
  • Versão pesada (verificação para cada tx): uma prova de cadeia cruzada mostrando a chave atualmente no keystore é necessária para cada transação.
    • Vantagem: menos complexidade sistêmica e a atualização do armazenamento de chaves é barata.
    • Lado negativo: caro por tx, portanto, requer muito mais engenharia para tornar as provas de cadeia cruzada aceitavelmente baratas. Também não é facilmente compatível com o ERC-4337, que atualmente não oferece suporte à leitura entre contratos de objetos mutáveis durante a validação.

Como é uma prova de cadeia cruzada?

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:

  • Uma prova que comprova a raiz do estado atual do Linea, dada a raiz do estado atual do Ethereum que Kakarot conhece
  • Uma prova que comprova as chaves atuais no keystore, dada a raiz do estado atual do Linea

Há duas questões primárias de implementação complicadas aqui:

  1. Que tipo de prova usamos? (São provas de Merkle? O senhor não está se sentindo bem?)
  2. Em primeiro lugar, como o L2 aprende a raiz do estado recente do L1 (Ethereum) (ou, como veremos, potencialmente o estado completo do L1)? E, alternativamente, como o L1 aprende a raiz do estado L2?
    • Em ambos os casos, qual é o tempo de espera entre a ocorrência de algo em um lado e a comprovação desse algo pelo outro lado?

Que tipos de esquemas de prova podemos usar?

Há cinco opções principais:

  • Provas de Merkle
  • ZK-SNARKs de uso geral
  • Provas para fins especiais (por exemplo. com a KZG)
  • Provas de Verkle, que estão entre KZG e ZK-SNARKs, tanto em relação à carga de trabalho quanto ao custo da infraestrutura.
  • Não há provas e depende da leitura direta do estado

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.

Como funcionam as provas de Merkle?

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:

  • Um ramo de Merkle provando a raiz de estado do L2 que contém o armazenamento de chaves, dada a raiz de estado mais recente do Ethereum que o L2 conhece. A raiz de estado do L2 que contém o repositório de chaves é armazenada em um slot de armazenamento conhecido de um endereço conhecido (o contrato em L1 que representa o L2) e, portanto, o caminho pela árvore pode ser codificado.
  • Um ramo de Merkle que comprova as chaves de verificação atuais, dada a raiz de estado do L2 de armazenamento de chaves. Aqui, mais uma vez, a chave de verificação é armazenada em um slot de armazenamento conhecido de um endereço conhecido, de modo que o caminho pode ser codificado.

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.

Como funcionam as provas ZK-SNARK?

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.

Como funcionariam as provas KZG para fins especiais?

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:

  • Podemos representar um conjunto de dados [D_1 ... D_n] com uma prova KZG de um polinômio derivado dos dados: especificamente, o polinômio P em que P(w) = D_1, P(w²) = D_2 ... P(wⁿ) = D_n. w aqui é uma "raiz da unidade", um valor em que wᴺ = 1 para algum tamanho de domínio de avaliação N (tudo isso é feito em um campo finito).
  • Para "comprometer-se" com P, criamos um ponto de curva elíptica com(P) = P₀ G + P₁ S₁ + ... + Pₖ * Sₖ. Aqui:
    • G é o ponto gerador da curva
    • Pᵢ é o coeficiente de grau i do polinômio P
    • Sᵢ é o iº ponto na configuração confiável
  • Para provar que P(z) = a, criamos um polinômio quociente Q = (P - a) / (X - z) e criamos um compromisso com(Q) para ele. Só é possível criar esse polinômio se P(z) for realmente igual a a.
  • Para verificar uma prova, verificamos a equação Q * (X - z) = P - a fazendo uma verificação de curva elíptica na prova com(Q) e o compromisso polinomial com(P): verificamos e(com(Q), com(X - z)) ?= e(com(P) - com(a), com(1))

Algumas das principais propriedades que é importante entender são:

  • Uma prova é apenas o valor com(Q), que é de 48 bytes
  • com(P₁) + com(P₂) = com(P₁ + P₂)
  • Isso também significa que o senhor pode "editar" um valor em um compromisso existente. Suponha que saibamos que D_i é atualmente a, que queiramos defini-lo como b e que o compromisso existente com D seja com(P). Um compromisso com "P, mas com P(wⁱ) = b, e nenhuma outra avaliação foi alterada", então definimos com(new_P) = com(P) + (b-a) * com(Lᵢ), em que Lᵢ é o "polinômio de Lagrange" que é igual a 1 em wⁱ e 0 em outros pontos de wʲ.
  • Para realizar essas atualizações com eficiência, todos os N compromissos com os polinômios de Lagrange (com(Lᵢ)) podem ser pré-calculados e armazenados por cada cliente. Dentro de um contrato na cadeia, pode ser muito difícil armazenar todos os N compromissos, portanto, em vez disso, o senhor poderia assumir um compromisso KZG com o conjunto de valores com(L_i) (ou hash(com(L_i)), de modo que, sempre que alguém precisar atualizar a árvore na cadeia, poderá simplesmente fornecer o com(L_i) apropriado com uma prova de sua correçã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:

  • O último com (lista de chaves) no L2 que mantém o repositório de chaves (48 bytes)
  • Prova KZG de que com(key list) é um valor dentro de com(mirror_list), o compromisso com a lista de todos os compromissos da lista de chaves (48 bytes)
  • KZG prova de sua chave em com(key list) (48 bytes, mais 4 bytes para o índice)

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.

Como funcionam as árvores Verkle?

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.

Agregação

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.

Leitura direta do estado

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.

Como o L2 aprende a raiz do estado recente do Ethereum?

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:

  • Se os L2s implementarem a funcionalidade de "leitura direta de L1" de forma preguiçosa, lendo apenas as raízes de estado finalizadas de L1, o atraso normalmente será de 15 minutos, mas no caso extremo de vazamentos de inatividade (que o senhor precisa tolerar), o atraso pode ser de várias semanas.
  • Os L2s podem ser projetados para ler raízes de estado L1 muito mais recentes, mas como o L1 pode reverter (mesmo com a finalidade de slot único, as reversões podem ocorrer durante vazamentos de inatividade), o L2 também precisaria ser capaz de reverter. Isso é tecnicamente desafiador do ponto de vista da engenharia de software, mas pelo menos o Optimism já tem esse recurso.
  • Se o senhor usar a ponte de depósito para trazer as raízes do estado L1 para o L2, a viabilidade econômica simples poderá exigir um longo período de tempo entre as atualizações do depósito: se o custo total de um depósito for de 100.000 gas, e presumirmos que a ETH está em US$ 1.800, e as taxas estão em 200 gwei, e as raízes L1 são trazidas para o L2 uma vez por dia, isso representaria um custo de US$ 36 por L2 por dia, ou US$ 13.148 por L2 por ano para manter o sistema. Com um atraso de uma hora, esse valor sobe para US$ 315.569 por L2 por ano. Na melhor das hipóteses, um fluxo constante de usuários ricos e impacientes cobre as taxas de atualização e mantém o sistema atualizado para todos os outros. Na pior das hipóteses, algum ator altruísta teria que pagar por isso.
  • Os "oráculos" (pelo menos, o tipo de tecnologia que algumas pessoas definem como "oráculos") não são uma solução aceitável aqui: o gerenciamento de chaves de carteira é uma funcionalidade de baixo nível muito crítica para a segurança e, portanto, deve depender de, no máximo, algumas peças de infraestrutura de baixo nível muito simples e criptograficamente confiável.

Além disso, na direção oposta (L1s lendo L2):

  • Em rollups otimistas, as raízes estaduais levam uma semana para chegar a L1 devido ao atraso na prova de fraude. Nos rollups ZK, leva algumas horas por enquanto, devido a uma combinação de tempos de prova e limites econômicos, embora a tecnologia futura vá reduzir isso.
  • As pré-confirmações (de sequenciadores, atestadores, etc.) não são uma solução aceitável para a leitura de L1 em L2. O gerenciamento de carteiras é uma funcionalidade de baixo nível muito crítica em termos de segurança e, portanto, o nível de segurança da comunicação L2 -> L1 deve ser absoluto: não deve ser possível nem mesmo forçar uma falsa raiz de estado L1 assumindo o conjunto de validadores L2. As únicas raízes de estado em que o L1 deve confiar são as raízes de estado que foram aceitas como finais pelo contrato de retenção de raiz de estado do L2 no L1.

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.

De quanta conexão com a Ethereum outra cadeia precisa para manter carteiras cujos armazenamentos de chaves estão enraizados na Ethereum ou em uma L2?

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.

Preservação da privacidade

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:

  • Não é de conhecimento público que essas carteiras estão todas conectadas umas às outras.
  • Os guardiões da recuperação social não ficam sabendo quais são os endereços que estão protegendo.

Isso gera alguns problemas:

  • Não podemos usar diretamente as provas de Merkle, pois elas não preservam a privacidade.
  • Se usarmos KZG ou SNARKs, a prova precisará fornecer uma versão cega da chave de verificação, sem revelar o local da chave de verificação.
  • Se usarmos a agregação, o agregador não deverá saber o local em texto simples; em vez disso, o agregador deverá receber provas cegas e ter uma maneira de agregá-las.
  • Não podemos usar a "versão light" (usar provas de cadeia cruzada somente para atualizar chaves), porque ela cria um vazamento de privacidade: se muitas carteiras forem atualizadas ao mesmo tempo devido a um procedimento de atualização, o momento vaza a informação de que essas carteiras provavelmente estão relacionadas. Portanto, temos que usar a "versão pesada" (provas de cadeia cruzada para cada transação).

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.

Resumo

  • Para ter carteiras de recuperação social entre cadeias, o fluxo de trabalho mais realista é uma carteira que mantém um repositório de chaves em um local e carteiras em vários locais, onde a carteira lê o repositório de chaves (i) para atualizar sua visão local da chave de verificação ou (ii) durante o processo de verificação de cada transação.
  • Um ingrediente fundamental para tornar isso possível são as provas de cadeia cruzada. Precisamos otimizar essas provas com afinco. As melhores opções parecem ser os ZK-SNARKs, aguardando as provas de Verkle, ou uma solução KZG personalizada.
  • A longo prazo, os protocolos de agregação em que os empacotadores geram provas agregadas como parte da criação de um pacote de todas as UserOperations que foram enviadas pelos usuários serão necessários para minimizar os custos. Isso provavelmente deve ser integrado ao ecossistema ERC-4337, embora provavelmente sejam necessárias alterações no ERC-4337.
  • Os L2s devem ser otimizados para minimizar a latência da leitura do estado L1 (ou pelo menos a raiz do estado) de dentro do L2. Os L2s que leem diretamente o estado L1 são ideais e podem economizar espaço de prova.
  • As carteiras não podem estar apenas em L2s; o senhor também pode colocar carteiras em sistemas com níveis mais baixos de conexão com a Ethereum (L3s, ou até mesmo cadeias separadas que só concordam em incluir as raízes de estado da Ethereum e reorg ou hard fork quando a Ethereum se reorg ou hardfork).
  • No entanto, os armazenamentos de chaves devem estar em L1 ou no ZK-rollup L2 de alta segurança. Estar em L1 economiza muita complexidade, embora, a longo prazo, até mesmo isso possa ser muito caro, daí a necessidade de keystores em L2.
  • A preservação da privacidade exigirá trabalho adicional e tornará algumas opções mais difíceis. No entanto, provavelmente deveríamos avançar para soluções que preservem a privacidade de qualquer forma e, pelo menos, garantir que tudo o que propusermos seja compatível com a preservação da privacidade.

Isenção de responsabilidade:

  1. Este artigo foi reimpresso de[vitalik], Todos os direitos autorais pertencem ao autor original[Vitalik Buterin]. Se houver alguma objeção a essa reimpressão, entre em contato com a equipe do Gate Learn, que tratará do assunto imediatamente.
  2. Isenção de responsabilidade: Os pontos de vista e opiniões expressos neste artigo são de responsabilidade exclusiva do autor e não constituem consultoria de investimento.
  3. As traduções do artigo para outros idiomas são feitas pela equipe do Gate Learn. A menos que mencionado, é proibido copiar, distribuir ou plagiar os artigos traduzidos.
Jetzt anfangen
Registrieren Sie sich und erhalten Sie einen
100
-Euro-Gutschein!
Benutzerkonto erstellen