go-ethereum: gas estimate
Smart contract gas fee
우리가 스마트 컨트랙트의 코드를 실행할 때, 해당 코드는 EVM에서 실행되고 gas estimate를 거친 뒤, 특정 가스비가 소모된다.
geth 클라이언트는 몇 차례 써본 적이 있으나 사실 가스비가 정확하게 어떻게 책정되고 어떻게 소모되는지에 대해서는 모른다.
그러나 분명히 어딘가에 코드로 해당 비즈니스 로직을 구현한 곳이 있을 것이다.
이에 대해서 go-ethereum 코드를 분석하면서 알아보려고 한다.
코드가 워낙 방대해서 시행착오가 꽤 많았다.
우선 geth 패키지를 이용해서 gas estimate를 하는 부분에 대해서 찾아봤다.
gas estimate를 하는 함수는 다음과 같다.
var conn *ethclient.Client
var err error
// Dial rpc endpoint
if conn, err = ethclient.Dial(rpcEndpoint); err != nil {
return nil, err
}
conn.EstimateGas(context.Background(), ethereum.CallMsg{
...
})
EstimatGas
함수의 definition을 찾아보면 아래와 같다.
package ethclient
// EstimateGas tries to estimate the gas needed to execute a specific transaction based on
// the current pending state of the backend blockchain. There is no guarantee that this is
// the true gas limit requirement as other transactions may be added or removed by miners,
// but it should provide a basis for setting a reasonable default.
func (ec *Client) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
var hex hexutil.Uint64
err := ec.c.CallContext(ctx, &hex, "eth_estimateGas", toCallArg(msg))
if err != nil {
return 0, err
}
return uint64(hex), nil
}
봐야할 부분이 굉장히 많다. 천리 길도 한 걸음 부터라고 하니까 하나하나 뜯어보자.
hexutil
은 단순히 16진수를 위해 go-ethereum에서 사용하는 패키지이다.
Client 객체 자체는 아래와 같이 생겼다. ec.c
부분의 c
는 rpc client를 말한다.
package ethclient
// Client defines typed wrappers for the Ethereum RPC API.
type Client struct {
c *rpc.Client
}
rpc client를 통해서 CallContext
라는 함수를 실행한다.
해당 메소드를 보자.
package rpc
// CallContext performs a JSON-RPC call with the given arguments. If the context is
// canceled before the call has successfully returned, CallContext returns immediately.
//
// The result must be a pointer so that package json can unmarshal into it. You
// can also pass nil, in which case the result is ignored.
func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
if result != nil && reflect.TypeOf(result).Kind() != reflect.Ptr {
return fmt.Errorf("call result parameter must be pointer or nil interface: %v", result)
}
msg, err := c.newMessage(method, args...)
if err != nil {
return err
}
op := &requestOp{ids: []json.RawMessage{msg.ID}, resp: make(chan *jsonrpcMessage, 1)}
if c.isHTTP {
err = c.sendHTTP(ctx, op, msg)
} else {
err = c.send(ctx, op, msg)
}
if err != nil {
return err
}
// dispatch has accepted the request and will close the channel when it quits.
switch resp, err := op.wait(ctx, c); {
case err != nil:
return err
case resp.Error != nil:
return resp.Error
case len(resp.Result) == 0:
return ErrNoResult
default:
if result == nil {
return nil
}
return json.Unmarshal(resp.Result, result)
}
}
여기서의 Client는 rpc.Client이다.
컨텍스트와 결과값, 메소드, 인자를 받아서 rpc 호출을 시도한다.
좀 전에 EstimateGas
에서 CallContext
를 호출할 때 result로 hex 변수의 주소값을 넘겼다.
따라서 result에 결과값을 담는 방식으로 진행이 될 것이다.
여기서 중요한 것은 message를 통해서 결국 “eth_estimateGas” 라는 메소드를 실행한다는 것인데, 결국 가스비를 estimate하는 로직은 해당 메소드에 있을거라는 것이다.
따라서 우리가 찾아야하는 것은 “eth_estimateGas” 의 구현체이다.
go-ethereum의 ethapi
패키지를 찾아보면, 다음 메소드를 찾을 수 있다.
package ethapi
// EstimateGas returns the lowest possible gas limit that allows the transaction to run
// successfully at block `blockNrOrHash`, or the latest block if `blockNrOrHash` is unspecified. It
// returns error if the transaction would revert or if there are unexpected failures. The returned
// value is capped by both `args.Gas` (if non-nil & non-zero) and the backend's RPCGasCap
// configuration (if non-zero).
func (s *BlockChainAPI) EstimateGas(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Uint64, error) {
bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
if blockNrOrHash != nil {
bNrOrHash = *blockNrOrHash
}
return DoEstimateGas(ctx, s.b, args, bNrOrHash, overrides, s.b.RPCGasCap())
}
블록 넘버/해시를 받아서 DoEstimateGas
함수를 실행한다.
DoEstimatsGas
역시 같은 파일에 위치하는데, 함수가 좀 길다.
package ethapi
// DoEstimateGas returns the lowest possible gas limit that allows the transaction to run
// successfully at block `blockNrOrHash`. It returns error if the transaction would revert, or if
// there are unexpected failures. The gas limit is capped by both `args.Gas` (if non-nil &
// non-zero) and `gasCap` (if non-zero).
func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, gasCap uint64) (hexutil.Uint64, error) {
// Binary search the gas limit, as it may need to be higher than the amount used
var (
lo uint64 // lowest-known gas limit where tx execution fails
hi uint64 // lowest-known gas limit where tx execution succeeds
)
// Use zero address if sender unspecified.
if args.From == nil {
args.From = new(common.Address)
}
// Determine the highest gas limit can be used during the estimation.
if args.Gas != nil && uint64(*args.Gas) >= params.TxGas {
hi = uint64(*args.Gas)
} else {
// Retrieve the block to act as the gas ceiling
block, err := b.BlockByNumberOrHash(ctx, blockNrOrHash)
if err != nil {
return 0, err
}
if block == nil {
return 0, errors.New("block not found")
}
hi = block.GasLimit()
}
// Normalize the max fee per gas the call is willing to spend.
var feeCap *big.Int
if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) {
return 0, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
} else if args.GasPrice != nil {
feeCap = args.GasPrice.ToInt()
} else if args.MaxFeePerGas != nil {
feeCap = args.MaxFeePerGas.ToInt()
} else {
feeCap = common.Big0
}
state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return 0, err
}
if err := overrides.Apply(state); err != nil {
return 0, err
}
// Recap the highest gas limit with account's available balance.
if feeCap.BitLen() != 0 {
balance := state.GetBalance(*args.From) // from can't be nil
available := new(big.Int).Set(balance)
if args.Value != nil {
if args.Value.ToInt().Cmp(available) >= 0 {
return 0, core.ErrInsufficientFundsForTransfer
}
available.Sub(available, args.Value.ToInt())
}
allowance := new(big.Int).Div(available, feeCap)
// If the allowance is larger than maximum uint64, skip checking
if allowance.IsUint64() && hi > allowance.Uint64() {
transfer := args.Value
if transfer == nil {
transfer = new(hexutil.Big)
}
log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance,
"sent", transfer.ToInt(), "maxFeePerGas", feeCap, "fundable", allowance)
hi = allowance.Uint64()
}
}
// Recap the highest gas allowance with specified gascap.
if gasCap != 0 && hi > gasCap {
log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
hi = gasCap
}
// We first execute the transaction at the highest allowable gas limit, since if this fails we
// can return error immediately.
failed, result, err := executeEstimate(ctx, b, args, state.Copy(), header, gasCap, hi)
if err != nil {
return 0, err
}
if failed {
if result != nil && result.Err != vm.ErrOutOfGas {
if len(result.Revert()) > 0 {
return 0, newRevertError(result)
}
return 0, result.Err
}
return 0, fmt.Errorf("gas required exceeds allowance (%d)", hi)
}
// For almost any transaction, the gas consumed by the unconstrained execution above
// lower-bounds the gas limit required for it to succeed. One exception is those txs that
// explicitly check gas remaining in order to successfully execute within a given limit, but we
// probably don't want to return a lowest possible gas limit for these cases anyway.
lo = result.UsedGas - 1
// Binary search for the smallest gas limit that allows the tx to execute successfully.
for lo+1 < hi {
mid := (hi + lo) / 2
if mid > lo*2 {
// Most txs don't need much higher gas limit than their gas used, and most txs don't
// require near the full block limit of gas, so the selection of where to bisect the
// range here is skewed to favor the low side.
mid = lo * 2
}
failed, _, err = executeEstimate(ctx, b, args, state.Copy(), header, gasCap, mid)
if err != nil {
// This should not happen under normal conditions since if we make it this far the
// transaction had run without error at least once before.
log.Error("execution error in estimate gas", "err", err)
return 0, err
}
if failed {
lo = mid
} else {
hi = mid
}
}
return hexutil.Uint64(hi), nil
}
하나하나 살펴보자.

lo, hi 변수를 각각 선언하고, 성공했을 때와 실패했을 때로 나누어서 사용하는 것 같다.

args.From
이 nil인 경우 new(common.Address)
를 통해서 ZeroAddress
를 생성한다.
예외처리로 에러를 반환하는게 아니라 0주소값을 할당하고 그대로 진행하는듯 하다.
그 다음 구문의 조건문을 해석해보자.
args.Gas
가 nil이 아니고, args.Gas의 value를 uint64로 캐스팅했을때 params.TxGas보다 높다면
이라는 조건이다. 이쯤에서 함수의 프로토타입을 다시보자.

args
는 트랜잭션 인자이다. params는 어디에 있을까? go-ethereum의 params
패키지의 TxGas라는 constant 값이 있다.

주석을 참고하면, 컨트랙트 생성이 아닌 트랜잭션에 대한 기본 가스비로 생각할 수 있다.
그러나 payable은 불가능하다는 뜻으로 보인다.
아무튼 돌아와서 다시 살펴보자.

이제는 한 번에 알 수 있다. 트랜잭션 인자의 가스가 기본 가스비 (21,000)보다 높다면
hi(트랜잭션이 성공했을 때의 가스)를 해당 가스비로 설정하라는 뜻이다.
else …
구문에 해당되려면 args.Gas
가 nil이거나, args.Gas
가 params.TxGas
보다 낮아야 한다.
그런 경우에는 현재 블록을 가져오고, 블록의 GasLimit
으로 적용한다.

방금 위에서 한 것은 hi
를 설정하는 가정이었다. 이제 feeCap을 설정해야한다.
GasFeeCap: 트랜잭션에 대해 지불하고자 하는 최대 금액 (base fee + priority fee)
첫 번째 if문은 전체 nil guard에 대한 부분이고, 그 다음은 세 가지 경우로 나뉜다.
GasPrice가 nil이 아니다.
MaxFeePerGas가 nil이 아니다.
MaxPriorityFeePerGas가 nil이 아니다.
1번의 경우에는 feeCap이 GasPrice로 설정된다.
2번의 경우에는 Max 상한선이 설정되어있으므로 그대로 설정해준다.
3번의 경우에는 0으로 설정한다. (의아하지만 일단 넘어간다)

그 다음 쭉쭉 넘어가서, from 주소의 잔고와 비교해서 변수들을 재 점검하는 로직인듯하다.
allowance를 설정하는 부분을 보면 현재 계정의 잔고를 feeCap으로 나눈다.
그리고 그 allowance가 uint64 범위 이상인 경우에는 체크를 멈춘다.
그 밑을 보면 hi
가 gasCap
보다 높은 경우에는 gasCap
으로 다시 설정해준다.
결국 여기까지의 로직은 모두 가스 한도를 설정해주는 로직이다.
그 다음부터의 로직이 우리가 원하던, 실제로 코드를 실행시켜서 가스비를 계산하는 로직이 나온다.

맨 위에서 우선 allowance의 범위 내에서 가장 높은 가스비를 사용해서 코드를 실행하고, 실패할 경우 곧장 에러를 반환한다.
만약 성공한 경우에는 이분탐색으로 가장 낮은 가스비를 찾는다.
즉 eth_estimateGas
는 코드를 EVM 상에서 여러 번 수행해봄으로써 가장 낮은 가스비를 찾아주는 것이다.
이제 그럼 executeEstimate
함수를 들춰보자.
// executeEstimate is a helper that executes the transaction under a given gas limit and returns
// true if the transaction fails for a reason that might be related to not enough gas. A non-nil
// error means execution failed due to reasons unrelated to the gas limit.
func executeEstimate(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, gasCap uint64, gasLimit uint64) (bool, *core.ExecutionResult, error) {
args.Gas = (*hexutil.Uint64)(&gasLimit)
result, err := doCall(ctx, b, args, state, header, nil, nil, 0, gasCap)
if err != nil {
if errors.Is(err, core.ErrIntrinsicGas) {
return true, nil, nil // Special case, raise gas limit
}
return true, nil, err // Bail out
}
return result.Failed(), result, nil
}
일단 로직만 살펴보자.
doCall
함수를 이용해서 실패인지 아닌지에 대한 부분만 리턴한다.
doCall
함수도 까보자.
func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, overrides *StateOverride, blockOverrides *BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
if err := overrides.Apply(state); err != nil {
return nil, err
}
// Setup context so it may be cancelled the call has completed
// or, in case of unmetered gas, setup a context with a timeout.
var cancel context.CancelFunc
if timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, timeout)
} else {
ctx, cancel = context.WithCancel(ctx)
}
// Make sure the context is cancelled when the call has completed
// this makes sure resources are cleaned up.
defer cancel()
// Get a new instance of the EVM.
msg, err := args.ToMessage(globalGasCap, header.BaseFee)
if err != nil {
return nil, err
}
blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil)
if blockOverrides != nil {
blockOverrides.Apply(&blockCtx)
}
evm, vmError := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}, &blockCtx)
// Wait for the context to be done and cancel the evm. Even if the
// EVM has finished, cancelling may be done (repeatedly)
go func() {
<-ctx.Done()
evm.Cancel()
}()
// Execute the message.
gp := new(core.GasPool).AddGas(math.MaxUint64)
result, err := core.ApplyMessage(evm, msg, gp)
if err := vmError(); err != nil {
return nil, err
}
// If the timer caused an abort, return an appropriate error message
if evm.Cancelled() {
return nil, fmt.Errorf("execution aborted (timeout = %v)", timeout)
}
if err != nil {
return result, fmt.Errorf("err: %w (supplied gas %d)", err, msg.GasLimit)
}
return result, nil
}
일단 overrides.Apply(state)
함수를 추측해보건데, 아마 컨트랙트의 현재 상태를 가져오는 것이 아닐까? 라는 생각이 든다.
// StateOverride is the collection of overridden accounts.
type StateOverride map[common.Address]OverrideAccount
// Apply overrides the fields of specified accounts into the given state.
func (diff *StateOverride) Apply(state *state.StateDB) error {
if diff == nil {
return nil
}
for addr, account := range *diff {
// Override account nonce.
if account.Nonce != nil {
state.SetNonce(addr, uint64(*account.Nonce))
}
// Override account(contract) code.
if account.Code != nil {
state.SetCode(addr, *account.Code)
}
// Override account balance.
if account.Balance != nil {
state.SetBalance(addr, (*big.Int)(*account.Balance))
}
if account.State != nil && account.StateDiff != nil {
return fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
}
// Replace entire state if caller requires.
if account.State != nil {
state.SetStorage(addr, *account.State)
}
// Apply state diff into specified accounts.
if account.StateDiff != nil {
for key, value := range *account.StateDiff {
state.SetState(addr, key, value)
}
}
}
// Now finalize the changes. Finalize is normally performed between transactions.
// By using finalize, the overrides are semantically behaving as
// if they were created in a transaction just before the tracing occur.
state.Finalise(false)
return nil
}
아무래도 보기좋게 빗나간 것 같다. 일단 상태를 오버라이딩 하는 것 까지는 이름에서 충분히 추측할 수 있었지만, 컨트랙트의 상태가 아니라 계정의 상태를 말하는 것 같다.
그 다음은 timeout이 설정되어 있으면 Context WithTimeout으로, 없을 경우 Context WithCancel 함수로 컨텍스트와 cancel func를 생성해준다.

그 다음 코드로 넘어가보자.

일단 globalGasCap
, header.BaseFee
를 사용해서 들어온 트랜잭션 argument를 메세지 타입으로 만들어준다.
그 이후에 core.NewEVMBlockContext
가 바로 새로운 EVM 인스턴스를 생성하는 부분이다.
여기서 일단 Header 구조체를 살펴보고 가자.
주석을 보면, 이더리움의 블록 헤더에 대한 구조체라고 나와있다.
// Header represents a block header in the Ethereum blockchain.
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time uint64 `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
// BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
// WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers.
WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"`
// BlobGasUsed was added by EIP-4844 and is ignored in legacy headers.
BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"`
// ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers.
ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"`
}
그 다음은 NewChainContext 인자로 backend(API 인터페이스)와 컨텍스트를 넘긴다.
이제 NewEVMBlockContext
를 한 번 코드로 살펴보자.
// NewEVMBlockContext creates a new context for use in the EVM.
func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext {
var (
beneficiary common.Address
baseFee *big.Int
random *common.Hash
)
// If we don't have an explicit author (i.e. not mining), extract from the header
if author == nil {
beneficiary, _ = chain.Engine().Author(header) // Ignore error, we're past header validation
} else {
beneficiary = *author
}
if header.BaseFee != nil {
baseFee = new(big.Int).Set(header.BaseFee)
}
if header.Difficulty.Cmp(common.Big0) == 0 {
random = &header.MixDigest
}
return vm.BlockContext{
CanTransfer: CanTransfer,
Transfer: Transfer,
GetHash: GetHashFn(header, chain),
Coinbase: beneficiary,
BlockNumber: new(big.Int).Set(header.Number),
Time: header.Time,
Difficulty: new(big.Int).Set(header.Difficulty),
BaseFee: baseFee,
GasLimit: header.GasLimit,
Random: random,
ExcessBlobGas: header.ExcessBlobGas,
}
}
author가 nil인지 아닌지로 분기처리를 한 모습이 보이는데, 위에서 author에 nil을 집어넣었다.
따라서 estimate를 할 때는 항상 author==nil
조건을 충족할 것이다.
나머지는 단순히 기본 값으로 넣어주는 듯 하다.
그 이후로 GetEvm
, ApplyMessage
등으로 실행한다.
결국 가장 중요한 것이 아래 부분이 된다.

누가봐도 ApplyMessage가 정말 의심스럽다. go-ethereum에서 추상화를 매우 잘해놔서 함수를 몇 번 타고 들어가다보면 다음 메소드를 찾을 수 있다.
// TransitionDb will transition the state by applying the current message and
// returning the evm execution result with following fields.
//
// - used gas: total gas used (including gas being refunded)
// - returndata: the returned data from evm
// - concrete execution error: various EVM errors which abort the execution, e.g.
// ErrOutOfGas, ErrExecutionReverted
//
// However if any consensus issue encountered, return the error directly with
// nil evm execution result.
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
// First check this message satisfies all consensus rules before
// applying the message. The rules include these clauses
//
// 1. the nonce of the message caller is correct
// 2. caller has enough balance to cover transaction fee(gaslimit * gasprice)
// 3. the amount of gas required is available in the block
// 4. the purchased gas is enough to cover intrinsic usage
// 5. there is no overflow when calculating intrinsic gas
// 6. caller has enough balance to cover asset transfer for **topmost** call
// Check clauses 1-3, buy gas if everything is correct
if err := st.preCheck(); err != nil {
return nil, err
}
if tracer := st.evm.Config.Tracer; tracer != nil {
tracer.CaptureTxStart(st.initialGas)
defer func() {
tracer.CaptureTxEnd(st.gasRemaining)
}()
}
var (
msg = st.msg
sender = vm.AccountRef(msg.From)
rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time)
contractCreation = msg.To == nil
)
// Check clauses 4-5, subtract intrinsic gas if everything is correct
gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai)
if err != nil {
return nil, err
}
if st.gasRemaining < gas {
return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas)
}
st.gasRemaining -= gas
// Check clause 6
if msg.Value.Sign() > 0 && !st.evm.Context.CanTransfer(st.state, msg.From, msg.Value) {
return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From.Hex())
}
// Check whether the init code size has been exceeded.
if rules.IsShanghai && contractCreation && len(msg.Data) > params.MaxInitCodeSize {
return nil, fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, len(msg.Data), params.MaxInitCodeSize)
}
// Execute the preparatory steps for state transition which includes:
// - prepare accessList(post-berlin)
// - reset transient storage(eip 1153)
st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList)
var (
ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
)
if contractCreation {
ret, _, st.gasRemaining, vmerr = st.evm.Create(sender, msg.Data, st.gasRemaining, msg.Value)
} else {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From, st.state.GetNonce(sender.Address())+1)
ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, msg.Value)
}
if !rules.IsLondon {
// Before EIP-3529: refunds were capped to gasUsed / 2
st.refundGas(params.RefundQuotient)
} else {
// After EIP-3529: refunds are capped to gasUsed / 5
st.refundGas(params.RefundQuotientEIP3529)
}
effectiveTip := msg.GasPrice
if rules.IsLondon {
effectiveTip = cmath.BigMin(msg.GasTipCap, new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee))
}
if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 {
// Skip fee payment when NoBaseFee is set and the fee fields
// are 0. This avoids a negative effectiveTip being applied to
// the coinbase when simulating calls.
} else {
fee := new(big.Int).SetUint64(st.gasUsed())
fee.Mul(fee, effectiveTip)
st.state.AddBalance(st.evm.Context.Coinbase, fee)
}
return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
}, nil
}
드디어 그토록 찾던 실행부 구현체이다.

맨 위에서 부터 살펴보면, 6가지 컨센서스 조건을 모두 충족해야 메세지를 적용한다.

아래 사진을 보면, 컨트랙트 생성인 경우와 아닌 경우로 나뉘어서 다시 실행된다.

그 뒤로도 계속해서 추상화를 통해 각종 예외처리와, 상태 DB에서 현재 상태를 가져오고, 코드를 컴파일 하는 등의 코드들이 나온다.
그렇게 실행한 코드의 결과값 등이 차례로 리턴되고, 실행에 소요된 가스값이 리턴된다.
최초의 ethClient.EstimateGas
로 부터 거의 10개가 넘는 인터페이스를 지나서 도달할 수 있었다.
go-ethereum의 코드를 보면서 느낀 것은 코드 자체의 퀄리티가 높은 것도 있지만, 개인적으로 다음 5가지에 주목이 됐다.
관심사에 따라 인터페이스로 추상화를 잘 해둔 것 같다.
함수 호출에 필요한 인자가 많아지더라도 새로운 구조체를 생성하지 않고 가독성을 해치지 않도록 각 변수들을 주입하도록 했다.
예외처리가 매우 엄격하게 이루어졌다.
Getter, Setter를 적극적으로 활용했다.
함수 내에서 사용할 변수들을
var ()
로 묶어서 선언, 주석을 추가함으로써 가독성을 높이고 앞으로 함수 내에서 진행될 비즈니스 로직을 예상 가능하게끔 한다.
Last updated