Muito obrigado a Francesco, Jochem, Vitalik, Ignacio, Gary, Dankrad, Carl, Ansgar e Tim pelo feedback e revisão.
DR: Os construtores de blocos incluem listas de acesso e diferenças de estado em blocos para validadores validarem mais rapidamente → dimensionar L1!
Um dos principais tópicos no esforço renovado para escalar a Camada 1 do Ethereum – especialmente a camada de execução – é listas de acesso em nível de bloco (BALs) – EIP-7928.
BALs são listas estruturadas que o construtor de bloco deve incluir em cada bloco, especificando quais slots de armazenamento as transações individuais acessarão. Se essas listas forem imprecisas ou incompletas, o bloqueio torna-se inválido. Como resultado, O mecanismo de consenso da Ethereum pode impor conformidade estritaexigindo que os construtores forneçam BALs corretos.
Os validadores se beneficiam significativamente dos BALs ao acelerar a verificação de blocos. Saber exatamente quais contas e slots de armazenamento serão acessados permite que os validadores apliquem estratégias simples de paralelização para leituras de disco (IO) e execução (EVM). Isto pode levar a validação de bloco mais rápida e abra a porta para aumentando os limites de gás no futuro.
Confira esta postagem anterior em dependências de execução e as simulações de Dankrad sobre paralelização de armazenamento podem ser lidas aqui.
Um objetivo crítico de design para BALs é mantendo o tamanho compacto tanto no cenário médio como no pior cenário. A largura de banda já é uma restrição para nós e validadores, por isso é vital que os BALs não adicionem carga desnecessária à rede. Os BALs serão colocados no corpo do bloco, com um hash do BAL armazenado no cabeçalho.
Atualmente, os clientes Ethereum contam com seus próprios estratégias de paralelização otimistas. Estes geralmente funcionam bem em blocos médiosmas luta com piores cenárioslevando a diferenças significativas de desempenho.
- O que incluir?
- Além dos locais de armazenamento
(address, storage key)
também podemos incluir:- valores de armazenamento
- saldos
- diferenças de equilíbrio
- Para valores de armazenamento, podemos distinguir entre:
- valores de execução pré-bloco vs. pré-transação
- valores de execução pré e pós-transação
- Nosso objetivo é ser independente de hardware ou queremos otimizar para determinadas especificações de hardware comumente usadas?
- Além dos locais de armazenamento
A seguir, focaremos em três variantes principais de BALs: acesso, execução e estado.
Acesse BALs mapeiam transações para (address, storage key)
tuplas.
- Pequeno em tamanho.
- Habilite leituras paralelas de disco, mas são menos eficazes para validação de transações paralelas devido a possíveis dependências.
- O tempo de execução é
parallel IO + serial EVM
.
Para maior eficiência, uma estrutura BAL leve poderia ter esta aparência usando SSZ:
# Type aliases
Address = ByteVector(20)
StorageKey = ByteVector(32)
TxIndex = uint16
# Constants; chosen to support a 630m block gas limit
MAX_TXS = 30_000
MAX_SLOTS = 300_000
MAX_ACCOUNTS = 300_000
# Containers
class SlotAccess(Container):
slot: StorageKey
tx_indices: List(TxIndex, MAX_TXS) # Transactions (by index) that access this slot (read or write)
class AccountAccess(Container):
address: Address
accesses: List(SlotAccess, MAX_SLOTS)
# Top-level block fields
BlockAccessWrites = List(AccountAccess, MAX_ACCOUNTS)
BlockAccessReads = List(AccountAccess, MAX_ACCOUNTS)
O lista externa é uma lista deduplicada de endereços acessados durante o bloqueio.
- Para cada endereço, há uma lista de chaves de armazenamento acessado.
- Para cada chave de armazenamento:
List(TxIndex)
: índices de transação ordenados que acessaram esta chave.
Por exemplo, o BAL para o número do bloco 21_935_797
ficaria assim:
(
('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
(
('0xa63b...c2', (0)),
('0x8b3e...a7', (0)),
('0xfb19...a8',
(1, 2, 3, 4, 84, 85, 91)
),
# ... additional entries
)
),
# ... additional entries
)
BALs de execução mapeiam transações para (address, storage key, value)
tuplas e incluem diferenças de equilíbrio.
- Tamanho um pouco maior devido à inclusão de valor.
- Facilite leituras paralelas de disco e execução paralela.
- O tempo de execução é
parallel IO + parallel EVM
.
Uma estrutura eficiente usando SSZ:
# Type aliases
Address = ByteVector(20)
StorageKey = ByteVector(32)
StorageValue = ByteVector(32)
TxIndex = uint16
Nonce = uint64
# Constants; chosen to support a 630m block gas limit
MAX_TXS = 30_000
MAX_SLOTS = 300_000
MAX_ACCOUNTS = 300_000
MAX_CODE_SIZE = 24576 # Maximum contract bytecode size in bytes
# SSZ containers
class PerTxAccess(Container):
tx_index: TxIndex
value_after: StorageValue # value in state after the last access within the transaction
class SlotAccess(Container):
slot: StorageKey
accesses: List(PerTxAccess, MAX_TXS) # empty for reads
class AccountAccess(Container):
address: Address
accesses: List(SlotAccess, MAX_SLOTS)
code: Union(ByteVector(MAX_CODE_SIZE), None) # Optional field for contract bytecode
BlockAccessList = List(AccountAccess, MAX_ACCOUNTS)
# Pre-block nonce structures
class AccountNonce(Container):
address: Address # account address
nonce_before: Nonce # nonce value before block execution
NonceDiffs = List(AccountNonce, MAX_TXS)
A estrutura é a mesma da versão access, com StorageValue
adicionado para representar o valor após o último acesso de cada transação.
-
Excluir
SlotAccess.accesses
para leituras: vazioSlotAccess.accesses
indica um ler.- Isto significa que apenas escrever operações consistem em
(StorageKey, List(TxIndex), StorageValue)
tuplas, reduzindo significativamente o tamanho do objeto.
- Isto significa que apenas escrever operações consistem em
-
Em vez de valores pós-execução, poderíamos incluir valores pré-execução para leituras e gravações para cada transação. Assim, a execução do EVM não teria que esperar pelas leituras do disco. Este é um design completamente separado, com suas próprias vantagens e desvantagens, reduzindo o tempo de execução para
max(parallel IO, parallel EVM)
.
Para sincronizar (cf. fase de cura), é necessário ter diferenças de estado (portanto, os valores pós-tx) para gravações para acompanhar a cadeia durante a atualização do estado. Em vez de receber novos valores de estado diretamente com suas provas, podemos curar o estado usando as diferenças de estado dentro dos blocos e verificar a exatidão do processo comparando a raiz de estado derivada final com a raiz de estado do bloco principal recebida de um nó leve (h/t dankrad).
Para implantações contratuais, o code
deve conter o bytecode de tempo de execução do contrato recém-implantado.
O NonceDiffs
estrutura DEVE registrar os valores nonce pré-transação para todos CREATE
e CREATE2
contas do implementador e os contratos implantados no bloco.
Um exemplo de BAL para bloco 21_935_797
pode ser assim:
(('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
(('0xa63b...c2',
(0),
'0x...'),
('0x8b3e...a7',
(0),
'0x...'),
('0xfb19...a8',
(1, 2, 3, 4, 84, 85, 91),
'0x...'),
...
)
)
)
Diferenças de equilíbrio são necessários para lidar corretamente com a execução que depende do saldo de uma conta. Essas diferenças incluem todos os endereços tocados por uma transação envolvendo transferências de valor, juntamente com os deltas de saldo, remetentes da transação, destinatários e a base de moedas do bloco.
# Type aliases
Address = ByteVector(20)
TxIndex = uint64
BalanceDelta = ByteVector(12) # signed, two's complement encoding
# Constants
MAX_TXS = 30_000
MAX_ACCOUNTS = 70_000 # 630m / 9300 (cost call to non-empty acc with value)
# Containers
class BalanceChange(Container):
tx_index: TxIndex
delta: BalanceDelta # signed integer, encoded as 12-byte vector
class AccountBalanceDiff(Container):
address: Address
changes: List(BalanceChange, MAX_TXS)
BalanceDiffs = List(AccountBalanceDiff, MAX_ACCOUNTS)
- Deduplicado por endereço.
- Cada tupla lista a alteração exata do saldo para cada transação relevante.
Exemplo:
(
(
'0xdead...beef',
(
(0, -1000000000000000000), # tx 0: sent 1 ETH
(2, +500000000000000000) # tx 2: received 0.5 ETH
)
),
# ... additional entries
)
Esta estrutura dissocia totalmente a execução do estadopermitindo que os validadores ignorem qualquer disco ou tentem pesquisas durante a execução, contando apenas com os dados fornecidos no bloco. O pre_accesses
lista fornece o valores iniciais de todos os slots acessados antes do início do bloco, enquanto tx_accesses
rastreia o acesso por transação padrões e valores pós-acessopermitindo execução e verificação paralelas refinadas.
- Maior em tamanho
- O tempo de execução é
max(parallel IO, parallel EVM)
.
Um objeto SSZ eficiente poderia ter a seguinte aparência:
# Type aliases
Address = ByteVector(20)
StorageKey = ByteVector(32)
StorageValue = ByteVector(32)
TxIndex = uint16
# Constants
MAX_TXS = 30_000
MAX_SLOTS = 300_000
MAX_ACCOUNTS = 300_000
# Sub-containers
class PerTxAccess(Container):
tx_index: TxIndex
value_after: StorageValue
class SlotAccess(Container):
slot: StorageKey
accesses: List(PerTxAccess, MAX_TXS)
class AccountAccess(Container):
address: Address
accesses: List(SlotAccess, MAX_SLOTS)
class SlotPreValue(Container):
slot: StorageKey
value_before: StorageValue
class AccountPreAccess(Container):
address: Address
slots: List(SlotPreValue, MAX_SLOTS)
# Unified top-level container
class BlockAccessList(Container):
pre_accesses: List(AccountPreAccess, MAX_ACCOUNTS)
tx_accesses: List(AccountAccess, MAX_ACCOUNTS)
O equilíbrio e o nonce diff permanecem os mesmos.
…
Que tal excluir os valores lidos iniciais?
Outra variante do BAL estadual exclui valores lidose inclui apenas valores pré e pós para gravações. Neste modelo, pre_accesses
e tx_accesses
contêm apenas slots de armazenamento nos quais foram gravados, juntamente com seus correspondentes value_before
(do estado) e value_after
(do resultado da transação).
Isso reduz o tamanho e ainda permite a reconstrução completa do estado, já que os slots de gravação definem as únicas alterações persistentes. Supõe-se implicitamente que os acessos de leitura sejam resolvidos por meio de pesquisas de estado tradicionais ou armazenados em cache no cliente.
Acesse BAL
A transação de pior caso consome todo o limite de gás do bloco (36 milhões, em abril de 2025) para acessar o maior número possível de slots de armazenamento dentro de diferentes contratos, incluindo-os na lista de acesso EIP-2930.
Por isso, (36_000_000 - 21_000) // 1900
nos dá o número máximo de leituras de endereços (20 bytes) + chaves de armazenamento (32 bytes) que podemos fazer, resultando em 18_947
leituras de armazenamento e aproximadamente 0,93 MiB.
Este é um pessimista medir. É praticamente inviável utilizar o gás do bloco exclusivamente para SLOADs. Com um contrato personalizado (veja aqui), consegui acionar 16.646 SLOADs, não mais. Veja este exemplo de transação na Sepolia.
Isso é menor que o tamanho de bloco de pior caso atual (e pós-Prectra) alcançável por meio de calldata.
O tamanho médio do BAL amostrado em mais de 1.000 blocos entre 21.605.993 e 2.223.0023 foi de cerca de 57 KiB Codificado em SSZ. Em média, os blocos nesse período continham cerca de 1.181 chaves de armazenamento e 202 contratos por bloco.
BALs de execução
Incluir um valor de 32 bytes por entrada de gravação não aumenta o tamanho do BAL no pior caso. Para leitura 18_947
cargas de armazenamento, permanece em 0,93 MiB.
As diferenças de saldo no pior caso ocorrem se uma única transação envia um valor mínimo (1 wei) para vários endereços:
- Com o
21,000
custo base e9,300
gás para ligações, obtemos no máximo 3.868 endereços chamados em uma transação ((36_000_000-21_000)/9_300
). Incluindo otx.from
etx.to
dessa transação + coinbase do bloco, obtemos3,871
endereços. Com endereços de 20 bytes e deltas de equilíbrio de 12 bytes, obtemos um tamanho de diferença de equilíbrio de 0,12 MiB (12 bytes são suficientes para representar o fornecimento total de ETH). - Alternativamente, usando múltiplas transações, cada uma enviando 1 wei para uma conta diferente, podemos teoricamente agrupar 1.188 transações em um bloco. Com 3 endereços (
callee
,tx.from
etx.to
) na diferença de saldo e deltas de 12 bytes, obtemos 0,12 MiB em tamanho.
Um tamanho de diferença de saldo amostrado em mais de 1.000 blocos (2.160.5993–2.223.0023) conteria cerca de 182 transações e 250 endereços com deltas de saldo em média. Isso resulta em uma média de 9,6 KiB, codificados em SSZ.
BALs estaduais
A inclusão de outros 32 bytes para leituras e gravações aumentou o tamanho do BAL no pior caso para cerca de 1,51 MiB.
O projeto atual especificado em EIP-7928 adota o modelo Execution BAL com valores pós-tx. Essa variante oferece uma compensação atraente: permite o paralelismo de E/S e EVM e inclui diferenças de estado suficientes para sincronização, sem os custos adicionais de largura de banda associados aos instantâneos de estado pré-transação.
Referências
Fontesethresear