[밑바닥비트코인] 7. 스크립트

2021. 8. 1. 22:29Blockchain

'스크립트(Script)'는 비트코인 네트워크 내에서 비트코인이 어떠한 조건에서 소비되는지를 기술한 

 

프로그래밍 언어이다. 

 

비트코인 스크립트의 특징은 다음과 같다. 

1. 반복 작업을 위한 루프 기능이 존재하지 않는다. (튜링 완전하지 않다)

2. 원소연산자 두가지의 요소로 이루어져있다. 

 

반복문이 존재하지 않는 이유는 무한 반복문을 만들어서 네트워크에 부정적 영향을 미칠 수 있기 때문이다. 

 

원소는 고정된 상수라고 생각할 수 있고, 연산자는 원소에 대해서 특정 연산을 수행한다. 

class Script:

    def __init__(self, cmds=None):
        if cmds is None:
            self.cmds = []
        else:
            self.cmds = cmds

스크립트는 원소와 연산자를 담고 있는 cmds라는 리스트를 멤버 변수로 가지고 있다. 

 

그렇다면 Raw Transaction에 들어있는 스크립트를 파싱하는 과정을 알아보자.

 

 

 

스크립트 파싱


    @classmethod
    def parse(cls, s):
        length = read_varint(s)
        cmds = []
        count = 0
        while count < length:
            current = s.read(1)
            count += 1
            current_byte = current[0]
            if current_byte >= 1 and current_byte <= 75:
                n = current_byte
                cmds.append(s.read(n))
                count += n
            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:
                # we have an opcode. set the current byte to op_code
                op_code = current_byte
                # add the op_code to the list of cmds
                cmds.append(op_code)
        if count != length:
            raise SyntaxError('parsing script failed')
        return cls(cmds)
length = read_varint(s)

1) 먼저 스트림에서 바이트를 읽어들인다. 이것이 스크립트의 전체 길이이다. 

while count < length:
            current = s.read(1)
            count += 1
            current_byte = current[0]

2) 스크립트 끝에 닿을 때까지 한 바이트씩 읽어들인다. 

if current_byte >= 1 and current_byte <= 75:
                n = current_byte
                cmds.append(s.read(n))
                count += n

3) 읽어들인 값이 1~75일 경우 그만큼의 바이트 길이를 읽어들이고 이 원소를 cmds에 추가한다. 

 

(76, 77인 경우 한 바이트(77은 두 바이트)만큼 더 읽은 길이가 읽어들일 바이트 길이이다.) 

            else:
                op_code = current_byte
                cmds.append(op_code)

4) 이렇게 읽어들인 바이트가 op code이면 cmds에 해당 op code를 추가해준다. 

 

 

 

스크립트 직렬화


이제 스크립트를 직렬화하는 방법을 알아보자. 

 

    def raw_serialize(self):
        # initialize what we'll send back
        result = b''
        # go through each cmd
        for cmd in self.cmds:
            # if the cmd is an integer, it's an opcode
            if type(cmd) == int:
                # turn the cmd into a single byte integer using int_to_little_endian
                result += int_to_little_endian(cmd, 1)
            else:
                # otherwise, this is an element
                # get the length in bytes
                length = len(cmd)
                # for large lengths, we have to use a pushdata opcode
                if length < 75:
                    # turn the length into a single byte integer
                    result += int_to_little_endian(length, 1)
                elif length > 75 and length < 0x100:
                    # 76 is pushdata1
                    result += int_to_little_endian(76, 1)
                    result += int_to_little_endian(length, 1)
                elif length >= 0x100 and length <= 520:
                    # 77 is 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

스크립트 직렬화 전체 코드는 위와 같다. 

if type(cmd) == int:
                # turn the cmd into a single byte integer using int_to_little_endian
                result += int_to_little_endian(cmd, 1)

1) 먼저 읽어들인 커맨드가 int 형식일 경우(예를 들면 길이정보) 이를 1바이트의 리틀엔티언 정수로 변환한다.

            else:
                length = len(cmd)
                # for large lengths, we have to use a pushdata opcode
                if length < 75:
                    # turn the length into a single byte integer
                    result += int_to_little_endian(length, 1)
                elif length > 75 and length < 0x100:
                    # 76 is pushdata1
                    result += int_to_little_endian(76, 1)
                    result += int_to_little_endian(length, 1)
                elif length >= 0x100 and length <= 520:
                    # 77 is pushdata2
                    result += int_to_little_endian(77, 1)
                    result += int_to_little_endian(length, 2)

2) int 형식이 아닐경우 이는 원소이므로 먼저 길이 정보를 표현하기 위해 길이를 계산한다. 

 

이 길이정보를 1바이트 리틀엔디언으로 붙여주고

       result += cmd

3) 마지막으로 해당 원소를 추가해준다. 

 

 

 

스크립트 결합


출처 : https://developer.bitcoin.org/devguide/transactions.html

 

비트코인을 사용하기 위해서는 자신에게 송금된 이전 트랜잭션의 잠금스크립트를 

 

자신이 생성하려는 트랜잭션의 해제 스크립트로 해제시켜야한다. 

 

이를 위해서 잠금 스크립트와 해제 스크립트를 결합하는 과정을 거쳐야한다. 

    def __add__(self, other):
        return Script(self.cmds + other.cmds)

각 스크립트 객체의 커맨드(리스트)를 연결하는 것으로 + 연산자를 재정의하였다. 

 

 

 

p2pk 스크립트


스크립트도 여러가지 종류가 있는데(트랜잭션 잠금/해제 스크립트의 방식이 다양하기 때문에),

 

최초 스크립트 중 하나인 p2pk 스크립트를 알아보자. 

출처 : https://www.researchgate.net/figure/P2PK-script-validation_fig4_335146032

 

p2pk에서는 잠금 스크립트(ScriptPubkey)는 Pubk(공개키) OP_CHECKSIG로 이루어져있고, 

 

해제 스크립트(ScriptSig)는 Sig(서명)으로 이루어져있다. 

 

이 둘을 결합하여 스택(Stack)에 저장한 뒤에 연산자에 따라 원소들을 검증한다. 

 

이를 코드로 구현해보자. 

 

 

 

스크립트 실행 메소드


    def evaluate(self, z):
        cmds = self.cmds[:]
        stack = []
        altstack = []
        while len(cmds) > 0:
            cmd = cmds.pop(0)
            print('POP!!!')
            if type(cmd) == int:
                operation = OP_CODE_FUNCTIONS[cmd]
                if cmd in (99, 100):
                    # op_if/op_notif require the cmds array
                    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 require the altstack
                    if not operation(stack, altstack):
                        LOGGER.info('bad op: {}'.format(OP_CODE_NAMES[cmd]))
                        return False
                elif cmd in (172, 173, 174, 175):
                    # these are signing operations, they need a sig_hash
                    # to check against
                    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:
                # add the cmd to the stack
                stack.append(cmd)
        if len(stack) == 0:
            return False
        if stack.pop() == b'':
            return False
        return True

전체 코드는 위와 같다. 

        while len(cmds) > 0:
            cmd = cmds.pop(0)
            print('POP!!!')

1) 가장 위에 있는 원소(혹은 연산자)를 POP 한다. 

            if type(cmd) == int:
                operation = OP_CODE_FUNCTIONS[cmd]

2) Pop한 대상이 Int형이라면 이에 해당하는 OP code의 함수를 인식한다. 

                if cmd in (99, 100):
                    if not operation(stack, cmds):
                        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

해당 OP code를 실행하고, operation의 결과가 True가 아니라면 False를 반환한다.