본문 바로가기
블록체인

이더스캔 분석 1 - 블록 페이지

by dbadoy 2023. 2. 26.

이더스캔 분석 2 - 트랜잭션 Overview 페이지

 

이더리움 기반에서 개발을 한다면 이더스캔을 한 번쯤은 이용해보았을 것이다. 제공하는 데이터가 풍부하여 디버깅에 많은 도움을 준다. 트랜잭션에서 발생한 이벤트들을 직접 가져와 파싱하는 과정은 꽤 번거로운데, 이더스캔에서는 바로 눈으로 볼 수 있다.

이더스캔에서 보여주는 데이터들은 어디서 어떤걸 얻고, 어떻게 가공되어서 우리에게 보여지고 있을까? 이 값들을 분석하려면 각 필드들이 어떤 역할을 하는지 알고 있어야 하기 때문에, 이를 분석해보는 것은 의미 있는 일이지 않을까 싶다.

 

'이더스캔 분석'에서는 Go언어와 go-ethereum에 속한 ethclient 라이브러리를 이용할 것이다. 그리고 원하는 데이터를 구하는 과정은 기록하지만 DB에 저장하는 과정은 생략하려고 한다. 당장 블록 익스플로러를 서비스하려는 목적이 아니라, 개인적인 연구 목적이기 때문에... 하지만 데이터 가공 방법을 안다면 RDBMS건 Document DB건, 저장하는 것은 그리 어렵지 않은 작업이다.

 

서론은 이만 줄이고, 첫 번째 글은 블록 페이지를 분석해보려 한다.

많은 필드는 이더리움에서 블록을 가져오면 채워줄 수 있어 보인다. 위 사진의 블록을 ethclient를 이용하여 가져와보자.

{
  "parentHash": "0x623617c362c60a6dfdf001d0c18f99e0d51763eea02ae748c0d78eaa3466c0de",
  "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  "miner": "0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5",
  "stateRoot": "0x5d1572bb02434469693eb9aa159b8b0b051de25c9f2a369cff7527d6f56cca8e",
  "transactionsRoot": "0x74aa0e05f039b59be008259497df7fe733b7150efe366b9556d9b6c1953476b2",
  "receiptsRoot": "0xf599827f251a42270cd5bee9eaeee9809653eea16eb1eea41b2710ab89723af3",
  "logsBloom": "0x9cf3eafcdd87e77cfde9b4fad34bfba9d943de47ecedefde1cddda7ff4f37de9d22be3f8ff8d7fe356477fd83aabd5bbfbbd36e79f7be96d4ffefbb570eefe41aefbfbf4ddf77b3e6b5b77fdc7d8aeb1b177d7e17755697999afbd3cff639d7cdfef47f59e6edffbf04e53e9f78f7afdb79c0c273d3fbf5f17eda75e9ddaefe2f5def7f77137395e8ddbdaff65fd98eeeff4bfb9bf5cfa5f78397ffebbbbbd7ffffd971e1afced3af7f66cdc9dfedff2977cd924e7bdf3dff17e3be7fdde9f77eb84094360e37dac969fdee2d8debbced376becf8f1bcad74e2eff7fe9abfe7b3cfbffbfbfe0f6bb3e3fddb3f509e725ddf6d3ceb3fe667e6bc76bd3d775ffe3",
  "difficulty": "0x0",
  "number": "0xfecb28",
  "gasLimit": "0x1c9c380",
  "gasUsed": "0x1c9984a",
  "timestamp": "0x63f8ab6f",
  "extraData": "0x496c6c756d696e61746520446d6f63726174697a6520447374726962757465",
  "mixHash": "0x776049966eb01334af64c74f58e49e462c97e45261d9f988988a360a3e1c99ca",
  "nonce": "0x0000000000000000",
  "baseFeePerGas": "0x5c23eced6",
  "withdrawalsRoot": null,
  "hash": "0x557956ff35a81ced740060e4bd1fd9876ee55e845547aa764f8876e0e2602d8d"
}

 

이더스캔의 블록 페이지에서 필요한 항목들을 정리하고, 블록 헤더로부터 바로 얻을 수 있는 데이터는 취소선으로 지워보겠다. 그리고 하나씩 해결 해 나가보자.

Height, Status, Timestamp, Proposed On, Transactions, Fee Recipient, Block Reward, Total Defficulty, Size, Gas Used, Gas Limit, Base Fee Per Gas, Burnt Fees, Extra Data, Hash, Parent Hash, StateRoot, Nonce

 

Status

Status는 Finalized, Unfinalized (Safe), Unfinalized 세 가지의 상태값을 갖는다. Finalized가 뭘까?

Finalized는 블록이 revert 되지 않는 상태라는 뜻이다. 이는 반대로말하면 Unfinalized 상태의 블록은 revert 될 수 있다는 뜻이 된다.

비트코인의 PoW에서는 특정 블록뒤에 6개의 블록이 붙으면 Finalized를 갖는다고 한다. 6개의 블록이 뒤에 붙기 전에는 해당 블록이 취소될 가능성이 있기 때문이다. 왜 이런 현상이 발생할까?

PoW는 간단히 말해서 아주 어려운 문제를 가장 빨리 푸는 노드가 블록을 생성하고 보상을 받는 합의 알고리즘이다. 그러나 세계 곳곳에 많은 노드들이 분산되어 있기 때문에 어려운 문제를 동시에 푸는 경우가 생기거나, 지역에 따라 전파되는 블록이 다를 수도 있다. 비트코인은 하나의 상태를 갖는 분산원장을 구현하기 위해, 각 노드가 다른 원장 상태로 들어섰을 때(포크 발생) 하나의 원장을 골라 정당한 체인이라고 결정하고, 그와 다른 원장을 갖고 있던 노드들은 보유한 원장을 되돌린 후 정당한 체인의 원장을 따라가게 된다. 

비트코인에서 정당한 체인을 선택하는 규칙을 '나카모토 컨센서스'라고 부른다. 나카모토 컨센서스는 단순히 길이가 더 긴 체인을 정당한 체인으로 선택한다.

https://medium.com/dsrv/classic-one-%EB%82%98%EC%B9%B4%EB%AA%A8%ED%86%A0-%EC%BB%A8%EC%84%BC%EC%84%9C%EC%8A%A4-150a54d33a18

위 그림과 같이 두 갈래로 포크가 일어난 상태에서 각자의 체인에 블록이 생성되었다고 가정해보자. 나카모토 컨센서스에 따라 위의 체인은 1개, 아래의 체인은 2개의 블록이 더 연결되어 있기 때문에 아래의 체인을 정당한 체인으로 선택하게 될 것이다.

'비트코인에서는 특정 블록에 6개 블록이 연결되면 Finalized 블록' 이라는 말은, 체인에 포크가 발생하더라도 각 체인에 여섯번이나 동시에 블록이 발생하여 연결될 가능성이 매우 희박하다는 것에 근거하는 확률적 Finalized 기반의 블록체인이라는 뜻이다(Safety, Liveness 키워드를 같이 봐도 좋다). 

 

이더리움도 Finalized, Unfinalized 상태를 갖는 확률적 Finalized 기반 블록체인이다. 이더리움에서는 어떤 식으로 정당한 체인을 고르는지 알아보면, 이더스캔 블록 페이지에 Status를 채워 넣을 수 있겠다.

 

Ethereum Casper

https://www.coindeskkorea.com/news/articleView.html?idxno=80258

이더리움은 비트코인처럼 블록 단위로 정당한 체인을 선택하는 것이 아닌, 32개 블록의 간격을 갖는 체크포인트마다 정당한 체인을 선택하게 된다. 이에 대한 자세한 내용은 DSRV에서 정리한 https://www.coindeskkorea.com/news/articleView.html?idxno=80258 포스트를 참고하는 것이 좋겠다.

요점만 말하자면 다음 체크포인트를 정할 때, 검증인들끼리 어느 체인을 체크포인트로 결정할 것인지에 대한 투표를 진행하고 2/3 이상의 투표를 받아 연결되는 것을 'Supermajority Link'라고 한다. 연결을 하면, 현재 체크포인트는 Justified 상태라고 할 수 있다.

 

                                      현재 체크 포인트 - - - Supermajority Link - - -> 다음 체크 포인트

                                          (Justified)

 

이 상태에서 다음 체크포인트가, 다다음 체크포인트에 Supermajority Link 연결하면 현재 체크포인트는 Finalized 블록이 되었다고 한다.

 

         현재 체크 포인트 - - - Supermajority Link - - -> 다음 체크 포인트 - - - Supermajority Link - - -> 다다음 체크 포인트

            (Finalized)                                                            (Justified) 

    

이더스캔에서는 Justified 상태를 Unfinalized (safe) 로 정의하고 있다.

그렇다면 특정 번호가 Finalized인지 Unfinalized인지 판단하여 보여주려면 블록번호와 블록 Height를 비교해보면 되지 않을까?

if 블록 Height > 현재 블록번호 + (32 * 2)
	Finalized
else if 블록 Height > 현재 블록번호 + 32
	Unfinalized (safe)
else 
	Unfinalized

이와 같이 하면 조금 늦더라도 값을 보여줄 수는 있을 것 같은데... 이게 가장 좋은 방법이라는 생각은 안든다. 만약 찾게 되면 업데이트 하도록 한다.

Proposed On

이더스캔에서는 비콘 체인의 slot 번호와 epoch를 보여주고 있다. 이를 보여주기 위해서는 Consensus layer 노드에서 데이터를 가져와야 할 것으로 보인다. CL에서 가져와야 하는 데이터까지 다룬다면 너무나 범위가 넓어질 것 같아 일단 스킵한다.

CL 노드를 따로 띄우지 않더라도 비콘 체인 값을 가져올 방법이 있다면 추후에 추가하도록 한다.

Transactions

트랜잭션 수와 인터널 트랜잭션 수를 보여주어야 한다.

len(block.Transactions())

 

인터널 트랜잭션은? 

debug, trace 등의 RPC method로 블록의 OPCODE 별 가스 사용 현황등을 자세히 가져올 수 있다. 가져온 값에서 OPCODE가 Call, DelegateCall 인 값들이 인터널 트랜잭션이고 이 수량을 보여주도록 한다.

 Fee Recipient

블록 헤더의 Coinbase() 메서드 리턴값을 넣어준다.

Block Reward

Block Reward를 구하려면 EIP-1559를 알아야 한다. EIP-1559는 이더리움에 특정 이벤트(에어드랍, 민팅...)가 발생해 트랜잭션이 몰릴 경우 가스비가 급격하게 증가하여 사용자 입장에서 이를 예측하기 힘들며 가스비가 부담되는 문제를 해결하기 위한 제안이다. 또, 블록 보상 중 일부를 소각하여 이더가 디플레이션 코인이 되도록 한다.

이를 적용하면 블록에 BaseFee 필드가 추가된다. BaseFee는 블록 생성자가 정할 수 있는 값이 아니고 프로토콜 레이어에서 값을 동적으로 변경한다. 기존의 Block Gas Limit이 두 배 증가하여 30,000,000이 되고, Block Gas Used가 50% 미만으로 사용되었을 경우 BaseFee가 12.5% 감소, 50% 초과하여 사용되었을 경우 12.5% 증가, 정확히 50%라면 고정된다. 이와 같이 증감소량에 최대치 제한이 있기 때문에 트랜잭션이 몰리더라도 가스비 증가량에 대한 예측이 어느정도 가능하다.

EIP-1559가 적용되면 BaseFee * GasUsed 만큼 소각이 된다. 그럼 채굴자에게 제공할 보상량은 어떻게 계산할까? 블록에 BaseFee 필드가 추가되고, 트랜잭션에도 maxPriorityFeePerGas와 maxFeePerGas라는 필드가 추가되었으며, 아래의 식을 이용하여 채굴자에게 제공할 tip을 계산한다.

tip = min(maxPriorityFeePerGas, maxFeePerGas - baseFee)

 

결과적으로 tip * GasUsed 만큼의 양을 채굴자에게 보상으로 준다.

 

블록의 데이터만을 이용한다면 소각량은 구할 수 있다(BaseFee * GaseUsed). 하지만 블록 리워드는 트랜잭션마다 설정되어 있는 tip 값을 알아야 하기 때문에 여기서는 처리할 수 없다. 이는 해당 블록에 포함된 트랜잭션들의 fee 총합을 보여주는 방식으로 구현되어야 할 것 같다. (더 좋은 방법을 알게 되면 추가한다)

Total Defficulty

블록 헤더의 Difficulty() 메서드 리턴값을 넣어준다.

Size

블록 헤더의 Size() 메서드 리턴값을 넣어준다.

Base Fee Per Gas

블록 헤더의 BaseFee를 넣어준다.

Burnt Fees

위에서 말했듯이 BaseFee * GasUsed 값을 넣어주면 된다.