5️⃣

세그윗

세그윗은 무엇인가?

세그윗은 ‘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의 결합 스크립트를 구성하여 명령 집합에 추가한다.
이후 과정은 P2PKH 방식과 유사하다. (P2PKH 참고)
향후 세그윗 버전은 지속적으로 업그레이드하여 적용될 수 있다.
이는 비트코인 스크립트의 버전 관리를 가능하게 한 것이다.
향후 슈노르 서명 등 완전히 다른 스크립트가 버전으로 구분되어 적용될 수 있다.
이를 지원하는 주소는 Bech32 접두사를 사용하여 bc1으로 시작한다.

P2SH-P2WPKH 스크립트

P2SH 지갑은 P2WPKH주소로 송금할 수 없다.
따라서 P2SH 형식으로 P2WPKH를 구현할 수 있는 방법을 고안하였다.
일반적인 P2SH주소이지만 리딤 스크립트의 구성이 다르다. (P2SH 참고)
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에는 해제 스크립트에 서명이 있지만, 세그윗이기 때문에 서명이 없다.
스크립트에 4개의 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
복사