geth 소스 읽기 Part2 Day02
이 글은 자바 개발자의 go-ethereum(geth 클라이언트) 소스 읽기 시리즈 Part2 연재 중 두 번째 글입니다.
- Day 01: 마이닝 과정 실습
- (본 글) Day 02: 마이닝 시작 과정 코드 읽기 (1)
- Day 03: 마이닝 시작 과정 코드 읽기 (2)
- TBD
전체 연재 목록은 아래 페이지에서 확인해 주세요.
http://www.notforme.kr/block-chain/geth-code-reading
Part2 연재의 대상 독자 및 목표
이 글은 독자 분들이 적어도 Java와 같은 OOP 계열의 언어로 프로그래밍 경험이 있다는 것을 가정합니다. 또한 계정, 채굴 등 블록체인과 이더리움과 관련된 기초적인 개념과 이더리움 백서나 황서의 내용을 간단하게 알고 있다고 가정합니다. geth
코드를 읽으면서 백서 및 황서에서 정의된 스펙이 어떻게 구현되었는지를 확인하는 것이 목적입니다.
더불어 이 연재는 다음 3가지 목적을 염두하고 쓴 것입니다.
- 새로운 언어(
Go
)를 오픈소스 코드를 읽으며 배운다. - 오픈소스를 읽으며 코드리딩 능력을 배양한다.
- 블록체인의 기술을 직접 코드를 통해서 익힌다.
지난 Part1에서는 총 6번의 글을 통해서 geth
구조와 실행방법에 대해서 살펴봤습니다. Part2에서는 좀 더 구체적으로 이더리움의 핵심 기능들이 어떻게 구현되었는지 Part1에서와 마찬가지로 코드를 통해서 알아보려고 합니다
다루는 내용
지난 글에서는 로컬에 하나의 geth
노드를 띄운 후 마이너를 실행하고 블록이 생성되는 과정을 직접 실습했습니다. 이 과정에서 발생한 이더리움을 다른 신규계정으로 전송하는 것 또한 실습했습니다.
오늘은 이더리움을 마이닝하는 과정을 실습이 아닌 코드로 따라가 보려고 합니다. 물론 마이닝 과정 가운데 일부 로직은 좀 더 세부적으로 살펴볼 것도 있습니다. 예를 들면 PoW
를 실행하는 ethash
나 블록상태를 저장하는 로직 등이 그렇습니다. 오늘은 이러한 세부 로직을 모두 파악하기 보다는 코드 레벨에서 지난 번 실습한 마이닝 동작과정을 큰 흐름으로 살펴보는 것이 목적입니다.
한 번에 많은 내용을 살펴보고 글로 담기가 쉽지 않을 것 같습니다. 앞으로도 되도록 너무 길지 않게 분석할 코드의 내용을 논리적으로 잘개 쪼개서 포스팅을 하려고 합니다.
마지막으로 오늘부터 본격적으로 시작하는 Part2에서 읽는 코드의 커밋 해시 0fe479e입니다.
마이닝 시작 포인트
첫 출발은 마이닝을 실행하는 지점에서 출발하려고 합니다. geth
에서 마이너를 실행시키는 방법은 2가지가 있습니다. 하나는 geth
실행 시에 --mine
옵션을 주는 것이고 다른 하나는 지난 시간에 했던 방법처럼 web3
를 사용하여 rpc
로 실행하는 방법입니다.
마이너를 실행시키는 2가지 방법
geth
실행 시 mine 옵션 주기web3
를 활용하여miner.start()
명령을 콘솔에 입력하기
2가지 방법은 호출 방식만 다를 뿐 결국 Ethereum
객체의 필드인 miner
의 Start
함수를 호출합니다. 지난 글 마지막에 콘솔에서 miner.start()
를 호출할 때 실행되는 함수에 디버깅 포인트를 걸어서 GoLand
에서 실행된 geth
의 동작 흐름을 멈추게 했었습니다. 이 지점이 바로 miner
의 Start
함수였습니다.
그럼 miner
객체의 코드에서부터 시작해야 겠네요!
마이너의 생성 로직
miner
관련 코드를 읽기에 앞서 miner
객체의 생성 로직을 살펴보고 넘어갈까 합니다. Part1 마지막 글에서 geth
의 노드 실행 과정 중에서 Ethereum
객체의 생성과정을 이미 살펴봤는데 이 때 miner
생성된 것을 기억하시나요? 복습 차원에서 코드를 다시 가져와 봅니다.
eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)
eth.miner.SetExtra(makeExtraData(config.ExtraData))
지난 글에서는 miner.New()
가 호출 된다는 것까지 확인을 했었습니다. 오늘은 New
함수 코드를 들여다 보겠습니다.
func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine) *Miner {
miner := &Miner{
eth: eth,
mux: mux,
engine: engine,
worker: newWorker(config, engine, common.Address{}, eth, mux),
canStart: 1,
}
miner.Register(NewCpuAgent(eth.BlockChain(), engine))
go miner.update()
return miner
}
함수에서 인자로 넘겨 받은 내용은 다음과 같습니다.
eth
객체: eth의 속성인miner
에게 스스로의 참조를 전달하고 있네요.config
: 타입이름을볼 때 블록체인 관련 설정mux
: 이벤트 처리를 할 때 쓸 것으로 추정engine
: 마이닝을 위해서는 합의 과정이 관련이 있겠지요?
이렇게 넘겨받은 인자로 Miner
타입의 객체를 생성합니다. 이 때 worker
라는 속성은 newWorker
함수로 생성이 되는 것을 볼 수 있습니다. 그리고 나서 Register
함수를 NewCpuAgent
함수의 반환 값으로 호출하고 있네요. Register
함수는 간단합니다.
func (self *Miner) Register(agent Agent) {
if self.Mining() {
agent.Start()
}
self.worker.register(agent)
}
인자로 Agent
를 받네요. 앞에서 NewCpuAgent
가 Agent
타입의 객체를 반환한다는 것을 유추할 수 있겠네요. 코드의 로직은 간단합니다. 이미 마이닝 중이면 agent
의 Start
를 실행하고, 아니면 worker
에 인자로 받은 agent
를 등록합니다. 아하! 실제 등록되는 위치는 miner
가 아니라 worker
군요!
그럼 이제worker
를 생성한 newWorker
함수를 봐야할 것 같습니다.
func newWorker(config *params.ChainConfig, engine consensus.Engine, coinbase common.Address, eth Backend, mux *event.TypeMux) *worker {
worker := &worker{
config: config,
engine: engine,
eth: eth,
mux: mux,
txsCh: make(chan core.NewTxsEvent, txChanSize),
chainHeadCh: make(chan core.ChainHeadEvent, chainHeadChanSize),
chainSideCh: make(chan core.ChainSideEvent, chainSideChanSize),
chainDb: eth.ChainDb(),
recv: make(chan *Result, resultQueueSize),
chain: eth.BlockChain(),
proc: eth.BlockChain().Validator(),
possibleUncles: make(map[common.Hash]*types.Block),
coinbase: coinbase,
agents: make(map[Agent]struct{}),
unconfirmed: newUnconfirmedBlocks(eth.BlockChain(), miningLogAtDepth),
}
// Subscribe NewTxsEvent for tx pool
worker.txsSub = eth.TxPool().SubscribeNewTxsEvent(worker.txsCh)
// Subscribe events for blockchain
worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh)
worker.chainSideSub = eth.BlockChain().SubscribeChainSideEvent(worker.chainSideCh)
go worker.update()
go worker.wait()
worker.commitNewWork()
return worker
}
worker
가 많은 속성을 갖고 있네요! 이쯤 되면 miner
객체는 마이닝의 실행/종료를 위한 컨테이너 정도의 역할이고 로직의 많은 부분은 worker
가 담당한다는 것을 짐작할 수 있습니다. 속성 중에 자세히 들여다 보면 agents
가 맵으로 선언되어 있네요. 앞에서 Register
할 때 worker
에 했던 부분이 여기서 연결되네요.
객체를 생성한 후에는 여러 이벤트를 구독하기 위한 채널 설정이 있습니다. 친절하게 주석으로 설명해주는 것이 보이시죠? 트랜잭션 Pool에 추가된 새 트랜잭션을 구독하는 이벤트와 블록체인의 변경 상태를 구독하는 채널을 셋팅합니다. 이어서 go
문법을 사용하여 worker.update()
와 worker.wait()
루틴을 각각 실행하고 commitNewWork
함수를 실행한 뒤에 생성된 worker
를 반환합니다.
계속해서 앞으로 코드에서 Work라는 단어가 나올텐데요. 체인에 포함되지 않은 블록을 마이닝하여 포함시키는 과정. 즉 우리가 개념적으로 하는 그 작업 증명이 코드레벨에서는 Work, Worker와 연관된 것이라 보면 될 것 같습니다.
worker
를 반환하기 전에 고루틴으로 호출한 update
, wait
은 for 문을 계속 돌면서 특정 이벤트에 따라 실행하는 로직입니다. 우선은 이 정도까지만 확인하고 이 부분은 이후에 다시 확인하는 것으로 하겠습니다.
마지막 worker.commitNewWork()
이 실제 작업증명을 위해 컨펌되지 않은 블록을 포함시키는 부분인데요. 이 함수는 앞으로 보게될 마이너를 시작하는 함수에서 다시 호출될 겁니다. 여기서는 로직상 호출은 되지만 내부에서 마이닝 실행중이 아니라 실제 블럭을 커밋하지 않습니다. newWorker
에서 왜 이 함수를 호출하는지는 아직 정확하게는 모르겠네요;...
자 다시 miner
의 생성 로직인 New
함수로 돌아오면 miner
객체를 생성한 후 이를 반환하기 직전에 고루틴으로 다음과 같은 함수를 실행합니다.
go miner.update()
이 함수는 마이닝 하기 전에 기존에 네트워크에 있는 블록체인 정보를 받아와서 싱크하는 로직입니다. 들어가 보면 전체 블록 정보를 받기 전까지는 마이닝을 하지 않고 모두 받고 나서 시작하는 로직이 구현되어 있습니다. 이 함수의 구현은 궁금한 분들을 위해서 링크로 대체합니다.
마이너의 시작
앞에서 마이너를 시작하는 방법은 geth
실행 시 옵션으로 실행하는 방법과 콘솔에서 명령을 주는 방법이 있다고 했습니다. 간단히 각 코드 부분이 어디인지 확인해 보겠습니다.
geth 실행 시 마이너 시작부분
이 코드는 geth
패키지 main.go
파일의 startNode
함수에 있습니다. Part1에서 utils.StartNode
함수만 설명하고 이후 로직은 자세하게 다루지 않았는데요. 이 함수의 마지막 부분을 보면 다음과 같은 로직이 있습니다.
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) {
// 마이닝을 실행하기 위한 조건 확인 하는 부분은 생략....
// Set the gas price to the limits from the CLI and start mining
ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
if err := ethereum.StartMining(true); err != nil {
utils.Fatalf("Failed to start mining: %v", err)
}
}
보시는 바와 같이 if문의 조건으로 utils.MiningEnabledFlag.Name
을 볼 수 있습니다. 이 플래그가 바로 터미널에서 옵션으로 주는 --mine
입니다. 마이닝 옵션이 있으면 아래 ethereum.StartMining
함수를 호출하는 것을 확인할 수 있습니다. 이 함수가 실제 마이닝을 시작하는 함수가 됩니다. 그럼 콘솔에서 rpc
호출은 어떻게 되는지 보겠습니다.
rpc로 실행 시 마이너 시작부분
geth
는 rpc
엔드포인드를 관리하는 로직이 rpc
패키지에 있고 실제 각 API는 관련 패키지의 api.go
에 선언되어 있습니다. 하지만 마이닝을 시작하는 엔드포인트는 miner
패키지는 아닙니다. 실제로는 eth
패키지의 api.go
에 있습니다. Ethereum
객체가 하나의 컨테이너로서 관련된 로직을 위임하고 있는 구조라 실제 API엔드포인트가 여기 있는 것 같습니다. 코드를 그럼 한 번 볼까요?
// Start the miner with the given number of threads. If threads is nil the number
// of workers started is equal to the number of logical CPUs that are usable by
// this process. If mining is already running, this method adjust the number of
// threads allowed to use.
func (api *PrivateMinerAPI) Start(threads *int) error {
// Set the number of threads if the seal engine supports it
if threads == nil {
threads = new(int)
} else if *threads == 0 {
*threads = -1 // Disable the miner from within
}
type threaded interface {
SetThreads(threads int)
}
if th, ok := api.e.engine.(threaded); ok {
log.Info("Updated mining threads", "threads", *threads)
th.SetThreads(*threads)
}
// Start the miner and return
if !api.e.IsMining() {
// Propagate the initial price point to the transaction pool
api.e.lock.RLock()
price := api.e.gasPrice
api.e.lock.RUnlock()
api.e.txPool.SetGasPrice(price)
return api.e.StartMining(true)
}
return nil
}
친절하게 주석에 API 설명이 있네요. 함수 초반 몇가지 로직을 일단 지나치고 보면 마지막 return
부분에 StartMining
함수가 보이네요. 바로 이전에 geth
실행 시 호출 했던 것과 동일한 함수입니다!
StartMining 함수
이 함수는 마이너를 실행을 감싸고 있습니다. 코드는 다음과 같습니다.
func (s *Ethereum) StartMining(local bool) error {
eb, err := s.Etherbase()
if err != nil {
log.Error("Cannot start mining without etherbase", "err", err)
return fmt.Errorf("etherbase missing: %v", err)
}
if clique, ok := s.engine.(*clique.Clique); ok {
wallet, err := s.accountManager.Find(accounts.Account{Address: eb})
if wallet == nil || err != nil {
log.Error("Etherbase account unavailable locally", "err", err)
return fmt.Errorf("signer missing: %v", err)
}
clique.Authorize(eb, wallet.SignHash)
}
if local {
// If local (CPU) mining is started, we can disable the transaction rejection
// mechanism introduced to speed sync times. CPU mining on mainnet is ludicrous
// so none will ever hit this path, whereas marking sync done on CPU mining
// will ensure that private networks work in single miner mode too.
atomic.StoreUint32(&s.protocolManager.acceptTxs, 1)
}
go s.miner.Start(eb)
return nil
}
첫 줄의 코드는 마이닝시 생성된 이더리움을 전달할 코인베이스 계정을 가져오는 함수입니다. 함수 구현을 보면 특별한 예외가 없는한 geth
가 실행되는 곳의 지갑 첫번째 계정을 반환합니다. 지난 시간 실습 때 아무런 계정 생성없이 miner.start()
하면 에러가 반환되어 계정 생성 했던 부분이 바로 이 코드와 관련이 있습니다.
마이닝을 실행하는 실제 로직은 함수의 제일 마지막 return
문 위에 있는 고루틴 s.miner.Start
함수입니다. 코인베이스를 인자로 전달하는 이 함수가 마이너를 시작하는 지점입니다.
Miner의 Start 함수
이 함수가 바로 지난 시간 디버깅 포인트를 걸었던 부분입니다. 전체 코드를 한 번 살펴볼까요?
func (self *Miner) Start(coinbase common.Address) {
atomic.StoreInt32(&self.shouldStart, 1)
self.SetEtherbase(coinbase)
if atomic.LoadInt32(&self.canStart) == 0 {
log.Info("Network syncing, will start miner afterwards")
return
}
atomic.StoreInt32(&self.mining, 1)
log.Info("Starting mining operation")
self.worker.start()
self.worker.commitNewWork()
}
인자로 받은 코인 베이스를 마이너 객체에 저장하고 마이닝 과정을 시작해도 되는지 canStart
변수로 확인하고 있습니다. 이 변수는 최초 생성 시에는 1
로 셋팅되어 있기 때문에 위 조건으로 실패할 일은 없습니다. 다만 앞에서 마이너 객체를 생성할 때 기존의 블록체인을 받아오는 함수 miner.update()
에서 값이 변경될 수도 있습니다. 이 경우는 update
할 내용이 있어서 모든 데이터를 받아오면 그때 canStart
를 1
로 변경한 후 Start
를 실행합니다.
다음으로 시작할 준비가 되면 mining
변수에 1
을 셋팅합니다. 이후에도 코드 여기저기에서 self.mining
의 상태를 보고 마이닝 중인지 확인합니다. 이어서 start
함수와 commitNewWork
함수를 각각 호출합니다.
worker.start 함수
앞에서 마이너 객체를 생성할 때 Register
함수에서도 봤지만 마이너 객체는 실제 마이닝 작업을 하는 객체가 아닙니다. 이 함수의 코드를 보면 실제 실행은 agent
임을 확인할 수 있습니다.
func (self *worker) start() {
self.mu.Lock()
defer self.mu.Unlock()
atomic.StoreInt32(&self.mining, 1)
// spin up agents
for agent := range self.agents {
agent.Start()
}
}
최종적으로 위 코드를 통해서 마이닝 작업을 수행하는 함수는 agent
라는 것을 알 수 있습니다. 그렇다면 self.agents
에는 어떤 agent
가 들어있을까요? 우선 miner.New()
함수에서 CpuAgent
가 등록했다는 것을 떠올려보면 최소한 하나의 agent
는 들어 있다는 것을 보장할 수 있습니다. 추가로 rpc
로 마이닝을 실행할 경우에도 RemoteAgent
타입이 추가됩니다. 따라서 최대 2개의 agent
가 실행됩니다.
depth가 깊어지네요… CpuAgent
와 RemoteAgent
의 각각 Start
함수를 살펴보기 전에 여기서 한번 정리를 해볼까요.
geth
실행 시점이든,rpc
호출이든 =>Ethereum
객체의StartMining
함수를 호출StartMining
함수는 =>Miner
객체의Start
함수 호출Miner
객체의Start
함수는 =>Worker
객체의Start
함수 호출Worker
객체의Start
함수는 => 각Agent
의Start
함수 호출
이제 각 Agent
의 함수를 살펴봅시다. 먼저 RemoteAgent
함수의 코드부터 보겠습니다.
func (a *RemoteAgent) Start() {
if !atomic.CompareAndSwapInt32(&a.running, 0, 1) {
return
}
a.quitCh = make(chan struct{})
a.workCh = make(chan *Work, 1)
go a.loop(a.workCh, a.quitCh)
}
2개의 채널을 생성한 후 이 값을 loop
함수에 인자로 전달하면서 고루틴으로 실행하고 있네요.loop
함수의 코드를 보면 인자로 전달 받은 채널에 값이 있을 때에 따른 처리로직만 있을 뿐 실제 마이닝 관련 코드는 없습니다. 함수에 선언된 주석을 보면 모니터링 관련 코드라고 하는 군요. 따라서 loop
의 코드를 굳이 자세히 들여다 보지 않겠습니다.
이제 우리의 관심사는 CpuAgent
입니다. 이 함수의 코드는 아주 간단합니다.
func (self *CpuAgent) Start() {
if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1) {
return // agent already started
}
go self.update()
}
이 함수는 현재 마이닝이 실행중인지 확인 후 고루틴으로 update
함수를 실행하고 있네요. 아… 마이닝 로직이 한 depth 더 들어가는 군요. update
함수까지 코드를 들어가 봅시다.
func (self *CpuAgent) update() {
out:
for {
select {
case work := <-self.workCh:
self.mu.Lock()
if self.quitCurrentOp != nil {
close(self.quitCurrentOp)
}
self.quitCurrentOp = make(chan struct{})
go self.mine(work, self.quitCurrentOp)
self.mu.Unlock()
case <-self.stop:
self.mu.Lock()
if self.quitCurrentOp != nil {
close(self.quitCurrentOp)
self.quitCurrentOp = nil
}
self.mu.Unlock()
break out
}
}
}
보이시나요? 코드 한 가운데 고루틴으로 mine
함수를 호출하는 것이!!! 자 이제 이 함수가 어떻게 mine
을 호출하는지를 알아야겠지요? 이 코드는 사실 golang
의 채널과 goto
문의 독특한 문법으로 돌고 있는 코드입니다. 설명하자면 이렇습니다. 우선 for
문은 계속 돕니다. 이 for
문 안에서 self.workCh
채널과 self.stop
채널에 데이터가 들어오기를 기다립니다. 이 중에 workCh
을 통해서 블록체인에 포함할 작업이 work
로 들어올때 바로 self.mine()
함수까지 실행이 되는 것입니다.
반면 stop
채널로 데이터가 들어오면 break out
으로 인해 for
문을 탈출합니다. 기존의 C 언어를 공부한 분이라면 goto
문이 가리키는 곳이 for문 위라서 다시 for
문이 실행되는 것으로 오해할 수 있는데요.(제가 그랬습니다.) golang
은 해당 레이블로 돌아간 뒤에 돌고 있던 루프문의 다음 문장으로 이동한다고 합니다! 앞으로도 여기저기서 위와 같이 채널과 goto
문을 활용하여 특정 이벤트나 데이터를 수신하면서 처리하는 로직을 보게 될 겁니다. 지금 눈에 잘 익혀 두세요 ^^
자 그럼! 여기서 자연스럽게 질문이 하나 나와야 합니다. 바로 누가 workCh
에 데이터를 밀어 넣느냐 하는 것입니다. 이건 바로 위에서 worker.start
함수 다음으로 실행 한 worker.commitNewWork()
함수의 역할입니다. 오늘 글에서 이 부분까지 다루면 좋겠지만 이미 적지 않은 내용을 다룬 것 같아… 여기서 한번 호흡을 가다듬고 마이닝을 위한 나머지 코드는 다음 글에서 다뤄야 할 것 같습니다.
결론
오늘은 지난 시간에 실습했던 마이닝 과정을 코드로 차근히 따라가보는 일을 했습니다. 오늘 살펴본 코드는 위에서 한번 정리했지만 다시 정리하면 다음과 같은 흐름이었습니다.
- geth
실행 시점이든,
rpc호출이든 =>
Ethereum객체의
StartMining`함수를 호출StartMining
함수는 =>Miner
객체의Start
함수 호출Miner
객체의Start
함수는 =>Worker
객체의Start
함수 호출Worker
객체의Start
함수는 => 각Agent
의Start
함수 호출Agent
가운데CpuAgent
함수가 바로 마이닝 관련 함수이고 여기서 =>update
함수 호출update
함수 안에서workCh
을 수신하다가 => 데이터가 들어오면mine
함수 호출
긴 여정이었습니다. 다음 글에서 오늘 미쳐 다루지 못한 mine
함수와 함께 worker.commitNewWork
함수에서 어떻게 workCh
채널에 Work
객체를 넣는지 살펴보고자 합니다.
그럼 다음 연재에서 뵙겠습니다.
실행 관련내용 잘봤네요. 정말 멋지네요.
부족한 내용 잘 봐주셔서 감사합니다. :)
항상 정성글은 추천이라고 배웠습니다. ㅎㅎㅎ 다른 내용도 많이 적어주십쇼 ㅎㅎ
와 귀한 글을 발견했네요.
자바 기반 글 찾기가 왜 이렇게 힘든지,
잘 보겠습니다.
블록체인도 관심있고 고 언어도 알고 싶어 겸사 겸사 분석하면서 이렇게 글을 쓰고 있네요 ^^