스마트 계약
•
스마트 계약이란 블록체인상 코인의 전송을 프로그램으로 기술하는 것을 어렵게 쓴 말이다.
•
비트코인 스크립트는 비트코인의 스마트 계약 언어로서 비트코인이 소비되는 조건을 기술하는 프로그래밍 언어이다.
•
비트코인 스크립트는 포스(Forth)라는 언어와 유사한 역폴란드 표기법의 스택 기반의 언어이고, 기능이 제한적이다.
역폴란드 표기법(Reverse Polish Notation)
•
연산자를 피연산자 뒤에 쓰는 연산 표기법으로 후위 표기법(Postfix Notation)이라고도 한다.
•
우리에게 친숙한 중위 표기법 3 + 5 x 2를 역폴란드 표기법으로 바꾸면 3 5 2 x + 이다.
•
이러한 수식은 스택에 피연산자를 넣고(PUSH), 연산자를 만나면 피연산자 두 개를 꺼내서(POP) 계산 후 결과값을 스택에 반환(PUSH)한다.
•
반복작업을 위한 루프 기능이 없어 튜링 불완전하다고 말할 수 있다.
비트코인이 튜링 불완전한 이유
•
튜링 완전하다는 의미는 복잡한 연산이 가능하고 반복작업을 위한 루프 기능이 있다는 의미이다.
•
모든 노드에서 스크립트 프로그램을 실행하기 때문에 단순하고 빠른 처리가 필요하다.
•
스마트 계약이 복잡하면 의도치 않은 오류나 버그를 발생할 수 있다. 블록체인에서는 이를 돌이킬 수 없기 때문에 작은 오류나 버그도 매우 치명적이다.
•
반복문은 의도치 않은 또는 의도적 서비스 거부공격(DoS)을 위해 무한루프를 작성하는 경우 전체 네트워크의 마비가 올 수 있다.
•
비트코인의 가장 기본적인 스크립트는 입력에서 디지털 서명을 통해 자신의 비트코인임을 검증하는 해제 스크립트와 받는 사람의 서명을 요청하는 잠금스크립트가 대표적이다.
스크립트의 실행
•
스크립트는 프로그래밍 언어이다.
•
스크립트는 다른 프로그래밍 언어와 유사하게 연산자와 원소로 이루어진다.
•
원소는 실행을 위한 데이터를 의미한다.
•
예를들어 서명 값, 공개키 등은 원소에 해당한다.
•
연산자는 데이터를 통해 무언가를 하는 함수이다.
•
예를들어 서명 검증, 해시 알고리즘 실행 등이 있다.
•
모든 원소와 연산자는 16진수로 표현되는 OP코드로 대응되어 프로그래밍 된다. 다음은 OP코드의 예시이다.
◦
0x01~0x4b : 이 이후에 오는 1~75(0x4b)바이트는 원소이다. (1~75바이트 길이 원소 표현)
◦
0x4c (OP_PUSHDATA1) : 다음 한 바이트는 원소의 갯수이고 그 이후는 원소이다. (76~255바이트 길이 원소 표현)
◦
0x4d (OP_PUSHDATA2) : 다음 두 바이트는 원소의 길이이고 그 이후는 원소이다. (256~520바이트 길이 원소 표현)
◦
0x4e (OP_PUSHDATA4) : 다음 네 바이트는 원소의 길이이고 그 이후는 원소이다. 네트워크 허용 길이를 넘기 때문에 실질적으로 미사용된다.
◦
0x76 (OP_DUP) : 가장 위에 있는 원소를 하나 복사하여 스택에 넣는다.
◦
0x94 (OP_ADD) : 가장 위에 있는 두 개의 원소를 더하여 스택에 넣는다.
◦
0xa9 (OP_HASH160) : 가장 위에 있는 원소를 2회 해시한다. (SHA-256, RIPEMD-160)
•
모든 종류의 OP코드를 보고 싶다면 아래의 페이지에서 확인하길 바란다.
•
OP 코드 적용의 예시 : 지난번에 보았던 출력의 스크립트를 해석해보자
◦
아래 직렬화 트랜잭션에서 76 a9 14 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 88 ac 부분이 스크립트였다.
◦
76 : OP_DUP
◦
a9 : OP_HASH160.
◦
14 : 다음 20 바이트(16진수로 14)는 데이터임을 의미한다.
◦
ab68025513c3dbd2f7b92a94e0581f5d50f654e7 : 총 20 바이트의 원소영역
◦
88 : OP_EQUALVERIFY
◦
ac : OP_CHECKSIG
•
스크립트는 왼쪽에서 한 바이트씩 스택에 넣어가며 연산을 수행한다.
•
스크립트 예시를 통해 이해해보자.
◦
위 그림은 간단한 스크립트 연산을 보여준다.
◦
실행 포인터(Execution pointer)가 좌측에서 우측으로 이동하며 원소와 연산자를 왼쪽에 있는 스택에 넣는다.
◦
2를 스택에 넣는다.
◦
3을 스택에 넣는다.
◦
OP_ADD는 스택에서 원소 2개를 꺼내어 더한 뒤 결과를 스택에 넣는다. 5를 넣는다.
◦
5를 스택에 넣는다. (5가 두개가 됐다.)
◦
OP_EQUAL은 스택에서 원소 2개를 꺼내어 같으면 TRUE, 다르면 FALSE를 넣는다. TRUE를 넣었다.
잠금 스크립트와 해제 스크립트
•
트랜잭션의 입력과 출력에 각각 스크립트가 들어있다.
•
위 그림은 트랜잭션 입력 부분에 있는 해제 스크립트(Unlocking Script)와 출력 부분에 있는 잠금 스크립트(Locking Script)를 나타낸다.
•
해제 스크립트는 주로 디지털 서명을 검증하는 역할을 하므로 scriptSig라고 한다.
•
잠금 스크립트는 비트코인을 받을 대상자의 공개키를 지정하므로 scriptPibKey라고 한다.
•
트랜잭션에서의 스크립트 실행은 다음과 같은 과정을 따른다.
◦
먼저 해제 스크립트가 실행된다.
◦
해제 스크립트가 오류없이 실행되면 잠금 스크립트가 실행된다.
◦
잠금 스크립트의 실행 결과가 TRUE(or 1)가 되면 UTXO가 실행된다.
◦
TRUE 이 외에 다른 결과가 남아있으면 지출 요건이 실행되지 못한 것이므로 무효가 된다.
서명의 직렬화
•
해제 스크립트에는 서명값이 포함된다.
•
다음은 입력 스크립트의 예시이다.
◦
이를 해석해보자.
◦
30: 시작 인덱스
◦
45: 열의 총 길이 (69 bytes)
◦
02: 이어 나오는 R값이 정수형임을 나타냄.
◦
21: R의 길이를 의미함. (16진수 21은 십진수로 33. 바이트 단위로 33 bytes)
◦
00884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb: r값
◦
02: 이어 나오는 S값이 정수형임을 나타냄.
◦
20: S의 길이를 의미함. (16진수 20은 십진수로 32. 바이트 단위로 32 bytes)
◦
4b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813: s값
◦
01: 서명해시 유형 (여기서는 SIGHASH_ALL이 적용되어 있음.)
•
서명 검증에는 r, s ,z가 필요한데 z는 메시지의 해시값이다.
•
마지막 바이트인 서명해시 유형(SIGHASH)에 따라 어떤 메시지의 해시값인지 결정된다.
P2PK (Pay-to-PubKey) 스크립트
•
비트코인 초기에 널리 사용된 스크립트로 공개키에 비트코인을 지불한다는 의미이다.
•
다음은 P2PK 잠금 스크립트의 예시이다.
◦
공개키와 연산자 OP_CHECKSIG가 존재하는 것을 알 수 있다.
•
다음은 P2PK 해제 스크립트의 예시이다.
◦
서명값이 들어있는것을 볼 수 있다.
•
해제 스크립트와 잠금 스크립트를 순서대로 처리하면 다음과 같다.
1.
먼저 잠금 스크립트를 실행하고 이후에 해제 스크립트를 실행하기 위해 스택을 구성한다. (오른쪽)
2.
현재 스택은 비어있다.
3.
가장 위에 있는 서명값<signature>가 스택에 담긴다.
4.
다음 공개키<pubkey>가 스택에 담긴다.
5.
OP_CHECKSIG 연산자는 두 개의 원소를 스택에서 꺼내 서명을 검증하는 연산자이다. 서명이 맞으면 1, 틀리면 0을 스택에 넣는다.
P2PKH(Pay-to-Public Key Hash)
•
P2PK는 다소 문제점이 있다.
◦
공개키의 길이가 너무 길다. 압축형 공개키의 경우 33바이트나 된다.
◦
너무 길어서 인코딩에서 짤리는 경우가 많다.
◦
공개키를 주소로 적기에 너무 길다.
◦
UTXO 집합에 보관하기에 풀노드에게 부담이 된다.
◦
훗날 ECDSA가 깨지면 개인키 노출의 위험이 있다.
•
P2PKH는 공개키의 해시에 지불하는 것을 말한다.
•
P2PKH는 P2PK의 문제를 해결한다.
◦
공개키가 아닌 공개키의 해시값을 사용하여 더 짧다.
◦
추가 해시를 적용하여 보안적으로 더 안전하다.
•
공개키를 잠금 스크립트가 아닌 해제 스크립트에 포함한다.
◦
미지출된 출력 UTXO에서는 공개키가 없어서 풀노드에 부담이 적다.
◦
지불하기 전의 UTXO는 공개키를 알 수 없기 때문에 이 UTXO를 해킹하려면 이산로그 문제를 두 번 풀어야 한다.
◦
UTXO가 지불되면 공개키가 노출되지만, 이미 지불이 마친 출력이기 때문에 상관없다.
◦
한번 노출된 공개키는 다시 사용하지 않는것이 안전하다.
•
다음은 P2PKH 잠금 스크립트의 예시이다.
◦
공개키 대신에 공개키의 해시값이 들어간다.
•
다음은 P2PKH 해제 스크립트의 예시이다.
◦
서명값과 공개키가 포함되어 있다.
•
P2PKH 스크립트를 따라가보자.
1.
잠금 스크립트와 해제 스크립트를 합친다.
2.
현재 스택은 비어있다.
3.
스택에 두 원소 공개키<pubkey>와 서명값<signature>를 올린다.
4.
OP_DUP는 스택에서 원소를 하나 꺼내 복사하여 다시 집어 넣는 연산자이다. 공개키를 복사한다.
5.
OP_HASH160은 스택에서 원소를 하나 꺼내서 SHA-160(SHA-256 해시값의 RIPEMD-160 해시값)해시코드<hash>를 스택에 다시 넣는다.
6.
스택에 공개키의 해시값<hash>을 넣는다.
7.
OP_EQUALVERIFY는 스택에서 2개의 원소를 가져와서 같은 원소인지 검사한다. 검증에 실패하면 스크립트 검증은 실패로 끝난다.
8.
서명을 검증한다. 검증에 성공하면 1을 스택에 남긴다.
비표준 스크립트
•
출력 스크립트에 꼭 서명 검증을 넣을 필요는 없다.
•
다음과 같은 잠금 스크립트를 쓰면 매우 간단한 퀴즈에 상금을 부여할 수 있다.
◦
잠금스크립트
◦
해제 스크립트
◦
잠금 스크립트는 5에 무엇을 더하면 9와 같은지를 정의하고 있고, 해제 스크립트는 4를 정의하고 있다.
구현 : Script
#P2PKH 스크립트 정의
def p2pkh_script(h160):
return Script([0x76, 0xa9, h160, 0x88, 0xac]
class Script:
def __init__(self, cmds=None):
if cmds is None:
self.cmds = []
else:
self.cmds = cmds
def __repr__(self):
result = []
for cmd in self.cmds:
if type(cmd) == int:
if OP_CODE_NAMES.get(cmd):
name = OP_CODE_NAMES.get(cmd)
else:
name = 'OP_[{}]'.format(cmd)
result.append(name)
else:
result.append(cmd.hex())
return ' '.join(result)
def __add__(self, other):
return Script(self.cmds + other.cmds)
#스크립트를 원소와 연산자로 나누어 배열에 넣는 함수
@classmethod
def parse(cls, s):
#스크립트의 길이
length = read_varint(s)
#명령어가 저장될 배열
cmds = []
count = 0
while count < length:
#1바이트를 읽는다.
current = s.read(1)
count += 1
#바이트를 10진수로 변경
current_byte = current[0]
#만약 1에서 75사이라면
if current_byte >= 1 and current_byte <= 75:
#원소를 저장한다.
n = current_byte
#커멘드에 저장한다
cmds.append(s.read(n))
count += n
#76이면
elif current_byte == 76:
#op_pushdata1 이다.
data_length = little_endian_to_int(s.read(1))
#데이터 길이만큼 커멘드에 저장한다.
cmds.append(s.read(data_length))
count += data_length + 1
elif current_byte == 77:
# op_pushdata2 이다.
data_length = little_endian_to_int(s.read(2))
cmds.append(s.read(data_length))
count += data_length + 2
else:
#나머지는 연산자이다.
op_code = current_byte
#op code를 그대로 커멘드에넣는다.
cmds.append(op_code)
if count != length:
raise SyntaxError('parsing script failed')
return cls(cmds)
#스크립트 길이를 포함하지 않는 직렬화
def raw_serialize(self):
# initialize what we'll send back
result = b''
#커멘드를 돌면서 명령어를 넣는다.
for cmd in self.cmds:
#원소가 만약 숫자이면 연산자이다.
if type(cmd) == int:
#그대로 붙인다.
result += int_to_little_endian(cmd, 1)
else:
#아니면 원소이다.
#길이를 구해서
length = len(cmd)
#각 원소의 길이에 따라 코드를 삽입한다.
if length < 75:
result += int_to_little_endian(length, 1)
elif length > 75 and length < 0x100:
# pushdata1
result += int_to_little_endian(76, 1)
result += int_to_little_endian(length, 1)
elif length >= 0x100 and length <= 520:
# pushdata2
result += int_to_little_endian(77, 1)
result += int_to_little_endian(length, 2)
else:
raise ValueError('too long an cmd')
result += cmd
return result
#스크립트 길이를 포함하는 직렬화
def serialize(self):
#직렬화 데이터를 가져와서
result = self.raw_serialize()
total = len(result)
#스크립트 길이를 붙이고 리턴한다.
return encode_varint(total) + result
#스크립트를 검증하는 함수
#op코드 정의와 연산은 다음 소스코드를 참고하기 바란다.
#https://github.com/jimmysong/programmingbitcoin/blob/master/code-ch07/op.py
def evaluate(self, z):
#커멘드를 별도의 공간에 담는다.
cmds = self.cmds[:]
stack = []
altstack = []
#cmds 변수가 빌 때 까지
while len(cmds) > 0:
#일단 하나를 먼저 꺼낸다.
cmd = cmds.pop(0)
#연산자이면
if type(cmd) == int:
#매핑 함수를 가져온다.
operation = OP_CODE_FUNCTIONS[cmd]
#op_if, op_notif 인 경우
if cmd in (99, 100):
#op_if/op_notif는 cmd 배열의 조작을 필요로 한다.
if not operation(stack, cmds):
LOGGER.info('bad op: {}'.format(OP_CODE_NAMES[cmd]))
return False
elif cmd in (107, 108):
# op_toaltstack/op_fromaltstack은 다른 스택을 사용한다.
if not operation(stack, altstack):
LOGGER.info('bad op: {}'.format(OP_CODE_NAMES[cmd]))
return False
elif cmd in (172, 173, 174, 175):
#모두가 서명에 관련된 연산자이다. 서명 해시 z를 필요로 한다.
if not operation(stack, z):
LOGGER.info('bad op: {}'.format(OP_CODE_NAMES[cmd]))
return False
else:
if not operation(stack):
LOGGER.info('bad op: {}'.format(OP_CODE_NAMES[cmd]))
return False
else:
stack.append(cmd)
if len(stack) == 0:
return False
if stack.pop() == b'':
return False
return True
Python
복사