geth 소스 읽기 Part2 Day02steemCreated with Sketch.

in #kr7 years ago (edited)

main_logo

이 글은 자바 개발자의 go-ethereum(geth 클라이언트) 소스 읽기 시리즈 Part2 연재 중 두 번째 글입니다.

  1. Day 01: 마이닝 과정 실습
  2. (본 글) Day 02: 마이닝 시작 과정 코드 읽기 (1)
  3. Day 03: 마이닝 시작 과정 코드 읽기 (2)
  4. TBD

전체 연재 목록은 아래 페이지에서 확인해 주세요.
http://www.notforme.kr/block-chain/geth-code-reading

Part2 연재의 대상 독자 및 목표

이 글은 독자 분들이 적어도 Java와 같은 OOP 계열의 언어로 프로그래밍 경험이 있다는 것을 가정합니다. 또한 계정, 채굴 등 블록체인과 이더리움과 관련된 기초적인 개념과 이더리움 백서나 황서의 내용을 간단하게 알고 있다고 가정합니다. geth 코드를 읽으면서 백서 및 황서에서 정의된 스펙이 어떻게 구현되었는지를 확인하는 것이 목적입니다.

더불어 이 연재는 다음 3가지 목적을 염두하고 쓴 것입니다.

  1. 새로운 언어(Go)를 오픈소스 코드를 읽으며 배운다.
  2. 오픈소스를 읽으며 코드리딩 능력을 배양한다.
  3. 블록체인의 기술을 직접 코드를 통해서 익힌다.

지난 Part1에서는 총 6번의 글을 통해서 geth구조와 실행방법에 대해서 살펴봤습니다. Part2에서는 좀 더 구체적으로 이더리움의 핵심 기능들이 어떻게 구현되었는지 Part1에서와 마찬가지로 코드를 통해서 알아보려고 합니다

다루는 내용

지난 글에서는 로컬에 하나의 geth 노드를 띄운 후 마이너를 실행하고 블록이 생성되는 과정을 직접 실습했습니다. 이 과정에서 발생한 이더리움을 다른 신규계정으로 전송하는 것 또한 실습했습니다.

오늘은 이더리움을 마이닝하는 과정을 실습이 아닌 코드로 따라가 보려고 합니다. 물론 마이닝 과정 가운데 일부 로직은 좀 더 세부적으로 살펴볼 것도 있습니다. 예를 들면 PoW를 실행하는 ethash나 블록상태를 저장하는 로직 등이 그렇습니다. 오늘은 이러한 세부 로직을 모두 파악하기 보다는 코드 레벨에서 지난 번 실습한 마이닝 동작과정을 큰 흐름으로 살펴보는 것이 목적입니다.

한 번에 많은 내용을 살펴보고 글로 담기가 쉽지 않을 것 같습니다. 앞으로도 되도록 너무 길지 않게 분석할 코드의 내용을 논리적으로 잘개 쪼개서 포스팅을 하려고 합니다.

마지막으로 오늘부터 본격적으로 시작하는 Part2에서 읽는 코드의 커밋 해시 0fe479e입니다.


마이닝 시작 포인트

첫 출발은 마이닝을 실행하는 지점에서 출발하려고 합니다. geth에서 마이너를 실행시키는 방법은 2가지가 있습니다. 하나는 geth 실행 시에 --mine옵션을 주는 것이고 다른 하나는 지난 시간에 했던 방법처럼 web3를 사용하여 rpc로 실행하는 방법입니다.

마이너를 실행시키는 2가지 방법

  1. geth 실행 시 mine 옵션 주기
  2. web3를 활용하여 miner.start() 명령을 콘솔에 입력하기

2가지 방법은 호출 방식만 다를 뿐 결국 Ethereum 객체의 필드인 minerStart 함수를 호출합니다. 지난 글 마지막에 콘솔에서 miner.start()를 호출할 때 실행되는 함수에 디버깅 포인트를 걸어서 GoLand에서 실행된 geth의 동작 흐름을 멈추게 했었습니다. 이 지점이 바로 minerStart 함수였습니다.

그럼 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
}


함수에서 인자로 넘겨 받은 내용은 다음과 같습니다.

  1. eth 객체: eth의 속성인 miner 에게 스스로의 참조를 전달하고 있네요.
  2. config: 타입이름을볼 때 블록체인 관련 설정
  3. mux: 이벤트 처리를 할 때 쓸 것으로 추정
  4. engine: 마이닝을 위해서는 합의 과정이 관련이 있겠지요?

이렇게 넘겨받은 인자로 Miner 타입의 객체를 생성합니다. 이 때 worker라는 속성은 newWorker 함수로 생성이 되는 것을 볼 수 있습니다. 그리고 나서 Register 함수를 NewCpuAgent 함수의 반환 값으로 호출하고 있네요. Register 함수는 간단합니다.

func (self *Miner) Register(agent Agent) {
    if self.Mining() {
        agent.Start()
    }
    self.worker.register(agent)
}


인자로 Agent를 받네요. 앞에서 NewCpuAgentAgent타입의 객체를 반환한다는 것을 유추할 수 있겠네요. 코드의 로직은 간단합니다. 이미 마이닝 중이면 agentStart를 실행하고, 아니면 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로 실행 시 마이너 시작부분

gethrpc 엔드포인드를 관리하는 로직이 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 할 내용이 있어서 모든 데이터를 받아오면 그때 canStart1로 변경한 후 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가 깊어지네요… CpuAgentRemoteAgent의 각각 Start함수를 살펴보기 전에 여기서 한번 정리를 해볼까요.

  1. geth 실행 시점이든, rpc 호출이든 => Ethereum객체의 StartMining함수를 호출
  2. StartMining함수는 => Miner 객체의 Start 함수 호출
  3. Miner 객체의 Start 함수는 => Worker 객체의 Start 함수 호출
  4. Worker객체의 Start함수는 => 각 AgentStart 함수 호출

이제 각 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()함수의 역할입니다. 오늘 글에서 이 부분까지 다루면 좋겠지만 이미 적지 않은 내용을 다룬 것 같아… 여기서 한번 호흡을 가다듬고 마이닝을 위한 나머지 코드는 다음 글에서 다뤄야 할 것 같습니다.


결론

오늘은 지난 시간에 실습했던 마이닝 과정을 코드로 차근히 따라가보는 일을 했습니다. 오늘 살펴본 코드는 위에서 한번 정리했지만 다시 정리하면 다음과 같은 흐름이었습니다.

  1. geth실행 시점이든,rpc호출이든 =>Ethereum객체의StartMining`함수를 호출
  2. StartMining함수는 => Miner 객체의 Start 함수 호출
  3. Miner 객체의 Start 함수는 => Worker 객체의 Start 함수 호출
  4. Worker객체의 Start함수는 => 각 AgentStart 함수 호출
  5. Agent 가운데 CpuAgent 함수가 바로 마이닝 관련 함수이고 여기서 => update 함수 호출
  6. update함수 안에서 workCh을 수신하다가 => 데이터가 들어오면 mine 함수 호출

긴 여정이었습니다. 다음 글에서 오늘 미쳐 다루지 못한 mine함수와 함께 worker.commitNewWork 함수에서 어떻게 workCh 채널에 Work 객체를 넣는지 살펴보고자 합니다.

그럼 다음 연재에서 뵙겠습니다.

Sort:  

실행 관련내용 잘봤네요. 정말 멋지네요.

부족한 내용 잘 봐주셔서 감사합니다. :)

항상 정성글은 추천이라고 배웠습니다. ㅎㅎㅎ 다른 내용도 많이 적어주십쇼 ㅎㅎ

와 귀한 글을 발견했네요.
자바 기반 글 찾기가 왜 이렇게 힘든지,
잘 보겠습니다.

블록체인도 관심있고 고 언어도 알고 싶어 겸사 겸사 분석하면서 이렇게 글을 쓰고 있네요 ^^