세그윗은 무엇인가?
•
세그윗은 ‘segrated witness’의 약자로서 ‘분리된 증인’을 뜻한다.
◦
암호학에서 witness는 서명이나 공개키 같은 어떤 조건을 만족하는 존재를 증명할 때 쓰인다.
◦
여기서의 증인은 서명값이다.
◦
서명값은 해제 스크립트에 포함되므로 결국 해제 스크립트를 분리하는 것이다.
•
2017년 8월부터 BIP-9소프트포크를 통해 비트코인에 적용되었다.
•
서명이 포함되어 있는 해제 스크립트를 별도로 분리하는 것이 골자이다.
•
기존의 트랜잭션과 비교
◦
기존 트랜잭션
▪
해제 스크립트(서명값 스크립트)가 트랜잭션에 존재한다.
▪
따라서 TxID(트랜잭션 ID)를 추출하기 위해 해시를 구할 때 해제 스크립트가 포함된다.
▪
따라서 해제 스크립트를 변경하면 TxID가 변경된다. (당연한 이야기이다.)
◦
세그윗 트랜잭션
▪
해제 스크립트가 트랜잭션 밖으로 빠져나왔다.
▪
TxID에 서명값이 포함되지 않는다.
▪
해제 스크립트를 변경하여도 TxID가 변경되지 않는다.
세그윗 도입의 장점
•
트랜잭션의 가변성(Malleability)
◦
트랜잭션의 내용을 유지한 채 트랜잭션ID가 변경될 수 있었다.
▪
서명값 r, s와 r, N-s는 모두 유효한 서명이다.
▪
동일한 결과를 가져오는 다른 해제 스크립트를 작성한다.
◦
가변성 문제로 인한 혼란의 예
1.
앨리스가 s를 사용하여 트랜잭션 tx1을 생성한다. (tx1은 아직 블록에 포함되지 않았다.)
2.
앨리스가 만약 N-s를 사용하여 트랜잭션 tx2를 생성한다.
3.
tx1, tx2는 TxID가 다르기 때문에 포크가 일어난다.
4.
시간이 지나면서 하나의 블록만 선택된다.
5.
불필요한 포크는 채굴자에게 피해를 줄 수 있다.
◦
세그윗의 해결책
▪
TxID에서 더 이상 해제 스크립트를 포함하지 않아 모호성을 제거하여 혼란을 줄인다.
•
예전에는 해제 스크립트를 포함하여 TxID를 추출했다면,
•
세그윗에서는 해제 스크립트를 비우고 witness 필드에 해제 스크립트를 작성한다.
•
witness 필드는 트랜잭션 해시를 추출할 때 제외된다.
•
따라서 witness필드(해제 스크립트)가 변해도 TxID는 동일하다.
•
스크립트 버전 관리
◦
증인 스크립트는 버전을 명시하고 어떤 버전의 스크립트인지 확인할 수 있다.
◦
스크립트의 업그레이드가 어려운 비트코인의 단점을 어느정도 극복한다.
•
네트워크 및 스토리지 확장
◦
비트코인 블록 크기를 확장하는 경우 하드포크가 불가피하다.
◦
하드포크는 중앙화된 의사결정이 반영될 수 있어서 가능하면 하지 않는 것이 좋다.
◦
세그윗은 실질적으로 블록사이즈를 4배로 늘렸지만 하드포크 하지 않았다.
▪
세그윗 이전에는 블록사이즈가 1Mb 였다.
▪
Weight Unit(wu)라는 개념을 도입해 블록사이즈를 4Mwu로 늘렸다.
•
입력과 출력 데이터는 1바이트에 4wu로 책정한다.
•
증인 데이터는 1바이트에 1wu로 책정한다.
◦
채굴자는 필요에 따라 서명값을 포함하지 않아도 된다.
◦
이를 제거하여 더 많은 트랜잭션을 담을 수 있다.
•
서명 확인의 최적화
◦
스크립트 연산자 OP_CHECKSIG, OP_CHECKMULTISIG를 업그레이드 했다.
•
수수료 개선
◦
기존에는 서명값이 트랜잭션 비용에 반영되었다.
◦
수수료 책정 시 증인(서명)의 사이즈를 25%만 반영하여 실질적으로 수수료가 저렴해졌다.
•
지갑의 보안성 강화
◦
세그윗 이전에는 서명 시 이전 출력의 비트코인의 양을 넣지 않았다.
◦
따라서 오프라인 상태에서는 출력의 비트코인 양을 모르기 때문에 온체인이 되어야하만 수수료를 계산할 수 있었다.
◦
세그윗에서는 서명 메시지에 출력 비트코인의 양을 포함시켜야 하기 때문에 송금자가 직관적으로 수수료를 확인할 수 있다.
#세그윗에서 서명 메시지의 해시값을 구하는 함수
def sig_hash_bip143(self, input_index, redeem_script=None, witness_script=None):
tx_in = self.tx_ins[input_index]
#세그윗 스팩
s = int_to_little_endian(self.version, 4)
s += self.hash_prevouts() + self.hash_sequence()
s += tx_in.prev_tx[::-1] + int_to_little_endian(tx_in.prev_index, 4)
if witness_script:
script_code = witness_script.serialize()
elif redeem_script:
script_code = p2pkh_script(redeem_script.cmds[1]).serialize()
else:
script_code = p2pkh_script(tx_in.script_pubkey(self.testnet).cmds[1]).serialize()
s += script_code
#비트코인 수량을 포함한다.
s += int_to_little_endian(tx_in.value(), 8)
s += int_to_little_endian(tx_in.sequence, 4)
s += self.hash_outputs()
s += int_to_little_endian(self.locktime, 4)
s += int_to_little_endian(SIGHASH_ALL, 4)
return int.from_bytes(hash256(s), 'big')
Python
복사
P2WPKH(Pay-to-Witness Pubkey Hash) 스크립트
•
트랜잭션의 가변성 문제를 해결하기 위해 제안된 BIP-141과 BIP-143에서 정의된 스크립트 유형이다.
•
P2PKH와 가장 큰 차이점은 해제 스크립트의 데이터가 분리된다는 것이다.
•
P2PKH와 P2WPKH의 차이점을 비교해보자
◦
잠금 스크립트가 단순화되었다.
▪
다음은 P2PKH의 잠금 스크립트 예시이다.
•
DUP HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 EQUALVERIFY CHECKSIG
▪
다음은 P2WPKH의 잠금 스크립트의 예시이다.
•
0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7
▪
잠금 스크립트가 매우 단순해졌다는 것을 알 수 있다.
◦
다음은 입력의 모습이다.
▪
P2PKH 입력의 예시이다.
[...]
"Vin" : [
"txid": "0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2",
"vout": 0,
"scriptSig": "<Bob’s scriptSig>",
]
[...]
Plain Text
복사
▪
다음은 P2WPKH 입력의 예시이다.
[...]
"Vin" : [
"txid": "0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2",
"vout": 0,
"scriptSig": "",
]
[...]
"witness": "<Bob’s witness data>"
[...]
Plain Text
복사
▪
서명값이 입력 안으로 들어가지 않고 별도의 필드로 분리되었다.
•
세그윗 이전과 이후의 트랜잭션을 비교해보자.
◦
세그윗 이전의 P2WPKH
◦
세그윗 이후의 P2WPKH
witness에 오타가 있다. 61→ac
▪
세그윗 이후에는 세그윗 마커(Segwit marker)와 세그윗 플래그(Segwit flag), 증인(witness)가 추가되었다.
▪
해제스크립트는 00으로 비어있다. (서명값이 없다.)
▪
증인필드 안에는 서명과 공개키 두 개의 원소를 가지고 있다.
▪
잠금 스크립트에는 OP_0과 20바이트의 해시값이 들어있다.
•
세그윗에도 P2SH와 유사하게 증인을 명령어 집합에 들어가는 패턴 조건이 있다.
◦
스크립트에 특정 원소가 2개(OP_0, 20바이트 해시[pubKeyHash])가 스크립트에 있을 때 발동된다.
◦
이 조건을 만나면 증인 필드의 원소를 스크립트로 파싱하여 명령 집합에 추가한다.
◦
증인 필드의 스크립트 안에 있는 공개키의 해시가 스크립트에 남아있는 <20-byte hash>와 동일해야 한다.
•
세그윗 버전 0의 구성
1.
잠금 스크립트
2.
스택는 결국 다음이 남게 된다. 이는 증인 필드를 삽입하는 발동 조건이다.
3.
스크립트에서 저 패턴을 만나면 증인 필드에 있는 p2pkh의 결합 스크립트를 구성하여 명령 집합에 추가한다.
◦
•
향후 세그윗 버전은 지속적으로 업그레이드하여 적용될 수 있다.
•
이는 비트코인 스크립트의 버전 관리를 가능하게 한 것이다.
•
향후 슈노르 서명 등 완전히 다른 스크립트가 버전으로 구분되어 적용될 수 있다.
•
이를 지원하는 주소는 Bech32 접두사를 사용하여 bc1으로 시작한다.
P2SH-P2WPKH 스크립트
•
P2SH 지갑은 P2WPKH주소로 송금할 수 없다.
•
따라서 P2SH 형식으로 P2WPKH를 구현할 수 있는 방법을 고안하였다.
•
•
P2SH-P2WPKH 구동 과정
1.
여기까지는 P2SH의 구성과 동일하다.
2.
특별 규칙이 발동되는 조건이 되었다.
3.
리딤 스크립트를 스택에 올린다.
4.
<hash>를 검사한다.
5.
검사가 성공한다면 스택에 1이 남을 것이고 리딤 스크립트가 파싱되어 스크립트에 추가된다.
6.
결국 P2WPKH가 발동하는 조건이 되었다.
P2WSH(Pay-to-Witness Script Hash) 스크립트
•
세그윗에서도 스크립트를 통한 지불이 가능하다.
•
P2SH와 다른점은 해제 스크립트의 데이터가 증인필드에 위치한다는 점이다.
•
다음은 세그윗에서 P2WSH 트랜잭션의 예시이다.
◦
세그윗의 필드 항목은 앞에서 살펴본 바와 같이 동일하다.
◦
하지만 증인 필드는 약간 다르다.
◦
해제스크립트는 ‘00’으로 비어있다.
•
P2WSH에서는 P2WPKH와 증인 필드 발동 조건이 다르다.
◦
스크립트에 특정 원소가 2개(OP_0, 32바이트 해시[scriptHash])가 있을 때 발동된다.
◦
이 32 바이트 해시는 증인 필드 내에 있는 증인 스크립트의 해시값과 동일해야 한다.
◦
만약 동일하면 증인 스크립트가 파싱되어 명령 집합에 삽입된다.
•
증인필드
◦
설명에 오타가 있다.
▪
47 - Length of <signaturex>
▪
3044…01 - <signaturex>
▪
48 - length of <signaturey>
▪
3045…01 - <signaturey>
◦
이 스크립트는 2-of-3 다중서명으로 이루어져 있다.
◦
마지막 항목은 증인 스크립트이다. 트랜잭션 잠금 스크립트에 있는 32바이트 해시와 동일해야 한다.
•
구동 과정
1.
해제 스크립트는 비어있고, 잠금 스크립트는 다음과 같이 구성된다.
2.
P2WSH 세그윗 발동 조건이 되었다.
3.
스택에 값을 옮기면 이제 증인 필드를 추가한다.
4.
증인필드를 검증한다.
•
증인 필드의 예
◦
마지막 항목은 증인 스크립트이다. 스택에 있는 원소<32-byte hash>와 동일해야 한다.
5.
증인 스크립트와 원소<32-byte hash>와 검증이 완성되면 증인 스크립트가 추가된다.
•
증인 스크립트의 예
◦
이 그림에도 오타가 있다.
▪
52 - OP_2
▪
21 - Length of <pubkeyx>
▪
027c…f3 - <pubkeyx>
▪
21 - Length of <pubkeyy>
▪
03be…35 - <pubkeyy>
▪
21 - Length of <pubkeyz>
▪
02b3…ac - <pubkeyz>
▪
53 - OP_3
▪
ae - OP_CHECKMULTISIG
◦
이 증인 스크립트는 2-of-3 다중서명이다.
•
스크립트에 추가되면 다음과 같은 모습이 된다.
6.
스크립트에 따라 다중서명 검증을 수행한다.
P2SH-P2WSH 스크립트
•
이 또한 P2PKH-P2WPKH 스크립트 처럼 예전 지갑들로 P2WSH를 지원하기 위한 방법이다.
•
아래 그림은 P2SH-P2WSH의 예시이다.
•
구동 방식
1.
잠금 스크립트는 P2SH와 동일하며 해제 스크립트에는 리딤 스크립트만 들어있다. (서명값은 없다.)
•
P2SH에는 해제 스크립트에 서명이 있지만, 세그윗이기 때문에 서명이 없다.
•
2.
스택에 먼저 리딤 스크립트를 올린다.
3.
리딤 스크립트의 해시값을 검사한다.
4.
해시값 검사를 통과하면 리딤 스크립트를 올린다.
•
리딤 스크립트는 다음과 같다.
•
이 리딤 스크립트는 증인 스크립트 발동 조건을 일으키는 스크립트이다.
5.
증인 필드를 스크립트에 올린다.
•
증인 필드의 값을 파싱하여 스크립트에 올린다.
6.
증인 필드에서 증인 스크립트를 추출해 증인 스크립트와 <32-byte-hash>가 일치하는지 확인한다.
7.
일치하는 경우 증인 스크립트를 올린다.
•
증인 스크립트의 예시이다.
8.
다중 서명을 검증한다.
구현 : 트랜잭션
#트랜잭션 클래스를 수정한다.
class Tx:
command = b'tx'
#세그윗 필드를 추가한다.
def __init__(self, version, tx_ins, tx_outs,
locktime, testnet=False, segwit=False):
self.version = version
self.tx_ins = tx_ins
self.tx_outs = tx_outs
self.locktime = locktime
self.testnet = testnet
self.segwit = segwit
self._hash_prevouts = None
self._hash_sequence = None
self._hash_outputs = None
#...생략
@classmethod
#세그윗 파싱을 위한 함수
#기존의 parse는 parse_legacy로 변경하였다.
def parse(cls, s, testnet=False):
#세그윗 마커 확인
s.read(4)
#세그윗 트랜잭션 확인. 다섯번째 바이트가 0이면 세그윗이다.
if s.read(1) == b'\x00':
#세그윗 파싱을 실행한다.
parse_method = cls.parse_segwit
else:
parse_method = cls.parse_legacy
#스트림 포인터를 원위치한다.
s.seek(-5, 1)
return parse_method(s, testnet=testnet)
@classmethod
def parse_segwit(cls, s, testnet=False):
version = little_endian_to_int(s.read(4))
marker = s.read(2)
#세그윗 마커
if marker != b'\x00\x01':
raise RuntimeError('Not a segwit transaction {}'.format(marker))
num_inputs = read_varint(s)
inputs = []
for _ in range(num_inputs):
inputs.append(TxIn.parse(s))
num_outputs = read_varint(s)
outputs = []
for _ in range(num_outputs):
outputs.append(TxOut.parse(s))
#증인(witness)필드 파싱
for tx_in in inputs:
num_items = read_varint(s)
items = []
for _ in range(num_items):
item_len = read_varint(s)
if item_len == 0:
items.append(0)
else:
items.append(s.read(item_len))
tx_in.witness = items
locktime = little_endian_to_int(s.read(4))
return cls(version, inputs, outputs, locktime,
testnet=testnet, segwit=True)
#세그윗 직렬화. 기존의 직렬화는 serialize_legacy로 변경되었다.
def serialize_segwit(self):
result = int_to_little_endian(self.version, 4)
#마커 추가
result += b'\x00\x01'
result += encode_varint(len(self.tx_ins))
for tx_in in self.tx_ins:
result += tx_in.serialize()
result += encode_varint(len(self.tx_outs))
for tx_out in self.tx_outs:
result += tx_out.serialize()
#증인 필드 직렬화
for tx_in in self.tx_ins:
result += int_to_little_endian(len(tx_in.witness), 1)
for item in tx_in.witness:
if type(item) == int:
result += int_to_little_endian(item, 1)
else:
result += encode_varint(len(item)) + item
result += int_to_little_endian(self.locktime, 4)
return result
def verify_input(self, input_index):
tx_in = self.tx_ins[input_index]
script_pubkey = tx_in.script_pubkey(testnet=self.testnet)
#P2SH 이면
if script_pubkey.is_p2sh_script_pubkey():
cmd = tx_in.script_sig.cmds[-1]
raw_redeem = int_to_little_endian(len(cmd), 1) + cmd
redeem_script = Script.parse(BytesIO(raw_redeem))
#P2SH-P2WPKH인 경우
if redeem_script.is_p2wpkh_script_pubkey():
z = self.sig_hash_bip143(input_index, redeem_script)
witness = tx_in.witness
#P2SH-P2WSH인 경우
elif redeem_script.is_p2wsh_script_pubkey():
cmd = tx_in.witness[-1]
raw_witness = encode_varint(len(cmd)) + cmd
witness_script = Script.parse(BytesIO(raw_witness))
z = self.sig_hash_bip143(input_index, witness_script=witness_script)
witness = tx_in.witness
else:
z = self.sig_hash(input_index, redeem_script)
witness = None
else:
#P2WPKH
if script_pubkey.is_p2wpkh_script_pubkey():
z = self.sig_hash_bip143(input_index)
witness = tx_in.witness
#P2WSH
elif script_pubkey.is_p2wsh_script_pubkey():
cmd = tx_in.witness[-1]
raw_witness = encode_varint(len(cmd)) + cmd
witness_script = Script.parse(BytesIO(raw_witness))
z = self.sig_hash_bip143(input_index, witness_script=witness_script)
witness = tx_in.witness
else:
z = self.sig_hash(input_index)
witness = None
combined = tx_in.script_sig + script_pubkey
return combined.evaluate(z, witness)
Python
복사
구현 : 스크립트
class Script:
#...생략
#P2WPKH
def p2wpkh_script(h160):
#증인 필드 발동 조건
return Script([0x00, h160])
#P2WSH
def p2wsh_script(h256):
#증인 필드 발동 조건
return Script([0x00, h256])
#...생략
#P2PKH 확인
def is_p2pkh_script_pubkey(self):
# 5개의 명령어가 있어야 한다.
# OP_DUP (0x76), OP_HASH160 (0xa9), 20-byte hash, OP_EQUALVERIFY (0x88),
# OP_CHECKSIG (0xac)
return len(self.cmds) == 5 and self.cmds[0] == 0x76 \
and self.cmds[1] == 0xa9 \
and type(self.cmds[2]) == bytes and len(self.cmds[2]) == 20 \
and self.cmds[3] == 0x88 and self.cmds[4] == 0xac
#P2SH 확인
def is_p2sh_script_pubkey(self):
# 3개의 명령어가 있어야 한다.
# OP_HASH160 (0xa9), 20-byte hash, OP_EQUAL (0x87)
return len(self.cmds) == 3 and self.cmds[0] == 0xa9 \
and type(self.cmds[1]) == bytes and len(self.cmds[1]) == 20 \
and self.cmds[2] == 0x87
#P2WPKH 확인
def is_p2wpkh_script_pubkey(self):
return len(self.cmds) == 2 and self.cmds[0] == 0x00 \
and type(self.cmds[1]) == bytes and len(self.cmds[1]) == 20
#P2WSH 확인
def is_p2wsh_script_pubkey(self):
return len(self.cmds) == 2 and self.cmds[0] == 0x00 \
and type(self.cmds[1]) == bytes and len(self.cmds[1]) == 32
#...생략
# 스크립트 검증
def evaluate(self, z, witness):
cmds = self.cmds[:]
stack = []
altstack = []
while len(cmds) > 0:
cmd = cmds.pop(0)
if type(cmd) == int:
operation = OP_CODE_FUNCTIONS[cmd]
if cmd in (99, 100):
if not operation(stack, cmds):
LOGGER.info('bad op: {}'.format(OP_CODE_NAMES[cmd]))
return False
elif cmd in (107, 108):
if not operation(stack, altstack):
LOGGER.info('bad op: {}'.format(OP_CODE_NAMES[cmd]))
return False
elif cmd in (172, 173, 174, 175):
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(cmds) == 3 and cmds[0] == 0xa9 \
and type(cmds[1]) == bytes and len(cmds[1]) == 20 \
and cmds[2] == 0x87:
redeem_script = encode_varint(len(cmd)) + cmd
cmds.pop()
h160 = cmds.pop()
cmds.pop()
if not op_hash160(stack):
return False
stack.append(h160)
if not op_equal(stack):
return False
if not op_verify(stack):
LOGGER.info('bad p2sh h160')
return False
redeem_script = encode_varint(len(cmd)) + cmd
stream = BytesIO(redeem_script)
cmds.extend(Script.parse(stream).cmds)
# P2WPKH
# 0 <20 byte hash> P2WPKH의 발동 조건
if len(stack) == 2 and stack[0] == b'' and len(stack[1]) == 20:
h160 = stack.pop()
stack.pop()
cmds.extend(witness)
cmds.extend(p2pkh_script(h160).cmds)
# P2WSH
# 0 <32 byte hash> P2WSH의 발동 조건
if len(stack) == 2 and stack[0] == b'' and len(stack[1]) == 32:
#해시값
s256 = stack.pop()
#세그윗 버전
stack.pop()
#증인 스크립트를 제외하고 명령어 집합 확장
cmds.extend(witness[:-1])
#증인 스크립트
witness_script = witness[-1]
#증인 스크립트 해시 검사
if s256 != sha256(witness_script):
print('bad sha256 {} vs {}'.format
(s256.hex(), sha256(witness_script).hex()))
return False
stream = BytesIO(encode_varint(len(witness_script))
+ witness_script)
#증인 스크립트 파싱
witness_script_cmds = Script.parse(stream).cmds
cmds.extend(witness_script_cmds)
if len(stack) == 0:
return False
if stack.pop() == b'':
return False
return True
Python
복사