無知

갈 길이 먼 공부 일기

기술 공부/블록체인

스마트 컨트랙트 (2) | Bitcoin Scripts

moozii 2022. 4. 3. 22:35

 

0. 비트코인 스크립트란?

비트코인을 가리켜 종종 프로그래밍 가능한 돈이라 하기도 합니다. 비트코인의 디지털 특성 때문에, 사용자는 상당히 유연한 방식으로 자금 사용 조건을 설정할 수 있습니다. 비트코인에 대해 논의할 때, 우리는 지갑과 코인에 대해 이야기 합니다. 그러나 우리는 지갑을 키로, 코인을 수표로, 블록체인을 줄지어 늘어선 금고로 생각할 수 있습니다. 각 금고에는 작은 틈이 존재하여, 누구나 수표를 입금하거나 얼마나 많은 금액이 보관되어 있는지 들여다볼 수 있습니다. 그러나 키를 보유한 이만 금고 내부에 접근할 수 있습니다.
키를 보유한 이가 누군가에게 자금을 전달하고자 한다면, 자신의 금고의 잠금을 해제합니다. 이들은 이전의 수표를 참조하는 새로운 수표를 생성하고(이전의 수표는 파기), 수신자가 열 수 있는 금고에 이를 넣고 잠급니다. 새로운 수신자가 이를 사용하고자 한다면, 동일한 과정을 반복합니다. 이번 아티클에서 우리는 스크립트에 대해 자세히 살펴볼 것이며, 이는 블록체인 네트워크상의 노드에 의해 해석되는 프로그래밍 언어입니다. 스크립트는 언급된 금고의 잠금 및 해제 메커니즘을 관리하는 것입니다.

비트코인은 어떻게 작동하나요?
위의 비유를 계속해서 살펴보자면, 모든 트랜잭션에는 키(여러분의 금고 잠금을 해제하는)와 잠금장치(lock)가 존재한다고 할 수 있습니다. 여러분은 사용하고자 하는 수표가 들어 있는 금고를 열기 위해 키를 사용하며, 새로운 수표를 새로운 금고에 다른 잠금장치와 함께 넣습니다. 새로운 금고를 사용하기 위해서는, 다른 키가 필요합니다. 무척 간단합니다. 여러분은 또한 해당 시스템 내 잠금장치에 약간의 변화를 줄 수도 있습니다. 일부 금고에서는 다중서명 키가 필요할 것이며, 다른 금고에서는 여러분이 비밀을 알고 있음을 증명해야 할 수도 있습니다. 사용자들이 설정할 수 있는 많은 조건들이 존재합니다. 
우리의 키를 scriptSig라 합니다. 잠금장치는 scriptPubKey입니다. 우리가 그 구성 요소를 조금 더 자세히 살펴본다면, 이들은 데이터 비트와 코드 블록으로 이뤄져 있음을 알 수 있습니다. 이들을 결합하여, 그들은 작은 프로그램을 만듭니다.
트랜잭션을 생성할 때, 여러분은 해당 조합을 네트워크에 전송합니다. 이를 수신하는 각 노드는 트랜잭션의 유효성 여부를 말해주는 프로그램을 확인할 것입니다. 트랜잭션이 유효하지 않다면 이는 폐기될 것이며, 동결된 자금을 사용할 수 없게 됩니다.
여러분이 보유하고 있는 수표(코인)를 UTXOs(Unspent Transaction Outputs)라 합니다. 이 자금은 해당 잠금장치에 해당하는 키를 제시하는 누구나 사용할 수 있습니다. 보다 구체적으로 말하자면, 해당 키는 scriptSig, 잠금 장치는 scriptPubKey입니다. 여러분의 지갑에 UTXOs가 있을 경우, 이는 이 공개 키의 소유권을 증명할 수 있는 이만 자금 동결을 해제할 수 있음이라고 말하는 상황일 것입니다. 잠금을 해제하려면 디지털 서명을 포함하는 scriptSig를 제공하고, scriptPubKey에 명시된 공개 키와 연관된 개인 키를 사용해야 합니다. 다음의 설명을 통해 이를 보다 분명하게 이해할 수 있을 것입니다.

https://academy.binance.com/ko/articles/an-introduction-to-bitcoin-script 
 

비트코인 스크립트 설명 | Binance Academy

비트코인 내부에는 자금을 어떻게 사용하며 또 누가 이를 사용할 수 있는지를 지시하는 스크립팅 언어가 존재합니다. 바이낸스 아카데미에서 비트코인 스크립트에 대해 자세히 알아보시기 바

academy.binance.com

 

 

 

 

0-1. Testnet 실습

>>> from bitcoin import *

>>> priv = sha256('****************')
>>> pub = privtopub(priv)

>>> addr = pubtoaddr(pub)
>>> addr
'1L8zP6iitxPx3YKXfJQSJpifhNgwZnUszM'

>>> addr = pubtoaddr(pub, 111)
>>> addr
'mzewg9ohhyqCpeo9NsNp8jvzZNHeSPYSiM'

>>> tx = testnet_fetchtx('5ca41898f43179ed5273f112b655b9a11f6895f56d8c61bd0e15052f6031bb2b')
Traceback (most recent call last):
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\urllib\request.py", line 1348, in do_open
    h.request(req.get_method(), req.selector, req.data, headers,
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\http\client.py", line 1282, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\http\client.py", line 1328, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\http\client.py", line 1277, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\http\client.py", line 1037, in _send_output
    self.send(msg)
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\http\client.py", line 975, in send
    self.connect()
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\http\client.py", line 1447, in connect
    super().connect()
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\http\client.py", line 941, in connect
    self.sock = self._create_connection(
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\socket.py", line 824, in create_connection
    for res in getaddrinfo(host, port, 0, SOCK_STREAM):
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\socket.py", line 955, in getaddrinfo
    for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno 11001] getaddrinfo failed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\user\PycharmProjects\pybitcointools\bitcoin\bci.py", line 21, in make_request
    return opener.open(*args).read().strip()
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\urllib\request.py", line 519, in open
    response = self._open(req, data)
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\urllib\request.py", line 536, in _open
    result = self._call_chain(self.handle_open, protocol, protocol +
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\urllib\request.py", line 496, in _call_chain
    result = func(*args)
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\urllib\request.py", line 1391, in https_open
    return self.do_open(http.client.HTTPSConnection, req,
  File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\urllib\request.py", line 1351, in do_open
    raise URLError(err)
urllib.error.URLError: <urlopen error [Errno 11001] getaddrinfo failed>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\user\PycharmProjects\pybitcointools\bitcoin\bci.py", line 380, in testnet_fetchtx
    data = make_request('https://testnet.blockchain.info/rawtx/'+txhash+'?format=hex')
  File "C:\Users\user\PycharmProjects\pybitcointools\bitcoin\bci.py", line 27, in make_request
    raise Exception(p)
Exception: <urlopen error [Errno 11001] getaddrinfo failed>
2022 update - testnet.blockchain.info does not seem to be working - does anyone have an updated url for this service from blockchain.info or do I have to find another provider? 
https://bitcoin.stackexchange.com/questions/4445/is-there-a-blockchain-info-for-testnet 

상기 오류로 우선 이론적인 부분 등만 익혀 비트코인 스크립트를 파악해두고자 한다.

우선 실습 관련 내용은 테스트넷 faucet을 통해 비트코인을 실습용으로 지급받아 송금 트랜잭션을 작성하는 등의 실습을 진행하는 방식으로, 이전 주차에서도 사용한 pybitcointools가 활용된다.

 

 

 

 

 

1. UTXO Model (Keep the Change)

 

blockinfo_unspent(addr, 'testnet') 함수를 통해, addr 주소 내 아직 사용하지 않은 UTXO를 확인할 수 있다. 해당 함수의 결과값은 transaction id: index로 구성된 output과 함께 그 값(value, 사토시 단위)로 구성된 UTXO의 리스트이다. 

 

이를 기반으로, input = blockinfo_unspent(addr, 'testnet')[0] 을 통해 송금 트랜잭션 인풋을 설정하고 (UXTO 리스트 중 몇번째 인덱스의 UTXO 항목인지), output = {'value': 50000, 'address': 'mkHS9ne12qx9pSQVojpwU5xtRd4T7X7ZUt'} 를 통해 송금트랜잭션 아웃풋을 설정해준 뒤, tx=mktx([input], [output]) 로 트랜잭션을 만들어, tx2=sign(tx, 0, priv) 로 앞서 만든 트랜잭션을 프라이빗 키를 통해 서명하면 된다. 서명 시 0이라는 입력값은, index 값으로 input 리스트의 인덱스를 의미한다. (여러개의 input이 있다면 모두 각기 서명)

 

최종적으로 testnet_pushtx(tx2) 를 통해 서명한 트랜잭션을 비트코인 네트워크로 보내면 블록에 추가된다.

 

그 결과값을 확인하기 위해 Bitcoin Explorer를 사용하게 되면, 확인해보면 송금액 5만 사토시를 제외한 잔액 5만 사토시 전체가 수수료로 지급되어, 잔고값이 0 사토시임을 알 수 있는데, 잔고의 남은 잔량 전체는 채굴업자에게 보상금으로 지급하는 라이브러리를 사용 중이기 때문이다. 

 

이처럼, 송금 시 잔돈을 받지 않는 keep the change 옵션, 잔고의 남은 잔량을 채굴업자 보상금 차원의 수수료로 모두 가져가는 저금통 형태의 모델을 UTXO 모델이라 한다. 따라서 해당 모델 내 너무 과도한 수수료가 확인되면 실수로 인식해 블록 포함이 거부되기도 한다. 

 

만약 해당 모델에서 본인 몫으로 잔액을 남겨두기 위해서는 다음과 같은 스크립트로 작성해야 한다.

addr_faucet = 'mkHS9ne12qx9pSQVojpwU5xtRd4T7X7ZUt'
input = blockinfo_unspent(addr, 'testnet')
output = [{'value': 100000, 'address': addr_faucet}, {'value': 395000, 'address': addr}]
tx=mktx( input, output)
tx2=sign(tx, 0, priv)
testnet_pushtx(tx2)

즉, 본인 주소로 남기고 싶은 잔액을 송금한다는 것을 output으로 명시해주면, 위에서 명시한 output 값을 제외한 나머지 액수만 수수료로 자동 지급된다는 것이다. 

 

다만, 그 수수료 액수가 너무 낮으면 우선순위가 낮아 채굴업자들이 블록에 포함하지 않아 거래 승인이 오래 걸릴 수 있기 때문에 수수료 계산의 경우 조금 더 복잡하고 엄밀하게 책정하는 것이 중요하다. 

 

 

 

 

 

2. 비트코인 스크립트 (P2PKH)

 

https://academy.binance.com/ko/articles/an-introduction-to-bitcoin-script

 

P2PKH는 퍼블릭 키로 지불하는 해시 방식으로, Pay-to-Public-Key-Hash 방식의 줄임말이다. 다른 개인의 지갑 주소로 코인을 전송하는 것 등을 표기할 수 있다. 그외로는 P2SH, Pay-to-Script-Hash 방식이 존재하는데 우선 P2PKH 방식부터 그 작동 원리인 스크립트를 알아보도록 하자.

 

우선, 스크립트는 역폴란드 표기법, postfix notation, RPN (Reverse-polish notation)을 사용한다.  피연산자 피연산자 연산자 순으로 기록하며, Stack 형태로 연산을 진행하는 것이 특징이다. 트랜잭션 검증 스크립트를 실행했을 때 true 혹은 0이 아닌 값이 마지막으로 stack에 남으면 검증 성공이고, 그렇지 않은 false나 0이 나올 경우에는 검증 실패이다. 스크립트 실제 작동 방식은 다음 링크로 시각화하여 학습할 수 있다.

https://wschae.github.io/build/editor.html

 

Bitcoin IDE

Debugger --> Run Step Continue Next Argument

wschae.github.io

 

 

트랜잭션 인풋, 아웃풋에는 모두 스크립트가 존재한다.

트랜잭션 아웃풋에는 locking script가 포함된다. scriptPubKey로도 부르며, 일반적으로 공개 키가 담긴다.  해당 공개 키에 대한 비밀 키, private key를 보유한 사람만 해당 코인을 사용할 수 있도록 하는 것이다. 비트코인에서는 pub key 대신 pub key hash라는, 공개키 기반으로 생성한 해시값을 사용한다. 

트랜잭션 인풋에는 unlocking script가 포함된다. scriptSig로도 부르며, 일반적으로 디지털 서명이 담긴다. (디지털 서명과 pub key를 담는다)

 

트랜잭션 검증은, 트랜잭션 인풋인 지불하는 지갑이 value만큼 지불가능하고 아직 사용하지 않은 UTXO를 보유했음을 검증하여 트랜잭션을 승인해주는 절차를 의미한다. 

 

 

 

트랜잭션 검증은 다음 단계로 진행된다. 

 

1. Input의 UTXO Pointer를 통해 검증 대상 UTXO를 찾는다

 

2. Input의 Unlocking Script (scriptSig)를 실행한다.

2-1. Unlocking Script = <Signature><Public Key> 형태이다.

2-2. <Public Key> OP_DUP 연산을 수행한다. OP_DUP는 Stack 위 최상단 값을 복사해 추가하므로 Stack은 <Signature> <Public Key> <Public Key> 순이 된다.

2-3. <Public Key> OP_HASH160 연산을 수행한다. OP_HASH160 연산자는 Stack 위 최상단 값의 해시 값을 구해 Stack 최상단에 둔다. 즉, 결과값 Stack은 <Signature> <Public Key> <Public Key Hash> 순이 된다.

 

3. 이전 트랜잭션의 UTXO 내 locking Script (public key)를 실행한다. 

3-1. Locking Script인 <Public Key Hash>가 Stack 최상단에 추가된다. 즉, 현재 결과값은 <Signature> <Public Key> <Public Key Hash> <Public Key Hash from Locking Script>가 된다.

3-2. <Public Key Hash> <Public Key Hash from Locking Script> OP_EQUALVERIFY 연산을 수행한다. OP_EQUALVERIFY는 Stack 최상단 2개 피연산자를 가지고 같은지를 비교검증하는 연산이다. 같을 경우 Stack에서 피연산자들이 사라지고 넘어가고, 만약 같지 않다면 전체 연산이 멈추고 트랜잭션 검증이 실패를 반환해 트랜잭션이 승인되지 않는다. 즉, 성공할 경우 Stack은 <Signature> <Public Key>만 남는다.

3-3. <Signature> <Public Key> OP_CHECKSIG 연산을 수행한다.   OP_CHECKSIG 연산은 Stack 내 서명과 공개 키 값을 읽어 서로 매팅이 되는지 확인해, 맞으면 true, 틀리면 false를 반환해 최종적으로 stack에 결과값을 남긴다. 

 

4. true일 경우 검증 성공, false일 경우 검증 실패이다. 

 

 

이러한 트랜잭션 스크립트는 어셈블리어, 즉 바이트로 인코딩된 형태로 전달되게 된다. 또한 이런 스크립트 프로그램은 자유롭지만, 실제 메인넷의 경우에는 보안상 몇가지의 표준 트랜잭션만을 허용한다.  

 

 

 

 

 

3. Multisignature Transaction 다중서명 트랜잭션

 

이전에 트랜잭션을 위해 하나의 서명만 필요했던 것과 달리, 다중서명 트랜잭션이 필요한 이유는 다음과 같다. 서명을 해야 하는 송금 대상자가 비밀 키를 잃어버리게 될 경우에는 코인이 무용지물이 되기도 하고, 만약 복잡한 소유관계가 얽혀있는 계정이라 할지라도 송금 대상자 계정 소유자가 승인하면 다른 이해관계자의 승인 없이 독단적으로 코인을 사용할 수 있기 때문이다. 

 

다중서명 트랜잭션 구현을 통해 여러 사람의 공동 소유 계정으로의 송금을 통해, 송금 대상자 일부만 서명/승인해도 사용하는 경우, 혹은 송금 대상자 전원이 서명/승인해야 코인 사용이 가능한 경우 등 이전보다 복잡한 케이스를 핸들링할 수 있다. 

 

다중서명 트랜잭션 구현을 위해서는 이전 단일 서명 모델의 OP_CHECKSIG와 달리,

M <Public Key 1> <Public Key 2> ... <Public Key N> N OP_CHECKMULTISIG 연산으로 구현한다. 

 

M은 코인 사용을 위해 필요한 서명의 개수이고, N은 M 이상의 트랜잭션 승인 가능 총 서명 수이다. M과 N을 명시한 M-of-N transaction 다중 서명 트랜잭션을 구현하면 다음과 같이 작동한다. 

 

예를 들어 2-of-2일 경우, 

1. Input의 UTXO Pointer를 통해 검증 대상 UTXO를 찾는다

2. Input의 Unlocking Script (scriptSig)를 실행한다.

2-1. Unlocking Script = 0 <Signature_A><Signature_B> 형태이다. 서명 대상자 모두의 서명 값을 가지며, 가장 앞에는 0 값을 넣어 트랜잭션 연산 과정에서 피연산자를 읽는 데에 오류가 없도록 한다. 즉, 사용되지는 않고 개수 확인에만 쓰이는 필수 값이다. 

 

3. 이전 트랜잭션의 UTXO 내 locking Script (public key)를 실행한다. 

3-1. Locking Script인 2 <Public Key Hash_A> <Public Key Hash_B> 2 OP_CHECKMULTISIG 가 Stack에 추가된다. 

 

3-3. 0 <Signature_A><Signature_B> <Signature> 2 <Public Key Hash_A> <Public Key Hash_B> 2 OP_CHECKMULTISIG 연산을 수행한다.   

 

3-3-1. OP_CHECKMULTISIG는 스택 최상단 N 값(트랜잭션 승인 가능 총 서명 수), 즉 예시 속 2를 읽는다. 

3-3-2. OP_CHECKMULTISIG는 N만큼 스택 피연산자 2개를 읽는다. 즉, <Public Key Hash_A> <Public Key Hash_B> 값을 읽는다. 

3-3-3. OP_CHECKMULTISIG는 스택 최상단 M 값(트랜잭션 승인 필수 총 서명 수), 즉 예시 속 2를 읽는다. 

3-3-4. OP_CHECKMULTISIG는 M+1만큼 스택 피연산자 3개를 읽는다. 즉, 0 <Signature_A><Signature_B> 값을 읽는다. 

3-3-5. 읽어들인 서명 피연산자와 공개 키 피연산자를 기반으로 일치하면 참 값을 출력해 트랜잭션을 승인한다. 

 

# Exercise : A to B&C 2-of-2

priv_A = sha256('password_A')
priv_B = sha256('password_A')
priv_C = sha256('password_A')

pub_A = privtopub(priv_A)
pub_B = privtopub(priv_B)
pub_C = privtopub(priv_C)

addr_A = pubtoaddr(pub_A, 111)

# mk_multisig_script(pub1, ..., pub_n, m, n)
multi_locking_script = mk_multisig_script(pub_B, pub_C, 2, 2)

input = blockinfo_unspent(addr_A, 'testnet') #get UTXO
output = [{'value': 1000000, 'address': addr_A}]

# mksend(input, output, addr_sender, transaction_fee)
tx = mksend(input, output, addr_A, 10000)
dtx = deserialize(tx)

>>> replace dtx output locking script to multi_locking_script manually
dtx = ....
tx = serialize(dtx)
tx2 = sign(tx, 0, priv_A)
testnet_pushtx(tx2)
# Exercise : B&C to D 2-of-2

priv_D = sha256('password_d')
pub_D = privtopub(priv_D)
addr_D = pubtoaddr(pub_D, 111)

blockinfo_unspent(addr_A, 'testnet') #get previous UTXO

>> make input manually by using previous UTXO but different index 
>> in order to select the righ transaction, not transaction for keeping change in balance

input = {'output': '4a7893384a7726c256139a98aa44e541be9f8bf4fc0da61d6ec2ee751ed0c788:0', 'value': 1000000}
output = [{'value': 990000, 'address': addr_D}]

tx = mktx([input], [output])
deserialize(tx)


# mk_multisig_script(pub1, ..., pub_n, m, n)
multi_unlocking_script = mk_multisig_script(pub_B, pub_C, 2, 2)
sig_B = multisign(tx, 0, multi_unlocking_script, priv_B)
sig_C = multisign(tx, 0, multi_unlocking_script, priv_C)

multi_unlocking_script_final = '00' + hex(len(sigB)/2) + sig_B + \
    hex(len(sigC)/2) + sig_C


>>> replace dtx input unlocking script to multi_unlocking_script_finalt manually
dtx = ...
tx2 = serialize(dtx)
testnet_pushtx(tx2)

 

다중서명 트랜잭션은 현재 몇가지 한계가 있다. 

1. 스크립트 상 참여자의 공개 키를 모두 포함해야 하므로 그 코드의 크기가 매우 커지게 된다. 

2. 참여자 중 필수 서명 수에 따라 서로 다른 스크립트가 완성된다. 

3. 길어진 스크립트는 UTXO 포함 과정까지 시스템에 지속적인 부담을 제공한다. 

4. 스크립트 작성 과정에서 pybitcointools로는 구현이 되지 않아 수동적으로 스크립트를 수정해야 한다.

 

이에 대한 해결방안으로 보다 간편한 기능이 별도로 존재하는데, 이는 추후 확인 예정이다. 

 

 

https://komodoplatform.com/en/academy/bitcoin-script/

 

Bitcoin Script: An Introduction For Beginners

Bitcoin Script is a simple, stack-based programming language that enables the processing of transactions on the Bitcoin blockchain. To understand more about Bitcoin Script, we’ll first look at its characteristics and a basic example of how this programmi

komodoplatform.com

https://academy.bit2me.com/en/what-is-bitcoin-script/

 

What is Bitcoin Script?

Bitcoin Script is the programming language of Bitcoin and the fundamental piece that allows all possible operations to be carried out on this blockchain.

academy.bit2me.com

https://academy.binance.com/ko/articles/an-introduction-to-bitcoin-script

 

비트코인 스크립트 설명 | Binance Academy

비트코인 내부에는 자금을 어떻게 사용하며 또 누가 이를 사용할 수 있는지를 지시하는 스크립팅 언어가 존재합니다. 바이낸스 아카데미에서 비트코인 스크립트에 대해 자세히 알아보시기 바

academy.binance.com