記事の内容
自分で勉強している時に空ブロックが大量に作られるのが煩わしかったので、トランザクションが送られてきた時だけブロックを生成するように改造しました。
環境
geth:v1.9.8-stable
golang:1.13.3
gethのclone
今回使用したgethをcloneしてきます。
※当記事執筆時点ではmasterからダウンロードすると「unstable」のバージョンが落ちてきたので、安定版を指定して落としています。
git clone -b v1.9.8 https://github.com/ethereum/go-ethereum.git
コードの修正(miner/worker.go)
該当のコードを修正します。
修正ソースは「miner/worker.go」の547行目から始まる「resultLoop」関数です。
まずは修正前のコードです。
// resultLoop is a standalone goroutine to handle sealing result submitting
// and flush relative data to the database.
func (w *worker) resultLoop() {
for {
select {
case block := <-w.resultCh:
// Short circuit when receiving empty result.
if block == nil {
continue
}
// Short circuit when receiving duplicate result caused by resubmitting.
if w.chain.HasBlock(block.Hash(), block.NumberU64()) {
continue
}
var (
sealhash = w.engine.SealHash(block.Header())
hash = block.Hash()
)
w.pendingMu.RLock()
task, exist := w.pendingTasks[sealhash]
w.pendingMu.RUnlock()
if !exist {
log.Error("Block found but no relative pending task", "number", block.Number(), "sealhash", sealhash, "hash", hash)
continue
}
// Different block could share same sealhash, deep copy here to prevent write-write conflict.
var (
receipts = make([]*types.Receipt, len(task.receipts))
logs []*types.Log
)
for i, receipt := range task.receipts {
// add block location fields
receipt.BlockHash = hash
receipt.BlockNumber = block.Number()
receipt.TransactionIndex = uint(i)
receipts[i] = new(types.Receipt)
*receipts[i] = *receipt
// Update the block hash in all logs since it is now available and not when the
// receipt/log of individual transactions were created.
for _, log := range receipt.Logs {
log.BlockHash = hash
}
logs = append(logs, receipt.Logs...)
}
// Commit block and state to database.
stat, err := w.chain.WriteBlockWithState(block, receipts, task.state)
if err != nil {
log.Error("Failed writing block to chain", "err", err)
continue
}
log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash,
"elapsed", common.PrettyDuration(time.Since(task.createdAt)))
// Broadcast the block and announce chain insertion event
w.mux.Post(core.NewMinedBlockEvent{Block: block})
var events []interface{}
switch stat {
case core.CanonStatTy:
events = append(events, core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
events = append(events, core.ChainHeadEvent{Block: block})
case core.SideStatTy:
events = append(events, core.ChainSideEvent{Block: block})
}
w.chain.PostChainEvents(events, logs)
// Insert the block into the set of pending ones to resultLoop for confirmations
w.unconfirmed.Insert(block.NumberU64(), block.Hash())
case <-w.exitCh:
return
}
}
}
若干自分の理解に自信が無いところはありますが、理解した範囲で解説
この関数は「w.exitCh」となるまで、無限ループをしている訳ですが、この「w」が「miner.start()」を行うと動くゴルーチンの様です。
そのため、「w.exitCh」は「miner.stop()」が実行されるまでは動きません。
ブロックの生成処理自体は「w.chain.WriteBlockWithState(block, receipts, task.state)」の処理を追っていった先で行われています。
そのため、現在のgethの処理ではトランザクションの有無によるブロック生成処理の要否判定を行っていません。
つまり、「w.chain.WriteBlockWithState(block, receipts, task.state)」この処理をトランザクションが無い場合は通らないように改修します。
改修箇所はこの処理の前後もブロック生成の前処理、後処理が実装されていますので、前後の処理もトランザクションが無い場合は通らないようにします。
// resultLoop is a standalone goroutine to handle sealing result submitting
// and flush relative data to the database.
func (w *worker) resultLoop() {
for {
select {
case block := <-w.resultCh:
// Short circuit when receiving empty result.
if block == nil {
continue
}
// Short circuit when receiving duplicate result caused by resubmitting.
if w.chain.HasBlock(block.Hash(), block.NumberU64()) {
continue
}
var (
sealhash = w.engine.SealHash(block.Header())
hash = block.Hash()
)
w.pendingMu.RLock()
task, exist := w.pendingTasks[sealhash]
w.pendingMu.RUnlock()
if !exist {
log.Error("Block found but no relative pending task", "number", block.Number(), "sealhash", sealhash, "hash", hash)
continue
}
// Different block could share same sealhash, deep copy here to prevent write-write conflict.
var (
receipts = make([]*types.Receipt, len(task.receipts))
logs []*types.Log
)
if len(task.receipts) != 0 {
for i, receipt := range task.receipts {
// add block location fields
receipt.BlockHash = hash
receipt.BlockNumber = block.Number()
receipt.TransactionIndex = uint(i)
receipts[i] = new(types.Receipt)
*receipts[i] = *receipt
// Update the block hash in all logs since it is now available and not when the
// receipt/log of individual transactions were created.
for _, log := range receipt.Logs {
log.BlockHash = hash
}
logs = append(logs, receipt.Logs...)
}
// Commit block and state to database.
stat, err := w.chain.WriteBlockWithState(block, receipts, task.state)
if err != nil {
log.Error("Failed writing block to chain", "err", err)
continue
}
log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash,
"elapsed", common.PrettyDuration(time.Since(task.createdAt)))
// Broadcast the block and announce chain insertion event
w.mux.Post(core.NewMinedBlockEvent{Block: block})
var events []interface{}
switch stat {
case core.CanonStatTy:
events = append(events, core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
events = append(events, core.ChainHeadEvent{Block: block})
case core.SideStatTy:
events = append(events, core.ChainSideEvent{Block: block})
}
w.chain.PostChainEvents(events, logs)
// Insert the block into the set of pending ones to resultLoop for confirmations
w.unconfirmed.Insert(block.NumberU64(), block.Hash())
}
case <-w.exitCh:
return
}
}
}
修正箇所は「for i, receipt := range task.receipts {」の前に「if len(task.receipts) != 0 {」を追加しています。
この条件判定で処理待ちトランザクションが0ではない場合という条件になります。
動作検証
まずはビルドです。
make geth
次にgethを起動します。
geth --networkid 1111 --nodiscover --datadir "/home/eth_private_net" console 2>> /home/eth_private_net/geth_err.log
※別のコンソールでログを監視しておきます。
ここからは「Geth JavaScript console」のコマンドです。
> miner.start()
null
ログ監視コンソールでは以下のログ以降は何も出力されません。
普通ならブロック生成のログがたくさん出力されます。
INFO [02-15|20:27:04.047] Updated mining threads threads=1
INFO [02-15|20:27:04.047] Transaction pool price threshold updated price=1000000000
トランザクションを送信してみます。
> personal.unlockAccount(eth.accounts[0])
Unlock account 0xb17853e0a116c46875132625e564d55bd85a433e
Password:
true
> eth.sendTransaction({from:eth.accounts[0], to:eth.accounts[1], value:web3.toWei("1", "ether")})
"0x1a46b274639a6d2d4a36a562313cc3cafb9f1ba80591aaf9b196090370855221"
> eth.getTransaction("0x1a46b274639a6d2d4a36a562313cc3cafb9f1ba80591aaf9b196090370855221")
{
blockHash: "0x5f7caf6cd09b0020deb5a16f501d442f0f47c293685d5ed6908935cd32fca62d",
blockNumber: 280,
from: "0xb17853e0a116c46875132625e564d55bd85a433e",
gas: 21000,
gasPrice: 1000000000,
hash: "0x1a46b274639a6d2d4a36a562313cc3cafb9f1ba80591aaf9b196090370855221",
input: "0x",
nonce: 3,
r: "0x6f1e5ba7be3ccb695b2a33ac74812609cb489f5d3910266badeb192d1ef24042",
s: "0x2913b8a2151b05835a879f1b74df2b0c3279d70b83c58146fdadd9b58f2e811",
to: "0xee72c44a047856be6d4a095b0920de53e030f58d",
transactionIndex: 0,
v: "0x8d2",
value: 1000000000000000000
}
トランザクションの送信が成功し、ブロックに取り込まれるところまで確認できました。
ログも見てみます。
INFO [02-15|20:30:49.713] Setting new local account address=0xB17853E0a116C46875132625E564D55bD85a433E
INFO [02-15|20:30:49.713] Submitted transaction fullhash=0x1a46b274639a6d2d4a36a562313cc3cafb9f1ba80591aaf9b196090370855221 recipient=0xee72C44a047856BE6d4A095b0920De53e030f58D
INFO [02-15|20:30:52.117] Commit new mining work number=280 sealhash=e74429…eaec65 uncles=0 txs=1 gas=21000 fees=2.1e-05 elapsed=307.299µs
INFO [02-15|20:31:05.219] Successfully sealed new block number=280 sealhash=e74429…eaec65 hash=5f7caf…fca62d elapsed=13.102s
INFO [02-15|20:31:05.220] 🔨 mined potential block number=280 hash=5f7caf…fca62d
トランザクションの送信ログとブロックの生成ログのみ出力され、このログ以降のログは出力されていません。
凄く簡単にですが、動作確認は完了です。
どうでもいいですが、コンソールだと「・・・」って出てるところをそのまま貼り付けると「🔨」になるんですね・・。
感想
今回はトランザクションの有無でブロックを生成するかどうか判定を加えました。
トランザクションがある場合に一度のブロック生成で処理するトランザクション数などもこの辺りの処理に追加してあげるとパフォーマンスが安定したりするのかもなんて思いました。