비트코인/비트코인 구조

[비트코인 구조] 세그윗(Segwit), Bech32 주소

라이튼 2022. 9. 23. 15:45

미리 알아야 할 내용들


 

[비트코인 구조] DER 형식 서명(Signature) 생성

미리 알아야 할 내용들 '비트코인/암호학' 카테고리의 글 목록 평범한 대학생의 블록체인 기술 관련 블로그 입니다. kwjdnjs.tistory.com [비트코인 암호학] 4.1 디지털 서명  디지털 서명은 공개키 암

kwjdnjs.tistory.com

 

[비트코인 구조] P2PKH

미리 알아야 할 내용들 [비트코인 구조] 개인키(Private key), 공개키(Public key), 주소(Address) 생성 미리 알아야 할 내용들 '비트코인/암호학' 카테고리의 글 목록 평범한 대학생의 블록체인 기술 관련

kwjdnjs.tistory.com

 

[비트코인 구조] P2SH

미리 알아야 할 내용들 [비트코인 구조] P2PKH 미리 알아야 할 내용들 [비트코인 구조] 개인키(Private key), 공개키(Public key), 주소(Address) 생성 미리 알아야 할 내용들 '비트코인/암호학' 카테고리의

kwjdnjs.tistory.com


세그윗(Segwit)

 

 세그윗은 당시 큰 논란이 되었던 2017년 비트코인 소프트포크를 통해 등장하였습니다. 기존과는 다르게 Bech32로 인코딩 된 주소를 사용하며, 이전보다 더 효율적으로 트랜잭션을 다룰 수 있도록 도와줍니다. 이번 글에서는 가장 먼저 세그윗이 무엇인지에 대해서 먼저 알아보겠습니다.

 

트랜잭션 가변성 문제(transaction malleability problem)와 세그윗

트랜잭션 가변성 문제

 

 세그윗을 이해하기 위해서는 먼저 트랜잭션 가변성 문제를 이해해야 합니다. 트랜잭션 가변성 문제란 생성된 트랜잭션이 블록에 포함되어 블록체인에 올라가기 전에 트랜잭션의 해시가 변경될 수 있는 문제를 말합니다. 일반적으로 한 번 서명된(해제 스크립트가 포함된) 트랜잭션의 해시는 변경되지 않습니다. 하지만 특정한 상황에서는 트랜잭션의 해시가 블록에 포함되기 전에 변경될 수 있습니다.

 

 이전 글에서 DER 서명을 생성하는 과정에서 타원곡선을 이용하여 r값과 s값을 구했습니다. 문제는 r값에 해당하는 s값이 두 개 존재한다는 점입니다. 이는 타원곡선이 대칭을 이루기 때문입니다. 따라서 r값에 대한 s값은 [(-s) mod n (여기서의 n은 secp256k1에 정의된 값)]도 가능합니다. 즉, 같은 형태의 해제 스크립트를 사용하더라도 포함된 서명의 s값이 달라 트랜잭션의 해시값이 다른 두 트랜잭션이 존재할 수 있게 됩니다. 물론 이 문제는 하나의 형태의 s값만을 허용하는 방식으로 규칙을 수정하면 크게 문제가 되지 않습니다. 하지만 DER 서명 이외에도 트랜잭션 가변성 문제가 발생할 수 있는 부분들이 여전히 존재합니다. 예를 들어 이미 서명이 포함된 해제 스크립트에 추가적으로 OP_5등 검증 과정에서 영향을 미치지 못하는 의미 없는 값을 추가할 수 있습니다. 이러한 문제들을 해결하기 위해 세그윗이 제안되었습니다.

 

세그윗

 

 세그윗은 Segregated Witness의 약자입니다. 이름 그대로 세그윗은 증인 필드(Witness)라고 할 수 있는 해제 스크립트를 트랜잭션에서 제외하여 분리(Segregated)하는 방식으로 설계되어 있습니다. 이 과정에서 네이티브 세그윗은 P2WPKH, P2WSH 방식을 네스티드 세그윗은 P2SH-P2WPKH, P2SH-P2WSH 방식의 스크립트를 사용합니다. P2SH에 대해서는 이전 글에서 다뤘으므로, 이번 글에서는 P2WPKH부터 다뤄보겠습니다.

 

P2WPKH

 P2WPKH는 Pay-To-Witness-PubKey-Hash의 약자입니다. 기존 스크립트들과는 다르게 해제 스크립트 없이 트랜잭션과는 별도로 분리된 증인 필드를 사용한다는 특징이 있습니다.

 

 P2WPKH의 잠금 스크립트는 다음과 같습니다.

 

잠금 스크립트: [OP_0] <공개키 해시(hash160, 20바이트)>

 

 P2WPKH의 해제 스크립트는 다음과 같습니다.

 

해제 스크립트:

 

 위 내용은 실수로 비어있는 것이 아닙니다. 글의 초반부에서 알아봤던 것처럼 네이티브 세그윗에서는 해제 스크립트로 사용될 서명 데이터를 트랜잭션이 속한 블록 외부에 존재하는 별도의 증인 필드에 저장합니다. 따라서 P2WPKH 해제 스크립트는 특별한 내용이 없이 비어있습니다.

 

 지금부터는 잠금 스크립트와 해제 스크립트로 서명을 검증해보겠습니다. 해제 스크립트가 존재하지 않으므로 잠금 스크립트를 스택에 담습니다.

 

 

 네이티브 세그윗을 지원하지 않는 노드는 추가적인 스크립트가 남아있지 않으므로 서명 검증을 종료합니다. 공개키 해시값은 0이 아니므로 서명 검증은 성공으로 처리합니다. 물론 네이티브 세그윗 미지원 노드가 잘못된 트랜잭션을 검증이 성공한 것으로 받아들여 블록을 생성하더라도, 네이티브 세그윗 지원 노드들이 블록을 연결하는 과정에서 증인 필드에 잘못된 서명이 포함된 것을 인지하고 해당 블록을 거부할 것입니다. 따라서 네이티브 세그윗 지원 노드가 전체 노드의 50% 이상을 차지한다면, 이 부분은 크게 문제가 되지 않습니다.

 

 네이티브 세그윗을 지원하는 노드의 경우 위와 같은 형태의 잠금 스크립트가 존재할 때 추가적인 검증 작업을 진행합니다. 이 과정에서 별도로 분리되어 있던 증인 필드에서 데이터를 가져와 잠금 스크립트의 공개키 해시값과 비교하여 검증합니다. 다음은 P2WPKH의 증인 필드 데이터입니다.

 

증인 필드 데이터: <서명> <공개키>

 

 네이티브 세그윗 지원 노드는 증인 필드의 공개키의 해시 값을 구해 잠금 스크립트의 공개키 해시 값을 비교합니다. 그리고 서명 값을 이용해 서명을 검증합니다. 이 과정에서 비트코인 스크립트가 사용되지 않습니다. 즉 스택을 이용하지 않고 네이티브 세그윗 노드 내부에 내장된 전용 코드를 이용하여 검증합니다.

 

P2WSH

 P2WSH는 P2WPKH와는 다르게 공개키 대신 증인 필드 데이터에 추가로 속해있는 증인 스크립트를 사용합니다.

 

 P2WSH의 잠금 스크립트는 다음과 같습니다.

 

 잠금 스크립트: [OP_0] <증인 스크립트 해시(sha-256, 32바이트)>

 

 증인 필드는 다음과 같습니다.

 

 증인 필드 데이터: <증인 필드 아이템> <증인 스크립트>

 

 서명 검증 과정은 P2WPKH 그리고 P2SH와 유사합니다.

 

네이티브 세그윗 주소(Bech32 주소)

 네이티브 세그윗 주소는 이전 Base58Check 인코딩 방식을 사용했던 주소들과는 다르게 Bech32라고 부르는 새로운 인코딩 방식을 사용합니다. 기존에 사용하던 Base58Check의 경우 I와 l등 구별하기 어려운 영문자를 제거해 가독성을 높였지만, 대문자와 소문자가 모두 존재하여 사용이 여전히 불편했습니다. Bech32는 이러한 불편함을 없애기 위해 소문자만을 사용하도록 구성하여 대소문자의 구분을 없앴습니다.

 

 공개키를 이용하여 네이티브 세그윗 주소를 생성해보겠습니다. P2WSH의 경우 공개키 대신 증인 스크립트를 사용하면 됩니다.

 

1. 공개키 hash160 값(또는 증인 스크립트 sha-256 값): 4d2f18c566944ff7ce000b657d237e8a2fecbf87

 

2. 8bit 기준으로 된 1번 값을 5bit 기준으로 변환한 후 5bit 단위로 16진수로 읽습니다. 아래는 해당 작업을 진행하는 파이썬 코드입니다.

p = '4d2f18c566944ff7ce000b657d237e8a2fecbf87'
bytes_p = bytes.fromhex(p)

# byte 값을 bit로 변환
def bits(bytes):
    result = ''
    for b in bytes:
        temp = ''
        for i in range(8):
            temp += str((b >> i) & 1)
        result += (temp[::-1])

    return result

# 5 bit씩 불러오기
count = 1
bits_5 = ''
result = ''
for b in bits(bytes_p):
    bits_5 += b
    
    if count==5:
        result += f'{int(bits_5, 2):02x}'
        count = 1
        bits_5 = ''
    else:
        count += 1

result = '0x' + result
print(result)

 

결괏값: 0x0914171111110b061211071f0f131000010d12171a081b1e1108171e190f1c07

 

3. 네이티브 세그윗 버전을 맨 앞에 추가합니다. (현재 버전 0)

 

결괏값: 000914171111110b061211071f0f131000010d12171a081b1e1108171e190f1c07

 

4. 3번 값을 메인넷, 테스트넷 구분자(H.R.P)와 함께 체크섬을 계산합니다.(비트코인 메인넷 구분자: 'bc') 다음은 체크섬을 구하는 파이썬 코드입니다.

# bech32 checksum 코드
# https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
def bech32_polymod(values):
  GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
  chk = 1
  for v in values:
    b = (chk >> 25)
    chk = (chk & 0x1ffffff) << 5 ^ v
    for i in range(5):
      chk ^= GEN[i] if ((b >> i) & 1) else 0
  return chk

def bech32_hrp_expand(s):
  return [ord(x) >> 5 for x in s] + [0] + [ord(x) & 31 for x in s]

def bech32_create_checksum(hrp, data):
  values = bech32_hrp_expand(hrp) + data
  polymod = bech32_polymod(values + [0,0,0,0,0,0]) ^ 1
  return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]

# 입력 데이터
hrp = 'bc'
hex_data = '000914171111110b061211071f0f131000010d12171a081b1e1108171e190f1c07'

# 체크섬 구하기
data = []
temp = ''
for i in hex_data:
  temp += i

  if len(temp) == 2:
    data.append(int(temp, base=16))
    temp = ''

result = bech32_create_checksum(hrp, data)

checksum = ''
for i in result:
  checksum += f'{i:02x}'

print(checksum)

 

체크섬 결괏값: 1c001a0e1d04

 

위 결괏값을 3번 값 맨 뒤에 붙입니다.

 

결괏값: 000914171111110b061211071f0f131000010d12171a081b1e1108171e190f1c071c001a0e1d04

 

5. 4번 값을 bech32로 인코딩한 후, 맨 앞에 비트코인 메인넷을 뜻하는 'bc' 와 separator '1'을 추가합니다. 이 값이 바로 bech32 주소입니다. 다음은 4번 값을 이용해 주소를 구하는 파이썬 코드입니다.

 

CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

hex_data = '000914171111110b061211071f0f131000010d12171a081b1e1108171e190f1c071c001a0e1d04'

temp = ''
address = ''
for i in hex_data:
  temp += i

  if len(temp) == 2:
    address += CHARSET[int(temp, base=16)]
    temp = ''


address = 'bc1' + address

print(address)

 

생성된 Bech32 주소: bc1qf5h333txj38l0nsqpdjh6gm73gh7e0u8uq6way

 

P2SH-P2WPKH(네스티드 세그윗, Nested Segwit)

 P2WPKH 네이티브 세그윗 스크립트는 Bech32 주소를 사용하기 때문에 이전 비트코인 주소와 호환이 되지 않습니다. 이 문제를 해결하기위해 네스티드 세그윗(혹은 세그윗)이라고도 부르는 P2SH-P2WPKH가 등장했습니다. 이 방식은 기존의 P2SH 스크립트의 리딤 스크립트를 P2WPKH 스크립트로 사용하는 방식입니다. 따라서 P2WPKH 방식과는 다르게 해제 스크립트가 존재합니다.

 

 먼저 P2SH-P2WPKH의 잠금 스크립트는 다음과 같습니다.

 

P2SH-P2WPKH 잠금 스크립트: [OP_HASH160] <리딤 스크립트 해시> [OP_EQUAL]

 

 P2SH-P2WPKH의 해제스크립트는 다음과 같습니다.

 

P2SH-P2WPKH 해제 스크립트: <리딤 스크립트>

 

  리딤 스크립트는 다음과 같습니다.

 

P2SH-P2WPKH 리딤 스크립트: [OP_0] <공개키 해시>

 

 리딤 스크립트가 P2WPKH 스크립트이기 때문에 이 스크립트에 해당하는 증인 필드가 존재합니다. 증인 필드 값은 다음과 같습니다.

 

증인 필드 데이터: <서명> <공개키>

 

 결론적으로 P2SH-P2WPKH 스크립트는 잠금과 해제 스크립트는 P2SH 스크립트로, 리딤스크립트는 P2WPKH로 구성하여 기존 주소와의 호환성을 제공하도록 구성되었습니다. 서명의 증명 과정은 P2SH, P2WPKH와 같습니다. 먼저 해제 스크립트를 스택에 올립니다.

 

 

 그 다음 잠금 스크립트를 하나씩 불러옵니다.

 

 

 

 리딤 스크립트에 대한 증명이 성공하면 P2SH 지원 노드는 리딤 스크립트의 내용을 불러옵니다.

 

 

 위와 같은 형태는 P2WPKH 스크립트이므로 네이티브 세그윗 지원 노드는 증인 필드에서 데이터를 불러와 공개키 해시와 함께 검증하게 됩니다.

 

 참고로 P2WSH를 위한 P2SH-P2WSH도 존재합니다. 전체적인 스크립트는 P2SH와 P2WSH가 혼합된 형태입니다.

 

지금까지 세그윗에 대해 알아봤습니다. 감사합니다.

 

이어지는 글들


 

[비트코인 구조] 트랜잭션 기본 규칙

미리 알아야 할 내용들 [비트코인 구조] 비트코인 트랜잭션(Transaction) 기초 미리 알면 좋은 내용들 [블록체인 용어] 블록체인(Block Chain)  블록체인이란 발생한 거래들을 블록에 담고 블록들을 연

kwjdnjs.tistory.com