1️⃣

블록

블록의 의미

트랜잭션의 집합
블록에는 평균 10분 동안 비트코인 네트워크에서 발생한 복수 개의 트랜잭션이 들어간다.
내가 보낸 트랜잭션이 블록에 포함되어야만 분산 장부에 기록된다.
트랜잭션의 검증자
트랜잭션이 블록에 담기려면 트랜잭션이 프로토콜 검증에서 통과 되어야 한다.
내 트랜잭션이 어떤 블록에 담겼다면, 내 트랜잭션은 규칙을 잘 지켰구나 라고 판단해도 된다.
거래 승인의 단위
은행, 카드사 등 전통적 금융은 단 건의 트랜잭션을 단위로 승인한다.
비트코인은 여러 개의 트랜잭션을 모아 한꺼번에 승인한다.
비트코인에서 거래 승인 단위를 블록이라 한다.
채굴의 단위
비트코인 채굴은 블록을 만드는 행위를 의미한다.
프로토콜을 준수했다는 전제하에 가장 빠르게 블록을 만든 사람에게 채굴 보상이 주어진다.
합의의 단위
만약에 양립할 수 없는 두 개의 트랜잭션이 발생하여 합의가 필요해졌다고 가정해보자.
단 하나의 트랜잭션이 서로 다르다 하여도, 해당 트랜잭션이 포함된 블록 단위로 합의를 진행한다.
즉, 비트코인에서 합의는 “이 블록이 맞는지, 저 블록이 맞는지 합의해보자”의 논점을 해결하는 과정이다.
난이도 조절의 단위
채굴자가 엄청나게 많아지고, 채굴기 성능이 좋아져도 비트코인을 빠르게 채굴할 수없다.
이를 난이도 조절이라고 하는데, 난이도 조절 단위 또한 블록으로 구분된다.
난이도는 매 2016블록마다 변경된다.
거래의 안정성 척도
내가 비트코인을 보낸 기록이 얼마나 안정적인지, 혹시 공격의 여지가 없는지에 대한 여부를 결정할 때 블록이 사용된다.
예를 들어 내 거래 기록이 만약 1 승인이면 약간 불안하고, 6승인이면 어느정도 안심하며, 100승인이면 매우 안심할 수 있다.
이 x승인의 단위는 “내 거래 뒤에 몇 개의 블록이 쌓여있느냐”로 측정된다.
반감기의 단위
비트코인 채굴 보상은 엄격한 프로토콜 안에서 지켜진다.
이 채굴 보상은 매 21만 블록마다 반으로 줄어든다.
0~209,999블록까지는 50BTC, 이후 21만 블록은 25BTC … 로 구성된다.

블록체인

블록체인은 여러 트랜잭션이 담겨있는 블록이 그 이전 블록과 연결되어 있는 형태의 데이터 구조이다.
블록체인은 수직 방향의 스택처럼 보인다.
그래서 블록의 순서를 높이(height)라 부른다.
블록은 약 10분 간격으로 만들어지며, 블록이 승인되면 블록에 포함되어 있는 트랜잭션도 승인된다.
블록체인은 다음과 같이 구조화 된다.
자식 블록은 단 하나의 부모 만을 가질 수 있다.
부모도 단 하나의 자식을 가지나, 일시적으로 둘 이상의 자식을 가질 수 있다.
두 자식 중 한 자식을 선택하는 과정을 합의라고 한다.

블록의 구조

블록의 구조는 다음과 같다.
크기
필드 명
설명
4바이트
블록 크기
뒤에 나오는 블록의 크기 (단위:바이트)
80바이트
블록 헤더
블록을 나타내는 여러 항목
1~9바이트(VarInt)
트랜잭션 수
가변
트랜잭션
이 블록에 포함된 트랜잭션

블록 헤더

블록 헤더의 구조는 다음과 같다.
크기
필드 명
설명
4바이트
버전
소프트웨어/프로토콜의 버전 정보
32바이트
이전 블록 해시
부모 블록 헤더의 해시값
32바이트
머클 루트
블록에 포함된 트랜잭션의 머클 트리의 루트
4바이트
타임 스탬프
블록의 대략적인 생성 시간
4바이트
난이도
작업증명의 난이도
4바이트
논스
작업증명의 정답값
다음은 제네시스 블록(높이 0)에 기록되어있는 내용이다. CBlock의 내용을 보자.
GetHash() = 0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f hashMerkleRoot = 0x4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b txNew.vin[0].scriptSig = 486604799 4 0x736B6E616220726F662074756F6C69616220646E6F63657320666F206B6E697262206E6F20726F6C6C65636E61684320393030322F6E614A2F33302073656D695420656854 txNew.vout[0].nValue = 5000000000 txNew.vout[0].scriptPubKey = 0x5F1DF16B2B704C8A578D0BBAF74D385CDE12C11EE50455F3C438EF4C3FBCF649B6DE611FEAE06279A60939E028A8D65C10B73071A6F16719274855FEB0FD8A6704 OP_CHECKSIG block.nVersion = 1 block.nTime = 1231006505 block.nBits = 0x1d00ffff block.nNonce = 2083236893 CBlock(hash=000000000019d6, ver=1, hashPrevBlock=00000000000000, hashMerkleRoot=4a5e1e, nTime=1231006505, nBits=1d00ffff, nNonce=2083236893, vtx=1) CTransaction(hash=4a5e1e, ver=1, vin.size=1, vout.size=1, nLockTime=0) CTxIn(COutPoint(000000, -1), coinbase 04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73) CTxOut(nValue=50.00000000, scriptPubKey=0x5F1DF16B2B704C8A578D0B) vMerkleTree: 4a5e1e
Plain Text
복사
hash는 블록의 식별자로 사용한다. 블록의 헤더를 SHA-256 2회 해시하여 얻는다.
ver은 버전으로써 프로토콜의 버전을 의미한다.
hashPrevBlock은 이전 블록의 헤더의 해시값이다. 바로 앞에 있는 블록을 해시를 의미한다.
mashMerkleRoot는 머클루트라 하며 트랜잭션을 묶은 해시를 의미한다.
nTime은 타임스탬프로서 블록 생성 시간을 의미한다. 유닉스 타임을 쓴다.
nBits는 난이도로서 채굴 난이도를 의미한다. 향후 알아보자.
nNonce는 논스라 하며 채굴을 위한 정답값을 의미한다.
vtx는 트랜잭션의 버전을 의미한다.
CTransaction은 트랜잭션들을 순서대로 나타낸다.
hashPrevBlock의 의미
이 필드는 내가 어떤 부모의 자식인지를 나타낸다.
이 필드는 부모의 해시코드를 나타내기 때문에 부모 블록이 변경되면 자식 블록도 변경되어야 한다.
만약 해커가 부모 블록을 위변조하면 자식과 모든 손자들을 변경해야 한다.
이를 재계산하기 위해서는 엄청난 규모의 계산을 수행하므로 비트코인의 보안이 유지된다.
따라서 자식의 세대가 길어질수록 그 블록은 점점 더 강건해진다.

코인베이스 트랜잭션

코인베이스는 동명의 암호화폐 거래소와는 관련이 없다.
코인베이스 트랜잭션은 매 블록마다 들어가는 첫 번째 트랜잭션이다.
여기에는 채굴자가 자신의 지갑으로 보내는 채굴 보상이 담긴다.
다음은 코인베이스 트랜잭션의 예시이다.
일반적인 트랜잭션과 동일한 구조를 갖지만, 몇 가지 차이가 있다.
출력은 존재하지만 입력(해제 스크립트)이 없다. UTXO로 부터 지불되는 비트코인이 아니고, 최초로 생성된 비트코인이기 때문이다.
입력에서 이전 스크립트와 인덱스는 00000…, fffff… 로 기록한다.
입력 스크립트는 보통 자기 자신의 공개키를 넣은 P2PKH 스크립트를 사용한다.
해제 스크립트는 채굴자가 100바이트를 넘지 않는 범위에서 채굴자가 임의의 내용을 넣을 수 있다.
제네시스 블록의 해제 스크립트 사토시 나카모토는 는 제네시스 블록의 해제 스크립트에 다음을 입력 하였다. (위 그림 참조)
04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73
이를 아스키 코드로 변환하면 다음과 같은 내용을 발견할 수 있다.
이것은 제네시스 블록이 채굴된 2009년 1월 3일의 타임즈지 기사를 표현한다.
BIP0034 제안
채굴자는 누구나 출력 트랜잭션을 구성할수 있어서 발생하는 문제가 있었다.
이는 동일한 트랜잭션 ID를 가질 확률이 높다는 것이다.
BIP0034에서는 이를 막기위해 블록의 높이를 코인베이스의 첫 원소로 넣는 것을 규정한다.
이를 통해 트랜잭션의 중복을 막을 수 있고, 쉽게 블록의 높이를 알 수 있다.

블록 버전

블록의 버전은 프로토콜의 버전을 의미한다.
버전 2는 BIP0034가 적용되었다는 것을 의미한다.
버전 3은 BIP0066이 적용되었다는 것을 의미한다.
버전 4는 BIP0065가 적용되었다는 것을 의미한다.
하지만 이러한 방식은 동시에 여러 진행상황을 적용할 수 없다는 단점이 있다.
이를 해결하고자 BIP0009가 제안된다.
동시에 최대 29개까지 업데이트를 적용할 수 있다.
BIP0009를 받아들인 블록은 32비트 중 앞 세 비트를 ‘001’로 표기한다.
BIP0009를 받아들이기 이전의 블록과 구분하기 위해서이다. 바이트로 표기하면 ‘2xxx’ 또는 ‘3xxx’가 된다.
BIP0009이전의 마지막 버전은 버전 4였다. ‘0004’
나머지 29개 비트로 어떤 BIP제안을 받아들였는지 버전으로 표현할 수 있다.
BIP0009이후 각 BIP에는 어떤 비트를 적용할지 표기한다.
BIP0079/BIP0112/BIP0113 을 적용한다면 첫 번째 비트를 1로 바꾼다.
BIP0141(Segwit)을 적용한다면 두 번째 비트를 1로 바꾼다.
비트코인의 소프트포크
BIP0009 규정에 따르면 2016 블록 기간 (2주, 난이도 조정 기간)동안 채굴된 블록 중 95% 이상의 블록이 그 내용을 받아들였는지 확인하여 소프트포크를 진행한다.
따라서 모든 BIP에는 적용 기간을 명시하고 있다.
95%라는 임계값은 엄격한 것은 아니다.
BIP0091은 80% 임계값을 정의하였다.

머클 루트

머클루트는 블록 내에 트랜잭션을 32바이트 해시값으로 변환한 값이다.
머클트리는 규모가 큰 데이터 집합을 효율적으로 검증하는 데이터 구조이다.
컴퓨터공학에서는 이를 ‘이진 해시 트리’라고도 한다.
머클트리의 가장 상위에 정점이 머클 루트이다.
머클트리의 가장 하단에는 트랜잭션을 SHA-256해시를 2회 수행한 것이다.
HA=SHA256(SHA56(TransactionA))H_A=SHA256(SHA56(Transaction A))
그 부모는 해시 두 개를 연결하여 64바이트의 문자열을 만든 후 역시 이중 해시를 수행한다.
HAB=SHA256(SHA56(HAHB))H_{AB}=SHA256(SHA56(H_A||H_B))
아래 그림은 더 많은 트랜잭션으로 구성된 머클트리이다.
아무리 트랜잭션 수가 많아도 머클루트는 32바이트로 동일하다.
머클트리는 트랜잭션의 위변조 여부를 검증하는데 활용된다.
트랜잭션 리프 중 하나라도 위변조가 발생하면 결국 머클루트의 정합성이 깨진다.
자식이 홀수개인 경우 마지막 트랜잭션 해시를 한번 복사하고 계산한다.
아래 코드는 100000번째 블록의 머클루트를 계산하는 예시이다.
import hashlib def merkle(hashList): if len(hashList) == 1: return hashList[0] newHashList = [] for i in range(0, len(hashList)-1, 2): newHashList.append(hash2(hashList[i], hashList[i+1])) # 끝이 홀수인경우 하나를 복사한다. if len(hashList) % 2 == 1: newHashList.append(hash2(hashList[-1], hashList[-1])) return merkle(newHashList) def hash2(a, b): # 빅엔디언을 리틀엔디언으로 변경해야 한다. # 16진수인 경우 '8c14f0' -> 'f0148c'로 변경된다. a1 = bytes.fromhex(a)[::-1] b1 = bytes.fromhex(b)[::-1] # 2회 SHA-256해시를 진행한다. h = hashlib.sha256(hashlib.sha256(a1 + b1).digest()).digest() # 다시 빅엔디언으로 변경한다. return h[::-1].hex() # 블록에는 4개의 트랜잭션이 담겨있다. txHashes = [ "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87", "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4", "6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4", "e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d", ] print(merkle(txHashes)) #----- 결과 ------ f3e94742aca4b5ef85488dc37c06c3282295ffec960994b2c0d5ac2a25a95766
Python
복사

타임 스탬프

타임스탬프는 유닉스 시간으로 표현된 4바이트 값이다.
비트코인에서는 타임스탬프를 다음과 같은 방법으로 검증한다.
이전 블록의 중간값 보다 크고, 서버의 시간에서 2시간보다 작아야 한다.
중간값은 다음과 같이 계산한다.
자신을 포함한 직전 11개 블록을 가져온다.
11개의 블록을 시간 순서대로 정렬한다.
그 중 6번째 블록의 타임스탬프가 중간값이다.
타임스탬프는 록타임을 결정할때와 난이도를 결정할 때 사용된다.

구현: 블록

from io import BytesIO from unittest import TestCase from helper import ( bits_to_target, hash256, int_to_little_endian, little_endian_to_int, merkle_root, ) GENESIS_BLOCK = bytes.fromhex('0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c') TESTNET_GENESIS_BLOCK = bytes.fromhex('0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae18') LOWEST_BITS = bytes.fromhex('ffff001d') class Block: def __init__(self, version, prev_block, merkle_root, timestamp, bits, nonce, tx_hashes=None): self.version = version self.prev_block = prev_block self.merkle_root = merkle_root self.timestamp = timestamp self.bits = bits self.nonce = nonce self.tx_hashes = tx_hashes @classmethod def parse(cls, s): #버전 4바이트 version = little_endian_to_int(s.read(4)) #이전블록 32바이트, 리틀엔디언으로 전환 prev_block = s.read(32)[::-1] #머클루트 32바이트, 리틀엔디언으로 전환 merkle_root = s.read(32)[::-1] #타임스탬프 timestamp = little_endian_to_int(s.read(4)) #난이도 bits = s.read(4) #논스 nonce = s.read(4) return cls(version, prev_block, merkle_root, timestamp, bits, nonce) def serialize(self): result = int_to_little_endian(self.version, 4) result += self.prev_block[::-1] result += self.merkle_root[::-1] result += int_to_little_endian(self.timestamp, 4) result += self.bits result += self.nonce return result def hash(self): s = self.serialize() h256 = hash256(s) return h256[::-1] #BIP0009의 버전 표현. 가장 앞 비트가 '001'이다. def bip9(self): return self.version >> 29 == 0b001 def bip91(self): return self.version >> 4 & 1 == 1 def bip141(self): return self.version >> 1 & 1 == 1 #머클루트 검증 def validate_merkle_root(self): hashes = [h[::-1] for h in self.tx_hashes] root = merkle_root(hashes)[::-1] return root == self.merkle_root
Python
복사

구현: 머클트리

import math from io import BytesIO from unittest import TestCase from helper import ( bytes_to_bit_field, little_endian_to_int, merkle_parent, read_varint, ) class MerkleTree: def __init__(self, total): self.total = total #머클트리의 깊이를 먼저 구한다. self.max_depth = math.ceil(math.log(self.total, 2)) self.nodes = [] #깊이를 하나씩 들어간다. for depth in range(self.max_depth + 1): #이번 깊이의 노드 인덱스 num_items = math.ceil(self.total / 2**(self.max_depth - depth)) #트리 초기화 level_hashes = [None] * num_items self.nodes.append(level_hashes) #포인터 self.current_depth = 0 self.current_index = 0 def __repr__(self): result = [] for depth, level in enumerate(self.nodes): items = [] for index, h in enumerate(level): if h is None: short = 'None' else: short = '{}...'.format(h.hex()[:8]) if depth == self.current_depth and index == self.current_index: items.append('*{}*'.format(short[:-2])) else: items.append('{}'.format(short)) result.append(', '.join(items)) return '\n'.join(result) def up(self): #깊이를 하나 더 들어감. self.current_depth -= 1 self.current_index //= 2 def left(self): #왼쪽 자식 탐색 self.current_depth += 1 self.current_index *= 2 def right(self): #오른쪽 자식 탐색 self.current_depth += 1 self.current_index = self.current_index * 2 + 1 def root(self): return self.nodes[0][0] def set_current_node(self, value): self.nodes[self.current_depth][self.current_index] = value def get_current_node(self): return self.nodes[self.current_depth][self.current_index] def get_left_node(self): return self.nodes[self.current_depth + 1][self.current_index * 2] def get_right_node(self): return self.nodes[self.current_depth + 1][self.current_index * 2 + 1] def is_leaf(self): return self.current_depth == self.max_depth #현재 특정 깊이의 가장 우측 노드이고 자식이 홀수인 경우 오른쪽은 존재하지 않는다. 존재하면 True. def right_exists(self): return len(self.nodes[self.current_depth + 1]) > self.current_index * 2 + 1 #머클 트리 만들기 def populate_tree(self): #현재의 포인트가 루트일때 까지 반복한다. while self.root() is None: #단말 노드인 경우 if self.is_leaf(): self.up() else: #왼쪽 값을 가져온다. left_hash = self.get_left_node() #만약 값이 없으면, 왼쪽을 계산하기 위해 이동한다. if left_hash is None: tree.left() #오른쪽 자식이 존재하는 경우 elif self.right_exists(): #값을 가져온다. right_hash = self.get_right_node() #없으면, 이동한다. if right_hash is None: self.right() #있다면 else: #왼쪽자식과 오른쪽 자식을 정의한다. self.set_current_node(merkle_parent(left_hash, right_hash)) #한 단계 깊이 들어간다. self.up() #만약 오른쪽 자식이 없다면 else: #왼쪽 자식을 두 번 정의한다. self.set_current_node(merkle_parent(left_hash, left_hash)) self.up() hex_hashes = [ "9745f7173ef14ee4155722d1cbf13304339fd00d900b759c6f9d58579b5765fb", "5573c8ede34936c29cdfdfe743f7f5fdfbd4f54ba0705259e62f39917065cb9b", "82a02ecbb6623b4274dfcab82b336dc017a27136e08521091e443e62582e8f05", "507ccae5ed9b340363a0e6d765af148be9cb1c8766ccc922f83e4ae681658308", "a7a4aec28e7162e1e9ef33dfa30f0bc0526e6cf4b11a576f6c5de58593898330", "bb6267664bd833fd9fc82582853ab144fece26b7a8a5bf328f8a059445b59add", "ea6d7ac1ee77fbacee58fc717b990c4fcccf1b19af43103c090f601677fd8836", "457743861de496c429912558a106b810b0507975a49773228aa788df40730d41", "7688029288efc9e9a0011c960a6ed9e5466581abf3e3a6c26ee317461add619a", "b1ae7f15836cb2286cdd4e2c37bf9bb7da0a2846d06867a429f654b2e7f383c9", "9b74f89fa3f93e71ff2c241f32945d877281a6a50a6bf94adac002980aafe5ab", "b3a92b5b255019bdaf754875633c2de9fec2ab03e6b8ce669d07cb5b18804638", "b5c0b915312b9bdaedd2b86aa2d0f8feffc73a2d37668fd9010179261e25e263", "c9d52c5cb1e557b92c84c52e7c4bfbce859408bedffc8a5560fd6e35e10b8800", "c555bc5fc3bc096df0a0c9532f07640bfb76bfe4fc1ace214b8b228a1297a4c2", ] tree = MerkleTree(len(hex_hashes)) hashes = [bytes.fromhex(h) for h in hex_hashes] tree.nodes[4]= [bytes.fromhex(h) for h in hex_hashes] tree.populate_tree() print(tree) #------ 결과 ------ dc87b7e3... 6382df3f..., 6440941e... 3ba6c080..., 8e894862..., 7ab01bb6..., 996be980... 272945ec..., 9a38d037..., 4a64abd9..., ec7c95e1..., 3b67006c..., 850683df..., d40d268b..., f542a085... 9745f717..., 5573c8ed..., 82a02ecb..., 507ccae5..., a7a4aec2..., bb626766..., ea6d7ac1..., 45774386..., 76880292..., b1ae7f15..., 9b74f89f..., b3a92b5b..., b5c0b915..., c9d52c5c..., c555bc5f...
Python
복사