Muito obrigado a Francesco, Jochem, Vitalik, Ignacio, Gary, Dankrad, Carl, Ansgar e Tim para feedback e revisão.
TL; DR: Os construtores de blocos incluem listas de acesso e diferenças de estado em blocos para validadores validarem mais rápido → Escala 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 blocos deve incluir em cada bloco, especificando quais slots de armazenamento transações individuais acessarão. Se essas listas forem imprecisas ou incompletas, o bloco se tornará inválido. Como resultado, O mecanismo de consenso do Ethereum pode fazer cumprir a conformidade estritaexigindo que os construtores forneçam BALS corretos.
Os validadores se beneficiam significativamente do BALS, acelerando a verificação do bloco. Saber exatamente quais contas e slots de armazenamento serão acessados, permite que os validadores apliquem estratégias de paralelização simples para leituras de disco (IO) e execução (EVM). Isso pode levar a Validação de bloco mais rápida e abra a porta para levantando limites de gás no futuro.
Confira este post anterior em dependências de execução e as simulações de Dankrad sobre o armazenamento paralelo ladas aqui.
Uma meta crítica de design para BALS é Mantendo o tamanho compacto Nos cenários médios e de pior caso. 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. Bals será colocado no corpo do bloco, com um hash do BAL armazenado no cabeçalho.
Atualmente, os clientes da Ethereum confiam em seus próprios Estratégias otimistas de paralelização. Isso geralmente tem um bom desempenho Blocos médiosmas luta com Cenários de pior casolevando a diferenças significativas de desempenho.
- O que incluir?
- Além de locais de armazenamento
(address, storage key)
também podemos incluir:- valores de armazenamento
- saldos
- BALANÇO DIFS
- Para valores de armazenamento, podemos distinguir entre:
- Valores de execução pré-blocos vs. pré-transação
- Valores de execução pré-transação pós-transação
- Nosso objetivo é ser agnóstico de hardware ou queremos otimizar para certas especificações de hardware comumente usadas?
- Além de locais de armazenamento
A seguir, focaremos em três variantes principais de BALS: acesso, execução e estado.
Acesse as transações de mapa de BALS para (address, storage key)
Tuplas.
- Pequeno em tamanho.
- Ativar leituras de disco paralelo, mas são menos eficazes para a validação de transação paralela devido a possíveis dependências.
- O tempo de execução é
parallel IO + serial EVM
.
Para a eficiência, uma estrutura leve BAL pode ser assim usando o 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 desduplicada de endereços acessados durante o bloco.
- 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 acessavam essa chave.
Por exemplo, o BAL para o número do bloco 21_935_797
seria assim:
(
('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
(
('0xa63b...c2', (0)),
('0x8b3e...a7', (0)),
('0xfb19...a8',
(1, 2, 3, 4, 84, 85, 91)
),
# ... additional entries
)
),
# ... additional entries
)
Execução Bals mapeie transações para (address, storage key, value)
Tuplas e incluem Balance Diffs.
- Um pouco maior em tamanho devido à inclusão de valor.
- Facilitar leituras de disco paralelo 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 de acesso, com StorageValue
Adicionado para representar o valor após o último acesso por cada transação.
-
Excluir
SlotAccess.accesses
Para leituras: vazioSlotAccess.accesses
indica a ler.- Isso significa que apenas escrever Operações consistem em
(StorageKey, List(TxIndex), StorageValue)
Tuplas, reduzindo significativamente o tamanho do objeto.
- Isso significa que apenas escrever Operações consistem em
-
Em vez de valores pós-execução, poderíamos incluir valores de pré-execução para leituras e gravações para cada transação. Assim, a execução da EVM não precisaria esperar pelas leituras de disco. Este é um design completamente separado, chegando com suas próprias trade-offs, reduzindo o tempo de execução para
max(parallel IO, parallel EVM)
.
Para sincronizar (cf. fase de cicatrização), com diferenças de estado (portanto, os valores pós-TX) para gravações são necessários para recuperar o atraso na corrente enquanto atualiza o estado. Em vez de receber novos valores de estado diretamente com suas provas, podemos curar o estado usando os bloqueios internos do estado e verificar a correção do processo comparando a raiz final do estado derivado com a raiz do estado da cabeça recebida de um nó leve (H/T Dankrad).
Para implantações de contrato, o code
deve conter o bytecode de tempo de execução do contrato recém -implantado.
O NonceDiffs
a estrutura deve registrar os valores de não-transação pré-transação para todos CREATE
e CREATE2
Contas de implementadores e contratos implantados no bloco.
Um exemplo Bal para bloco 21_935_797
Pode ficar assim:
(('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
(('0xa63b...c2',
(0),
'0x...'),
('0x8b3e...a7',
(0),
'0x...'),
('0xfb19...a8',
(1, 2, 3, 4, 84, 85, 91),
'0x...'),
...
)
)
)
BALANÇO DIFS são necessários para lidar corretamente 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 equilíbrio, remetentes de transações, destinatários e a moeda 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)
- Desduplicado por endereço.
- Cada tupla lista a alteração exata do equilíbrio para todas as transações relevantes.
Exemplo:
(
(
'0xdead...beef',
(
(0, -1000000000000000000), # tx 0: sent 1 ETH
(2, +500000000000000000) # tx 2: received 0.5 ETH
)
),
# ... additional entries
)
Esta estrutura Decoupple totalmente a execução do estadopermitindo que os validadores ignorem qualquer pesquisa de disco ou trie durante a execução, confiando apenas nos dados fornecidos no bloco. O pre_accesses
A 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-acessoativando a execução e verificação paralela de grão fino.
- Maior em tamanho
- O tempo de execução é
max(parallel IO, parallel EVM)
.
Um objeto SSZ eficiente pode parecer o seguinte:
# 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 diferencial nonce permanecem os mesmos.
…
Que tal excluir os valores iniciais de leitura?
Outra variante do estado Bal exclui valores de leiturae inclui apenas valores pré e pós-valores para gravações. Neste modelo, pre_accesses
e tx_accesses
contêm apenas slots de armazenamento que foram escritos, juntamente com os correspondentes value_before
(do estado) e value_after
(do resultado da transação).
Isso reduz o tamanho enquanto ainda permite a reconstrução completa do estado, pois os slots de gravação definem as únicas mudanças persistentes. Os acessos de leitura são implicitamente assumidos como resolvidos por meio de pesquisas de estado tradicionais ou cache no cliente.
Access Bal
A transação de pior caso consome todo o limite de gás em 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 fornece o número máximo de endereços (20 bytes) + teclas de armazenamento (32 bytes) lê que podemos fazer, resultando em 18_947
leituras de armazenamento e aproximadamente 0,93 MIB.
Este é um pessimista medir. É praticamente inviável usar o gás do bloco exclusivamente para sloads. Com um contrato personalizado (veja aqui), pude acionar 16.646 sloads, não mais. Veja esta transação de exemplo na Sepolia.
Isso é menor que o tamanho de bloco atual (e pós-prétra), alcançável através do calldata.
Tamanho médio do BAL amostrado em mais de 1.000 blocos entre 21.605.993 e 2.223.0023 57 Kib SSZ codificado. Em média, os blocos nesse período continham cerca de 1.181 chaves de armazenamento e 202 contratos por bloco.
Execução Bals
A inclusão de um valor de 32 bytes por entrada de gravação não aumenta o pior tamanho da BAL. Para leitura 18_947
Carrega de armazenamento, It permanece em 0,93 MIB.
O pior dos casos de equilíbrio ocorrem se uma única transação enviar valor mínimo (1 wei) para vários endereços:
- Com o
21,000
custo base e9,300
Gas para chamadas, 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 + a moeda do bloco, nós obtemos3,871
endereços. Com endereços de 20 bytes e deltas de equilíbrio de 12 bytes, obtemos um tamanho de equilíbrio 0,12 MIB (12 bytes são suficientes para representar o suprimento total do ETH). - Como alternativa, usando várias transações, cada uma enviando 1 WEI para uma conta diferente, podemos teoricamente empacotar 1.188 transações em um bloco. Com 3 endereços (
callee
Assim,tx.from
etx.to
) no equilíbrio diff, e 12 bytes deltas, temos 0,12 MIB em tamanho.
Um tamanho de equilíbrio DIF, amostrado em mais de 1.000 blocos (2.160.5993-2223.0023) teriam contido cerca de 182 transações e 250 endereços com deltas de equilíbrio em média. Isso resulta em uma média de 9,6 kib, codificada pela SSZ.
Estado Bals
A inclusão de outros 32 bytes para leituras e gravações aumentou o pior tamanho da BAL para cerca de 1,51 MIB.
O projeto atual especificado no EIP-7928 adota o modelo de execução BAL com Valores pós-TX. Essa variante oferece um trade-off atraente: permite o paralelismo de E/S e EVM e inclui diferenças de estado suficientes para sincronizar, sem os custos adicionais de largura de banda associados aos instantâneos de estado de pré-transação.
Referências
Fontesethresear