Ao construir o seu Empacotador (Bundler) ERC-4337, chamado "Rundler", a Alchemy encontrou o componente mais desafiador para ser executado corretamente: a estimativa de gás para operações do usuário. Este post irá explicar os obstáculos que enfrentamos ao tentar fornecer aos usuários estimativas precisas de gás e as soluções que adotamos atualmente.
Esta é uma visão técnica destinada aos desenvolvedores que utilizam o ERC-4337. Se você é novo no estilo de abstração de conta do ERC-4337, sugerimos que comece lendo nossa série de introdução sobre Abstração de Contas.
Por que a estimativa de gás é importante para operações de usuários do ERC-4337?
Fornecer estimativas precisas de gás para operações de usuários é fundamental para a experiência do usuário do ERC-4337. Se uma estimativa de gás for muito baixa, uma operação do usuário pode ser revertida durante a simulação ou, pior, ser revertida na cadeia durante a fase de execução, fazendo com que o usuário pague pelo gás de uma operação revertida. Se a estimativa de gás for muito alta, um usuário pode ser desencorajado ou se tornar incapaz de enviar sua operação devido aos custos.
Embora seja importante ser preciso, a estimativa de gás não precisa ser 100% correta, contanto que os erros estejam sempre superestimados (mas não em excesso). No ERC-4337, os campos de gás são representados como limites e o usuário é reembolsado por qualquer gás que não consuma na cadeia. Assim, a estimativa de gás não impacta o custo real da operação.
Exceto para
preVerificationGas
, mas falaremos mais sobre isso depois.
Definições dos Campos de Gás do ERC-4337
Os campos de gás em uma operação de usuário e suas definições a partir da especificação do ERC-4337 são:
-
preVerificationGas
: a quantidade de gás a ser paga para compensar o empacotador pela execução de pré-verificação e pelos dados da chamada (calldata). -
verificationGasLimit
: a quantidade de gás a ser alocada para a etapa de verificação. -
callGasLimit
: a quantidade de gás a ser alocada para a chamada de execução principal. -
maxFeePerGas
: taxa máxima por gás (semelhante aomax_fee_per_gas
da EIP-1559). -
maxPriorityFeePerGas
: taxa de prioridade máxima por gás (semelhante aomax_priority_fee_per_gas
da EIP-1559).
Medição de Gás do ERC-4337
Um empacotador ERC-4337 paga o custo antecipadamente para enviar uma transação empacotada ao contrato de ponto de entrada. O ponto de entrada medirá o gás usado por cada operação do usuário, multiplicará esse valor pela taxa calculada e compensará o empacotador por este valor após a conclusão da operação do usuário.
O cálculo efetivo se parece com isso:
uint256 gasFee = min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
uint256 gasUsed = preVerificationGas + meteredVerificationGas + meteredCallGas;
uint256 gasCost = gasFee * gasUsed;
(bool success,) = bundler.call{value : gasCost}("");
Para simplificar, isso é válido apenas para redes que suportam a EIP-1559.
preVerificationGas
é adicionado como está, enquanto o gás de verificação e o gás de chamada são “medidos”. Ou seja, seu uso de gás é medido pelo ponto de entrada na cadeia e são cobrados pelo valor exato que usam, até seu limite. Se o limite for atingido em qualquer uma das fases de verificação ou chamada, a operação será revertida.
O ponto de entrada não tem como processar reembolsos de gás, pois mede o gás como parte de uma transação, enquanto os reembolsos de gás são emitidos após a conclusão de uma transação e, portanto, são devolvidos diretamente ao empacotador. Esta é uma fonte extra de custo para os usuários e uma possível fonte de receita para os empacotadores.
Fluxo da Operação do Usuário
O fluxo típico para enviar uma operação de usuário consiste no seguinte:
- Construa uma operação parcial do usuário com
sender
,nonce
,initCode
ecallData
preenchidos.- Preencha também
signature
epaymasterAndData
com valores "fictícios".
- Preencha também
- Estime o gás para esta operação parcial do usuário através de um RPC do empacotador com
eth_estimateUserOperationGas
.- Preencha
preVerificationGas
,verificationGasLimit
ecallGasLimit
a partir do valor retornado.
- Preencha
-
Estime as taxas de gás necessárias para a operação e preencha
maxFeePerGas
emaxPriorityFeePerGas
.- Esta etapa não depende das etapas 1 ou 2, pode ser executada a qualquer momento ou em paralelo.
- (Opcional) Envie a operação do usuário a um contrato pagador (paymaster) patrocinador para assinatura.
- Preencha
paymasterAndData
a partir do valor retornado.
- Preencha
- Assine a operação do usuário acima, preencha signature e envie a um empacotador via
eth_sendUserOperation
.
Este post se concentrará na etapa 2 e no método RPC do empacotador eth_estimateUserOperationGas
.
Estimativa de Gás da Operação do Usuário
eth_estimateUserOperationGas
é um método RPC que os empacotadores devem suportar conforme a especificação ERC-4337.
Sua definição:
Estime os valores de gás para uma UserOperation
(Operação de Usuário). Dada a UserOperation
, opcionalmente sem limites de gás e preços de gás, retorne os limites de gás necessários. O campo de assinatura é ignorado pela carteira, de forma que a operação não exigirá a aprovação do usuário. Ainda assim, pode ser necessário colocar uma assinatura "semiválida" (por exemplo, uma assinatura com o comprimento certo).
Parâmetros: os mesmos que em eth_sendUserOperation
. Os parâmetros de limites de gás (e preços) são opcionais, mas são usados se especificados. maxFeePerGas
e maxPriorityFeePerGas
têm valor padrão zero, então nenhum pagamento é exigido nem pela conta nem pelo contrato pagador.
Valores de Retorno:
-
preVerificationGas
: sobrecarga de gás destaUserOperation
. -
verificationGasLimit
: gás real usado pela validação destaUserOperation
. -
callGasLimit
: valor usado pela execução da conta interna.
Cálculo do PreVerificationGas
O PreVerificationGas
é usado para capturar qualquer uso de gás que o ponto de entrada não mede, compensando o empacotador por esse gás.
Simplificando, isso pode ser dividido em 3 cálculos separados:
- A parcela da operação do gás intrínseco para a transação empacotada.
- Observe que o empacotador deve assumir um tamanho de pacote para determinar isso antecipadamente.
- A parcela da operação do custo do gás dos dados de chamada.
- Isso é diretamente atribuível ao tamanho e composição de bytes da operação.
- A parcela da operação de qualquer sobrecarga de gás de execução que o ponto de entrada incorre fora do que é medido.
- Isso é determinado fora da cadeia, fazendo uma análise de uso de gás do contrato do ponto de entrada e atribuindo-o com base na operação do usuário.
Note que o preVerificationGas
não é um limite. Ou seja, o valor definido neste campo é sempre pago ao empacotador integralmente. Tenha cuidado com este campo, pois um valor incorreto pode significar enviar ao empacotador mais taxas do que é necessário!
Confira esta análise aprofundada do preVerificationGas
escrita pela equipe StackUp para mais detalhes.
Os empacotadores normalmente executarão seu cálculo de preVerificationGas
durante a fase de "pré-verificação" (por exemplo, verificações realizadas antes da simulação completa baseada em rastreio).
Se o preVerificationGas
de uma operação for menor que o valor calculado pelo empacotador, ele o rejeitará. Isso pode se tornar uma fonte de incompatibilidade entre implementações de empacotadores.
Se uma determinada implementação de empacotador, L, estima um valor menor para preVerificationGas
e uma implementação diferente, H, estima (e requer) um valor mais alto, H rejeitará operações que usaram L para estimativa.
Ainda está para ser visto como essa incompatibilidade impactará o mempool p2p, mas é provável que os usuários sejam incentivados a estimar um preVerificationGas
ligeiramente mais alto para maximizar suas chances de serem incluídos em um pacote.
Como o Empacotador ERC-4337 da Alchemy Estima Limites de Gás
É importante fornecer estimativas precisas para os campos de limite de gás, verificationGasLimit
e callGasLimit
. O objetivo pode ser declarado como: fornecer uma função que estime o gás usado durante essas fases, garantindo que apenas superestime, mas não tanto que desanime o usuário de enviar sua operação.
Vamos discutir as várias tentativas que fizemos para fornecer essa função no Rundler.
Tentativa 1: eth_estimateGas
O padrão Ethereum JSON-RPC oferece um bom método para estimar o gás de transações. Vamos usar o eth_estimateGas
!
O método é assim:
-
verificationGasLimit
: chama a funçãosimulateValidation
no ponto de entrada e passa paraeth_estimateGas
-
callGasLimit
: chama a funçãoinnerHandleOp
no ponto de entrada e passa paraeth_estimateGas
Isso parece funcionar bem. Embora tanto simulateValidation
quanto innerHandleOp
não capturem exatamente as partes medidas de cada fase, eles são estritamente superconjuntos e superestimarão um pouco.
Note que essas chamadas são completamente independentes, mas na cadeia são executadas em uma única transação. Neste método, o resultado do simulateValidation
não é persistido para impactar o innerHandleOp
.
E se o innerHandleOp
depender de algo feito durante o simulateValidation
?
Este é o caso para a implantação inicial de uma conta e, possivelmente, para alguns esquemas de validação avançada. O método de fábrica é chamado durante a fase de validação para implantar o contrato da conta. A fase de execução requer que esse contrato seja implantado e o innerHandleOp
será revertido se não isso não acontecer, falhando em todas as tentativas de estimativa.
Precisamos de uma forma de estimar ambos os campos de limite de gás, garantindo que a estimativa para a fase de execução seja executada na mesma chamada que a fase de validação.
Para a próxima tentativa...
Tentativa 2: Simular e Medir
A implementação ERC-4337 v0.6 fornece um método simulateHandleOp
que combina as fases de validação e execução em uma única chamada de função. Seu valor de retorno contém preOpGas
, que é a soma do gás usado durante a validação e o preVerificationGas
. Ele também contém o valor total pago pela operação do usuário, denominado paid
.
Para determinar o callGasLimit
, paid
pode ser convertido em gás usado ao se dividir pela taxa de gás. O empacotador pode determinar a taxa de gás fixando a simulação a uma altura de bloco (recente) com uma taxa base conhecida e definindo a taxa de prioridade como 0. Através de algumas conversas simples, todos os campos de gás podem ser calculados. Este método também pode superestimar o gás usado, já que parte da lógica do ponto de entrada não medida é atribuída ao gás usado.
Outro método razoável é medir o gás usado durante a fase de chamada, medindo o gás usado durante toda a chamada simulateHandleOp
e depois subtraindo o gás usado pela fase de validação.
Podemos implantar um contrato auxiliar que mede o gás usado pelo simulateHandleOp
e anexa isso ao valor de retorno. Este método tem o mesmo problema de superestimação que o anterior.
Para aqueles que são meticulosos sobre superestimativas, podemos refinar ainda mais isso.
Podemos rastrear o método simulateHandleOp
do ponto de entrada para calcular o gás?
Empacotadores devem ter a capacidade de executar uma chamada de rastreamento para realizar as verificações de simulação necessárias para se proteger contra invalidações na cadeia. Podemos usar essa funcionalidade para estimar o gás?
O método é assim:
- Rastreie o método
simulateHandleOp
do ponto de entrada, que convenientemente usa um esquema de "marcador de número" para que o rastreador possa saber exatamente em qual fase de execução se encontra. - No rastreador, observe a quantidade de gás restante no início de cada fase e no final.
- A partir desses valores, calcule a quantidade de gás usado por cada fase.
Ótimo! Usamos o rastreamento, que é ligeiramente mais caro, para calcular exatamente a quantidade de gás usada.
Mas não tão rápido! Usar qualquer um dos métodos acima pode fazer com que as operações fiquem sem gás para determinadas chamadas de contrato.
Por quê?
Para responder a isso, podemos obter dicas de como o Reth estima o gás. Notou a busca binária? O que está acontecendo ali? Por que o Reth não usa um método de rastreamento semelhante ao acima, já que tem acesso a todas as mesmas informações?
Bem, acontece que em uma chamada de função da EVM: gás usado ≠ gás necessário.
Uma visão geral de por que isso acontece pode ser encontrada no artigo de Sergio Lerner "The Dark Side of Ethereum 1/64th CALL Gas Reduction". A razão mais importante é chamada de "Regra 63/64" (ou "Regra 1/64", dependendo de com quem você está falando).
Esta regra afirma que a EVM só repassará 63/64 do gás restante para cada chamada de função. O resultado disso é que você precisa de um pouco mais de gás antecipadamente do que o que é eventualmente consumido. Isso acontece para cada chamada, então uma fase de validação/execução com uma pilha profunda de chamadas pode precisar reservar uma grande quantidade de gás antecipadamente para lidar com o gás reservado.
Queremos uma solução que funcione para TODAS as operações de usuário em potencial, então isso invalida quaisquer tentativas que usem medições de gás usado para estimativa de limite de gás. Para nossa próxima tentativa, vamos nos inspirar nos clientes EVM.
Pode ser possível realizar uma análise detalhada de uma função de rastreamento e reverter o gás usado em limite de gás, levando em consideração a regra 63/64 (e quaisquer outras peculiaridades da EVM). Ainda temos que explorar este ângulo.
Tentativa 3: Busca Binária
Não iremos reinventar a roda aqui. Para lidar com a complexidade descrita acima, clientes de nós utilizam buscas binárias durante a estimativa de gás, encontrando o menor valor de gás que leva a uma transação bem-sucedida. Podemos fazer algo semelhante para operações de usuário!
A abordagem é a seguinte:
Primeiramente, observe que, como a busca binária requer várias chamadas à mesma função, queremos garantir que a função tenha alta performance. Isso elimina o uso de rastreamento como uma abordagem razoável aqui e precisaremos confiar no eth_call
.
Para estimar verificationGasLimit
:
- Execute uma tentativa de
simulateHandleOp
emMAX_VERIFICATION_GAS
(uma configuração do empacotador que define o gás máximo que pode ser usado na fase de validação) para garantir que o sucesso seja possível em primeiro lugar.- Durante esta execução, defina
callGasLimit
como 0 para economizar computação.
- Durante esta execução, defina
- Execute a busca binária em
simulateHandleOp
para focar no menor valor deverificationGasLimit
que não esgota o gás.- Se a fase de validação ficar sem gás, a chamada será revertida. Como já verificamos que a chamada pode ter sucesso com um limite de gás mais alto, quaisquer reversões podem resultar em mover a borda inferior da busca binária para cima.
Uma otimização interessante aqui é observar que não precisamos de uma estimativa 100% precisa. O empacotador pode escolher o quão exato quer ser e terminar a busca binária assim que estiver dentro dessa faixa, sempre errando para mais. Isso pode economizar muitas rodadas de chamadas. Por exemplo, o Rundler define essa faixa de erro para 1000 unidades de gás, economizando 10 iterações.
A segunda otimização para economizar algumas iterações é determinar um ponto de partida melhor para a busca. Multiplique o gás usado em (1) por um escalar (por exemplo, o Reth usa 3x) e defina-o como o palpite inicial para o algoritmo de busca binária.
Para estimar callGasLimit
:
- Defina
verificationGasLimit
paraMAX_VERIFICATION_GAS
. - Execute uma tentativa em
MAX_EXECUTION_GAS
(não faz parte da especificação, mas é necessário para um empacotador se proteger contra DOS) e verifique se o sucesso da execução é possível.
É aqui que este método de estimativa falha. Reversões são capturadas pelo ponto de entrada e emitidas como logs (registros), e eth_call
não fornece uma maneira de inspecionar qualquer log emitido. Também desperdiça computação executando a validação totalmente a cada tentativa.
Podemos fornecer uma maneira de executar esta busca binária de callGasLimit
?
É possível detectar essa reversão ao usar um rastreamento, mas gostaríamos de evitar isso por motivos de custo. Pode valer a pena revisitar esta decisão se um empacotador estiver totalmente no controle do cliente do nó e puder usar um rastreador nativo.
Solução: Busca Binária em Solidity
Com base no procedimento de estimativa de verificationGasLimit
, a ideia principal por trás deste método é executar a busca binária para callGasLimit
em Solidity, codificando lógica que pode capturar se a parte de execução falha.
Vamos definir as restrições:
- A busca binária deve ocorrer após a execução da validação para garantir que quaisquer contratos de contas sejam implantados.
- Uma restrição desejável, mas não obrigatória, seria executar a validação apenas uma vez (ou limitar a quantidade de vezes) para reduzir o desperdício de computação.
- As chamadas de execução devem ser iniciadas a partir do ponto de entrada. Muitas implementações de contas só permitem que suas funções de execução sejam chamadas quando
msg.sender
é igual a um endereço de ponto de entrada codificado. - A busca binária deve retornar seu resultado final.
- Todas as funções de ponto de entrada devem se comportar exatamente como normalmente.
A solução que encontramos é usar um contrato proxy que usa DELEGATECALL
para encaminhar ao contrato de ponto de entrada, mas adiciona lógica adicional. Durante a estimativa, usamos substituições de eth_call
para mover o contrato de ponto de entrada original para um endereço aleatório e substituí-lo pelo contrato proxy. Isso satisfaz as restrições (2) e (4).
Como observado acima, o ponto de entrada oculta o resultado da fase de execução. Em vez disso, pulamos a chamada de execução padrão (definindo callGasLimit
como 0) e utilizamos os argumentos target/targetCallData
para simulateHandleOp
. Isso é executado após a validação e pode retornar informações, satisfazendo as restrições (1) e (3). Veja o código do ponto de entrada abaixo (comentários adicionados):
function simulateHandleOp(UserOperation calldata op, address target, bytes calldata targetCallData) external override {
UserOpInfo memory opInfo;
// A VALIDAÇÃO É EXECUTADA UMA VEZ POR CHAMADA
_simulationOnlyValidations(op);
(uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment(0, op, opInfo);
ValidationData memory data = _intersectTimeRange(validationData, paymasterValidationData);
numberMarker();
// PULA ESTE PASSO COM CALLGASLIMIT = 0
uint256 paid = _executeUserOp(0, op, opInfo);
numberMarker();
bool targetSuccess;
bytes memory targetResult;
if (target != address(0)) {
// EXECUTA A BUSCA BINÁRIA AQUI
(targetSuccess, targetResult) = target.call(targetCallData);
}
// O RESULTADO DO ALVO É RETORNADO AQUI
revert ExecutionResult(opInfo.preOpGas, paid, data.validAfter, data.validUntil, targetSuccess, targetResult);
}
Para nossa chamada de destino personalizada, chamamos um método, estimateCallGas
, no contrato proxy com dados de chamada que codificam:
- A conta e seus dados de chamada (da operação do usuário original)
- Os parâmetros da busca binária
Como o proxy está localizado no endereço do ponto de entrada original, estimateCallGas
é capaz de chamar a função de execução da conta, determinar se a chamada teve sucesso ou ficou sem gás, e executar o algoritmo de busca binária até obter sucesso.
Pronto! A busca binária agora pode ocorrer em um eth_call
para obter uma estimativa precisa para callGasLimit
!
Na prática, o Rundler divide este único eth_call
em várias chamadas, cada uma com um limite global máximo de gás, a fim de contornar os limites máximos de chamada de gás do nó. Cada chamada executará iterações de busca binária até que a próxima iteração faça com que a chamada fique sem gás.
Este método não leva em consideração uma pequena quantidade de sobrecarga imposta pelo ponto de entrada durante
innerHandleOp
. Esta sobrecarga é estática e é adicionada ao resultado da estimativa.
Mudança Potencial no Ponto de Entrada
Estivemos pensando em possíveis mudanças no ponto de entrada que poderiam tornar o processo de estimativa de gás descrito acima mais fácil de implementar e mais performático para os usuários. Este não é um problema fácil de resolver, por isso adoraríamos ouvir de outros empacotadores ou membros da comunidade do ERC-4337, se vocês tiverem ideias!
Queremos garantir que a fase de validação seja executada antes da fase de execução durante a estimativa de gás. A fase de execução pode depender do estado definido durante a fase de validação (por exemplo, uma transferência de USDC do contrato pagador e uma fase de execução que usa USDC) e, portanto, é mais preciso estimar logo depois.
Uma melhoria potencial seria fazer com que o ponto de entrada retornasse um booleano representando se a parte de execução foi revertida ou não durante simulateHandleOp
. O empacotador poderia então executar a busca binária fora da cadeia sem o contrato proxy e substituições de estado. No entanto, esta busca binária será ineficiente, pois estará executando a fase de validação durante cada iteração.
Isso nos leva a pegar a busca binária feita pelo contrato proxy na tentativa 3 acima e adicionar sua funcionalidade diretamente ao contrato do ponto de entrada (pseudocódigo).
contract EntryPoint {
...
function estimateValidationGas(userOp, min, max, rounding) {
// executa _validatePrepayment uma vez com o máximo de gás para ver se o sucesso é possível
// busca binária em _validatePrepayment
// termina quando a busca binária estiver completa (dentro da margem de arredondamento)
// ou quando a próxima iteração fizesse com que a chamada ficasse sem gás
// retorna min e max
revert(...)
}
function estimateCallGas(userOp, min, max, rounding) {
// executa _validatePrepayment com o máximo de gás para definir o estado de validação
// executa a chamada alvo uma vez com o máximo de gás para ver se o sucesso é possível
// busca binária na chamada alvo
// retorna min e max
// adiciona a sobrecarga do ponto de entrada fora da cadeia
revert(...)
}
}
Nossa implementação do acima mencionado (como um contrato proxy) será de código aberto em breve. Acreditamos que pode ser benéfico para os usuários do ERC-4337 ter uma estimativa via busca binária como parte do próprio contrato do ponto de entrada.
Fique de olho na nossa implementação do empacotador de código aberto que será lançada em breve! 🦀
Continue Lendo
Os próximos artigos desta série de 4 partes exploram os desafios de estimativa de gás que encontramos e as soluções que nossa equipe de engenharia implementou.
- Assinaturas Fictícias e Transferências de Token de Gás do ERC-4337
- Estimativa de Gás para Camada 2 e Agregadores de Assinatura
- Como as Taxas de Operação do Usuário são Estimadas e Cobradas
Artigo original publicado por Dan Coombs e David Philipson. Traduzido por Paulinho Giovannini.
Oldest comments (0)