Gas price != zero

2025. 3. 18. 11:23Blockchain/Ethereum

728x90
반응형

테스트코드를 작성 하고 있을 때 Geth node가 백그라운드에서 돌아가고 컨트랙트 배포 등 트랜잭션 처리를 도와야할 필요가 있어서 Geth로 노드를 돌아가게 하는 개발을 진행중이었다. 다른 코드들에서는 계속 예전 mining 하는 버전의 Geth를 쓰다가 이제는 beacon으로 컨센서스 돌리는 버전으로 바꿔야겠다는 생각이 들어 업데이트를 수행했다. (기존에는 EthHash 또는 Clique로 컨센서스 알고리즘을 사용했었다.)

백엔드 노드를 변경을 하고, 컨트랙트 배포 코드는 그대로 사용했더니 tx처리가 안되는 부분이 있어서 확인하고 기록을 남겨둔다.

설정 변경

Geth 버전은 1.15.0이며, genesis의 configparams.AllDevChainProtocolChanges를 사용했다. 이 ChaingConfig는 아래와 같다.


AllDevChainProtocolChanges = &ChainConfig{
    ChainID:                 big.NewInt(1337),
    HomesteadBlock:          big.NewInt(0),
    EIP150Block:             big.NewInt(0),
    EIP155Block:             big.NewInt(0),
    EIP158Block:             big.NewInt(0),
    ByzantiumBlock:          big.NewInt(0),
    ConstantinopleBlock:     big.NewInt(0),
    PetersburgBlock:         big.NewInt(0),
    IstanbulBlock:           big.NewInt(0),
    MuirGlacierBlock:        big.NewInt(0),
    BerlinBlock:             big.NewInt(0),
    LondonBlock:             big.NewInt(0),
    ArrowGlacierBlock:       big.NewInt(0),
    GrayGlacierBlock:        big.NewInt(0),
    ShanghaiTime:            newUint64(0),
    CancunTime:              newUint64(0),
    TerminalTotalDifficulty: big.NewInt(0),
    PragueTime:              newUint64(0),
    BlobScheduleConfig: &BlobScheduleConfig{
        Cancun: DefaultCancunBlobConfig,
        Prague: DefaultPragueBlobConfig,
    },
}

오사카 이전 프라하의 fork 버전이다. 여기서 chain ID만 변경하였다.

Genesis는 Geth의 core/genesis.goDeveloperGenesisBlock()을 참고해서 Alloc에 내가 사용할 private key의 주소를 사용하고, BaseFee를 0으로 설정했다.


config := params.AllDevChainProtocolChanges
config.ChainID = big.NewInt(chainId)

genesis := &core.Genesis{
    Config: config,
    Alloc: map[common.Address]types.Account{
        testAddr1: {Balance: TestBalance},
        testAddr2: {Balance: TestBalance},

        common.BytesToAddress([]byte{1}): {Balance: big.NewInt(1)}, // ECRecover
        common.BytesToAddress([]byte{2}): {Balance: big.NewInt(1)}, // SHA256
        common.BytesToAddress([]byte{3}): {Balance: big.NewInt(1)}, // RIPEMD
        common.BytesToAddress([]byte{4}): {Balance: big.NewInt(1)}, // Identity
        common.BytesToAddress([]byte{5}): {Balance: big.NewInt(1)}, // ModExp
        common.BytesToAddress([]byte{6}): {Balance: big.NewInt(1)}, // ECAdd
        common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul
        common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
        common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b
        // Pre-deploy system contracts
        params.BeaconRootsAddress:        {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0},
        params.HistoryStorageAddress:     {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},
        params.WithdrawalQueueAddress:    {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0},
        params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0},
    },
    BaseFee:    big.NewInt(0),
    Difficulty: big.NewInt(0),
}

Node config는 테스트를 위한 설정을 했다.


nodeConfig := node.Config{
    HTTPHost:              httpHost,
    HTTPPort:              httpPortInt,
    HTTPCors:              []string{"*"},
    HTTPVirtualHosts:      []string{"*"},
    InsecureUnlockAllowed: true,
    P2P: p2p.Config{
        // ListenAddr:  "127.0.0.1:0",
        NoDiscovery: true,
        MaxPeers:    0,
    },
    KeyStoreDir: MakeKeyStorePathWithTestTarget(baseDir, testTargetKeyStore),
}

stack, err := node.New(&nodeConfig)
if err != nil {
    return nil, nil, nil, fmt.Errorf("can't create new node: %v", err)
}

컨센서스는 geth --dev에 사용하는 simulated beacon을 사용하고, lifecycle을 추가했다.


// Create Ethereum Service
config := &ethconfig.Config{
    Genesis:        genesis,
    SyncMode:       ethconfig.FullSync,
    TrieTimeout:    time.Minute,
    TrieDirtyCache: 256,
    TrieCleanCache: 256,
}

backend, ethservice := utils.RegisterEthService(stack, config)
utils.RegisterFilterAPI(stack, backend, config)

simBeacon, err := catalyst.NewSimulatedBeacon(1, ethservice)
if err != nil {
    return nil, nil, nil, fmt.Errorf("can't create simulated beacon: %v", err)
}
catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon)
stack.RegisterLifecycle(simBeacon)
ethservice.SetSynced()

테스트 수행

테스트 조건이 gas를 소모시키면 안됐었다. 그렇기 때문에 이전에 했던대로 TransactionOpts 설정 시 gas limit은 estimate로, gas price는 0으로 두었다.


gasLimit := uint64(0)
gasPrice := big.NewInt(0)

ecdsaPrivateKey, err := crypto.ToECDSA(privKeyBytes)
if err != nil {
    return nil, err
}

nonceBigInt := big.NewInt(int64(nonce))
chainIdBigInt := big.NewInt(chainId)
contractAuth, err := bind.NewKeyedTransactorWithChainID(ecdsaPrivateKey, chainIdBigInt)
if err != nil {
    return nil, err
}

contractAuth.Nonce = nonceBigInt
contractAuth.Value = big.NewInt(0)
contractAuth.GasLimit = gasLimit
contractAuth.GasPrice = gasPrice

위 방식은 이전 테스트 시에는 이상없이 수행되었다.

그런데, tx를 전송하고 receipt를 기다리니 응답이 없었다. 블록은 생성되고 있었어서 확인해보니 pending이 걸리고 있었다. 블록은 생성되고 있었으니 블록 생성 노드가 tx를 포함을 안시킨 것 같았다. Gas price 문제일까 싶어서 price 값을 suggest로 변경시켰다.


gasLimit := uint64(0)
gasPrice := big.NewInt(0)
gasPrice = nil

그러니까 1,000,000 gas price를 알아서 넣더니 tx가 처리되고 receipt도 정상적으로 확인되었다. 테스트의 조건이 gas 소모가 없는 것이라 gas price를 0으로 만들어서 처리가 필요했다.

eth/ethconfig/config.goConfig에는 GPO라는 gas price oracle 파라미터가 있다. 해당 파라미터를 확인해보니 이전에 gas가 0이 아닌 값으로 tx를 처리할 때는 보지 못했던 필드가 있었다.


// eth/gasprice/gasprice.go

var (
    DefaultMaxPrice    = big.NewInt(500 * params.GWei)
    DefaultIgnorePrice = big.NewInt(2 * params.Wei)
)

type Config struct {
    Blocks           int
    Percentile       int
    MaxHeaderHistory uint64
    MaxBlockHistory  uint64
    MaxPrice         *big.Int `toml:",omitempty"`
    IgnorePrice      *big.Int `toml:",omitempty"`
}

바로 IgnorePrice라는 필드가 있었다. 언제 생겼지? 궁금해서 확인해보니 관련된 파라미터는 꽤 오래전부터 있더라.. (commit, PR)

위 PR을 요약하면, 갑작스럽게 eth_gasPrice가 낮아지는 경우가 발생했을 때를 방지하기 위하여 너무 낮은 gas price가 인입되면 무시하도록 설계가 된것이다.

테스트를 위해 gas price를 0으로 해야 하는 입장이었기 때문에 IgnorePrice 값을 조절하기 위해 Geth를 확인해 봤다.

이 GPO는 아래와 같은 형식으로 생성된다.


eth/gasprice/gasprice.go

type Config struct {
    Blocks           int
    Percentile       int
    MaxHeaderHistory uint64
    MaxBlockHistory  uint64
    MaxPrice         *big.Int `toml:",omitempty"`
    IgnorePrice      *big.Int `toml:",omitempty"`
}

// Oracle recommends gas prices based on the content of recent
// blocks. Suitable for both light and full clients.
type Oracle struct {
    backend     OracleBackend
    lastHead    common.Hash
    lastPrice   *big.Int
    maxPrice    *big.Int
    ignorePrice *big.Int
    cacheLock   sync.RWMutex
    fetchLock   sync.Mutex

    checkBlocks, percentile           int
    maxHeaderHistory, maxBlockHistory uint64

    historyCache *lru.Cache[cacheKey, processedFees]
}

// NewOracle returns a new gasprice oracle which can recommend suitable
// gasprice for newly created transaction.
func NewOracle(backend OracleBackend, params Config, startPrice *big.Int) *Oracle {
    blocks := params.Blocks
    if blocks < 1 {
        blocks = 1
        log.Warn("Sanitizing invalid gasprice oracle sample blocks", "provided", params.Blocks, "updated", blocks)
    }
    percent := params.Percentile
    if percent < 0 {
        percent = 0
        log.Warn("Sanitizing invalid gasprice oracle sample percentile", "provided", params.Percentile, "updated", percent)
    } else if percent > 100 {
        percent = 100
        log.Warn("Sanitizing invalid gasprice oracle sample percentile", "provided", params.Percentile, "updated", percent)
    }
    maxPrice := params.MaxPrice
    if maxPrice == nil || maxPrice.Int64() <= 0 {
        maxPrice = DefaultMaxPrice
        log.Warn("Sanitizing invalid gasprice oracle price cap", "provided", params.MaxPrice, "updated", maxPrice)
    }
    ignorePrice := params.IgnorePrice
    if ignorePrice == nil || ignorePrice.Int64() <= 0 {
        ignorePrice = DefaultIgnorePrice
        log.Warn("Sanitizing invalid gasprice oracle ignore price", "provided", params.IgnorePrice, "updated", ignorePrice)
    } else if ignorePrice.Int64() > 0 {
        log.Info("Gasprice oracle is ignoring threshold set", "threshold", ignorePrice)
    }

    ...

}

일단 Oracle 구조체 내의 ignorePrice는 internal이라 건드릴 수 없고, NewOracle() 에서는 ignorePrice이 nil값이나 0보다 작으면 default 값(2wei)으로 설정하는 것을 확인했다.

NewOracle()은 node에 RegisterEthService()를 통해 ethereum backend가 생성될 때 사용된다.


backend, ethservice := utils.RegisterEthService(stack, config)

cmd/utils/flags.go

// RegisterEthService adds an Ethereum client to the stack.
// The second return value is the full node instance.
func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (*eth.EthAPIBackend, *eth.Ethereum) {
    backend, err := eth.New(stack, cfg)
    if err != nil {
        Fatalf("Failed to register the Ethereum service: %v", err)
    }
    stack.RegisterAPIs(tracers.APIs(backend.APIBackend))
    return backend.APIBackend, backend
}

eth/backend.go

// New creates a new Ethereum object (including the initialisation of the common Ethereum object),
// whose lifecycle will be managed by the provided node.
func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {

    ...

    eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, config.GPO, config.Miner.GasPrice)

    // Start the RPC service
    eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID)

    // Register the backend on the node
    stack.RegisterAPIs(eth.APIs())
    stack.RegisterProtocols(eth.Protocols())
    stack.RegisterLifecycle(eth)

    // Successful startup; push a marker and check previous unclean shutdowns.
    eth.shutdownTracker.MarkStartup()

    return eth, nil
}

eth.APIbackend.gpo 에서 NewOracle() 값이 등록된다. 보이듯이 APIBackend 에서의 gpo도 internal이라 밖에서 핸들할 수 없었다.

결론

Gas price를 0으로 만들어서 tx를 처리하는 방법을 아직 못찾았다.... 이전에는 되었다가 버전 업데이트 시켜서 안되니 시간을 좀 잡아먹었다. IgnorePrice를 다룰 수 있는 방법을 확인하거나, 다른 방안이 있는지 찾아봐야겠다.

728x90
반응형

'Blockchain > Ethereum' 카테고리의 다른 글

ENS(Ethereum Name Service)  (0) 2023.06.15
Ethereum의 PoS 전환을 반기며  (0) 2023.06.15
Ethereum Gas  (0) 2023.06.15
Ethereum Bootnode 기능 확인  (0) 2023.06.14
Ethereum [5] (Smart contract)  (0) 2023.06.13