2025. 3. 18. 11:23ㆍBlockchain/Ethereum
테스트코드를 작성 하고 있을 때 Geth node가 백그라운드에서 돌아가고 컨트랙트 배포 등 트랜잭션 처리를 도와야할 필요가 있어서 Geth로 노드를 돌아가게 하는 개발을 진행중이었다. 다른 코드들에서는 계속 예전 mining 하는 버전의 Geth를 쓰다가 이제는 beacon으로 컨센서스 돌리는 버전으로 바꿔야겠다는 생각이 들어 업데이트를 수행했다. (기존에는 EthHash 또는 Clique로 컨센서스 알고리즘을 사용했었다.)
백엔드 노드를 변경을 하고, 컨트랙트 배포 코드는 그대로 사용했더니 tx처리가 안되는 부분이 있어서 확인하고 기록을 남겨둔다.
설정 변경
Geth 버전은 1.15.0이며, genesis의 config
는 params.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.go
의 DeveloperGenesisBlock()
을 참고해서 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 := ðconfig.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.go
의 Config
에는 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
를 다룰 수 있는 방법을 확인하거나, 다른 방안이 있는지 찾아봐야겠다.
'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 |