블록의 의미
•
트랜잭션의 집합
◦
블록에는 평균 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가 적용되었다는 것을 의미한다.
•
하지만 이러한 방식은 동시에 여러 진행상황을 적용할 수 없다는 단점이 있다.
•
◦
동시에 최대 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회 수행한 것이다.
•
그 부모는 해시 두 개를 연결하여 64바이트의 문자열을 만든 후 역시 이중 해시를 수행한다.
•
아래 그림은 더 많은 트랜잭션으로 구성된 머클트리이다.
•
아무리 트랜잭션 수가 많아도 머클루트는 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
복사