Go로 작성된 이더리움 클라이언트(이하 geth)에는 기본 두 종류의 합의 알고리즘이 존재한다. 하나는 일반적으로 우리가 알고있는 PoW(Proof-of-Work)의 일종인 ethash
이고, 나머지 하나는 PoA(Proof-of-Authority)인 clique
이다.
이번 시리즈에서는 rawpow
라 이름붙인 나만의 합의 알고리즘을 geth에 추가할 것이다. 구동 가능한 구현체는 다음의 깃허브 저장소에서 확인할 수 있다.
main.go에서 시작하기
geth 코드의 흐름에 따라 기본적인 구조를 살펴보자. 우리가 어떤 부분을 수정해야 새로운 합의 알고리즘을 추가할 수 있을지 생각해보자.
geth 바이너리를 실행하면 cmd/geth/main.go
의 main 함수가 실행된다.
물론 go 언어의 특성상 init 함수가 먼저 수행될 것이다. init()
에서는 CLI 앱을 초기화하고 geth를 시작한다. app.Action = geth
에 주목하자. 여기서 geth는 다음과 같이 정의된다.
geth(ctx *cli.Context)
는 명령줄 인자들로부터 기본 노드를 생성하고 구동하는 역할을 수행한다. makeFullNode(ctx)
를 살펴보자. config.go
에 정의돼 있다. 이어 RegisterEthService()
를 살펴보자. cmd/utils/flags.go
에 정의돼 있다.
RegisterEthService()
에서는 eth
서비스를 등록한다. eth.New()
는 eth/backend.go
에서 그 구조를 살펴볼 수 있다. eth
가 정의되고 초기화되는 부분에 주목하자.
engine: CreateConsensusEngine(ctx, &config.Ethash, chainConfig, chainDb)
가 바로 우리가 수정해야 할 부분이다. CreateConsensusEngine()
은 이더리움 서비스를 위해 지정된 합의 알고리즘 엔진을 생성한다.
CreateConsensusEngine
위는 geth의 CreateConsensusEngine()
코드이다. chainConfig
의 Clique
가 nil이 아니라면 Clique, 즉 PoA 합의 알고리즘으로 세팅됨을 알 수 있다. 만일 나머지 경우에는, 즉 PoW 합의 알고리즘의 경우에는 config
의 PowMode
에 따라 Fake, Test, Shared, 그리고 기본(default) ethash로 세팅된다.
chainConfig
chainConfig
는 params/config.go
에 정의돼 있다.
ChainConfig
는 블록체인 세팅을 담당하는 핵심 요소들의 구조체이다. 체인ID(ChainID) 설정에서부터, Homestead, Byzantium, Constantinople, Petersburg 업그레이드를 적용할 블록 번호, EIP의 적용, 그리고 어떤 합의 알고리즘을 사용할지를 지정하는 부분 등을 포괄한다. 이 ChainConfig
는 노드를 시작할 때 원하는 설정을 담은 json 파일을 지정하는 것으로 구성할 수 있다. 가령 다음과 같다.
{
"config": {
"chainId": 950327,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"clique": {
"period": 5,
"epoch": 30000
}
},
"difficulty": "20",
"gasLimit": "7000000",
"alloc": {
"6282ad5f86c03726722ec397844d2f87ced3af89": { "balance": "60000000000000000000" }
}
}
다음과 같이 위 json 파일을 지정할 수 있다.
geth init ./myConfig.json
Clique에 period와 epoch을 지정하는 것으로 PoA 합의 알고리즘의 세부 사항을 조정할 수 있다. 이는 CliqueConfig
가 다음과 같이 정의돼 있기 때문이다.
EthashConfig
는 다음과 같이 빈 구조체로 정의돼 있다.
Engine 인터페이스
CreateConsensusEngine()
의 반환값은consensus.Engine
이다. consensus/consensus.go
에 정의된 Engine 인터페이스(interface)를 살펴보자.
Engine 인터페이스는 위와 같은 메소드(method)들을 가지고 있다. 이들은 블록과 블록체인의 검증과 관계된 매우 중요한 메소드들이며, 각각을 수정하는 것만으로도 다른 종류의 합의 알고리즘을 만들 수 있다.
새로운 합의 알고리즘을 추가하고자 한다면 최소한 위 인터페이스의 규격을 준수해야 한다. 달리 말하자면, 우리의 새로운 rawpow 합의 알고리즘은 적어도 다음의 메소드들을 가질 것이다.
Author
Author()
는 주어진 블록에 대해 블록을 생성한 이더리움 주소를 반환한다. 합의 엔진에 따라 블록 헤더의 코인베이스(coinbase) 주소와 다를 수 있다.
VerifyHeader
VerifyHeader()
는 블록 헤더가 합의 규칙을 준수하는지 검증한다. 이 메소드에서 모든 검증을 수행할 수도 있지만, 별도의 VerifySeal()
메소드에서 seal의 검증을 수행할 수도 있다.
VerifyHeaders
VerifyHeaders()
는 VerifyHeader()
와 유사한 역할을 수행하나 하나의 블록 헤더가 아닌 다수의 블록 헤더 묶음을 검증한다.
VerifyUncles
VerifyUncles()
는 주어진 블록의 엉클 블록이 합의 규칙을 준수하는지 검증한다.
VerifySeal
VerifySeal()
은 블록 헤더에서 seal의 검증을 담당한다. 가령 Ethash에서는 주어진 블록이 PoW 난이도 요구를 충족했는지를 검증한다.
Prepare
Prepare()
은 블록 헤더 중 합의에 필요한 영역을 초기화한다. 가령 Ethash에서는 Prepare()
에서 난이도 영역을 CalcDifficulty()
를 통해 초기화한다.
Finalize
Finalize()
는 블록의 최종 집계이다. 가령 해당 블록의 보상, 엉클 블록의 보상, 그리고 최종 상태(state)를 집계해 최종 블록 헤더를 구성한다.
Seal
Seal()
은 주어진 입력에 대해 seal된 새 블록을 생성한다. 가령 Ethash에서는 흔히 채굴(mine)이라 불리는 과정을 수행하며, 블록 난이도 조건을 충족하는 논스(nonce)를 찾는다.
CalcDifficulty
CalcDifficulty()
는 난이도 조정 알고리즘이다. 블록이 충족해야 할 난이도를 반환한다.
APIs
APIs()
는 지정한 합의 엔진이 제공하는 RPC API들을 반환한다. 본 시리즈에서는 새로운 API를 추가하는 방법 역시 소개할 것이다.
Part 1에서는 합의 엔진을 중점으로 geth의 대략적인 흐름을 살펴봤다. 또한 새로운 합의 알고리즘을 추가하기 위해서 geth의 어떤 부분을 수정해야 할 것인지를 살펴봤다. eth/backend.go
와 params/config.go
를 수정해야 할 것으로 보인다. 다음 포스팅에서 실제로 이 두 코드를 수정해 rawpow 합의 알고리즘을 지원하도록 할 것이다.
또한 consensus/consensus.go
에 정의된 Engine 인터페이스의 메소드들을 살펴보고, 각 메소드의 역할을 가볍게 살펴봤다. 추가하고자하는 rawpow 합의 알고리즘은 어떤 규칙을 가지고 있으며, 따라서 각 메소드를 어떻게 구성해야 하는지는 다음 포스팅에서 자세히 다뤄보겠다.
박상현(Luke Park) @서울대학교 가상머신 및 최적화 연구실
lukepark327@gmail.com