-
파이썬(python)으로 만드는 간단한 블록 체인(block chain)Python/파이썬 자료구조 알고리듬 2020. 10. 30. 12:39반응형
제 블로그에서 가장 인기 있는 글이 해시더군요.
해시가 나온 김에 간단한 블록체인을 만들어 보겠습니다.'블록체인의 의미',
'블록체인이 어떻게 데이터의 보안을 유지하나'
까지만 이해할 수 있게 아주 가볍게 만들어 보겠습니다.
(네트워킹 쪽은 pass)블록체인이 암호화폐에서만 쓰이는 게 아니라
유통 과정을 기록하는 등에도 사용되니까
위 두 개념만 알아도 꽤 써먹을 데가 많습니다.선수학습:
파이썬 해시 라이브러리
import hashlib h = hashlib.sha256() h.update(b'Genesis') print(h.hexdigest())
81ddc8d248b2dccdd3fdd5e84f0cad62b08f2d10b57f9a831c13451e5c5c80a5
파이썬 해시 라이브러리의 사용법을 알아야겠습니다.
직관적이라 어렵지 않습니다만, 다음에 주의하세요.- update()에 문자열을 넣을 수 없다. bytes 만 가능.
- m.update(b"abc")와 m.update(b" def")를 하면, m.update(b"abc def")와 같다.
genesis는 기원이라는 뜻입니다.
블록체인의 기초
해시를 해시하자
블록체인은 해시를 해시하는 것부터 이해하면 될 것 같습니다.
from hashlib import sha256 prev_hash = b'Genesis' for _ in range(3): print(prev_hash.hex()) prev_hash = sha256(prev_hash).digest() print(prev_hash.hex())
update를 쓰지 않고 줄였습니다. 깔끔하군요.
동적 타입 언어에 익숙해져 있다가...
bytes를 다루는 건 좀 헷갈리는 일입니다.
type()로 확인해가면서 처리합시다.47656e65736973 81ddc8d248b2dccdd3fdd5e84f0cad62b08f2d10b57f9a831c13451e5c5c80a5 408ebd7408dda81b20d6d0c3d3664d15536bb3ea17d0e8baa0022011c3c590da dfa6ca742b4a970b543ad95f6a743e7dee8381eda9e29ceb08c91d45e06c4b79
이런 식으로 해시를 나열하면
해시의 특성 때문에
중간의 해시값을 하나라도 조작하면
그 뒤로는 해시값들이 줄줄이 산으로 갑니다.이 원리를 보안에 이용하는 것이 블록체인입니다.
블록을 만들자.
단순히 해시값을 해시해서는 별 의미가 없습니다.
여기에 데이터를 넣어야 의미가 있겠죠?데이터와 이전 해시값을 합친 값을
해시함수에 넣어서 새 해시를 만듭시다.연결 리스트(Linked List)와 자료구조가 유사합니다.
이 경우에도 '해시를 해시하자'의 결론과 같이
중간의 '데이터'나 '해시값'을 조작하면
그 뒤의 해시값들은
줄줄이 산으로 간다는 것을 알 수 있습니다.데이터와, 이전 해시값, 새 해시값을 합친 것을
'블록'이라고 합시다.* 일반적으론 다수의 거래내역을 묶어서 블록을 구성합니다.
* 블록의 index나 시간 기록 등은 간략히 코딩하기 위해 생략했습니다.특수한 블록 제네시스
블록체인의 처음에는 이전 해시값이 없는
특수한 블록이 있습니다.
보통 genesis 블록 등으로 이름 짓습니다.블록체인이란...
이 블록들을 체인처럼 모은 것을
'블록체인'이라고 합시다.일반적으로 블록체인이란
체인처럼 연결한 뒤,
네트워크 상의 노드들에 '분산 저장'하는 것까지를 의미합니다만,
간단한 코딩을 위하여... 네트워크는 pass블록을 검증하자.
마지막으로 검증이 있습니다.
검증은
0. '현 블록의 이전 해시값'과 '이전 블록의 현 해시값' 비교
1. 이전 블록의 해시값 재확인
2. 현 블록의 해시값 재확인
으로 이루어집니다.from hashlib import sha256 blockchain = [] def make_genesis_block(): """첫 블록을 만듭니다""" data = 'Genesis' prev_hash = b'' current_hash = make_hash(data, prev_hash) blockchain.append((data, prev_hash, current_hash)) def make_hash(data: str, prev_hash: bytes) -> bytes: """해시를 만듭니다.""" # 코드 한 줄이지만, 여기 저기 쓰이니 함수로 분리하는 게 좋습니다. # bytes형은 str형이랑 왔다 갔다 헷갈리는 경우가 있어서. # 파라미터 뒤에 타입을 기록해 두면 편하더군요. return sha256(data.encode() + prev_hash).digest() def add_block(data: str): """블록을 블록 체인에 추가합니다.""" _, _, prev_hash = blockchain[-1] current_hash = make_hash(data, prev_hash) blockchain.append((data, prev_hash, current_hash)) def show_blockchain(): """블록 체인을 보여줍니다.""" # 해시값을 bytes형으로 저장했기 때문에 # hex()로 16진수 str로 변환해야 print 시 읽기 좋습니다. for i, (data, prev_hash, current_hash) in enumerate(blockchain): print(f'블록 {i}\n{data}\n{prev_hash.hex()}\n{current_hash.hex()}') def verify_blockchain(): """블록 체인을 검증합니다.""" for i in range(1, len(blockchain)): data, prev_hash, current_hash = blockchain[i] last_data, last_prev_hash, last_current_hash = blockchain[i - 1] # 변수명이 뭐 같습니다. 코딩보다 작명이 더 어려운 것 같습니다. if prev_hash != last_current_hash: # 1. 현 블록의 이전 해시 값과 # 이전 블록의 현재 해시 값이 일치하는 지 확인합니다. print(f"블록 {i} 이전 해시 != 블록 {i - 1} 현 해시. \n" f"{prev_hash.hex()} != \n{last_current_hash.hex()}") return False if last_current_hash != (temp := make_hash(last_data, last_prev_hash)): # 2. 이전 블록을 해시 함수로 검증합니다. # 이 부분이 없으면 genesis 블록의 검증이 안됩니다. print(f"블록 {i - 1} 검증 실패. \n" f"{last_current_hash.hex()} != \n{temp.hex()}") return False if current_hash != (temp := make_hash(data, prev_hash)): # 3. 현 블록을 해시 함수로 검증합니다. print(f"블록 {i}, 검증 실패. \n" f"{current_hash.hex()} != \n{temp.hex()}") return False # print(f'[Block {i}: {blockchain[i][0]}] has been verified.') return True make_genesis_block() add_block('나는미남이다') add_block('진짜미남이다') add_block('아님말고') show_blockchain() print() print(verify_blockchain())
블록 0 Genesis 81ddc8d248b2dccdd3fdd5e84f0cad62b08f2d10b57f9a831c13451e5c5c80a5 블록 1 나는미남이다 81ddc8d248b2dccdd3fdd5e84f0cad62b08f2d10b57f9a831c13451e5c5c80a5 40405b3990f4fa49832a9b268aaf7001ac0c389e099958bba121c44eb5deeabd 블록 2 진짜미남이다 40405b3990f4fa49832a9b268aaf7001ac0c389e099958bba121c44eb5deeabd 33901bb3c92fed28f2e08d4872caf30b48f60b518c6603834931eea560178498 블록 3 아님말고 33901bb3c92fed28f2e08d4872caf30b48f60b518c6603834931eea560178498 7bb2fca9e4724c28cf9ddc42efc72240009239bcb66a7c0d67b04c5679a356cf True
블록 1을 조작합시다.
# Block 1번을 조작합시다. blockchain[1] = ('너는미남이아니다', blockchain[0][2], make_hash('너는미남이아니다', blockchain[0][2])) show_blockchain() print() print(verify_blockchain())
블록 0 Genesis 81ddc8d248b2dccdd3fdd5e84f0cad62b08f2d10b57f9a831c13451e5c5c80a5 블록 1 너는미남이아니다 81ddc8d248b2dccdd3fdd5e84f0cad62b08f2d10b57f9a831c13451e5c5c80a5 2dd22979b6a370836be47cab599c021452d499af99b71e6d5ab2eac0747a2403 블록 2 진짜미남이다 40405b3990f4fa49832a9b268aaf7001ac0c389e099958bba121c44eb5deeabd 33901bb3c92fed28f2e08d4872caf30b48f60b518c6603834931eea560178498 블록 3 아님말고 33901bb3c92fed28f2e08d4872caf30b48f60b518c6603834931eea560178498 7bb2fca9e4724c28cf9ddc42efc72240009239bcb66a7c0d67b04c5679a356cf 블록 2 이전 해시 != 블록 1 현 해시. 40405b3990f4fa49832a9b268aaf7001ac0c389e099958bba121c44eb5deeabd != 2dd22979b6a370836be47cab599c021452d499af99b71e6d5ab2eac0747a2403 False
지금까지 코딩한 것을 보면,
블록체인은 유통 과정 등을 기록하는데 적당한 구조라는 걸 알 수 있습니다.
유통과정, 시간 등을 블록체인에 기록하면 중간 과정을 고칠 수가 없습니다.
하지만 전체나, 특정 단계 이후를 모두 조작하는 식의 조작은 가능하고,
따라서 추가적인 장치가 필요함을 알 수 있습니다.PoW (Proof of Works, 작업 증명)
이제 그 추가적인 장치 중 핵심인 POW (Proof Of Works)를 살펴봅시다.
POW란 우리가 블록을 만들 때 마지막으로 해싱을 하는데요.
이때 해싱이 어렵도록 하는 장치입니다.어떻게(how) 어렵게 만들까요?
how와 why를 중심으로 생각하면 좋습니다.특정 조건의 해시값을 찾습니다.
00으로 시작하는 해시값을 찾아라....
0의 개수가 늘어날수록
해시 값 찾기가 어렵겠죠?난이도(0의 개수)도 블록에 추가됩니다.
검증을 위해 다시 해시를 검산할 때 0의 개수가 필요합니다.블록에만 난이도를 기록하는 건 '해킹해주세요'라는 것 과 같습니다.
쉬운 난이도로 긴 위조 체인을 만들어서 뿌릴 수 있기 때문입니다.
하지만 간단히 코딩하기 위해.. 여기까지만..그런데 이런 의문이 생깁니다.
데이터와 이전 해시값이 고정되어 묶여있고,
이 고정된 값에 대한 해시값은 역시 하나로 고정되어 있는데..
어떻게(how) 특정 조건의 해시값을 찾으란 말이지?
(하나의 데이터에 대한 해시값은 오직 하나입니다.)그래서 블록에 nonce라는 항목이 추가됩니다.
nonce 값을 바꿔가면서 엄청나게 해싱을 반복해서
00...으로 시작하는 해시값을 찾는 거죠.찾으면 나 찾았다. (심봤다)
데이터에 nonce값 xxxxxxxx을 추가해서 해싱을 하니까..
00...으로 시작하는 해시 값이 나왔다..
너네들 확인해봐..(네트워킹 쪽은 코딩하지 않습니다만...)
라고 주위 노드에게 뿌립니다.
주위 노드들은 데이터에 nonce 값을 추가한 뒤 해싱해서
00으로 시작되는 지 확인합니다.
검증은 한 번에 끝납니다.nonce와 hash를 비교할 때 편하도록
hash의 자료형을 bytes에서 str로 바꿨습니다.
digest()에서 hexdigest()로...add_block함수명을 add_normal_block으로 바꾸고 교통정리도 했습니다.
전역 변수도 생기고 코드가 조금 지저분해졌네요.
클래스의 필요가 느껴집니다.from hashlib import sha256 blockchain = [] difficulty = 4 def add_genesis_block(): """첫 블록을 만들고, 블록 체인에 추가합니다""" data = 'Genesis' prev_hash = '' nonce, current_hash = make_hash(data, prev_hash, difficulty) add_block(data, prev_hash, difficulty, nonce, current_hash) def make_hash(data: str, prev_hash: str, difficulty_: int) -> (int, str): """해시를 만듭니다. PoW""" new_hash = ' ' * difficulty_ checker = '0' * difficulty_ nonce = 0 while new_hash[:difficulty_] != checker: new_hash = sha256((data + str(nonce) + prev_hash).encode()).hexdigest() nonce += 1 return nonce, new_hash def add_block(data, prev_hash, difficulty_, nonce, current_hash): """실제로 블록을 추가합니다.""" # 한 줄이라고 두 함수의 공통 부분을 방치했더니 # 기능 추가시 양쪽을 다 수정해야만 했습니다. # 단순한 복붙에도 실수를 할 수 있습니다. # 공통 부분은 최대한 모아야 합니다. blockchain.append((data, prev_hash, difficulty_, nonce, current_hash)) def add_normal_block(data: str): """일반 블록을 만들고, 블록 체인에 추가합니다.""" _, _, _, _, prev_hash = blockchain[-1] nonce, current_hash = make_hash(data, prev_hash, difficulty) add_block(data, prev_hash, difficulty, nonce, current_hash) def show_blockchain(): """블록 체인을 보여줍니다.""" for i, (data, prev_hash, difficulty_, nonce, current_hash) in enumerate(blockchain): print(f'블록 {i}\n' f'{data}, {difficulty_}, {nonce}\n' f'{prev_hash}\n' f'{current_hash}') def verify_blockchain(): """블록 체인을 검증합니다.""" for i in range(1, len(blockchain)): data, prev_hash, difficulty_, nonce, current_hash = blockchain[i] last_data, last_prev_hash, last_difficulty, last_nonce, last_current_hash \ = blockchain[i - 1] if prev_hash != last_current_hash: print(f"[블록 {i}] 이전 해시 != [블록 {i - 1}] 현 해시. \n" f"{prev_hash} != \n{last_current_hash}") return False if (last_nonce, last_current_hash) != \ (temp := make_hash(last_data, last_prev_hash, last_difficulty)): show_verify_failed(i - 1, last_nonce, last_current_hash, temp[0], temp[1]) return False if (nonce, current_hash) != (temp := make_hash(data, prev_hash, difficulty_)): show_verify_failed(i, nonce, current_hash, temp[0], temp[1]) return False return True def show_verify_failed(block_num, ori_nonce, ori_hash, new_nonce, new_hash): print(f"블록 {block_num} 검증 실패. \n" f"{ori_nonce} != {new_nonce}\n" f"{ori_hash} != \n{new_hash}") add_genesis_block() add_normal_block('나는미남이다') add_normal_block('진짜미남이다') add_normal_block('아님말고') show_blockchain() print() print(verify_blockchain())
블록 0 Genesis, 4, 100816 00000551fb3e39f4f36d2121a6043c71999059f733ab13561539ebfe57f85961 블록 1 나는미남이다, 4, 21340 00000551fb3e39f4f36d2121a6043c71999059f733ab13561539ebfe57f85961 0000f818c1e5d987dbced40a9e40f1095248635e90cac77eba0e7332d8e85a44 블록 2 진짜미남이다, 4, 25151 0000f818c1e5d987dbced40a9e40f1095248635e90cac77eba0e7332d8e85a44 00002cfa10b49be7eb01dfe8623b4a1566284c4fb8835d62b1f7bcbf05f2be60 블록 3 아님말고, 4, 5344 00002cfa10b49be7eb01dfe8623b4a1566284c4fb8835d62b1f7bcbf05f2be60 0000093cfe16f29477622e7621b4cc18f38a46d43f0d0300a8f0b7effa6e5259 True
blockchain[1] = ('너는미남이아니다', blockchain[0][4], blockchain[0][2], *make_hash('너는미남이아니다', blockchain[0][4], blockchain[0][2])) show_blockchain() print() print(verify_blockchain())
블록 0 Genesis, 4, 100816 00000551fb3e39f4f36d2121a6043c71999059f733ab13561539ebfe57f85961 블록 1 너는미남이아니다, 4, 11661 00000551fb3e39f4f36d2121a6043c71999059f733ab13561539ebfe57f85961 00003bfc5c576c53bc0d753912967ac737304bd60cf05d9be82c2a4d0d354e1f 블록 2 진짜미남이다, 4, 25151 0000f818c1e5d987dbced40a9e40f1095248635e90cac77eba0e7332d8e85a44 00002cfa10b49be7eb01dfe8623b4a1566284c4fb8835d62b1f7bcbf05f2be60 블록 3 아님말고, 4, 5344 00002cfa10b49be7eb01dfe8623b4a1566284c4fb8835d62b1f7bcbf05f2be60 0000093cfe16f29477622e7621b4cc18f38a46d43f0d0300a8f0b7effa6e5259 [블록 2] 이전 해시 != [블록 1] 현 해시. 0000f818c1e5d987dbced40a9e40f1095248635e90cac77eba0e7332d8e85a44 != 00003bfc5c576c53bc0d753912967ac737304bd60cf05d9be82c2a4d0d354e1f False
PoW의 Why
PoW(작업증명)의 how는 필요한 만큼은 알려드린 것 같습니다.
그런데 why를 알려드리진 않은 것 같네요.
코딩도 끝나고 머리도 가벼워지셨을 테니 다시 채워보겠습니다.PoW의 역사부터..
1992년 (~1993년) 심시아 더크(Cynthia Dwork)와 모니 나노어(Moni Naor)이 작업 증명의 기본 개념을 고안했다.
http://www.wisdom.weizmann.ac.il/~naor/PAPERS/pvp.pdf
1997년 최초로 아담 백(Adam Back)이 해시캐시에 적용했다.
해시캐시(hashcash)는 대량 스팸메일을 막기 위해 개발한 암호화폐이다.
http://hashcash.org
1999년 마커스 제이콥슨(Markus_Jakobsson)과 아리 쥬엘스(Ari Juels)에 의해 Proof of Work라는 명칭이 붙었다.
2009년 사토시 나카모토 비트코인(bitcoin)에 적용
http://wiki.hash.kr/index.php/%EC%9E%91%EC%97%85%EC%A6%9D%EB%AA%85가끔 Bitcoin 창시자인 Satoshi Nakamoto가 PoW의 창시자라는 말을 하시는 분들도 계신데, 그건 좀.....
해시캐시의 원리
해시캐시가 어떤 원리로 작동되는지는 다음 링크에 자세히 나와 있습니다.
https://www.joinc.co.kr/w/man/12/blockChain/hashcash메일을 보내기 위해서는 아주 많은 연산을 해야 하고,
메일을 확인하기 위해서는 약간의 연산이 필요할 뿐이다.
즉 보내는 쪽이 대부분의 비용(=연산)을 책임진다.
스팸 메일을 보내기 위해 스팸으로 벌 수 있는 돈보다
큰 비용을 들여 연산을 하지 않을 것이다.
3자가 개입하지 않는 검증 시스템이라는 데 큰 의미가 있다.
생각보다 간단한 원리네요....PoW의 단점
단점이 먼저 나오는 게 순서상 이상합니다만...- 절반 이상의 해시 파워를 가지게 된다면 블록 내용의 조작이 가능합니다.
- 긴 체인이 올바른 것으로 판단합니다. 체인이 버려지는 경우 트랜잭션이 무효가 될 수 있습니다.
(이를 방지하기 위한 장치도 존재합니다만...) - 합의를 통해 신뢰성을 확인하기 때문에, 정보의 확산 및 합의에 걸리는 시간이 필요합니다.
- 필요 이상의 많은 자원(전력)을 소모합니다.
블록의 생성 과정
블록은 거래내역과 이전 해시값을 합쳐서 거래 기록들을 전달받는다.
특정 숫자를 찾아서 블록을 생성한다.
생성한 블록을 블록체인에 추가하고 전달하여 새롭게 채굴된 비트코인을 보상으로 받는다.https://brunch.co.kr/@ashhan/16
https://steemit.com/kr/@feyee95/4-1 http://wiki.hash.kr/index.php/%EB%B8%94%EB%A1%9D%EC%83%9D%EC%84%B1%EC%9E%90블록을 위조하려면...
위에서 가장 긴 블록체인이 진짜라고 말씀드렸습니다.
짧은 걸 진짜라고는 할 수는 없으니...빡센 연산은 위조 시에도 똑같이 적용이 됩니다.
본인을 제외한 전 세계 비트코인 노드들의 해시파워의 총합보다
더 큰 해시 파워(보통 초당 해시 생성수 =연산력)를 가지고 있다면,
더 긴 가짜 블록체인을 만들어 공격을 할 수 있습니다.비트코인의 위조가능성
현실적으로 불가능합니다.
16년에 100경 해시였었는데요..
m.blog.naver.com/softmate1/22060972211719년 8월에 비트코인 네트워크의 총 해시는 초당 7143경이었다고 합니다.
https://www.coindeskkorea.com/news/articleView.html?idxno=53404점점 늘어나죠..
18년 기준으로 4조 정도의 금액이 들어간다고 하네요.
medium.com/@woohyuk.jung88알트코인의 51% 공격 사례
하지만 해시 파워가 약한 알트 코인계에서는 꽤 많은 공격 성공사례가 있습니다.
버지, 모나코인, 비트코인골드, 젠캐시, 이더리움 클래식
wiki.hash.kr/index.php/51%25_%EA%B3%B5%EA%B2%A9#51.25_.EA.B3.B5.EA.B2.A9_.EC.82.AC.EB.A1.80'초대형' 코인에서 하드포크를 한 알트코인 '신생'이 있다고 합시다.
'신생' 코인은 '초대형' 코인에서 하드포크를 했으니까 '초대형'코인과 똑같은 방식으로 작동합니다.
그렇다면 '초대형' 코인 채굴기의 설정을 조금만 바꾸면 새로운 '신생' 코인을 채굴할 수 있겠죠?
그런데 '초대형' 코인의 해시파워에 비해서 '신생' 코인의 해시파워는 아주 약할겁니다.
이 때 '초대형' 코인 채굴기의 소유자가
본인의 해시파워가 '신생' 코인의 절반보다 크고
'신생' 코인을 해킹했을 때의 이익이
'초대형' 코인의 빡빡한 채굴 환경에서의 이익보다 큰 상황이 되면.........기타
비잔티움(비잔틴) 장군 문제도 같이 봐주면 좋습니다.
이런 영상도 좋습니다. youtu.be/bBC-nXj3Ng4반응형