[밑바닥비트코인] 5. 직렬화

2021. 7. 15. 19:21Blockchain

지금까지 구현한 클래스들의 객체(인스턴스)들을 네트워크 상에서 전파하거나 주고받기 위해서는 

 

직렬화 과정이 필요하다. S256Point, Signature 등의 객체들을 직렬화하는 방법을 알아보자.

 

 

 

SEC 형식


먼저 S256Point 객체의 직렬화 과정을 추가해보자.

    def sec(self, compressed=True):
        if compressed:
            if self.y.num%2 == 0:
                return b'\x02'+self.x.num.to_bytes(32,'big')
            else:
                return b'\x03'+self.x.num.to_bytes(32,'big')
        return b'\x04'+self.x.num.to_bytes(32, 'big') \
               + self.y.num.to_bytes(32,'big')

먼저 비압축 SEC(Standards for Efficient Cryptography) 형식을 알아보자!

        return b'\x04'+self.x.num.to_bytes(32, 'big') \
               + self.y.num.to_bytes(32,'big')

비압축 SEC에서는 16진법으로 '04' 접두부로 시작하고 x값과 y값을 각각 32바이트 빅엔디언 정수로 표현하여 연결한다. 

 

총 65바이트의 크기를 차지한다. 

 

다음은 압축 SEC 형식이다. 

 

압축 SEC 형식은 타원곡선의 특성상 같은 x값에 대해서 2가지 y값을 구할 수 있다는 점을 이용하여 y값을 생략하는데, 

 

※ 유한체 타원곡선에서 y값은 위수 p에 대해서 대칭이므로 같은 x에 대해 y, p-y 두 가지 값을 갖는다.

 

접두부에 y에 대한 간략한 정보를 담는다.

        if compressed:
            if self.y.num%2 == 0:
                return b'\x02'+self.x.num.to_bytes(32,'big')
            else:
                return b'\x03'+self.x.num.to_bytes(32,'big')

y가 짝수라면, 접두부는 16진수 02로 시작하여 x값만 32바이트 빅엔디언 정수로 표현하고, 

 

홀수라면 접두부는 \x03이 붙는다. 

 

이렇게 표현된 SEC 형식을 해석하는 parse 메소드를 작성하자!

    @classmethod
    def parse(cls, sec_bin):
        if sec_bin[0] == 4:
            x = int.from_bytes(sec_bin[1:33], 'big')
            y = int.from_bytes(sec_bin[33:65], 'big')
            return S256Point(x=x,y=y)
        is_even = sec_bin[0] == 2  # True 혹은 False
        x = S256Field(int.from_bytes(sec_bin[1:], 'big'))
        alpha = x ** 3 + S256Field(B) 
        beta = alpha.sqrt()  # y값을 구한다
        if beta.num % 2 == 0:
            even_beta = beta  # y는 짝수
            odd_beta = S256Field(P - beta.num)  # p-y는 홀수
        else:
            even_beta = S256Field(P - beta.num) # p-y는 짝수
            odd_beta = beta  # y는 홀수
        if is_even:
            return S256Point(x, even_beta)  # x, 짝수인 y 반환
        else:
            return S256Point(x, odd_beta) # x, 홀수인 y 반환

먼저 접두부가 04일 경우, x와 y를 있는 그대로 해석하여 int 형으로 만들어서 반환하고, 

 

아닐 경우(02 혹은 03) x를 먼저 해석하고, 그에 따른 y를 계산한다. p-y역시 계산해주는데 

 

y가 짝수이면 p-y는 홀수, y가 홀수이면 p-y는 짝수가 된다. 그에 따라서

 

접두부가 02라면 짝수인 쪽을 S256Point 객체에 담아서 반환해주고, 03이라면 홀수인 쪽을 담아서 반환해준다.

>>> e = 5001
>>> P = e*G
>>> P
S256Point(57a4f368868a8a6d572991e484e664810ff14c05c0fa023275251151fe0e53d1, 0d6cc87c5bc29b83368e17869e964f2f53d52ea3aa3e5a9efa1fa578123a0c6d)
>>> compSEC = P.sec(compressed=True).hex()
>>> compSEC
'0357a4f368868a8a6d572991e484e664810ff14c05c0fa023275251151fe0e53d1'

생성된 공개키 P를 압축 SEC 형식으로 만들어주고 이를 16진수 형태로 출력한다. 

>>> S256Point.parse(bytes.fromhex(compSEC))
S256Point(57a4f368868a8a6d572991e484e664810ff14c05c0fa023275251151fe0e53d1, 0d6cc87c5bc29b83368e17869e964f2f53d52ea3aa3e5a9efa1fa578123a0c6d)

생성된 압축 SEC 형식의 공개키를 parse한다. 위의 P와 동일한 결과를 출력한다. 

 

 

 

 

DER 서명 형식


이번에는 Signature 클래스의 객체를 직렬화해보자. 

 

서명 직렬화 표준은 DER(Distinguished Encoding Rules) 형식이다. 

 

Signature 클래스에 다음 메소드를 추가해주자. 

    def der(self):
        rbin = self.r.to_bytes(32, byteorder='big') # r을 32바이트 빅엔디언으로 만들어준다
        rbin = rbin.lstrip(b'\x00') # \x00을 제거해준다
        if rbin[0] & 0x80: # 첫 바이트가 \x08 이상일 경우
            rbin = b'\x00' + rbin # \x00을 붙여준다
        result = bytes([2,len(rbin)]) + rbin # rbin의 길이 추가, r의 시작을 의미하는 02 추가
        sbin = self.s.to_bytes(32, byteorder='big')
        sbin = sbin.lstrip(b'\x00')
        if sbin[0] & 0x80:
            sbin = b'\x00' + sbin
        result += bytes([2, len(sbin)]) + sbin
        return bytes([0x30, len(result)]) + result

DER 서명 형식으로의 변환과정은 위와 같다. 

Signature(6772cc6b032971c4c8f8c8d5919c6ce6c4780d06efc3c0f2bae86c364284b166,686cbc0678655f02768cedef6af31083c98c939658a4ad2dc4e9fecbd46acf4f)
>>> sig.der()
b'0D\x02 gr\xcck\x03)q\xc4\xc8\xf8\xc8\xd5\x91\x9cl\xe6\xc4x\r\x06\xef\xc3\xc0\xf2\xba\xe8l6B\x84\xb1f\x02 hl\xbc\x06xe_\x02v\x8c\xed\xefj\xf3\x10\x8
3\xc9\x8c\x93\x96X\xa4\xad-\xc4\xe9\xfe\xcb\xd4j\xcfO'

 

 

 

 

비트코인의 주소 형식


비트코인 주소는 공개키 암호 체계의 공개키로 사용할 수 있다. 

 

Base58 encoding은 공개키의 가독성, 길이, 보안성의 목적을 모두 달성하는 부호화 방식이다. 

BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

def encode_base58(s):
    count = 0
    for c in s:  # 몇 바이트가 0바이트인지 알 수 있다.
        if c == 0:
            count += 1
        else:
            break
    num = int.from_bytes(s, 'big')
    prefix = '1' * count
    result = ''
    while num > 0:
        num, mod = divmod(num, 58)
        result = BASE58_ALPHABET[mod] + result
    return prefix + result  # 맨 앞부분에 0으로 된 부분을 1으로 변경


def encode_base58_checksum(b):
    return encode_base58(b + hash256(b)[:4])

 

위 부호화 방식을 통해 공개키를 비트코인 주소로 바꾸는 방법을 알아보자. 

    def hash160(self, compressed=True):
        return hash160(self.sec(compressed))

    def address(self, compressed=True, testnet=False):
        h160 = self.hash160(compressed)
        if testnet:
            prefix = b'\x6f'
        else:
            prefix = b'\x00'
        return encode_base58_checksum(prefix+h160)

먼저 공개키를 SEC 형식으로 표현한 결과를 SHA256 해시함수에 넣고, 이를 다시 ripemd160 해시하는 것을

 

hash160이라고 한다. 비트코인의 경우 코인이 존재하는 네트워크에 해당하는 접두부를 이 해시값에 붙이고, 

 

이를 BASE58 부호화한 것에 체크섬을 이어붙인다. 

>>> e= 5002
>>> P = e*G
>>> P = e*G
>>> P.address(compressed=False, testnet=True)
'mmTPbXQFxboEtNRkwfh6K51jvdtHLxGeMA'

 

 

 

 

비밀키의 WIF 형식


비밀키 역시 다른 지갑으로 이동시킬 경우 직렬화 과정이 필요하다.

    def wif(self, compressed=True, testnet=False):
        secret_bytes = self.secret.to_bytes(32, 'big')
        if testnet:
            prefix = b'\xef'
        else:
            prefix = b'\x80'
        if compressed:
            suffix = b'\x01'
        else:
            suffix = b''
        return encode_base58_checksum(prefix + secret_bytes + suffix)

비밀키를 32바이트 빅엔디언으로 바꾼다.  테스트넷일 경우 '0xef' 메인넷일 경우 '0x80'으로 시작하며 

 

압축형식일 경우 '0x01'이 후두부에 붙는다. 이를 BASE58 부호화하여 체크섬을 붙인다. 

>>> e = 5003
>>> pv_key = PrivateKey(e)
>>> pv_key.wif()
'KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFUqzioMfUXC'