Agradecimentos especiais a Yoav Weiss, Dan Finlay, Martin Koppelmann e às equipas Arbitrum, Optimism, Polygon, Scroll e SoulWallet pelo feedback e revisão.
Neste post sobre as Três Transições, delineei algumas razões fundamentais pelas quais é importante começar a pensar explicitamente no suporte L1 + cross-L2, na segurança das carteiras e na privacidade como características básicas necessárias do ecossistema, em vez de criar cada uma destas coisas como complementos que podem ser concebidos separadamente por carteiras individuais.
Esta publicação centrar-se-á 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 resolução deste problema é crucial para a implementação de uma arquitetura de separação de activos / keystore, mas também tem casos de utilização valiosos noutras áreas, nomeadamente a otimização de chamadas fiáveis entre L2s, incluindo casos de utilização como a movimentação de activos entre L1 e L2s.
Quando as L2 se tornarem mais comuns, os utilizadores terão activos em várias L2 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 aceder a uma conta vão mudar com o tempo e as chaves antigas terão de deixar de ser válidas. Quando estas duas coisas acontecerem, um utilizador terá de ter uma forma de alterar as chaves que têm autoridade para aceder a muitas contas que vivem em muitos locais diferentes, sem fazer um número extremamente elevado de transacções.
Em particular, precisamos de uma forma de lidar com endereços contrafactuais: endereços que ainda não foram "registados" de forma alguma na cadeia, mas que, no entanto, precisam de receber e guardar fundos de forma segura. Todos nós dependemos de endereços contrafactuais: quando utiliza o Ethereum pela primeira vez, pode gerar um endereço ETH que alguém pode utilizar para lhe pagar, sem "registar" o endereço na cadeia (o que exigiria o pagamento de txfees e, por conseguinte, já possuir algum ETH).
Com as EOAs, todos os endereços começam por ser endereços contrafactuais. Com as carteiras de contratos inteligentes, os endereços contrafactuais ainda são possíveis, em grande parte graças ao CREATE2, que lhe permite ter um endereço ETH que só pode ser preenchido por um contrato inteligente que tenha código correspondente a um determinado hash.
Algoritmo de cálculo de endereços EIP-1014 (CREATE2).
No entanto, as carteiras de contratos inteligentes introduzem um novo desafio: a possibilidade de as chaves de acesso mudarem. 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 guardada no armazenamento da carteira, mas esse registo de armazenamento não se propaga magicamente para outros L2s.
Se um utilizador tiver muitos endereços em muitos L2s, incluindo endereços que (porque são contrafactuais) o L2 em que se encontra não conhece, então parece que só há uma forma de permitir que os utilizadores alterem as suas chaves: arquitetura de separação entre activos e armazenamento de chaves. Cada utilizador tem (i) um "contrato de armazenamento de chaves" (em L1 ou num L2 em particular), 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 lêem a cadeia para obter a chave de verificação.
Há duas formas de o fazer:
Para mostrar toda a complexidade, vamos explorar o caso mais difícil: quando o armazenamento de chaves está num L2 e a carteira está num L2 diferente. Se o armazenamento de chaves ou a carteira estiverem em L1, então apenas metade deste desenho é necessário.
Vamos assumir que o armazenamento de chaves está em Linea e a carteira está em Kakarot. Uma prova completa das chaves da carteira consiste em:
Há duas questões principais de implementação complicadas aqui:
Existem cinco opções principais:
Em termos de trabalho de infraestrutura necessário e de custo para os utilizadores, classifico-os aproximadamente da seguinte forma:
"Agregação" refere-se à ideia de agregar todas as provas fornecidas pelos utilizadores dentro de cada bloco numa grande meta-prova que as combina a todas. Isto é possível para os SNARKs e para o KZG, mas não para os ramos Merkle (pode combinar um pouco os ramos Merkle, mas só poupa log(txs por bloco) / log(número total de keystores), talvez 15-30% na prática, pelo que provavelmente não vale a pena o custo).
A agregação só se torna útil quando o esquema tem um número substancial de utilizadores, por isso, realisticamente, não há problema em que uma implementação da versão 1 deixe a agregação de fora e a implemente na versão 2.
Esta é simples: siga diretamente o diagrama da secção anterior. Mais precisamente, cada "prova" (assumindo o caso de dificuldade máxima de provar uma L2 noutra L2) conteria:
Infelizmente, as provas de estado Ethereum são complicadas, mas existem bibliotecas para as verificar e, se utilizar estas bibliotecas, este mecanismo não é demasiado complicado de implementar.
O maior problema é 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 numa árvore com N objectos tem 32 log2(N) bytes de comprimento e, como as árvores Patricia do Ethereum têm 16 folhas por filho, as provas para essas árvores têm 32 15 log16(N) ~= 125 log2(N) bytes de comprimento). Num estado com cerca de 250 milhões (~2²⁸) de contas, isto faz com que cada prova tenha 125 * 28 = 3500 bytes, ou seja, cerca de 56.000 gases, mais custos extra para descodificar e verificar hashes.
Duas provas juntas acabariam por custar cerca de 100 000 a 150 000 gases (sem incluir a verificação da assinatura, se esta for utilizada por transação) - significativamente mais do que os actuais 21 000 gases de base por transação. Mas a disparidade piora se a prova estiver a ser verificada em L2. A computação dentro de um L2 é barata, porque a computação é feita fora da cadeia e num ecossistema com muito menos nós do que o L1. Os dados, por outro lado, têm de ser lançados em L1. Assim, a comparação não é 21000 gás vs 150.000 gás; é 21.000 gás L2 vs 100.000 gás L1.
Pode calcular o que isto significa, se olhar para as comparações entre os custos do gás L1 e os custos do gás L2:
Atualmente, a L1 é cerca de 15-25 vezes mais cara do que a L2 para envios simples e 20-50 vezes mais cara para trocas de fichas. Os envios simples são relativamente pesados em termos de dados, mas as trocas são muito mais pesadas em termos computacionais. Por conseguinte, as trocas são uma melhor referência para aproximar o custo da computação L1 da computação L2. Tendo tudo isto em conta, se assumirmos uma relação de custo de 30x entre o custo de computação em L1 e o custo de computação em L2, isto parece implicar que colocar uma prova de Merkle em L2 custará o equivalente a talvez cinquenta transacções normais.
É claro que usar uma árvore Merkle binária pode reduzir os custos em ~4x, mas mesmo assim, na maioria dos casos, o custo será demasiado elevado - e se estivermos dispostos a fazer o sacrifício de deixar de ser compatível com a atual árvore de estados hexária do Ethereum, mais vale procurar opções ainda melhores.
Conceptualmente, a utilização de ZK-SNARKs também é fácil de compreender: 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 ~400 000 gases de computação e cerca de 400 bytes (compare: 21 000 gases e 100 bytes para uma transação básica, no futuro redutível a ~25 bytes com compressão). Assim, do ponto de vista computacional, uma ZK-SNARK custa hoje 19 vezes mais do que uma transação básica e, do ponto de vista dos dados, uma ZK-SNARK custa hoje 4x mais do que uma transação básica e 16 vezes mais do que uma transação básica poderá custar no futuro.
Estes números representam uma enorme melhoria em relação às provas de Merkle, mas continuam a ser bastante dispendiosos. Há duas formas de melhorar esta situação: (i) provas KZG para fins especiais, ou (ii) agregação, semelhante à agregação ERC-4337, mas utilizando matemática mais sofisticada. Podemos analisar ambos.
Atenção, esta secção é muito mais matemática do que as outras secções. Isto deve-se ao facto de estarmos a ir além das ferramentas de uso geral e a construir algo de propósito especial para ser mais barato, pelo que temos de ir muito mais "debaixo do capô". Se não gosta de matemática profunda, passe diretamente para a secção seguinte.
Em primeiro lugar, recapitule o funcionamento das autorizações KZG:
Algumas propriedades-chave que é importante compreender são:
Assim, temos uma estrutura em que podemos simplesmente continuar a acrescentar valores ao final de uma lista sempre crescente, 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 actualizados pode tornar-se parte da lógica do núcleo L2 ou pode ser implementado sem alterações no protocolo do núcleo L2 através de pontes de depósito e retirada.
Uma prova completa exigiria, portanto:
Na verdade, é possível juntar as duas provas KZG numa só, pelo que obtemos um tamanho total de apenas 100 bytes.
Note uma subtileza: como a lista de chaves é uma lista, e não um mapa chave/valor como o estado, a lista de chaves terá de atribuir posições sequencialmente. O contrato de compromisso de chave conteria o seu próprio registo interno, mapeando cada keystore para um ID, e para cada chave armazenaria hash(key, endereço do keystore) em vez de apenas key, para comunicar inequivocamente a outros L2s de que keystore uma determinada entrada está a falar.
A vantagem desta técnica é que funciona muito bem em L2. Os dados são 100 bytes, ~4x mais curtos do que um ZK-SNARK e muito mais curtos do que uma prova de Merkle. O custo de cálculo é, em grande parte, um cheque de emparelhamento de tamanho 2, ou cerca de 119 000 gases. Em L1, os dados são menos importantes do que a computação, pelo que, infelizmente, a KZG é um pouco mais cara 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, pode fazer um compromisso KZG com uma lista de 2²⁴ valores, cada um dos quais é um compromisso KZG com 2²⁴ valores. As árvores de tornozelo estão a ser <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 guardar mapas de valores-chave e não apenas listas (basicamente, pode fazer uma árvore de tamanho-2²⁵⁶ mas começá-la vazia, só preenchendo partes específicas da árvore quando precisar efetivamente de as preencher).
O aspeto de uma árvore Verkle. Na prática, pode dar a cada nó uma largura de 256 == 2⁸ para árvores baseadas em IPA, ou 2²⁴ para árvores baseadas em KZG.
As provas em árvores de Verkle são um pouco mais longas do que em KZG; podem ter algumas centenas de bytes. São também difíceis de verificar, especialmente se tentar agregar muitas provas numa só.
Realisticamente, as árvores de Verkle devem ser consideradas como árvores de Merkle, mas mais viáveis sem SNARKing (devido aos custos mais baixos dos dados) e mais baratas com SNARKing (devido aos custos mais baixos do provador).
A maior vantagem das árvores de Verkle é a possibilidade de harmonizar as estruturas de dados: As provas de Verkle podem ser utilizadas diretamente sobre o estado L1 ou L2, sem estruturas de sobreposição e utilizando exatamente o mesmo mecanismo para L1 e L2. Quando os computadores quânticos se tornarem um problema, ou quando a prova dos ramos de Merkle se tornar suficientemente eficiente, as árvores de Verkle podem ser substituídas no local por uma árvore de hash binária com uma função de hash adequada ao SNARK.
Se N utilizadores fizerem N transacções (ou, mais realisticamente, N ERC-4337 UserOperations) que precisem de provar N reivindicações entre cadeias, podemos poupar muito gás agregando essas provas: o construtor que combinaria essas transacções num bloco ou num pacote que vai para um bloco pode criar uma única prova que prove todas essas reivindicações simultaneamente.
Isto pode significar:
Nos três casos, as provas custariam apenas algumas centenas de milhares de euros cada. O construtor teria de fazer um destes em cada L2 para os utilizadores desse L2; assim, para que a sua construção seja útil, o esquema como um todo tem de ter uma utilização suficiente para que haja frequentemente pelo menos algumas transacções dentro do mesmo bloco em vários L2s principais.
Se forem utilizados ZK-SNARKs, o principal custo marginal é simplesmente a "lógica comercial" da passagem de números entre contratos, ou seja, talvez alguns milhares de gás L2 por utilizador. Se forem utilizadas provas múltiplas KZG, o provador teria de acrescentar 48 gases por cada L2 que contenha uma chave de armazenamento utilizada nesse bloco, pelo que o custo marginal do esquema por utilizador acrescentaria mais ~800 gases L1 por L2 (não por utilizador). Mas estes custos são muito inferiores aos custos da não agregação, que implicam inevitavelmente mais de 10 000 gases L1 e centenas de milhares de gases L2 por utilizador. Para as árvores de Verkle, pode utilizar diretamente as multi-provas de Verkle, adicionando cerca de 100-200 bytes por utilizador, ou pode fazer um ZK-SNARK de uma multi-prova de Verkle, que tem custos semelhantes aos ZK-SNARKs de ramos de Merkle, mas é significativamente mais barato de provar.
Do ponto de vista da implementação, é provavelmente melhor que os agrupadores agreguem provas de cadeia cruzada através da norma de abstração de conta ERC-4337. O ERC-4337 já tem um mecanismo que permite aos construtores agregar partes de UserOperations de forma personalizada. Existe mesmo uma <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi"> implementação deste processo para a agregação de assinaturas BLS, que poderá reduzir os custos do gás em L2 entre 1,5x e 3x, dependendo das outras formas de compressão incluídas.
Diagrama retirado de <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi"> Post de implementação da carteira BLS mostrando o fluxo de trabalho das assinaturas agregadas BLS numa versão anterior do ERC-4337. O fluxo de trabalho de agregação de provas entre cadeias será provavelmente muito semelhante.
Uma última possibilidade, e apenas utilizável para L2 que lê L1 (e não L1 que lê L2), é modificar L2s para lhes permitir fazer chamadas estáticas a contratos em L1 diretamente.
Isto poderia ser feito com um opcode ou uma pré-compilação, que permite chamadas para L1 onde você fornece o endereço de destino, gás e calldata, e retorna a saída, embora porque estas chamadas são chamadas estáticas elas não podem realmente mudar qualquer estado L1. Os L2 têm de ter conhecimento do L1 para processar os depósitos, pelo que não há nada de fundamental que impeça a sua implementação; trata-se sobretudo de um desafio de implementação técnica (ver: esta RFP do Optimism para suportar chamadas estáticas para o L1).
Repare que se o keystore estiver em L1 e os L2s integrarem a funcionalidade de chamada estática de L1, então não são necessárias quaisquer provas! No entanto, se os L2s não integrarem chamadas estáticas L1, ou se o keystore estiver no L2 (o que poderá eventualmente ter de acontecer, uma vez que o L1 se torna demasiado dispendioso para os utilizadores o utilizarem, nem que seja um pouco), então serão necessárias provas.
Todos os esquemas acima requerem que o L2 aceda ou à raiz do estado L1 recente, ou a todo o estado L1 recente. Felizmente, todos os L2s já têm alguma funcionalidade para aceder ao estado recente do L1. Isto deve-se ao facto de necessitarem dessa funcionalidade para processar as mensagens provenientes de L1 para L2, nomeadamente os depósitos.
E, de facto, se um L2 tiver uma funcionalidade de depósito, então pode usar esse L2 tal como está para mover raízes de estado de L1 para um contrato no L2: basta que um contrato em L1 chame o opcode BLOCKHASH e o passe para L2 como uma mensagem de depósito. O cabeçalho completo do bloco pode ser recebido, e a sua raiz de estado extraída, no lado L2. No entanto, seria muito melhor que cada L2 tivesse uma forma explícita de aceder diretamente ao estado L1 recente completo ou às raízes do estado L1 recente.
O principal desafio da otimização da forma como os L2 recebem as raízes recentes do estado L1 é conseguir simultaneamente segurança e baixa latência:
Além disso, na direção oposta (L1s lendo L2):
Algumas destas velocidades para operações sem confiança entre cadeias são inaceitavelmente lentas para muitos casos de utilização definidos; para esses casos, precisa de pontes mais rápidas com modelos de segurança mais imperfeitos. No entanto, para o caso de utilização de atualização de chaves de carteira, os atrasos mais longos são mais aceitáveis: não está a atrasar as transacções em horas, está a atrasar as alterações de chaves. Terá apenas de manter as chaves antigas por mais tempo. Se está a mudar de chaves porque as chaves são roubadas, então tem um período significativo de vulnerabilidade, mas isso pode ser atenuado, por exemplo, por carteiras com 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 de estado L1 de uma forma optimizada, em que cada bloco L2 (ou o registo de computação da raiz de estado) contenha um ponteiro para o bloco L1 mais recente, pelo que, se L1 reverter, L2 também pode reverter. Os contratos de armazenamento de chaves devem ser colocados na rede principal ou em L2s que são ZK-rollups e, portanto, podem ser rapidamente transferidos para L1.
Os blocos da cadeia L2 podem ter dependências não só de blocos L2 anteriores, mas também de um bloco L1. Se o L1 reverter para além dessa ligação, o L2 também reverte. Vale a pena notar que esta é 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 é assim tanto. Na verdade, nem sequer precisa de ser um rollup: se for um L3, ou um validium, então não há problema em manter as carteiras aí, desde que mantenha os keystores no L1 ou num rollup ZK. O que você precisa é que a cadeia tenha acesso direto às raízes do estado do Ethereum, e um compromisso técnico e social para estar disposto a reorgar se o Ethereum reorgar, e hard fork se o Ethereum hard forks.
Um problema de investigação interessante é identificar até que ponto é possível que uma cadeia tenha esta forma de ligação a várias outras cadeias (por exemplo, a cadeia de distribuição de alimentos). Ethereum e Zcash). Fazê-lo de forma ingénua é possível: a sua cadeia pode concordar em reorganizar-se se o Ethereum ou o Zcash se reorganizarem (e fazer um hard fork se o Ethereum ou o Zcash se reorganizarem), mas nesse caso os seus operadores de nós e a sua comunidade em geral têm o dobro das dependências técnicas e políticas. Assim, esta técnica pode ser utilizada para se ligar a algumas outras cadeias, mas a um custo crescente. Os esquemas baseados em pontes ZK têm propriedades técnicas atractivas, mas têm como principal fraqueza o facto de não serem resistentes a ataques de 51% ou a bifurcações difíceis. Poderá haver soluções mais inteligentes.
Idealmente, também queremos preservar a privacidade. Se tiver muitas carteiras que são geridas pelo mesmo repositório de chaves, queremos certificar-nos disso:
Isto cria alguns problemas:
Com os SNARKs, as soluções são concetualmente fáceis: as provas escondem informação por defeito e o agregador precisa de produzir um SNARK recursivo para provar os SNARKs.
Atualmente, o principal desafio desta abordagem é o facto de a agregação exigir que o agregador crie um SNARK recursivo, o que é bastante lento.
Com KZG, podemos utilizar <a href="https://notes.ethereum.org/@vbuterin/non_index_revealing_proof"> this trabalho sobre provas KZG não reveladoras de índices (ver 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.
A leitura direta de L1 a partir do interior de L2, infelizmente, não preserva a privacidade, embora a implementação da funcionalidade de leitura direta continue a ser muito útil, tanto para minimizar a latência como devido à sua utilidade para outras aplicações.
Partilhar
Conteúdos
Agradecimentos especiais a Yoav Weiss, Dan Finlay, Martin Koppelmann e às equipas Arbitrum, Optimism, Polygon, Scroll e SoulWallet pelo feedback e revisão.
Neste post sobre as Três Transições, delineei algumas razões fundamentais pelas quais é importante começar a pensar explicitamente no suporte L1 + cross-L2, na segurança das carteiras e na privacidade como características básicas necessárias do ecossistema, em vez de criar cada uma destas coisas como complementos que podem ser concebidos separadamente por carteiras individuais.
Esta publicação centrar-se-á 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 resolução deste problema é crucial para a implementação de uma arquitetura de separação de activos / keystore, mas também tem casos de utilização valiosos noutras áreas, nomeadamente a otimização de chamadas fiáveis entre L2s, incluindo casos de utilização como a movimentação de activos entre L1 e L2s.
Quando as L2 se tornarem mais comuns, os utilizadores terão activos em várias L2 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 aceder a uma conta vão mudar com o tempo e as chaves antigas terão de deixar de ser válidas. Quando estas duas coisas acontecerem, um utilizador terá de ter uma forma de alterar as chaves que têm autoridade para aceder a muitas contas que vivem em muitos locais diferentes, sem fazer um número extremamente elevado de transacções.
Em particular, precisamos de uma forma de lidar com endereços contrafactuais: endereços que ainda não foram "registados" de forma alguma na cadeia, mas que, no entanto, precisam de receber e guardar fundos de forma segura. Todos nós dependemos de endereços contrafactuais: quando utiliza o Ethereum pela primeira vez, pode gerar um endereço ETH que alguém pode utilizar para lhe pagar, sem "registar" o endereço na cadeia (o que exigiria o pagamento de txfees e, por conseguinte, já possuir algum ETH).
Com as EOAs, todos os endereços começam por ser endereços contrafactuais. Com as carteiras de contratos inteligentes, os endereços contrafactuais ainda são possíveis, em grande parte graças ao CREATE2, que lhe permite ter um endereço ETH que só pode ser preenchido por um contrato inteligente que tenha código correspondente a um determinado hash.
Algoritmo de cálculo de endereços EIP-1014 (CREATE2).
No entanto, as carteiras de contratos inteligentes introduzem um novo desafio: a possibilidade de as chaves de acesso mudarem. 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 guardada no armazenamento da carteira, mas esse registo de armazenamento não se propaga magicamente para outros L2s.
Se um utilizador tiver muitos endereços em muitos L2s, incluindo endereços que (porque são contrafactuais) o L2 em que se encontra não conhece, então parece que só há uma forma de permitir que os utilizadores alterem as suas chaves: arquitetura de separação entre activos e armazenamento de chaves. Cada utilizador tem (i) um "contrato de armazenamento de chaves" (em L1 ou num L2 em particular), 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 lêem a cadeia para obter a chave de verificação.
Há duas formas de o fazer:
Para mostrar toda a complexidade, vamos explorar o caso mais difícil: quando o armazenamento de chaves está num L2 e a carteira está num L2 diferente. Se o armazenamento de chaves ou a carteira estiverem em L1, então apenas metade deste desenho é necessário.
Vamos assumir que o armazenamento de chaves está em Linea e a carteira está em Kakarot. Uma prova completa das chaves da carteira consiste em:
Há duas questões principais de implementação complicadas aqui:
Existem cinco opções principais:
Em termos de trabalho de infraestrutura necessário e de custo para os utilizadores, classifico-os aproximadamente da seguinte forma:
"Agregação" refere-se à ideia de agregar todas as provas fornecidas pelos utilizadores dentro de cada bloco numa grande meta-prova que as combina a todas. Isto é possível para os SNARKs e para o KZG, mas não para os ramos Merkle (pode combinar um pouco os ramos Merkle, mas só poupa log(txs por bloco) / log(número total de keystores), talvez 15-30% na prática, pelo que provavelmente não vale a pena o custo).
A agregação só se torna útil quando o esquema tem um número substancial de utilizadores, por isso, realisticamente, não há problema em que uma implementação da versão 1 deixe a agregação de fora e a implemente na versão 2.
Esta é simples: siga diretamente o diagrama da secção anterior. Mais precisamente, cada "prova" (assumindo o caso de dificuldade máxima de provar uma L2 noutra L2) conteria:
Infelizmente, as provas de estado Ethereum são complicadas, mas existem bibliotecas para as verificar e, se utilizar estas bibliotecas, este mecanismo não é demasiado complicado de implementar.
O maior problema é 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 numa árvore com N objectos tem 32 log2(N) bytes de comprimento e, como as árvores Patricia do Ethereum têm 16 folhas por filho, as provas para essas árvores têm 32 15 log16(N) ~= 125 log2(N) bytes de comprimento). Num estado com cerca de 250 milhões (~2²⁸) de contas, isto faz com que cada prova tenha 125 * 28 = 3500 bytes, ou seja, cerca de 56.000 gases, mais custos extra para descodificar e verificar hashes.
Duas provas juntas acabariam por custar cerca de 100 000 a 150 000 gases (sem incluir a verificação da assinatura, se esta for utilizada por transação) - significativamente mais do que os actuais 21 000 gases de base por transação. Mas a disparidade piora se a prova estiver a ser verificada em L2. A computação dentro de um L2 é barata, porque a computação é feita fora da cadeia e num ecossistema com muito menos nós do que o L1. Os dados, por outro lado, têm de ser lançados em L1. Assim, a comparação não é 21000 gás vs 150.000 gás; é 21.000 gás L2 vs 100.000 gás L1.
Pode calcular o que isto significa, se olhar para as comparações entre os custos do gás L1 e os custos do gás L2:
Atualmente, a L1 é cerca de 15-25 vezes mais cara do que a L2 para envios simples e 20-50 vezes mais cara para trocas de fichas. Os envios simples são relativamente pesados em termos de dados, mas as trocas são muito mais pesadas em termos computacionais. Por conseguinte, as trocas são uma melhor referência para aproximar o custo da computação L1 da computação L2. Tendo tudo isto em conta, se assumirmos uma relação de custo de 30x entre o custo de computação em L1 e o custo de computação em L2, isto parece implicar que colocar uma prova de Merkle em L2 custará o equivalente a talvez cinquenta transacções normais.
É claro que usar uma árvore Merkle binária pode reduzir os custos em ~4x, mas mesmo assim, na maioria dos casos, o custo será demasiado elevado - e se estivermos dispostos a fazer o sacrifício de deixar de ser compatível com a atual árvore de estados hexária do Ethereum, mais vale procurar opções ainda melhores.
Conceptualmente, a utilização de ZK-SNARKs também é fácil de compreender: 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 ~400 000 gases de computação e cerca de 400 bytes (compare: 21 000 gases e 100 bytes para uma transação básica, no futuro redutível a ~25 bytes com compressão). Assim, do ponto de vista computacional, uma ZK-SNARK custa hoje 19 vezes mais do que uma transação básica e, do ponto de vista dos dados, uma ZK-SNARK custa hoje 4x mais do que uma transação básica e 16 vezes mais do que uma transação básica poderá custar no futuro.
Estes números representam uma enorme melhoria em relação às provas de Merkle, mas continuam a ser bastante dispendiosos. Há duas formas de melhorar esta situação: (i) provas KZG para fins especiais, ou (ii) agregação, semelhante à agregação ERC-4337, mas utilizando matemática mais sofisticada. Podemos analisar ambos.
Atenção, esta secção é muito mais matemática do que as outras secções. Isto deve-se ao facto de estarmos a ir além das ferramentas de uso geral e a construir algo de propósito especial para ser mais barato, pelo que temos de ir muito mais "debaixo do capô". Se não gosta de matemática profunda, passe diretamente para a secção seguinte.
Em primeiro lugar, recapitule o funcionamento das autorizações KZG:
Algumas propriedades-chave que é importante compreender são:
Assim, temos uma estrutura em que podemos simplesmente continuar a acrescentar valores ao final de uma lista sempre crescente, 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 actualizados pode tornar-se parte da lógica do núcleo L2 ou pode ser implementado sem alterações no protocolo do núcleo L2 através de pontes de depósito e retirada.
Uma prova completa exigiria, portanto:
Na verdade, é possível juntar as duas provas KZG numa só, pelo que obtemos um tamanho total de apenas 100 bytes.
Note uma subtileza: como a lista de chaves é uma lista, e não um mapa chave/valor como o estado, a lista de chaves terá de atribuir posições sequencialmente. O contrato de compromisso de chave conteria o seu próprio registo interno, mapeando cada keystore para um ID, e para cada chave armazenaria hash(key, endereço do keystore) em vez de apenas key, para comunicar inequivocamente a outros L2s de que keystore uma determinada entrada está a falar.
A vantagem desta técnica é que funciona muito bem em L2. Os dados são 100 bytes, ~4x mais curtos do que um ZK-SNARK e muito mais curtos do que uma prova de Merkle. O custo de cálculo é, em grande parte, um cheque de emparelhamento de tamanho 2, ou cerca de 119 000 gases. Em L1, os dados são menos importantes do que a computação, pelo que, infelizmente, a KZG é um pouco mais cara 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, pode fazer um compromisso KZG com uma lista de 2²⁴ valores, cada um dos quais é um compromisso KZG com 2²⁴ valores. As árvores de tornozelo estão a ser <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 guardar mapas de valores-chave e não apenas listas (basicamente, pode fazer uma árvore de tamanho-2²⁵⁶ mas começá-la vazia, só preenchendo partes específicas da árvore quando precisar efetivamente de as preencher).
O aspeto de uma árvore Verkle. Na prática, pode dar a cada nó uma largura de 256 == 2⁸ para árvores baseadas em IPA, ou 2²⁴ para árvores baseadas em KZG.
As provas em árvores de Verkle são um pouco mais longas do que em KZG; podem ter algumas centenas de bytes. São também difíceis de verificar, especialmente se tentar agregar muitas provas numa só.
Realisticamente, as árvores de Verkle devem ser consideradas como árvores de Merkle, mas mais viáveis sem SNARKing (devido aos custos mais baixos dos dados) e mais baratas com SNARKing (devido aos custos mais baixos do provador).
A maior vantagem das árvores de Verkle é a possibilidade de harmonizar as estruturas de dados: As provas de Verkle podem ser utilizadas diretamente sobre o estado L1 ou L2, sem estruturas de sobreposição e utilizando exatamente o mesmo mecanismo para L1 e L2. Quando os computadores quânticos se tornarem um problema, ou quando a prova dos ramos de Merkle se tornar suficientemente eficiente, as árvores de Verkle podem ser substituídas no local por uma árvore de hash binária com uma função de hash adequada ao SNARK.
Se N utilizadores fizerem N transacções (ou, mais realisticamente, N ERC-4337 UserOperations) que precisem de provar N reivindicações entre cadeias, podemos poupar muito gás agregando essas provas: o construtor que combinaria essas transacções num bloco ou num pacote que vai para um bloco pode criar uma única prova que prove todas essas reivindicações simultaneamente.
Isto pode significar:
Nos três casos, as provas custariam apenas algumas centenas de milhares de euros cada. O construtor teria de fazer um destes em cada L2 para os utilizadores desse L2; assim, para que a sua construção seja útil, o esquema como um todo tem de ter uma utilização suficiente para que haja frequentemente pelo menos algumas transacções dentro do mesmo bloco em vários L2s principais.
Se forem utilizados ZK-SNARKs, o principal custo marginal é simplesmente a "lógica comercial" da passagem de números entre contratos, ou seja, talvez alguns milhares de gás L2 por utilizador. Se forem utilizadas provas múltiplas KZG, o provador teria de acrescentar 48 gases por cada L2 que contenha uma chave de armazenamento utilizada nesse bloco, pelo que o custo marginal do esquema por utilizador acrescentaria mais ~800 gases L1 por L2 (não por utilizador). Mas estes custos são muito inferiores aos custos da não agregação, que implicam inevitavelmente mais de 10 000 gases L1 e centenas de milhares de gases L2 por utilizador. Para as árvores de Verkle, pode utilizar diretamente as multi-provas de Verkle, adicionando cerca de 100-200 bytes por utilizador, ou pode fazer um ZK-SNARK de uma multi-prova de Verkle, que tem custos semelhantes aos ZK-SNARKs de ramos de Merkle, mas é significativamente mais barato de provar.
Do ponto de vista da implementação, é provavelmente melhor que os agrupadores agreguem provas de cadeia cruzada através da norma de abstração de conta ERC-4337. O ERC-4337 já tem um mecanismo que permite aos construtores agregar partes de UserOperations de forma personalizada. Existe mesmo uma <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi"> implementação deste processo para a agregação de assinaturas BLS, que poderá reduzir os custos do gás em L2 entre 1,5x e 3x, dependendo das outras formas de compressão incluídas.
Diagrama retirado de <a href="https://hackmd.io/@voltrevo/BJ0QBy3zi"> Post de implementação da carteira BLS mostrando o fluxo de trabalho das assinaturas agregadas BLS numa versão anterior do ERC-4337. O fluxo de trabalho de agregação de provas entre cadeias será provavelmente muito semelhante.
Uma última possibilidade, e apenas utilizável para L2 que lê L1 (e não L1 que lê L2), é modificar L2s para lhes permitir fazer chamadas estáticas a contratos em L1 diretamente.
Isto poderia ser feito com um opcode ou uma pré-compilação, que permite chamadas para L1 onde você fornece o endereço de destino, gás e calldata, e retorna a saída, embora porque estas chamadas são chamadas estáticas elas não podem realmente mudar qualquer estado L1. Os L2 têm de ter conhecimento do L1 para processar os depósitos, pelo que não há nada de fundamental que impeça a sua implementação; trata-se sobretudo de um desafio de implementação técnica (ver: esta RFP do Optimism para suportar chamadas estáticas para o L1).
Repare que se o keystore estiver em L1 e os L2s integrarem a funcionalidade de chamada estática de L1, então não são necessárias quaisquer provas! No entanto, se os L2s não integrarem chamadas estáticas L1, ou se o keystore estiver no L2 (o que poderá eventualmente ter de acontecer, uma vez que o L1 se torna demasiado dispendioso para os utilizadores o utilizarem, nem que seja um pouco), então serão necessárias provas.
Todos os esquemas acima requerem que o L2 aceda ou à raiz do estado L1 recente, ou a todo o estado L1 recente. Felizmente, todos os L2s já têm alguma funcionalidade para aceder ao estado recente do L1. Isto deve-se ao facto de necessitarem dessa funcionalidade para processar as mensagens provenientes de L1 para L2, nomeadamente os depósitos.
E, de facto, se um L2 tiver uma funcionalidade de depósito, então pode usar esse L2 tal como está para mover raízes de estado de L1 para um contrato no L2: basta que um contrato em L1 chame o opcode BLOCKHASH e o passe para L2 como uma mensagem de depósito. O cabeçalho completo do bloco pode ser recebido, e a sua raiz de estado extraída, no lado L2. No entanto, seria muito melhor que cada L2 tivesse uma forma explícita de aceder diretamente ao estado L1 recente completo ou às raízes do estado L1 recente.
O principal desafio da otimização da forma como os L2 recebem as raízes recentes do estado L1 é conseguir simultaneamente segurança e baixa latência:
Além disso, na direção oposta (L1s lendo L2):
Algumas destas velocidades para operações sem confiança entre cadeias são inaceitavelmente lentas para muitos casos de utilização definidos; para esses casos, precisa de pontes mais rápidas com modelos de segurança mais imperfeitos. No entanto, para o caso de utilização de atualização de chaves de carteira, os atrasos mais longos são mais aceitáveis: não está a atrasar as transacções em horas, está a atrasar as alterações de chaves. Terá apenas de manter as chaves antigas por mais tempo. Se está a mudar de chaves porque as chaves são roubadas, então tem um período significativo de vulnerabilidade, mas isso pode ser atenuado, por exemplo, por carteiras com 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 de estado L1 de uma forma optimizada, em que cada bloco L2 (ou o registo de computação da raiz de estado) contenha um ponteiro para o bloco L1 mais recente, pelo que, se L1 reverter, L2 também pode reverter. Os contratos de armazenamento de chaves devem ser colocados na rede principal ou em L2s que são ZK-rollups e, portanto, podem ser rapidamente transferidos para L1.
Os blocos da cadeia L2 podem ter dependências não só de blocos L2 anteriores, mas também de um bloco L1. Se o L1 reverter para além dessa ligação, o L2 também reverte. Vale a pena notar que esta é 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 é assim tanto. Na verdade, nem sequer precisa de ser um rollup: se for um L3, ou um validium, então não há problema em manter as carteiras aí, desde que mantenha os keystores no L1 ou num rollup ZK. O que você precisa é que a cadeia tenha acesso direto às raízes do estado do Ethereum, e um compromisso técnico e social para estar disposto a reorgar se o Ethereum reorgar, e hard fork se o Ethereum hard forks.
Um problema de investigação interessante é identificar até que ponto é possível que uma cadeia tenha esta forma de ligação a várias outras cadeias (por exemplo, a cadeia de distribuição de alimentos). Ethereum e Zcash). Fazê-lo de forma ingénua é possível: a sua cadeia pode concordar em reorganizar-se se o Ethereum ou o Zcash se reorganizarem (e fazer um hard fork se o Ethereum ou o Zcash se reorganizarem), mas nesse caso os seus operadores de nós e a sua comunidade em geral têm o dobro das dependências técnicas e políticas. Assim, esta técnica pode ser utilizada para se ligar a algumas outras cadeias, mas a um custo crescente. Os esquemas baseados em pontes ZK têm propriedades técnicas atractivas, mas têm como principal fraqueza o facto de não serem resistentes a ataques de 51% ou a bifurcações difíceis. Poderá haver soluções mais inteligentes.
Idealmente, também queremos preservar a privacidade. Se tiver muitas carteiras que são geridas pelo mesmo repositório de chaves, queremos certificar-nos disso:
Isto cria alguns problemas:
Com os SNARKs, as soluções são concetualmente fáceis: as provas escondem informação por defeito e o agregador precisa de produzir um SNARK recursivo para provar os SNARKs.
Atualmente, o principal desafio desta abordagem é o facto de a agregação exigir que o agregador crie um SNARK recursivo, o que é bastante lento.
Com KZG, podemos utilizar <a href="https://notes.ethereum.org/@vbuterin/non_index_revealing_proof"> this trabalho sobre provas KZG não reveladoras de índices (ver 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.
A leitura direta de L1 a partir do interior de L2, infelizmente, não preserva a privacidade, embora a implementação da funcionalidade de leitura direta continue a ser muito útil, tanto para minimizar a latência como devido à sua utilidade para outras aplicações.