비트코인 스크립트와 디지털 서명, 검증 (scriptSig, scriptPubKey )

in #kr7 years ago (edited)

몇일 전 비트코인의 UTXO에 관련된 포스팅을 했었다. 그리고 부가 설명이 필요했던 스크립트에 대해 오늘 포스팅을 한다. (UTXO포스팅 https://steemit.com/blockchain/@niipoong/utxo-bitcoin-trading-and-utxo )


만약 비트코인 스크립트가 무엇인지 알고싶은 사람이라면 어느정도 p2p네트워크에 대해서는 이해를 하고있을것이라 짐작이된다. 

만약 본인이 비트코인 거래를 해본적이 있다면, blockchin.info 를 통해 자신이 생성한 txid를 검색해 보기도 했을것이다. 자신의 트랜잭션이 블록에 잘 저장되는 것, 승인이 이루어 진 것을 확인한 후 거래가 잘 이루어졌다고 안심하기도 했을 것이다. 

smartbit.com의 API를 통해 tx를 확인해보면 아래와 같은 데이터를 볼수있다. 

success : true 라는 정보와 transaction의 confirmations : 512215 컨펌정보 및 coinbase : true라는 정보로 채굴자에게 주어지는 보상 트랜잭션이란 것을 알 수 있겠다. 채굴 보상에 대한 트랜잭션이기에 당연히 이중지불도 되지 않았을 것이다.

그러나 위와같은 정보를 가지고 안심할 수 있을까? 

지난 포스팅 (https://steemit.com/kr/@niipoong/id-create-bitcoin-txid )을 통해 트랜잭션을 직접 만들어 보기도 했다. 아래는 트랜잭션을 만드는데 필요한 아이템들이다.

ver : 소프트웨어 버전 정보
input_count : 입력값 개수
prevout_hash : 이전 트랜잭션 Hash
Hashsequence : 현재 장애가 있는 Tx-대체기능, 0xffffffff로 설정
lockTime : 잠금시간
scriptSig : 해제 스크립트
value : BTC가치
scriptPubKey : 잠금 스크립트


위 아이템들만 있으면 우리는 트랜잭션을 인위적으로 생성할 수 있다. 하지만 저 아이템들 중  scriptSig 는 우리가 인위적으로 만들어 낼 수 없다. 트랜잭션 발생자의 개인키가 필요한 서명이 존재하기 때문이며, scriptPubKey를 통해 우리는 트랜잭션을 검증하고 서명을 확인한다. 지난 포스팅에서는 개인 서명이 다 포함된 scriptSig를 통째로 사용했기에 txid를 도출할 수 있었다. 


비트코인 스크립트란 트랜잭션을 발생시킨 주소의 서명을 통해 유효하다고 인정할 수 있고, BTC가치를 전달받은 주소에서 안심하고 소비할수 있게 하기 위한 방안이다.
이를 자세히 설명하기 위한 포스팅 구성은 아래와 같다.

1. 트랜잭션의 연결
2. scriptSig와 scriptPubkey의 구조와 생성
3. scriptSig와 scriptPubkey를 활용한 트랜잭션 검증 방법






1. 트랜잭션의 연결

UTXO의 연결을 위해 두개의 연결된 txid로 비교를 해볼 것이다.

먼저 API를 통해
tx1 : 3f285f083de7c0acabd9f106a43ec42687ab0bebe2e6f0d529db696794540fea
tx2 : 13217f0b0c63d3d364a5be4f3e5bd61d885d35d7b02c839706f67878f7b589f9

tx1 을 통해 얻은 output으로  tx2를 통해 소비했음을 먼저 확인해보자.

해당 tx1 를 조회해 보면 아래와 같은 결과가 나온다.
구분하기 편하게 tx1을 발생시킨 주소의 script는 파랑색, tx2를 발생시킨 script는 빨강색으로 표시했다.

                                                      <tx1>

먼저 이 트랜잭션을 해석해보면 

input값내부의 prev_out에 존재하는 1MMMMSUb1piy2ufrSguNUdFmAcvqrQF8M5 주소에서

out 값에 존재하는 1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa  주소로 0.00101234 BTC 를 전달, 

1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa  주소는 거래수수료 0.0001 BTC를 제외한 0.00091234BTC만큼 수령했다 정도로 일단 해석이 가능하다.

자 그렇다면 0.00091234BTC 를 수령했던  1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa  주소의 소유주는 해당 UTXO를 어떻게 사용했는지를 확인해보자. 

tx2를 API 콜을 통해 확인해보자.

                                                         <tx2>

이전 tx1의 out의 값이 tx2의 input으로 넘어온 것을 확인할수 있다.
아래 그림을 확인해보며 될것이다.  이부분의 해석은 위와 동일하게 본인이 직접 해석해 보기 바란다. 실제 데이터로 비교하는것이 눈은 조금 아플수 있지만 정확한 이해를 하는데는 많은 도움이 된다. 




자 아직 우리는 scriptSig와 scriptPubKey가 무엇인지 모른다. 아직 설명하지 않았다.

다만 대충 눈 짐작으로 유추할 수 있는 부분은 tx1의 out값이 tx2의 input값으로 넘어왔다는 사실이며, tx1에서 0.00091234사토시를 전달받은 1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa 주소의 소유주는 이중 0.0001BTC를 1LLLfmFp8yQ3fsDn7zKVBHMmnMVvbYaAE6 주소로 전달하는데 사용했다는 점이다. 이때는 거래 수수료가 발생하지 않았다.

여기까지는 엄밀히 말하면 UTXO의 개념이다.





2. scriptSig와 scriptPubkey의 구조와 생성

그렇다면 scriptSig와 scriptPubKey란 무엇일까?
이 두개는 열쇠와 자물쇠의 관계이다.

scriptPubKey = 트랜잭션을 검증하기 올바른지 검증하라는 key이다. 자물쇠의 역할이며, 잠금스크립트라고도 불린다.
scriptSig = 트랜잭션을 발생시킨 주체의 서명 + publicKey , 열쇠의 역할이다. 잠금스크립트를 풀기위한 해제 스크립트라고도 불린다.

p2p네트워크에서 해당트랜잭션을 받은 노드는 이 scriptPubKey에 scriptSig를 대조해서 일치하는지 확인하고 true라고 판명되면 올바른 트랜잭션이라고 간주하고 이웃노드로 전달하게 된다.

tx2를  보자. scriptSig 와 scripyPubKey 모두 tx2를 기반으로 만들것이다.

input내부에 빨간색으로 표시된 한쌍의 scriptSig와 scriptPubKey가 존재한다. 이 두가지를 통해 트랜잭션을 검증하면 되는 부분이며, 하단에 녹색으로 표시된 scriptPubKey 라는 자물쇠를 트랜잭션에 결과값에 같이 첨부를 하여 전달한다.  

이렇게 발생한 결과값 0.0001BTC 를 사용하기 위해서는
이를 전달받은  1LLLfmFp8yQ3fsDn7zKVBHMmnMVvbYaAE6 주소 또한 본인의 scriptSig를 통해 해당 scriptPubKey를 풀어야만 사용이가능하다. 이 두가지 검증이 false라면 어떤 노드에서도 유효한 트랜잭션이라 인정하지 않을 것이다.

이제 scriptSig와 scriptPubKey를 직접 만들어 보자


tx2을 발생시킨  1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa 주소(0.0001BTC를 사용하고자 하는) 가 scriptSig를 어떻게 만드는지 확인해보자.

구성은 아래와 같다.
<scriptSig 와 scriptPubKey 구성>

scriptSig: 

PUSHDATA <sig> + SIGHASH_ALL + PUSHDATA <pubKey>

PUSHDATA <sig > : 서명을 Stack 에 Push 하라는 명령      

SIGHASH_ALL (01) : 거래에 대한 모든 것이 서명되어 있음을 나타냄

PUSHDATA <pubKey> : Public-Key를 Stack 에 Push 하라는 명령



scriptPubKey: 

OP_DUP + OP_HASH160 + <pubKeyHash> + OP_EQUALVERIFY + OP_CHECKSIG

OP_DUP (76) : Duplicate(복사)하라는 명령

OP_HASH160 (a9): 160-bit Hash 값을 연산하라는 명령

OP_EQUALVERIFY (88): stack에 들어 있는 2개의 값이 동일한지 검증 하라는 명령

OP_CHECKSIG (ac): 서명을 검증 하라는 명령

* 괄호는 hex 약속된 hex값

위와 같은 구성은 복잡해 보이지만, 저런식으로 설계한 이유는 아래 트랜잭션 검증 시간에서 알려주겠다. 일단 저 구성대로 scriptSig와 scriptPubKey만을 만들어보자.

tx2내용을 간추리면 아래와 같다.
1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa
-> 1LLLfmFp8yQ3fsDn7zKVBHMmnMVvbYaAE6   : 0.0001 BTC  전달



(1) out 데이터의 scriptPubKey 만들기

먼저 할일은  1LLLfmFp8yQ3fsDn7zKVBHMmnMVvbYaAE6만이 풀 수 있는 scriptPubKey (잠금 스크립트)를 만들어 tx2의 out에 저장하는 일이다. 

scriptPubkey =
OP_DUP(76)
+ OP_HASH160(a9)
+ length(decodeBase58decode(1LLLfmFp8yQ3fsDn7zKVBHMmnMVvbYaAE6 ) ) publicKeyHash길이
+ Base58decode(1LLLfmFp8yQ3fsDn7zKVBHMmnMVvbYaAE6) - publicKeyHash
+ OP_EQUALVERIFY(88)
+ OP_CHECHSIG (ac)

base58decode를 편하게 하려면 http://lenschulwitz.com/base58 주소를 이용하자. 그리고 결과 값에서 앞 1바이트(version), 뒤 4바이트(checksum)를 제외하자. 생략하는 이유가 궁금하다면, 나의 이전 포스팅인 비트코인 주소가 어떻게 만들어지는지를 참고하기바란다. (https://steemit.com/bitcoin/@niipoong/4okvgp)

결과는 아래와 같다.

out : scripyPubKey  76a914d412b6223019407b50654b0f619709c54a9118ee88ac


해당 트랜잭션을 Blockchain.info에서 확인해보고 tx1 id를 API를 통해 확인해도 동일한 결과가 나올것이다.

(2) input 값에 포함될 scriptSig만들기

scriptSig만드는 일은 scriptPubKey를 만드는 일보다 훨씬 복잡하다는 사실을 먼저 인지하기 바란다.
또한 아래 진행은 개인키를 활용해야 하며, 실제 tx2의 보내는 주소인 
1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa  의 개인키를 활용하지 않을것이며 임의의 개인키를 사용할 것이다.


때문에 결과값은 다르다. 

만약 해당주소와 쌍을 이루는 개인키를 통해 scriptSig를 만든다고 하여도, 결과는 다르게 나온다. 그 이유는 scriptSig를 만드는 과정에는 ECDSA(The Elliptic Curve Digital Signature Algorithm) 타원곡선 디지털 서명 알고리즘을 사용하는데 다항시간내의 확률적 계산을 기반으로 하기에 일정 시간안에 나올수 있는 값은 그때 그때 다르다.

따라서 아래 진행할 scriptSig를 만들때 개인키는

private key WIF = KyFvbums8LKuiVFHRzq2iEUsg7wvXC6fMkJC3PFLjGVQaaN9F1Ln

으로 가정하고 만들겠다.

scriptSig에 필요한 구조는 아래와 같다.

scriptSig: PUSHDATA <sig> + SIGHASH_ALL + PUSHDATA <pubKey>

PUSHDATA <sig > : 서명을 Stack 에 Push 하라는 명령       

SIGHASH_ALL (01) : 거래에 대한 모든 것이 서명되어 있음을 나타냄

PUSHDATA <pubKey> : Public-Key를 Stack 에 Push 하라는 명령

scriptSig는 해당 tx1이 유효하며, 믿을만하다 라고 신뢰를 주기위해 서명을 하는것이다. 그리고 이 서명된 input값을 tx1에 넣어줌으로써 해당 tx1은 정상적인 트랜잭션임을 입증하는 것이다.

과정을 설명한다.
1. 서명할 문서를 만든다.
2. 서명한다.
3. 서명된 문서에 SIGHASH_ALL 과 pubKey를 덧붙인다.

자 그렇다면, 서명할 문서를 만들어보자. 서명할 문서에는 아래 오른쪽 표와 같다. 오른쪽 표의 값을 찾아서 채워 주도록 하자. 기본적으로 little endian ordering 순서로 byte를 채워야한다.

위 오른쪽 표의 값중 version과 sequence, locktime은 현재 비트코인 클라이언트 디폴트 값으로 지정하면 되고 sighash code 또한 기본적으로 01 이다.

이제 할일은 위 데이터를 순서대로 이어주는 것이다.

<Attach items - 서명 대상문서>
0100000001ea0f54946769db29d5f0e6e2eb0bab8726c43ea406f1d9abacc0e73d085f283f000000001976a914c8e90996c7c6080ee06284600c684ed904d14c5c88acffffffff0110270000000000001976a914d412b6223019407b50654b0f619709c54a9118ee88ac0000000001000000

자 이제 서명대상 문서를 만들었으니, 서명을 하면 될것이다.
서명을 하기 위해서는 먼저 위에서 가정한 1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa  의 개인키가 필요하다.

private key WIF = KyFvbums8LKuiVFHRzq2iEUsg7wvXC6fMkJC3PFLjGVQaaN9F1Ln
해당 개인키를 Hexadecimal Format 으로 변경해준다.
https://www.bitaddress.org/ 사이트를 이용하면 쉽게 변경이 가능하다. 아래와 같은 개인키를 얻을 수 있다.

Private Key Hexadecimal Format (64 characters [0-9A-F]):

3CD0560F5B27591916C643A0B7AA69D03839380A738D2E912990DCC573715D2C

이제 방금 얻은 개인키를 통해 서명을 해보자.
서명을 하기위한 코드는 아래 첨부하도록 하겠다.

순서대로 보도록 하자.
attachItems 를 더블해시하여 txhash 를 생성하였다.
이후 타원곡선 디지털서명 알고리즘을 이용하여 나의 개인키를 통해 signingKey를 생성해준다.

이후 txhash를 개인키로 서명하여 서명된 문서 SIG를 얻어오는 방법은
SIG = signingKey.sign_digest( txhash, sigencode = ecdsa.util.sigencode_der_canoize)
binascii.hexlify(SIG)
과정을 통해 SIG 라는 서명된 문서 정보를 얻을 수 있다.

결과는 아래와 같다. 실행할때 마다 값은 달라진다.

나머지는 매우 쉽다.
scriptSig: PUSHDATA <sig> + SIGHASH_ALL + PUSHDATA <pubKey>
   

이 중 맨앞에 SIG 값을 알아냈고 트랜잭션 내부 input에 서명을 했으니 01 을 붙이면 된다.
pubKey는 https://www.bitaddress.org/ 사이트를 통해
KyFvbums8LKuiVFHRzq2iEUsg7wvXC6fMkJC3PFLjGVQaaN9F1Ln 개인키에 대한 Public Key를 알아낼 수 있다.

기본적으로 비트코인 주소의 생성 방법에 대해서는 반드시 숙지하기 바란다. 한번더 첨부하겠다.
( https://steemit.com/bitcoin/@niipoong/4okvgp )

Public Key (compressed, 66 characters [0-9A-F]):

03BF350D2821375158A608B51E3E898E507FE47F2D2E8C774DE4A9A7EDECF74EDA


알아낸 정보를 정리하면 아래와 같다.






3. scriptSig와 scriptPubKey를 활용한 트랜잭션 검증 방법


scriptSig와 scriptPubKey를 직접 생성해 보았는데 이를 검증하는 방법이 궁금할것이다. 해당 트랜잭션을 생성하여 다른 노드로 전달하면, 다른 이웃노드들은 해당 트랜잭션의 서명이 올바른 서명인지를 검증할 것이다. 

검증내용은 2가지다. 

- 송신자의 공개키 자체의 검증

- 공개키를 통해 서명 검증

위에서 만든 tx2 트랜잭션을 검증해 보도록 하자. 검증 대상은 inputs 값의 scriptSig 와 prev_out에 있는 scriptPubKey이다. 아래 빨간색 부분을 말하는 것이다. 


다만 우리는 이전 scriptSig를 만들때 임의의 개인키를 사용했기에 중간에 한번 일치함을 가정할 것이다.

# scriptSig (push block header of the genesis block)

OP_PUSHDATA1 47 

304402202aa24365218510b56b8a43e265c6e7f0797d8a7a9c9334ee59b5bbb74d8ea6560220400a82a3a018afadb948aff34daa7a8936a17243600841e6fed874e4aa05fceb01 

OP_PUSHDATA2 21 

03BF350D2821375158A608B51E3E898E507FE47F2D2E8C774DE4A9A7EDECF74EDA


# scriptPubkey (take double-SHA256 hash and checks if the hash matches the genesis hash.)

OP_DUP

 OP_HASH160 

14 c8e90996c7c6080ee06284600c684ed904d14c5c 

OP_EQUALVERIFY 

OP_CHECKSIG

스크립트의 특징을 https://en.bitcoin.it/wiki/Script 위키를 인용해서 간단히 설명하겠다

Script is simple, stack-based, and processed from left to right. It is intentionally not Turing-complete, with no loops.단순하고 stack베이스 자료구조이다. 또한 왼쪽에서 오른쪽 방향으로 진행되며 반복문도 없고 튜링불완전하다.정도로 요약하겠다. 스택 자료구조를 이용한다는 점을 알아두어야 겠다.


원리를 설명하면 방정식과 같다.
x + 5 = 10 이라는 방정식이 있다고 하면 
x가 의미하는 부분이 scriptSig이며' + 5 = 10 ' 이 부분이 scriptPubKey의 의미라고 보면된다. 

순서는 매우 간단하다. 

1. scriptSig 를 먼저 스택에 넣는다. (x를 의미)

2. scriptPubKey를 다음 스택에 넣는다. ( '+ 5 = 10' 을 의미하는 부분을 넣어본다)

3. true인지 확인


그림으로보도록하자.오른쪽 사각형은 스택을 의미한다. 스택의 데이터가 어떤식으로 바뀌는지를 확인해보자.

먼저 scriptSig를 먼저 스택에 입력하자.  OP_PUSHDATA에 의해 SIG부분이 먼저 스택에 쌓인다.
이후 앞서 개인키를 통해 만들었던 공개키 PublicKey가 그 위로 쌓이게 된다.

이어서 바로 scriptPubKey를 넣어보자.

가장 먼저 OP_DUP 라는 Operation이 스택에 올라가는데 이부분은 복사하라는 의미이다. 따라서 스택2의 데이터를 복사하여 스택3에 복사하였다. 

이후 OP_HASH160 이라는 Operation만나게 되는데 Stack3에 있는 데이터를 Hash160으로 변환해준다. 그럼 기존에 publicKey -> publicKeyHash로 변환이 되어 stack3에 그대로 저장이 될것이다.


#

애초에 개인키를 임의로 정했기에 이때 publicKeyHash값이 임의로 일치하도록 조작 했다는 점 양해바란다. 

실제로 스택3의 데이터 

03BF350D2821375158A608B51E3E898E507FE47F2D2E8C774DE4A9A7EDECF74EDA을 Hash160했을 때 c8e90996c7c6080ee06284600c684ed904d14c5c 값이 나오지 않을 것이다.

#

이후 c8e90996c7c6080ee06284600c684ed904d14c5c 값을 스택4에 다시 올려놓는다.

OP_EQUALVERIFY를 통해 스택3 - 스택4 데이터가 일치하는지 (publicKeyHash) 값이 일치하는 지를 비교한다. 마지막으로 OP_CHECKSIG 란 Operation에 의해 스택1의 SIG서명이 스택2의 공개키 값과 호환되는지를 비교한후 True or False를 스택1에 저장하게된다.

우리가 검증은 맨마지막에 남을 스택1의 true or false정보를 확인하면된다.

꽤 긴 포스팅이었다.

이전 UXTO관련 글 에서도 언급을 했었다. 비트코인의 스크립트는 scriptSig와 scriptPubKey의 존재는 비트코인의 트랜잭션을 검증하는 방식이므로 매우 중요하다고 생각된다.

나중에 시간이 나면 트랜잭션 검증부분에 대해서는 툴을 이용해서 확인하는 포스팅을 하도록하겠다.

질문이나 추가정보 및 글에 오류가 있다면 댓글 바란다.