ERC-4337 가스 추정 작동 방식
작성자: Dan Coombs, David Philipson
Alchemy가 ERC-4337 Bundler인 "Rundler"를 구축하면서 정확하게 만들기 가장 어려웠던 컴포넌트는 user operation gas estimation이었습니다. 이 글에서는 사용자에게 정확한 gas estimate를 제공하려는 과정에서 마주친 문제들과, 현재 저희가 사용하고 있는 해결책을 설명합니다.
이 글은 ERC-4337 개발자를 위한 기술 개요입니다. ERC-4337 방식 account abstraction이 처음이라면 먼저 저희 Account Abstraction 소개 시리즈를 읽어보시길 권합니다.
ERC-4337 user operation에서 gas estimation이 중요한 이유는 무엇인가요?
정확한 user operation gas estimation을 제공하는 것은 ERC-4337의 사용자 경험에 중요합니다. gas estimate가 너무 낮으면 user operation이 simulation 도중 revert되거나, 더 나쁘게는 execution phase에서 온체인으로 revert되어 사용자가 revert된 operation의 gas를 부담하게 될 수 있습니다. 반대로 gas estimation이 너무 높으면 비용 때문에 사용자가 operation 전송을 주저하거나 아예 전송하지 못할 수 있습니다.
정확성이 중요하기는 하지만, gas estimation이 100% 정확할 필요는 없습니다. 오차가 항상 overestimate 방향이기만 하면 됩니다(단, 너무 크게 overestimate하지 않는 한). ERC-4337에서 gas 필드는 limit으로 표현되며, 사용자는 온체인에서 소비하지 않은 gas에 대해 환불받습니다. 따라서 gas estimation은 실제 operation 비용에 영향을 주지 않습니다.
💡 *preVerificationGas은 예외이며, 이에 대해서는 뒤에서 다룹니다.
ERC-4337 gas 필드 정의
user operation의 gas 필드와 ERC-4337 스펙에 따른 정의는 다음과 같습니다:
preVerificationGas: bundler에게 pre-verification execution 및 calldata에 대한 보상으로 지급할 gas 양.verificationGasLimit: verification 단계에 할당할 gas 양.callGasLimit: 메인 execution call에 할당할 gas 양.maxFeePerGas: gas당 최대 수수료 (EIP-1559의max\_fee\_per\_gas와 유사).maxPriorityFeePerGas: gas당 최대 priority fee (EIP-1559의max\_priority\_fee\_per\_gas와 유사).
ERC-4337 gas 계측
ERC-4337 bundler는 entry point 컨트랙트로 bundle transaction을 보내는 비용을 선지급합니다. entry point는 각 user operation이 사용한 gas를 계측하여 계산된 fee와 곱한 후, user operation이 완료된 뒤 이 값을 bundler에게 보상합니다.
실제 계산은 다음과 같습니다:*
💡 *단순화를 위해 이는 EIP-1559를 지원하는 네트워크에만 해당됩니다.
preVerificationGas은 그대로 더해지는 반면, verification과 call gas는 "계측"됩니다. 즉, 이들의 gas 사용량은 온체인에서 entry point가 측정하며, limit 한도 내에서 실제로 사용한 만큼만 청구됩니다. verification 또는 call phase 중 어느 한쪽에서라도 limit에 도달하면 operation은 revert됩니다.
entry point는 gas 환불을 처리할 방법이 없습니다. transaction의 일부로 gas를 계측하는 반면, gas 환불은 transaction이 완료된 후에 발생하여 Bundler에게 직접 반환되기 때문입니다. 이는 사용자에게 추가 비용이 발생하는 요인이자, bundler에게는 잠재적인 수익원입니다.
User operation 흐름
user operation 전송의 일반적인 흐름은 다음과 같습니다:
sender,nonce,initCode,callData를 채운 partial user operation을 구성합니다. a. 또한signature과paymasterAndData에 "dummy" 값을 채웁니다.eth\_estimateUserOperationGas를 사용해 bundler RPC를 통해 이 partial user operation의 gas를 estimate합니다. a. 반환값으로부터preVerificationGas,verificationGasLimit,callGasLimit을 채웁니다.- operation에 필요한 gas fee를 estimate하여
maxFeePerGas와maxPriorityFeePerGas을 채웁니다. a. 이 단계는 1단계나 2단계에 의존하지 않으므로 언제든지, 또는 병렬로 실행할 수 있습니다. - (선택 사항) 서명을 받기 위해 sponsoring paymaster에게 user operation을 전송합니다
a. 반환값으로부터
paymasterAndData을 채웁니다. - 위 user op에 서명하고
signature를 채운 후,eth\_sendUserOperation을 통해 bundler에게 전송합니다.
이 글에서는 2단계와 bundler RPC 메서드 eth\_estimateUserOperationGas에 초점을 맞춥니다.
User operation gas estimation
eth\_estimateUserOperationGas는 ERC-4337 스펙에 따라 bundler가 반드시 지원해야 하는 RPC 메서드입니다.
정의:
UserOperation의 gas 값을 estimate합니다. gas limit과 gas price가 없는 UserOperation이 주어지면 필요한 gas limit을 반환합니다. signature 필드는 wallet에서 무시되므로 operation이 사용자의 승인을 필요로 하지 않습니다. 다만 "semi-valid"한 signature (예: 올바른 길이의 signature)를 넣어야 할 수는 있습니다.
*파라미터**: eth_sendUserOperation과 동일합니다. gas limit(및 price) 파라미터는 선택 사항이지만 지정하면 사용됩니다. maxFeePerGas과 maxPriorityFeePerGas은 기본값이 0이므로 account와 paymaster 모두 결제가 필요하지 않습니다.*
반환 값:
- *preVerificationGas** 이 UserOperation의 gas 오버헤드*
- *verificationGasLimit** 이 UserOperation의 validation에서 실제로 사용된 gas*
- *callGasLimit** 내부 account execution에서 사용되는 값*
PreVerificationGas 계산
PreVerificationGas는 entry point가 계측하지 않는 gas 사용량을 포착하여 bundler에게 이를 보상하는 데 사용됩니다.
단순한 경우 다음 3가지 별도 계산으로 나눌 수 있습니다:
- bundle transaction에 대한 intrinsic gas 중 해당 operation의 몫. a. bundler는 이를 미리 결정하기 위해 bundle 크기를 가정해야 한다는 점에 유의하세요.
- calldata gas 비용 중 해당 operation의 몫. a. 이는 operation의 크기와 바이트 구성에 직접 좌우됩니다.
- entry point가 계측 범위 밖에서 발생시키는 execution gas 오버헤드 중 해당 operation의 몫. a. 이는 entry point 컨트랙트의 gas 사용량 분석을 오프체인에서 수행하여 user operation 단위로 배분함으로써 결정됩니다.
preVerificationGas은 limit이 아니라는 점에 유의하세요. 즉, 이 필드에 설정된 값은 항상 전액 bundler에게 지급됩니다. 이 필드는 주의해서 다뤄야 합니다. 값이 잘못되면 필요한 것보다 더 많은 수수료를 bundler에게 보내는 결과가 될 수 있습니다!
preVerificationGas에 대한 자세한 내용은 StackUp 팀이 작성한 심층 분석을 참고하세요.
bundler는 일반적으로 "pre-check" 단계(예: 전체 trace 기반 simulation 이전에 실행되는 체크)에서 preVerificationGas 계산을 실행합니다.
operation의 preVerificationGas이 bundler가 계산한 값보다 낮으면 bundler는 이를 거부합니다. 이는 bundler 구현체 간 호환성 문제의 원인이 될 수 있습니다.
특정 bundler 구현체 L이 preVerificationGas에 대해 더 낮은 값을 estimate하고, 다른 구현체 H가 더 높은 값을 estimate(및 요구)한다면, H는 L로 estimation한 operation을 거부할 것입니다.
이러한 비호환성이 P2P mempool에 어떤 영향을 미칠지는 아직 지켜봐야 하지만, 사용자는 bundle에 포함될 확률을 최대화하기 위해 preVerificationGas를 약간 높게 estimate하도록 유인될 가능성이 높습니다.
Alchemy의 ERC-4337 bundler가 gas limit을 estimate하는 방법
verificationGasLimit과 callGasLimit 같은 gas limit 필드에 대해 정확한 estimate를 제공하는 것은 중요합니다. 목표는 다음과 같이 표현할 수 있습니다: 이 단계들에서 사용되는 gas를 estimate하는 함수를 제공하되, 반드시 overestimate만 하고, 사용자가 operation 전송을 주저하게 할 정도로 지나치게 overestimate하지는 않아야 합니다.
이 함수를 Rundler에서 구현하기 위해 시도했던 여러 방법을 살펴보겠습니다.
시도 1: eth\_estimateGas
표준 Ethereum JSON-RPC는 transaction의 gas를 estimate하는 좋은 메서드를 제공하니, eth\_estimateGas를 그냥 사용해보겠습니다!
방법은 다음과 같습니다:
verificationGasLimit: entry point의simulateValidation함수를 호출하고eth\_estimateGas에 전달callGasLimit: entry point의innerHandleOp함수를 호출하고eth\_estimateGas에 전달
이는 꽤 잘 작동하는 것처럼 보입니다. simulateValidation과 innerHandleOp 모두 각 단계에서 계측되는 부분을 정확히 포착하지는 않지만, 엄밀히 superset이므로 약간 overestimate하게 됩니다.
이 호출들은 완전히 독립적으로 이루어지지만, 온체인에서는 하나의 transaction으로 실행된다는 점에 유의하세요. 이 방법에서는 simulateValidation의 결과가 innerHandleOp에 영향을 미치도록 지속되지 않습니다.
innerHandleOp이 simulateValidation 중 수행된 무언가에 의존한다면?
이는 account를 최초로 배포할 때, 그리고 일부 고급 validation 스킴에서 발생할 수 있는 경우입니다. factory 메서드는 validation phase 중에 호출되어 account 컨트랙트를 배포합니다. execution phase는 이 컨트랙트가 배포되어 있어야 하며, 그렇지 않으면 innerHandleOp가 revert되어 모든 estimation 시도가 실패합니다.
두 gas limit 필드를 모두 estimate하면서, execution phase에 대한 estimation이 validation phase와 동일한 call 안에서 실행되도록 보장할 방법이 필요합니다.
다음 시도로 넘어가겠습니다…
시도 2: simulate하고 측정하기
ERC-4337 v0.6 구현체는 validation phase와 execution phase를 하나의 함수 호출로 결합한 simulateHandleOp 메서드를 제공합니다. 이 메서드의 반환값에는 validation과 preVerificationGas 동안 사용된 gas의 합인 preOpGas가 포함됩니다. 또한 user op가 지불한 총액인 paid도 포함됩니다.
callGasLimit을 구하기 위해, paid을 gas fee로 나누면 사용된 gas로 변환할 수 있습니다. bundler는 알려진 base fee를 가진 (최근) 특정 block height에 simulation을 고정하고 priority fee를 0으로 설정하여 gas fee를 결정할 수 있습니다. 몇 가지 간단한 변환을 거치면 모든 gas 필드를 계산할 수 있습니다. 이 방법 역시 계측되지 않는 entry point 로직의 일부가 사용된 gas로 귀속되므로 gas 사용량을 overestimate할 수 있습니다.
call phase 동안 사용된 gas를 측정하는 또 다른 합리적인 방법은 전체 simulateHandleOp call 동안 사용된 gas를 측정한 후 validation phase에서 사용된 gas를 빼는 것입니다.
simulateHandleOp이 사용하는 gas를 계측하여 반환값에 추가하는 helper 컨트랙트를 배포할 수 있습니다. 이 방법 역시 앞선 방법과 동일한 overestimation 문제가 있습니다.
overestimate에 민감한 분들을 위해 이를 더 정교하게 다듬을 수 있습니다.
entry point의 simulateHandleOp 메서드를 trace하여 gas를 계산할 수 있을까요?
bundler는 온체인 무효화로부터 스스로를 보호하기 위해 필요한 simulation 체크를 수행하기 위해 trace call을 실행할 수 있는 능력을 가지고 있어야 합니다. 이 기능을 gas estimation에 사용할 수 있을까요?
방법은 다음과 같습니다:
- entry point의
simulateHandleOp메서드를 trace합니다. 이 메서드는 편리하게도 "number marker" 스킴을 사용하여 tracer가 현재 어느 실행 단계에 있는지 정확히 알 수 있게 합니다. - tracer에서 각 단계 시작 시점과 맨 마지막 시점에 남아 있는 gas 양을 기록합니다.
- 이 값들로부터 각 단계에서 사용된 gas 양을 계산합니다.
좋습니다! 조금 더 비용이 드는 trace를 사용하여 정확한 gas 사용량을 계산했습니다.
하지만 그렇게 간단하지 않습니다. 위의 어떤 방법을 사용하더라도 특정 컨트랙트 호출에서는 operation이 gas 부족으로 실패할 수 있습니다.
왜일까요?
이에 답하기 위해 Reth가 gas를 estimate하는 방법에서 힌트를 얻을 수 있습니다. binary search가 보이시나요? 거기서 무슨 일이 일어나고 있는 걸까요? Reth는 동일한 정보에 접근할 수 있는데, 왜 위와 비슷한 trace 방식을 그냥 쓰지 않는 걸까요?
사실 EVM 함수 호출에서는 사용된 gas ≠ 필요한 gas입니다.
이것이 왜 그런지에 대한 개요는 Sergio Lerner의 "The Dark Side of Ethereum 1/64th CALL Gas Reduction" 글에서 찾아볼 수 있습니다. 그 이유 중 가장 중요한 것이 바로 "63/64th Rule"(또는 대화 상대에 따라 "1/64th Rule")입니다.
이 규칙은 EVM이 각 함수 호출에 남은 gas의 63/64만 전달한다는 것을 말합니다. 그 결과 최종적으로 소비되는 것보다 약간 더 많은 gas를 미리 확보해야 합니다. 이는 각 call마다 발생하므로, call stack이 깊은 validation/execution phase는 따로 떼어놓아야 할 gas를 처리하기 위해 미리 많은 양의 gas를 확보해야 할 수 있습니다.
저희는 모든 잠재적인 user operation에 대해 작동하는 해결책을 원하므로, gas 사용량 측정치를 gas limit estimation에 사용하는 시도는 모두 무효가 됩니다. 다음 시도에서는 EVM 클라이언트에서 영감을 얻어보겠습니다.
💡 63/64th rule(및 기타 EVM의 세부 사항)을 고려하여 trace 함수를 상세히 분석하고 사용된 gas를 gas limit으로 역산하는 것이 가능할 수도 있습니다. 저희는 아직 이 방향을 탐구해보지 않았습니다.
시도 3: binary search
여기서 바퀴를 재발명할 필요는 없습니다. 위에서 설명한 복잡성을 다루기 위해, 노드 클라이언트는 gas estimation 시 binary search를 사용하여 transaction이 성공하는 가장 낮은 gas 값을 찾습니다. user operation에 대해서도 비슷한 방법을 사용할 수 있습니다!
접근 방식은 다음과 같습니다:
먼저, binary search는 동일한 함수를 여러 번 호출해야 하므로, 해당 함수의 성능이 높아야 한다는 점에 유의하세요. 이는 여기서 trace를 합리적인 접근법으로 사용하는 것을 배제하며, eth\_call에 의존해야 함을 의미합니다.
verificationGasLimit를 estimate하려면:
- 애초에 성공이 가능한지 확인하기 위해
MAX\_VERIFICATION\_GAS(bundler 설정으로, validation phase에서 사용할 수 있는 최대 gas를 정의)에서simulateHandleOp를 한 번 시도합니다. a. 이 실행 동안 계산을 절약하기 위해callGasLimit을 0으로 설정합니다. - gas 부족이 발생하지 않는 가장 낮은
verificationGasLimit값을 찾기 위해simulateHandleOp에 대해 binary search를 실행합니다. a. validation phase가 gas 부족이 되면 call은 revert됩니다. 더 높은 gas limit에서는 call이 성공함을 이미 확인했으므로, revert가 발생하면 binary search의 하한을 올릴 수 있습니다.
여기서 좋은 최적화 하나는 100% 정확한 estimation이 필요하지 않다는 점입니다. bundler는 얼마나 정확하게 estimate할지 선택할 수 있으며, 이 범위 내에 들어오면 항상 높은 쪽으로 오차를 두면서 binary search를 종료할 수 있습니다. 이렇게 하면 여러 라운드의 호출을 절약할 수 있습니다. 예를 들어, Rundler는 이 오차 범위를 1K gas로 설정하여 10회의 반복을 절약합니다.
몇 번의 반복을 더 절약하기 위한 두 번째 최적화는 검색을 위한 더 나은 시작점을 정하는 것입니다. (1)에서 사용된 gas에 scalar를 곱하고(예: Reth는 3배를 사용합니다), 이를 binary search 알고리즘의 초기 추정값으로 설정합니다.
callGasLimit를 estimate하려면:
verificationGasLimit을MAX\_VERIFICATION\_GAS로 설정합니다MAX\_EXECUTION\_GAS(스펙의 일부는 아니지만 bundler가 DOS로부터 스스로를 보호하기 위해 필요함)을 시도하고 execution success가 가능한지 확인합니다.
이 지점에서 이 estimation 방법은 한계에 부딪힙니다. revert는 entry point에 의해 포착되어 로그로 emit되는데, eth\_call는 emit된 로그를 조사할 방법을 제공하지 않습니다. 또한 매 시도마다 validation을 전체적으로 실행하느라 계산을 낭비합니다.
callGasLimit binary search를 실행할 방법을 제공할 수 있을까요?
💡 trace를 사용하면 이 revert를 감지할 수 있지만, 비용 문제로 이를 피하고자 합니다. bundler가 노드 클라이언트를 완전히 제어하며 native tracer를 사용할 수 있는 경우라면 이 결정을 재검토할 가치가 있을 수 있습니다.
해결책: Solidity로 binary search 구현하기
verificationGasLimit estimation 절차를 기반으로, 이 방법의 핵심 아이디어는 callGasLimit에 대한 binary search를 Solidity에서 실행하여, execution 부분이 실패했는지 여부를 포착할 수 있는 로직을 인코딩하는 것입니다.
제약 조건을 정의해보겠습니다:
- account 컨트랙트가 배포되었는지 확인하기 위해 binary search는 validation이 실행된 이후에 일어나야 합니다 a. 바람직하지만 필수는 아닌 제약 조건으로, 낭비되는 계산을 줄이기 위해 validation을 한 번만(또는 제한된 횟수만) 실행하는 것이 있습니다.
- execute call은 entry point로부터 시작되어야 합니다. 많은 account 구현체는
msg.sender이 하드코딩된 entry point 주소와 같을 때만 자신의 execute 함수 호출을 허용합니다. - binary search는 최종 결과를 반환해야 합니다.
- 모든 entry point 함수는 평소와 정확히 동일하게 동작해야 합니다.
저희가 찾은 해결책은 DELEGATECALL를 사용해 entry point 컨트랙트로 전달하되 추가 로직을 더하는 proxy 컨트랙트를 사용하는 것입니다. estimation 중에는 eth\_call override를 사용해 원래 entry point 컨트랙트를 임의의 주소로 옮기고, 그 자리를 proxy 컨트랙트로 대체합니다. 이는 제약 조건 (2)와 (4)를 충족합니다.
위에서 언급했듯이, entry point는 execution phase의 결과를 숨깁니다. 대신 저희는 표준 execution call을 건너뛰고(callGasLimit을 0으로 설정하여), simulateHandleOp의 target/targetCallData 인자를 활용합니다. 이는 validation 이후에 실행되며 정보를 반환할 수 있어 제약 조건 (1)과 (3)을 충족합니다. 아래 entry point 코드를 참고하세요(주석이 추가되어 있습니다):
custom target call을 위해, 다음을 인코딩한 call data와 함께 proxy 컨트랙트의 estimateCallGas 메서드를 호출합니다:
- account와 그 call data(원래 user operation에서 가져온)
- binary search 파라미터
proxy가 원래 entry point의 주소에 위치하므로, estimateCallGas는 account의 execute 함수를 호출하고, 호출이 성공했는지 아니면 gas 부족으로 실패했는지 판단하여 성공할 때까지 binary search 알고리즘을 실행할 수 있습니다.
짜잔! 이제 binary search가 eth\_call 안에서 일어나 callGasLimit에 대한 정확한 estimate를 얻을 수 있습니다!
실제로 Rundler는 노드의 최대 call gas limit을 우회하기 위해, 이 단일 eth\_call을 각각 최대 global gas limit을 가진 여러 call로 나눕니다. 각 call은 다음 반복이 gas 부족을 유발할 때까지 binary search 반복을 실행합니다.
💡 이 방법은 innerHandleOp 동안 entry point가 부과하는 약간의 오버헤드를 놓칩니다. 이 오버헤드는 고정값이며 estimation 결과에 더해집니다.
entry point의 잠재적 변경 사항
저희는 위에서 설명한 gas estimation 과정을 더 쉽게 구현하고 사용자에게 더 나은 성능을 제공할 수 있게 해줄 entry point의 잠재적 변경 사항에 대해 고민해왔습니다. 이는 쉽게 풀리는 문제가 아니므로, 다른 bundler나 ERC-4337 커뮤니티 구성원들에게서 아이디어를 듣고 싶습니다!
gas estimation 중에 validation phase가 execution phase보다 먼저 실행되도록 보장하고자 합니다. execution phase는 validation phase 동안 설정된 state에 의존할 수 있으므로(예: USDC paymaster 전송과 USDC를 사용하는 execution phase), validation 직후에 estimate하는 것이 가장 정확합니다.
한 가지 잠재적 개선책은 simulateHandleOp 동안 execution 부분이 revert되었는지 여부를 나타내는 boolean을 entry point가 반환하도록 하는 것입니다. 그러면 bundler는 proxy 컨트랙트나 state override 없이 오프체인에서 binary search를 실행할 수 있습니다. 다만 이 binary search는 매 반복마다 validation phase를 실행하게 되므로 비효율적일 것입니다.
이를 통해 위 시도 3에서 proxy 컨트랙트가 수행한 binary search를 entry point 컨트랙트 자체에 직접 추가하는 방안에 이르게 됩니다(의사 코드):
위 구현(proxy 컨트랙트 형태)은 곧 오픈소스로 공개될 예정입니다. binary search를 통한 estimation이 entry point 컨트랙트 자체의 일부가 되는 것이 ERC-4337 사용자들에게 유익할 것이라 생각합니다.
곧 공개될 저희 오픈소스 bundler 구현체를 기대해 주세요! 🦀
계속 읽기
이 4부작 시리즈의 다음 글들에서는 저희가 마주쳤던 gas estimation 문제들과 엔지니어링 팀이 구현한 해결책을 다룹니다.
Alchemy 뉴스레터
출시 소식을 가장 먼저 받아보세요
뉴스레터 구독하기
Alchemy의 최신 제품 업데이트와 리소스 받기
이메일 주소를 입력하면 마케팅 커뮤니케이션 및 제품 업데이트 수신에 동의하게 됩니다. Alchemy는 저희 개인정보 처리방침에 따라 수집한 정보를 처리합니다. 언제든지 구독을 취소할 수 있습니다.
관련 아티클

Glamsterdam: Ethereum 개발자를 위한 완전 가이드
Glamsterdam이 Sepolia에서 가동 중입니다. 개발자에게 무엇을 의미하는지, 그리고 Alchemy가 네트워크 업그레이드에 어떻게 준비했는지를 담은 완전 가이드입니다.

트레이딩 팀이 저지연 RPC로 Alchemy를 선택하는 이유
트레이딩 팀이 Alchemy를 선택하는 이유는, 시장이 움직일 때의 저지연 읽기와 쓰기, 그리고 거래량이 정점에 달해도 자금이 정산될 때의 신뢰할 수 있는 기록 때문이다.

도쿄에서 구축한다면 어떤 RPC 제공업체를 선택해야 할까
도쿄를 서비스하는 RPC 제공업체 다섯 곳을 다섯 개 체인에서 벤치마크했습니다. 비교 결과와, 지연 시간에 민감한 팀에게 차이가 어디서 나는지 정리합니다.