자바 개발자의 go-ethereum 소스 읽기: Day 6
자바 개발자의 go-ethereum 소스 읽기: Day 6
이 글은 자바 개발자의 go-ethereum(geth 클라이언트) 소스 분석기 시리즈의 연재 중 여섯 번째 글입니다. 앞으로 다음과 같은 내용으로 연재를 계획하고 있습니다.
- Day 01: Geth 1.0 소스 받기 및 코드 분석을 위한 개발환경 셋팅(VS Code)
- Day 02: CLI 라이브러리 기반
geth
의 전체 실행 구조 - Day 03: VS Code를 사용한
geth
디버깅 - Day 04:
geth
노드 실행 로직 - Day 05:
geth
의 실행과 종료 - (본 글) Day 06: 이더리움
fullNode
의 실행과 종료
전체 연재 목록은 아래 페이지에서 확인해 주세요
http://www.notforme.kr/block-chain/geth-code-reading
대상 독자 및 연재 목표
이 연재는 먼저 독자 분들이 적어도 Java와 같은 OOP 계열의 언어로 프로그래밍 경험이 있다는 것을 가정합니다. 또한 계정, 채굴 등 블록체인과 이더리움과 관련된 기초적인 개념을 알고 있다고 가정합니다.
더불어 이 연재는 다음 3가지 목적을 염두하고 쓴 것입니다.
- 새로운 언어(
Go
)를 오픈소스 코드를 읽으며 배운다. - 오픈소스를 읽으며 코드리딩 능력을 배양한다.
- 블록체인의 기술을 직접 코드를 통해서 익힌다.
다루는 내용
오늘은 이더리움 fullNode
의 실행과 종료 부분의 코드를 살펴볼 예정입니다. 오늘까지 읽고 분석하는 내용이 geth
의 실행과 관련된 일반적인 로직이 됩니다. 굳이 따지자면 오늘 글이 코드 읽기의 파트 1 마지막이라고 봐도 될 것 같습니다. 이번 글을 통해서 앞으로 이더리움 fullNode
의 기능별 로직을 하나씩 뽑아서 개별적으로 다루는 방식이 될것 같습니다.
오늘 글에서도 소소하게나마 golang
의 구문 설명이 간단히 있습니다.
- 상수와 iota
오늘 읽는 코드의 커밋 해쉬는 577d375 입니다. 참고 부탁드립니다.
RegisterEthService 함수 분석
지난 글에서 터미널에서 아무런 옵션 없이 geth
를 실행하고 종료할 때의 일반적인 로직을 살펴보면서 RegisterEthService
메서드를 지나쳤는데요. 오늘 볼 코드는 여기서부터 시작하고자 합니다.
지난 글을 복기 하기: serviceFuncs
지난 글에서 우리는 Node
의 Start
메서드 코드를 읽다가 다음과 같은 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)
}
}
자 그럼 코드를 분석해 봅시다. 이 함수는 인자로 받은 cfg
의 SyncMode
에 따라 등록하는 생성자 함수가 다릅니다. 생성자 부분을 요약하면 함수의 코드 구조는 다음과 같이 간단합니다.
// 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
)
좀 더 자세한 부분은 아래 글로 대신합니다.
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
는 함수를 들여다 보기 전까지 무슨 역할을 하는지 유추하기 쉽지 않습니다. 패키지 les
는 Light Ethereum Subprotocol
구현체인데요. 검색을 통해 공식 위키의 내용을 보면 현재 개발 중인 것으로 보입니다.
코드 읽기 할 때의 중요한 점은 너무 지엽적으로 하나를 들여다보기 보다 핵심을 먼저 훑어보고 이해하는 것이 중요합니다. 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와 관련된 코드를 모아서 함께 살펴봅시다.
여기까지가 기본 설정이므로 다음과 같이 로그상에 체인 설정이 끝났다고 남깁니다.
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),
}
이 코드 안에도 다음과 같이 앞으로 각각 살펴볼 내용들이 있습니다.
- accountManager:
geth
의 계정 관련된 주제로 다룰 부분 입니다. - engine: CreateConsensusEngine: 블록체인에 있어서 가장 중요한 부분 중 하나이겠지요? ^^... ethash를 주제로 여러 글로 나눠서 봐야 할 거 같습니다.
- 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는 기존에 여러 분들이 분석해주신 프로토콜과 관련 내용을 바탕으로 코드레벨에서 살펴보는 글이 되지 않을까 싶습니다.
- 체인 DB
- 블록체인 구조
- 계정관리
- 합의 알고리즘, etash
- 블룸필터
- 트랜잭션 및 Pool
- 마이닝
- ...
오늘까지 6개의 글로 파트1을 마감합니다.
감사합니다. :-)
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
멋진 글 감사합니다. 큰 도움 되었습니다.
본문 말미에 언급된 '이더리움 Oracle'에 대해 짤막하게 정리한 글이 있어 공유합니다 ( https://steemit.com/kr/@ingee/oracle ).
이더리움/블록체인에 대해 의논할 수 있는 분을 만난 것 같아 기쁩니다.
본문에 쓴 것처럼 Go도 눈에 익히고 이더리움도 공부하고 싶어서 조금씩 틈날 때 소스를 보는 중입니다.
@ingee 님 공유해 주신 글도 감사합니다. :)