블록체인 확장성 솔루션 시리즈 3–2 :: Relayer
Jae-Yun Kim (Ben)
Seoul Nat’l Univ. Blockchain Academy Decipher(@decipher-media)
서울대학교 블록체인 학회 ‘디사이퍼(Decipher)’에서 블록체인의 스케일링 솔루션에 관한 글들을 시리즈로 연재합니다. 시리즈의 네 번째 주인공은 Interchain solution 입니다. 3–1. Interchain Overview, 3–2. Relayer(BTC Relay)코드 리뷰로 나누어 설명합니다.
서론
Relayer가 무엇인지 설명하기에 앞서 Interchain solution이 어떻게 확장성 솔루션으로써 기능할 수 있는지에 대해 다시 한 번 자세하게 짚고 넘어가려고 한다.
서버 아키텍처 용어 중에 스케일 업(Scale-Up)과 스케일 아웃(Scale-out)이라는 개념이 있다. 서버가 처리할 수 있는 한계 이상의 데이터나 컴퓨팅 파워가 요구될 때 서버의 CPU나 메모리 등을 교체함으로써 성능 한계를 증가시키는 방식을 스케일 업이라고 부른다. 또한 서버의 수를 늘려 작업을 분산시킴으로써 성능 한계를 증가시킬 수도 있는데, 이러한 방식을 스케일 아웃이라고 한다. 두 방식 중에서 네트워크 성능을 크게 증가시킬 수 있는 것은 스케일 아웃이다. 하지만 블록체인은 분산 원장 기술을 이용해서 모든 노드가 똑같은 정보를 처리하고 저장하기 때문에 네트워크에 노드 수를 늘린다고 해서 네트워크 처리 성능이 올라가지 않는다. 오히려 노드 수가 증가하면 트랜잭션이 발생할 확률이 더 커지기 때문에 네트워크 속도가 더 느려질 수 있다. 왜냐하면, 네트워크에 참여하는 노드들은 네트워크를 유지하는 보상으로 디지털 자산인 코인을 받으므로 이것을 다른 곳으로 이동시키는 트랜잭션을 발생시킬 확률이 발생하기 때문이다. 즉, 블록체인에서는 전통적인 서버 아키텍처 방식의 최적화 기법을 그대로 적용하기 어렵다.
그렇다면 블록체인 아키텍처에서 성능을 증가시키는 방법은 무엇일까? 블록체인 아키텍처에서는 노드를 늘리는 것이 아니라 블록체인 네트워크를 늘림으로써 스케일 아웃의 효과를 누릴 수 있다. 만약 동일한 블록체인 네트워크를 하나씩 증가시킨다면 전체 블록체인 네트워크의 성능은 선형적으로 증가할 것이다. 혹은, 네트워크의 수를 늘리는 것이 아니라 이미 존재하는 네트워크를 여러 개로 쪼개어도 동일한 효과를 볼 수 있다. 이렇게 하면 네트워크에 추가적인 리소스를 투입하지 않고도 성능을 증가시킬 수 있지만, 각 네트워크를 유지하는 노드의 수가 줄어듦으로써 공격에 취약해지기 때문에 노드 수에 최소 한계(minimum threshold)를 두거나, PoA(Proof-of-Authority)[1] 등 적은 노드로도 운영이 가능한 합의 알고리즘을 사용해야 한다.
하지만 블록체인 네트워크를 늘리거나 쪼개서 네트워크 성능을 증가시키기 위해서는 각 네트워크끼리 정보 교환, 즉 커뮤니케이션이 가능해야 한다는 전제가 필요하다. 블록체인 네트워크끼리 커뮤니케이션을 가능하게 하는 방법으로는 Adam Back이 제시한 Two Way Pegging, Tier Nolan이 제시한 Atomic Swap[2], 블록체인 네트워크를 중계하는 Relay 방식[3]이 있으며, 이 외에도 다양한 방법들이 제시되고 있다. 본 글에서는 이 중에서 실제 구현체가 존재하는 이더리움 재단(Ethereum Foundation)과 컨센시스(Consensys)의 BTC Relay에 대한 코드 분석을 통해 Relayer가 어떻게 동작하여 블록체인 네트워크 간 커뮤니케이션을 가능하게 하는지 알아보려고 한다. 여기서부터는 편의상 ‘블록체인 네트워크’를 ‘체인’이라고 부르겠다.
Relayer란?
Relayer는 ‘중계자’라는 뜻으로, 어떤 블록체인을 관찰하여 그 정보를 다른 블록체인에게 알려주는 주체를 의미한다. Relayer는 체인 외부에 있는 주체이기 때문에 블록체인에 참여하는 참여자들은 Relayer를 믿을 수밖에 없는 중앙화 이슈가 발생한다. 이러한 문제를 해결하는 방법은 여러 Relayer들이 서로 경쟁하게 만듦으로써 탈중앙성을 확보하는 것이다. 본 글에서는 이더리움과 컨센시스가 함께 만든 BTC Relay의 소스코드를 분석함으로써 Relayer가 어떤 방식으로 동작하는지 자세하게 알아볼 것이다.
BTC Relay
BTC Relay는 비트코인을 사용해서 이더리움 기반의 서비스를 이용할 수 있게 하는 체인 간 중계(relay) 프레임워크이다. 이를 위해서는 이더리움 체인에 배포된 BTC Relay 스마트 컨트랙트에 비트코인 체인의 정보를 비트코인의 fullnode(블록체인의 모든 블록 정보를 저장하고 있고, 채굴을 통해 새로운 블록을 생성하는데 참여하는 노드)에서 주기적으로 관찰하여 저장하는 Relayer가 필요하다. 그러나 스마트 컨트랙트에 비트코인 블록체인의 정보를 모두 저장할 수는 없으므로 블록 헤더만을 저장해서 유효성을 검증하는 SPV(Simplified Payment Verification) 방식으로 비트코인 블록체인의 정보를 이더리움의 스마트 컨트랙트에 기록한다[4]. Relayer들은 블록 헤더값을 BTC Relay에 기록하면서 수수료(fee)를 기록하고, 어플리케이션은 그 블록 헤더값을 이용해서 유효성을 판단하고 싶은 트랜잭션에 대한 검증(verification)을 수행한다. 만약 검증 이후에 이상이 없다면 기록된 수수료를 지불하고 이더리움 상에서의 루틴을 수행하게된다. 수수료 덕분에 Relayer들은 자발적으로(autonomous) 동작하게 되고, 유저들은 이 모든 과정들을 몰라도(seamless) 비트코인을 사용해서 이더리움 어플리케이션을 실행시킬 수 있다.
BTC Relay의 구성 요소
* 이 문서는 Ethereum Repository에 있는 BTC Relay Repository(https://github.com/ethereum/btcrelay/)의 3cdf41a87750bfcc2ea62cb2c5d693fd71bdd47f 커밋을 기준으로 작성하였다.
*코드에서 핵심이 되거나 이해하기 어려운 로직에 대한 설명을 위주로 분석을 진행할 것이고, 코드를 세부적으로 지칭하기 위해서 {파일 이름}:{줄 번호}(code snippet번호) 포맷을 사용할 것이다. 예를 들면, fetchd.py:23(3)은 fetchd.py 파일의 23번째 줄, 본문에 삽입된 code snippet의 3번째 줄을 지칭한다.
BTC Relay는 Relayer, 스마트 컨트랙트, 그리고 이더리움 어플리케이션으로 구성되어 있다. 이제 각 구성 요소의 동작 방식을 코드를 통해 분석해보자.
Relayer
Relayer는 비트코인 블록체인의 블록 헤더값을 스마트 컨트랙트에 기록하고 댓가로 수수료를 받는 역할을 수행한다. Relayer는 마치 퍼블릭 블록체인을 구성하는 노드 같이 컴퓨팅 리소스만 있다면 누구나 자발적으로 참여할 수 있으므로 탈중앙성(decentralizaion)을 가진다고 볼 수 있다.
fetchd.py 파일에 Relayer의 동작에 대한 코드가 파이썬으로 작성되어 있다.
가장 먼저 각종 상수들을 정의한다. Relayer 데몬이 실행되는 주기(fetchd.py:23(3), 5분), Header를 저장하는데 드는 가스(fetchd.py:24(4), 1,200,000wei), 그리고 한 번 fetch할 때 가져오는 header의 갯수인 chunk size(fetchd.py:26(6), 5) 등이 정의되었다.
get_hash_by_height는 BTC Relay에 기록하기 위한 비트코인 블록 헤더값을 가져오는(fetch) 함수이다. url을 보면(fetchd.py:54(2)) blockchain.info 사이트를 fullnode로 사용한다는 것을 알 수 있다. 이는 하나의 fullnode를 믿는 것으로, blockchain.info가 잘못된 정보를 제공하거나 정보를 주지 않는다면 문제가 발생할 수 있다. 따라서 이러한 문제를 해결하려면 여러 개의 fullnode를 관찰하여 상호 검증을 하거나 서로 다른 fullnode를 관찰하는 여러 개의 Relayer가 필요한데, 현재 BTC Relay에는 fullnode 주소가 하드코드로 박혀 있으니 이 부분에 대한 고찰이 부족하다고 볼 수 있다. 여러 개의 Relayer가 여러 fullnode를 관찰하면서 참여할 수 있도록 유인하는 인센티브 구조를 포함한다면 fullnode에서 데이터를 가지고 오는 부분에서 탈중앙성을 가지도록 프로그램을 개선할 수 있을 것이다.
main 함수를 보면 argument로 daemon 변수를 주지 않거나 False로 설정했을 때는 run 함수가 한 번만 수행되고(fetchd.py:132–134(4–6)), True로 설정되었을 때는 앞서 설정한 SLEEP_TIME을 주기로 데몬으로써 계속 실행된다는 것을 알 수 있다(fetchd.py:136–151(9–24)). 이 때 컨트롤 가능한 예외 케이스가 발생하면 최대 4번까지 재시도를 하고, 그 이외의 예외 케이스가 발생했을 때는 데몬을 종료한다.
이제 Relayer의 핵심 로직인 run 함수를 살펴보자.
run 함수는 먼저 getBlockchainHead() 함수를 호출하여 BTC Relay 스마트 컨트랙트에서 가장 마지막 블록의 헤더값을 받아온다(fetchd.py:155(2)). 이 때 getBlockchainHead() 함수는 BTC Relay 스마트 컨트랙트에서 같은 이름을 가진 getBlockchainHead() 함수(btcrelay.se:232)를 호출한다. 만약, 마지막 블록의 헤더값이 존재하지 않으면 프로그램이 종료되므로 BTC Relay 컨트랙트를 배포(deploy)할 때 반드시 초기화 함수를 호출하여 첫 번째 블록 헤더값을 설정해 주어야 할 것이다.
그 다음으로 블록을 가져오기 위한 시작 블록을 지정한다. 만약에 argument로 startBlock이 지정되었으면 그 블록의 탐색을 시작할 height로 설정한다.(fetchd.py:169(9)) 만약 지정되지 않았다면 BTC Relay에 기록되어 있는 마지막 블록을 가져온다(fetchd.py:171(11)). 그리고 비트코인 블록체인에서 그 height에 해당하는 블록 헤더값을 가져온 뒤에(fetchd.py:172(12)) 다시 가져와야 할 블록의 height를 BTC Relay에서 가져온 블록으로 설정한다(fetchd.py:173(13)). 이러한 과정을 거치는 이유는 이더리움은 블록 생성 시간이 약 15초로 매우 빠르기 때문에 포크(fork)가 자주 발생하고, 따라서 메인 체인이 바뀌는 reorganization(줄여서 reorg)이 빈번하게 일어나기 때문이다. Reorg가 되어 메인 체인이 되지 못한 블록을 고아(orphan) 블록이라고 하는데, 이전까지 BTC Relay에 기록했던 비트코인의 블록 헤더값이 올바른 값이라고 하더라도(!) 블록이 고아가 돼버리면 스마트 컨트랙트에 기록되는 블록 해시값 및 높이와 실제 블록의 해시값 및 높이가 서로 달라질 수 있다. 따라서 이러한 경우가 발생했을 때는 스마트 컨트랙트에 기록된 블록체인을 가장 최근 블록에서부터 과거 블록으로 따라가면서(fetchd.py:191(31)) 고아 블록으로 reorg되기 직전의 블록을 찾아야 한다. 만약 10개 블록을 넘어가도록 서로 일치하는 블록을 발견하지 못한다면 더 과거에서 reorg되었다고 가정하고, 현재 이더리움 체인이 고아 체인이 되었다고 가정한다. 그리고 이더리움 블록체인을 다시 받아와서 서로 일치하는 블록을 찾는 과정을 처음부터 반복한다. 이 과정을 nTime번 반복하는데(fetchd.py:166(6)), 만약 nTime번 반복해도 서로 일치하는 블록을 찾지 못한다면 big reorg가 발생한 것으로 판단하고 프로그램을 종료한다(fetchd.py:193–198(33–38)). 이 때는 블록을 가져오는 startBlock을 작게 하여 big reorg가 발생하기 이전 블록부터 받아와야 할 것이다.
그런데 실제 코드에서는 마지막에 break문을 써서 실제로는 이러한 상황이 발생했을 때 서로 일치하는 블록을 반복해서 찾지 않는다(fetchd.py:202(42)). 이것은 코딩 상의 실수로 보이며, break문 위에 if chainHead == realHead: 코드를 추가함으로써 반복하도록 바꾸었다. 고친 코드는 BTC Relay repository에 pull request를 보내 두었는데, 현재 BTC Relay 프로젝트가 관리가 되고 있지 않은 것 같아서 merge가 될지는 모르겠다. Pull request는 아래 링크에서 확인할 수 있다.
Reorg에 대한 싱크가 다 맞춰지고 나면, 비트코인 체인에서 실제 높이를 받아와서(fetchd.py:175(1)) argument로 들어온 startBlock(fetchd.py:178(4)), 혹은 BTC Relay의 마지막 블록 높이(fetchd.py:180(6))와의 차이만큼 블록 헤더를 가져온다(fetchd.py:192–194(18–20)).
열심히 설명했지만 사실 BTC Relay는 현재 동작하지 않는다. 왜냐하면 비탈릭이 fetchd.py에서 비트코인 블록체인에서 정보를 가져오는 라이브러리인 pybitcointools 저장소를 날려버렸기 때문이다 (…)
“나는 이 라이브러리를 더 이상 유지할 시간이 도저히 나지 않는다. 만약에 당신이 이것을 포크할 의향이 있거나 유지보수 없이 사용할 생각이 있다면 로컬에 클론하고 1 commit만 되돌려라” — 비탈릭 부테린
비탈릭이 손을 놓았으니 pybitcointools를 포크하고 제대로 개발해서 Ethereum에 기여해 보는 것도 재미있는 일일 것이다.
BTC Relay 스마트 컨트랙트
BTC Relay 스마트 컨트랙트는 이더리움 체인에 배포되어 비트코인의 SPV Client 역할을 하는 스마트 컨트랙트이다. SPV Client는 사토시의 논문에서 등장했던 개념으로, 비트코인 블록체인의 헤더 값만을 저장하여 가지고 있다가 어떤 트랜잭션에 대한 검증이 필요할 때 그 트랜잭션이 들어 있는 블록이 SPV Client에 들어 있는지를 확인하고 블록 해시값을 다시 구해서 해당 fullnode가 제공한 블록이 조작되지 않았음을 보장하는 장치이다.
Serpent 파일(.se)은 현재 Syntax Coloring을 지원하지 않아서 code snippet을 가져올 때 Serpent의 모태가 된 언어인 파이썬의 확장자 .py를 붙여(.se.py) 보기 쉽게 Syntax Coloring을 하였다.
앞선 코드 분석에서 BTC Relay 컨트랙트의 마지막 블록 헤더값이 존재하지 않으면 Relayer가 종료되는 것을 확인하였다. setInitialParent는 마지막 블록의 헤더값을 설정해주기 위해서 BTC Relay 컨트랙트를 배포(deploy)하고 나서 반드시 호출해야 하는 초기화 함수이다. highScore를 flag로 하여 이 함수가 단 한번만 불리도록 만들고(btcrelay.se:63–67(2–6)), argument로 들어온 blockHash를 heaviestBlock으로 설정하고(btcrelay.se:69(8)), blockHash값을 key로 가지는 block에 height와 chainWork를 저장한다(btcrelay.se:71–78(10–17)). chainWork에 대한 자세한 설명은 아래 링크에서 확인할 수 있다.
BTC Relay 스마트 컨트랙트를 배포하고 초기화 하고 나면, 그 스마트 컨트랙트에 있는 다른 함수들은 모두 event-driven으로 동작하며, Relayer와 이더리움 어플리케이션이 각각 호출하는 함수가 다르다. 따라서 헷갈리지 않도록 이 항목에서는 Relayer가 호출하는 함수를 설명하고, 이더리움 어플리케이션이 호출하는 함수는 다음 항목인 이더리움 어플리케이션에서 설명하겠다.
storeBlockWithFeeAndReceipient는 Relayer가 블록 헤더값을 BTC Relay 스마트 컨트랙트에 기록할 때 호출하는 함수이다. 만약에 이 함수를 호출하는 Relayer와 수수료를 받을 노드가 같다면 storeBlockWithFee만 호출해도 된다. 이 함수에서는 blockHeaderBytes가 btcrelay.se 파일에 정의된 storeBlockHeader 함수를 호출해서 헤더 값을 저장하고 만약 이상 없이 잘 저장됐다면(incentive.se:22(9), res != 0) 수수료 정보를 저장한다. 마지막으로 remainingGas를 계산하는데(incentive.se:25(12)), 수수료를 저장하는 데 필요한 가스비를 동적으로 계산하여 나중에 다른 Relayer가 블록 헤더값을 다시 기록하려고 할 때 참고하기 위함이다.
수수료 정보를 저장하고 난 다음에는 다른 Relayer가 기록한 블록에 대하여 challenge를 하기 위해 앞서 정의된 gasPriceAndChangeRecipientFee(incentive.se:9)를 설정 한다. gasPriceAndChangeRecipientFee 변수에 저장되어 있는 바로 직전 트랜잭션에서 발생시킨 gasPrice를 가져와서(incentive.se:30(3)) 1/1024 스케일로 상한과 하한을 정하는 클램핑을 한다(incentive.se:31–38(4–11)). 클램핑을 하는 이유는 gasPrice의 변화량이 gasPriceAndChangeRecipientFee에 큰 영향을 미치지 않도록 하기 위함이다.
challenge에 필요한 gas price는 블록 헤더를 기록하는 데 필요한 가스 * 클램핑된 gas price * 2로 정해진다(incentive.se:40(13)). 클램핑 스케일에 대해서 덧붙이자면 주석에도 나와있듯이 이더리움에서 block gas limit을 조정하는 방식이라고 하니, gas price에 따라 바뀌는 변수를 사용할 때 스케일링 방식을 그대로 가져다 쓰면 된다.
feePaid함수는 수수료를 받을 Relayer, 즉 recipient에게 기록된 수수료를 지불하는 함수이다. 기록된 수수료보다 더 높은 금액이 송금됐고(incentive.se:52(6)), overflow가 아니면(incentive.se:53(7)) 해당 블록 헤더의 해시값에 매핑된 Relayer의 주소를 가져와서(incentive.se:54(8)) 송금된 금액을 보내준다(incentive.se:57(11)). recipient에게 송금을 성공하면 1, 실패하면 0을 리턴한다(incentive.se:61–62(15–16)).
이미 기록된 어떤 블록에 대하여 수수료를 받을 어카운트(recipient)를 바꾸려면 changeFeeRecipient 함수를 호출하면 된다. 먼저 블록 헤더를 기록한 Relayer에게 기록된 수수료를 지불하고(incentive.se:69(5)) 수수료와 recipient를 바꾼다(incentive.se:74(10)). 그래서 BTC Relay 스마트 컨트랙트에 블록을 기록한 Relayer는 적어도 한 번 이상의 수수료를 받게 된다. 단, 바꾸는 수수료는 이전 수수료보다 낮게 설정해야 하는 것이 규칙이다(incentive.se:73(9)).
Relayer 코드가 동작하지 않게 되어버렸으므로 BTC Relay 스마트 컨트랙트 역시 동작하지 않을 것이라고 예상할 수 있다. 실제로 etherscan.io에 배포된 BTC Relay 스마트 컨트랙트 주소로 들어가보면 2018년 2월 16일에 마지막으로 발생한 트랜잭션 이후 더 이상 새로운 트랜잭션이 발생하지 않는다는 것을 확인할 수 있다.
이더리움 어플리케이션
이더리움 어플리케이션은 비트코인을 사용해서 실행하고자 하는 이더리움 분산 어플리케이션이며, 특정한 비트코인 주소로 비트코인이 전송된 것을 BTC Relay 스마트 컨트랙트에 저장된 블록 헤더 정보로 검증하기 위해 BTC Relay 스마트 컨트랙트의 함수 중 일부를 사용한다.
verifyTx는 어떤 트랜잭션이 유효한지 검증하는 함수이다. 여기서 어떤 트랜잭션은 이더리움 어플리케이션을 이용하기 위해서 비트코인을 특정한 주소로 전송하는 트랜잭션이다. verifyTx 함수는 트랜잭션(txBytes), 트랜잭션 인덱스(txIndex), 블록에 함께 담긴 트랜잭션 정보(sibling), 그리고 블록 해시값(txBlockHash)을 가지고 머클 증명을 통해 해당 트랜잭션의 유효성을 검증한다(btcrelay.se:169(14)). 이 때 쓰이는 helperVerifyHash 함수를 간단하게 살펴보면 다음과 같다.
먼저 Relayer가 기록한 수수료 이상의 비용을 지불하는지를 확인하고(btcrelay.se:188(12)) 몇 가지 추가적인 검사를 한 뒤에 머클 증명을 수행한다(btcrelay.se:200(24)). 그리고 머클 증명이 성공하면 1을 리턴하고(btcrelay.se:205(29)), 그렇지 않다면 error를 리턴한다(btcrelay.se:208(32)). 머클 증명을 스마트 컨트랙트에서 수행할 수도 있고, 블록 해시값으로 블록 헤더만 받아서 직접 수행할 수도 있다. 머클 증명을 스마트 컨트랙트에서 한다면 완전히 신뢰가능하지만 비용이 많이 드는 어플리케이션을, 탈중앙성을 약간 포기하고 블록체인 밖에서 한다면 적은 비용이 들지만 중앙화 이슈가 존재하는 어플리케이션을 만들 수 있을 것이다. 어플리케이션 개발자는 블록체인을 이용한 어플리케이션을 설계할 때 이러한 tradeoff를 잘 고려해야 할 것이다.
이더리움 어플리케이션이 SPV Client로써 동작하는 BTC Relay 스마트 컨트랙트에서 블록 헤더를 가지고 올 수 있는 함수이다. 앞서 설명했던 feePaid 함수를 사용하여 어플리케이션이 Relayer가 기록했던 수수료보다 많은 이더리움을 송금하면(btcrelay.se:318(4)) 블록 헤더 정보를 리턴해준다(btcrelay.se:323(9)).
이더리움 어플리케이션은 이 정보를 바탕으로 조작되지 않은 블록 정보를 얻을 수 있고, 그 블록에 있는 트랜잭션 정보를 신뢰할 수 있으므로 비트코인 송금자에게 이더리움 서비스를 제공한다.
마치며
이상 BTC Relay를 구성하는 Relayer, BTC Relay 스마트 컨트랙트, 그리고 이더리움 어플리케이션에 대해서 살펴보았다. 비록 단방향으로 동작하고 reorg 처리가 굉장히 복잡하다는 점, 가스비가 많이 들어간다는 점, 그리고 하나의 fullnode에 의지한다는 단점이 있지만, Relayer들에게 인센티브를 주고 서로 수수료 경쟁을 시킴으로써 탈중앙성을 확보하여 비교적 신뢰성이 있는 체인 간 커뮤니케이션에 성공한 사례라고 볼 수 있다. 비록 지금은 서비스가 되고 있지 않지만 BTC Relay의 아이디어는 굉장히 참신한 것이었고, 단점을 보완하여 잘 발전시킨다면 체인 간 커뮤니케이션을 구현하는데 큰 도움이 될 것이라고 생각한다.
참고자료
[1] Poon, Joseph, and Vitalik Buterin. “Plasma: Scalable Autonomous Smart Contracts.” White paper (2017).
[2] Back, Adam, et al. “Enabling blockchain innovations with pegged sidechains.” URL: http://www. opensciencereview. com/papers/123/enablingblockchain-innovations-with-pegged-sidechains (2014).
[3] Warren, Will, and Amir Bandeali. “0x: An open protocol for decentralized exchange on the Ethereum blockchain.” (2017).
[4] Nakamoto, Satoshi. “Bitcoin: A peer-to-peer electronic cash system.” (2008).
[5] http://btcrelay.org/, May 22, 2018