자바 개발자의 go-ethereum 소스 읽기: Day 6steemCreated with Sketch.

in kr •  6 months ago

자바 개발자의 go-ethereum 소스 읽기: Day 6

main_logo

이 글은 자바 개발자의 go-ethereum(geth 클라이언트) 소스 분석기 시리즈의 연재 중 여섯 번째 글입니다. 앞으로 다음과 같은 내용으로 연재를 계획하고 있습니다.

  1. Day 01: Geth 1.0 소스 받기 및 코드 분석을 위한 개발환경 셋팅(VS Code)
  2. Day 02: CLI 라이브러리 기반 geth의 전체 실행 구조
  3. Day 03: VS Code를 사용한 geth 디버깅
  4. Day 04: geth 노드 실행 로직
  5. Day 05: geth의 실행과 종료
  6. (본 글) Day 06: 이더리움 fullNode의 실행과 종료

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

대상 독자 및 연재 목표

이 연재는 먼저 독자 분들이 적어도 Java와 같은 OOP 계열의 언어로 프로그래밍 경험이 있다는 것을 가정합니다. 또한 계정, 채굴 등 블록체인과 이더리움과 관련된 기초적인 개념을 알고 있다고 가정합니다.

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

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

다루는 내용

오늘은 이더리움 fullNode의 실행과 종료 부분의 코드를 살펴볼 예정입니다. 오늘까지 읽고 분석하는 내용이 geth의 실행과 관련된 일반적인 로직이 됩니다. 굳이 따지자면 오늘 글이 코드 읽기의 파트 1 마지막이라고 봐도 될 것 같습니다. 이번 글을 통해서 앞으로 이더리움 fullNode의 기능별 로직을 하나씩 뽑아서 개별적으로 다루는 방식이 될것 같습니다.

오늘 글에서도 소소하게나마 golang의 구문 설명이 간단히 있습니다.

  1. 상수와 iota

오늘 읽는 코드의 커밋 해쉬는 577d375 입니다. 참고 부탁드립니다.

RegisterEthService 함수 분석

지난 글에서 터미널에서 아무런 옵션 없이 geth를 실행하고 종료할 때의 일반적인 로직을 살펴보면서 RegisterEthService 메서드를 지나쳤는데요. 오늘 볼 코드는 여기서부터 시작하고자 합니다.

지난 글을 복기 하기: serviceFuncs

지난 글에서 우리는 NodeStart 메서드 코드를 읽다가 다음과 같은 for문을 만났습니다.

services := make(map[reflect.Type]Service)
for _, constructor := range n.serviceFuncs {


이 코드는 for 문을 순회하면서 serviceFuncs 에 있던 constructor 함수를 사용하여 다음과 같이 서비스 객체를 생성하고...

// Construct and save the service
        service, err := constructor(ctx)


그리고 다음 for 문에서 생성한 서비스들을 순차적으로 실행시켰습니다.

for kind, service := range services {
    // Start the next service, stopping all previous upon failure
    if err := service.Start(running); err != nil {


여기까지가 지난 시간에 살펴봤던 내용입니다. 터미널에서 아무런 옵션없이 geth를 실행할 경우 serviceFuncs에 등록된 생성자는 바로 다음 코드를 통해서 오직 이더리움 서비스 하나만 있다는 것도 지난 연재에서 다뤘습니다.

func makeFullNode(ctx *cli.Context) *node.Node {
    stack, cfg := makeConfigNode(ctx)

    utils.RegisterEthService(stack, &cfg.Eth


이 함수가 우리가 오늘 읽을 코드의 출발점입니다.

RegisterEthService 함수 조망하기

함수의 전체 코드가 길지 않아 먼저 코드부터 보기로 합니다.

// RegisterEthService adds an Ethereum client to the stack.
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
    var err error
    if cfg.SyncMode == downloader.LightSync {
        err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
            return les.New(ctx, cfg)
        })
    } else {
        err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
            fullNode, err := eth.New(ctx, cfg)
            if fullNode != nil && cfg.LightServ > 0 {
                ls, _ := les.NewLesServer(fullNode, cfg)
                fullNode.AddLesServer(ls)
            }
            return fullNode, err
        })
    }
    if err != nil {
        Fatalf("Failed to register the Ethereum service: %v", err)
    }
}


자 그럼 코드를 분석해 봅시다. 이 함수는 인자로 받은 cfgSyncMode에 따라 등록하는 생성자 함수가 다릅니다. 생성자 부분을 요약하면 함수의 코드 구조는 다음과 같이 간단합니다.

// RegisterEthService adds an Ethereum client to the stack.
func RegisterEthService(stack *node.Node, cfg *eth.Config) {
    var err error
    if cfg.SyncMode == downloader.LightSync {
        err = stack.Register(/* LightSync 일때 생성자 함수 */)
    } else {
        err = stack.Register(/* LightSync가 아닐때 생성자 함수 */)
    }
    if err != nil {
        Fatalf("Failed to register the Ethereum service: %v", err)
    }
}


그럼 이제 if 문을 분기하는 모드에 대해서 궁금할텐데요. 확인해보면 downloader 패키지에 선언된 상수로 다음과 같은 모드가 있습니다.

const (
    // Synchronise the entire blockchain history from full blocks
    FullSync  SyncMode = iota
    // Quickly download the headers, full sync only at the chain head
    FastSync             
    // Download only the headers and terminate afterwards
    LightSync                 
)


코드를 보면 세가지 모드 선언된 것을 확인할 수 있습니다. 첫 번째 FullSync에는 타입이 SyncMode로 명시되어 있는데요. 사실 이 타입은 바로 위에 다음과 같이 정의되어 있습니다.

// SyncMode represents the synchronisation mode of the downloader.
type SyncMode int


네 그냥 정수입니다. 자바개발자라면 이 코드를 보고 자연스럽게 열거형(Enum)을 떠올리셨을 것 같습니다.

상수와 iota

golang은 언어차원에서 열거형을 지원하지 않습니다. 대신 상수를 직접 선언하여 열거형 처럼 사용이 가능합니다. 앞의 코드에서 const와 소괄호로 3개의 모드를 선언한 것이 열거형의 선언이라고 볼 수 있습니다.

그럼 iota는 뭘까요? 언어의 스펙문서에서 iota는 타입없이 0부터 증가하는 정수라고 설명되어 있습니다. 따라서 위 코드는 간단히 다음과 같은 코드입니다.

const (
    FullSync = 0
    FastSync = 1       
    LightSync= 2                
)


좀 더 자세한 부분은 아래 글로 대신합니다.

https://splice.com/blog/iota-elegant-constants-golang/

RegisterEthService의 기본 flow

옵션 없이 geth를 실행하면 기본 설정은 FastSync로 되어 있습니다. 코드는 eth 패키지의 config.go를 보면 알 수 있습니다.

var DefaultConfig = Config{
    SyncMode: downloader.FastSync
    //...


RegisterEthService의 함수는 기본 설정이 LightSync가 아니라서 else 구문을 타겠네요. 그럼 serviceFuncs에 등록되어 호출된 생성자 함수는 다음 코드라는 것을 알았습니다.

err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
    fullNode, err := eth.New(ctx, cfg)
    if fullNode != nil && cfg.LightServ > 0 {
        ls, _ := les.NewLesServer(fullNode, cfg)
        fullNode.AddLesServer(ls)
    }
    return fullNode, err
})


코드를 한번 볼까요. 먼저 드디어 eth 패키지의 함수 eth.New로 이더리움 객체를 생성합니다. 여기서 생성된 객체를 fullNode라는 변수로 담은 것이 보이시나요? 이어서 cfg.LightServ의 값을 보고 값이 0보다 크면 les 패키지의 NewLesServer를 실행합니다. 이 때의 반환값을 AddLesServer 를 통해서 fullNode에 포함시키는군요.

처음 fullNode 생성한 후에 뒤따라오는 NewLesServer는 함수를 들여다 보기 전까지 무슨 역할을 하는지 유추하기 쉽지 않습니다. 패키지 lesLight Ethereum Subprotocol 구현체인데요. 검색을 통해 공식 위키의 내용을 보면 현재 개발 중인 것으로 보입니다.

https://github.com/ethereum/wiki/wiki/Light-client-protocol

코드 읽기 할 때의 중요한 점은 너무 지엽적으로 하나를 들여다보기 보다 핵심을 먼저 훑어보고 이해하는 것이 중요합니다. les 관련 내용은 존재만 확인하고 과감하게 넘어갑시다.

이제 RegisterEthService 함수에서 eth.New 함수로 넘어갈 차례이군요.

eth.New 함수

이 함수는 eth 패키지의 backend.go 파일에 선언되어 있습니다. 이 함수를 읽으려면 이더리움 백서황서와 관련된 여러가지 배경지식이 필요한데요. 오늘은 서두에 밝힌데로 연재의 첫번째 파트1을 마무리하는 느낌으로 논리적으로 코드를 묶어서 개괄하고 앞으로 읽을 내용들을 도출해 보려고 합니다.

함수의 시그니쳐

먼저 가볍게 함수의 시그니쳐와 주석부터 볼까요?

// New creates a new Ethereum object (including the
// initialisation of the common Ethereum object)
func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {


특별한 것은 없고 객체 생성을위한 컨텍스트(ctx)와 설정(config)을 인자로 받아 Ethereum 타입의 객체와 에러를 반환합니다. 그럼 함수의 본문을 보기 전에 Ethereum 타입부터 봅시다. 편의상 주석은 내용이 너무 긴 경우 해당 필드의 위로 제가 옮겨 놓았습니다.

// Ethereum implements the Ethereum full node service.
type Ethereum struct {
    config      *Config
    chainConfig *params.ChainConfig

    // Channel for shutting down the service
    shutdownChan  chan bool    // Channel for shutting down the Ethereum
    stopDbUpgrade func() error // stop chain db sequential key upgrade

    // Handlers
    txPool          *core.TxPool
    blockchain      *core.BlockChain
    protocolManager *ProtocolManager
    lesServer       LesServer

    // DB interfaces
    chainDb ethdb.Database // Block chain database

    eventMux       *event.TypeMux
    engine         consensus.Engine
    accountManager *accounts.Manager
    // Channel receiving bloom data retrieval requests
    bloomRequests chan chan *bloombits.Retrieval
    // Bloom indexer operating during block imports
    bloomIndexer  *core.ChainIndexer             

    ApiBackend *EthApiBackend

    miner     *miner.Miner
    gasPrice  *big.Int
    etherbase common.Address

    networkId     uint64
    netRPCService *ethapi.PublicNetAPI
    
    // Protects the variadic fields (e.g. gas price and etherbase)
    lock sync.RWMutex 
}


여러 요소를 필드로 갖고 있는데요. New 함수의 본문이 이 필드의 값을 생성/초기화하고 때론 실행하는 부분까지 있다고 보면됩니다. 이 구조체의 필드 하나 하나가 앞으로 파트 2에서 읽고 분석할 꼭지들이라고 볼 수 도 있을 것 같습니다. ^^;...

그럼 본문을 조금씩 끊어서 살펴보겠습니다.

간단한 validation

시작은 언제나 이 함수를 실행하기 적절한지 검증하는 로직입니다. 문제가 있으면 에러 객체와 함께 함수를 종료합니다.

    if config.SyncMode == downloader.LightSync {
        return nil, errors.New("can't run eth.Ethereum in light sync mode, use les.LightEthereum")
    }
    if !config.SyncMode.IsValid() {
        return nil, fmt.Errorf("invalid sync mode %d", config.SyncMode)
    }

블록체인 환경 준비

다음은 블록체인 환경을 준비하는 코드입니다.

    chainDb, err := CreateDB(ctx, config, "chaindata")
    if err != nil {
        return nil, err
    }
    stopDbUpgrade := upgradeDeduplicateData(chainDb)
    chainConfig, genesisHash, genesisErr := core.SetupGenesisBlock(chainDb, config.Genesis)
    if _, ok := genesisErr.(*params.ConfigCompatError); genesisErr != nil && !ok {
        return nil, genesisErr
    }


제일 먼저 체인DB를 생성하는 로직이 나옵니다. upgradeDeduplicateData는 체인db의 버전에 따라 마이그레이션과 유사한 역할을 하는 함수 호출 정도로 볼 수 있습니다. 생성한 chainDb와 기본 Genesis 블록 설정을 가지고 core.SetupGenesisBlock 함수를 호출합니다. 정리하면 코드 자체가 체인을 위한 준비과정이라고 볼 수 있습니다.

이 코드에서 우리는 다음에 자세하게 다룰 하나의 주제가 나오네요. 바로 체인DB입니다. geth는 체인DB로 구글이 오픈소스화한 레벨 db를 씁니다. 정확히는 레벨db는 C++로 구현된 것이고 여기서는 레벨db의 Go 구현체를 따로 씁니다. 다음에 체인 db와 관련된 코드를 모아서 함께 살펴봅시다.

https://github.com/syndtr/goleveldb

여기까지가 기본 설정이므로 다음과 같이 로그상에 체인 설정이 끝났다고 남깁니다.

log.Info("Initialised chain configuration", "config", chainConfig)

Ethereum 객체 생성

이어서 앞서 살펴본 Ethereum 타입의 객체를 생성합니다.

    eth := &Ethereum{
        config:         config,
        chainDb:        chainDb,
        chainConfig:    chainConfig,
        eventMux:       ctx.EventMux,
        accountManager: ctx.AccountManager,
        engine:         CreateConsensusEngine(ctx, &config.Ethash, chainConfig, chainDb),
        shutdownChan:   make(chan bool),
        stopDbUpgrade:  stopDbUpgrade,
        networkId:      config.NetworkId,
        gasPrice:       config.GasPrice,
        etherbase:      config.Etherbase,
        bloomRequests:  make(chan chan *bloombits.Retrieval),
        bloomIndexer:   NewBloomIndexer(chainDb, params.BloomBitsBlocks),
    }


이 코드 안에도 다음과 같이 앞으로 각각 살펴볼 내용들이 있습니다.

  1. accountManager: geth의 계정 관련된 주제로 다룰 부분 입니다.
  2. engine: CreateConsensusEngine: 블록체인에 있어서 가장 중요한 부분 중 하나이겠지요? ^^... ethash를 주제로 여러 글로 나눠서 봐야 할 거 같습니다.
  3. bloom*: 이더리움은 블룸필터를 사용하는데요. 블룸필터의 내용과 함께 어떻게 쓰이고있는지도 코드로 살펴볼 내용입니다.

여기까지 진행되면 이제 프로토콜 버전및 네트워크 id를 한번 로그로 남깁니다.

    log.Info("Initialising Ethereum protocol", "versions", ProtocolVersions, "network", config.NetworkId)

버전 확인 과 블록체인 생성

현재 실행하는 버전이 유효한 블록체인버전인지 체크하고 있습니다.

if !config.SkipBcVersionCheck {
    bcVersion := core.GetBlockChainVersion(chainDb)
    if bcVersion != core.BlockChainVersion && bcVersion != 0 {
        return nil, fmt.Errorf("Blockchain DB version mismatch (%d / %d). Run geth upgradedb.\n", bcVersion, core.BlockChainVersion)
    }
    core.WriteBlockChainVersion(chainDb, core.BlockChainVersion)
}

블록체인 생성

다음은 블록체인을 생성하는 부분입니다. 역시 이더리움에서 블록체인 자체가 어떤 구조를 가지고 있는지 코드레벨에서 앞으로 살펴볼 주제입니다.

eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, eth.chainConfig, eth.engine, vmConfig)
if err != nil {
    return nil, err
}


다음은 메인 로직은 아닌듯 보이고 호환성 관련된 처리를 위해 체인을 조정하는 코드가 따라옵니다.

// Rewind the chain in case of an incompatible config upgrade.
if compat, ok := genesisErr.(*params.ConfigCompatError); ok {
    log.Warn("Rewinding chain to upgrade configuration", "err", compat)
    eth.blockchain.SetHead(compat.RewindTo)
    core.WriteChainConfig(chainDb, genesisHash, chainConfig)
}

블룸필터 인덱서 시작

한 줄이지만 블룸필터 인덱서를 시작하는 코드가 이어집니다.

eth.bloomIndexer.Start(eth.blockchain)

트랜잭션 Pool 생성

블록체인에 담을 트랜잭션 Pool을 생성합니다.

if config.TxPool.Journal != "" {
    config.TxPool.Journal = ctx.ResolvePath(config.TxPool.Journal)
}
eth.txPool = core.NewTxPool(config.TxPool, eth.chainConfig, eth.blockchain)

프로토콜 매니저

지금까지 생성/초기화한 여러 기능들을 관리하는 매니저를 생성하는 것 같습니다. 주석에는 서브프로토콜을 관리한다고 하네요.

if eth.protocolManager, err = NewProtocolManager(eth.chainConfig, config.SyncMode, config.NetworkId, eth.eventMux, eth.txPool, eth.engine, eth.blockchain, chainDb); err != nil {
        return nil, err
}

마이닝 설정

마이닝과 관련된 것으로 보이는 miner 를 생성하고 설정합니다.

eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)
eth.miner.SetExtra(makeExtraData(config.ExtraData))

가스 가격 결정 관련 코드

geth 에서 현재 체인의 정보를 바탕으로 가스 가격을 결정하는 모듈을 Oracle이라 부르는 것 같습니다. 지금은 이정도만 파악하고 역시 추후에 살펴볼 주제가 되겠습니다.

eth.ApiBackend = &EthApiBackend{eth, nil}
gpoParams := config.GPO
if gpoParams.Default == nil {
    gpoParams.Default = config.GasPrice
}
eth.ApiBackend.gpo = gasprice.NewOracle(eth.ApiBackend, gpoParams)

return eth, nil

eth.New 정리

지금까지 살펴본 코드를 바탕으로 앞으로 다룰 주제들을 많이 발견했습니다. New 함수는 geth 클라이언트 노드를 이해하는 핵심 함수라고 볼 수 있습니다. 바로 이어서 Start 코드도 보겠지만 대부분의 로직은 여기서 다뤄졌습니다.

eth.Start 함수

앞서 살펴본 New함수로 생성/초기화된 fullNode 객체는 다음의 Start 함수와 함께 시작되는데요. 코드를 보면 시작의 의미가 결국 네트워크 관련된 코드를 활성화 시키는 부분이란 걸 알 수 있습니다.

// Start implements node.Service, starting all internal goroutines needed by the
// Ethereum protocol implementation.
func (s *Ethereum) Start(srvr *p2p.Server) error {
    // Start the bloom bits servicing goroutines
    s.startBloomHandlers()

    // Start the RPC service
    s.netRPCService = ethapi.NewPublicNetAPI(srvr, s.NetVersion())

    // Figure out a max peers count based on the server limits
    maxPeers := srvr.MaxPeers
    if s.config.LightServ > 0 {
        if s.config.LightPeers >= srvr.MaxPeers {
            return fmt.Errorf("invalid peer config: light peer count (%d) >= total peer count (%d)", s.config.LightPeers, srvr.MaxPeers)
        }
        maxPeers -= s.config.LightPeers
    }
    // Start the networking layer and the light server if requested
    s.protocolManager.Start(maxPeers)
    if s.lesServer != nil {
        s.lesServer.Start(srvr)
    }
    return nil
}


주석이 이미 역할별로 끊어서 잘 설명해주고 있어서 부연할 것이 없는 간단한 로직이네요. 여기서 실행한는 모든 서비스들이 별도의 독립적인 고루틴으로 실행된다고 주석에서 알려줍니다. 마지막으로 종료함수를 봅시다.

eth.Stop 함수

종료함수는 간단합니다. "조립은 분해의 역순"처럼 각각 독립적으로 돌고있을 고루틴을 하나씩 종료하는군요.

// Ethereum protocol.
func (s *Ethereum) Stop() error {
    if s.stopDbUpgrade != nil {
        s.stopDbUpgrade()
    }
    s.bloomIndexer.Close()
    s.blockchain.Stop()
    s.protocolManager.Stop()
    if s.lesServer != nil {
        s.lesServer.Stop()
    }
    s.txPool.Stop()
    s.miner.Stop()
    s.eventMux.Stop()

    s.chainDb.Close()
    close(s.shutdownChan)

    return nil
}


여기서 종료하는 로직들 하나하나가 결국 독립적인 모듈이라 파트2에서 살펴볼 주제가 됩니다.

마치며

오늘은 사실 지난 글에서 들어가야할 내용이지만 길이가 너무 길어질 것 같아 분리했습니다. Day01부터 해서 오늘 Day06까지 이제 우리는 geth의 기본적인 실행과 종료의 큰 흐름을 읽을 수 있었습니다. 여기까지가 사실상 파트1 정도가 될것 같습니다.

다음 연재에서는 오늘 도출된 다음의 주제들 중에서 하나씩 공부한 것을 정리하면서 공유해보도록 하겠습니다.
파트 2는 기존에 여러 분들이 분석해주신 프로토콜과 관련 내용을 바탕으로 코드레벨에서 살펴보는 글이 되지 않을까 싶습니다.

  1. 체인 DB
  2. 블록체인 구조
  3. 계정관리
  4. 합의 알고리즘, etash
  5. 블룸필터
  6. 트랜잭션 및 Pool
  7. 마이닝
  8. ...

오늘까지 6개의 글로 파트1을 마감합니다.
감사합니다. :-)

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Congratulations @woojin.joe! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of upvotes

Click on any badge to view your own Board of Honor on SteemitBoard.
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

Upvote this notification to help all Steemit users. Learn why here!

멋진 글 감사합니다. 큰 도움 되었습니다.
본문 말미에 언급된 '이더리움 Oracle'에 대해 짤막하게 정리한 글이 있어 공유합니다 ( https://steemit.com/kr/@ingee/oracle ).
이더리움/블록체인에 대해 의논할 수 있는 분을 만난 것 같아 기쁩니다.

·

본문에 쓴 것처럼 Go도 눈에 익히고 이더리움도 공부하고 싶어서 조금씩 틈날 때 소스를 보는 중입니다.

@ingee 님 공유해 주신 글도 감사합니다. :)