위 그림은 비트코인 output 의 모습을 간단하게 보여준다.
이 output 은 2개의 output을 나타내고, 첫번째 output 은 0.015 를 지불하며 이때 output script 는 scriptPubKey 로 위와 같은 문자열이 들어간다.
이 output 이 밥에게 비트코인을 보내는 아웃풋이라면, 밥이 비트코인을 꺼내서 쓸 때는 input script 에 저 output script 의 값이 참이 되도록 하는 값을 제시하도록 한다.
위의 빨간색 문자열이 퍼블릭 키 (밥의 비트코인 주소) 를 나타내고, 밥은 이 퍼블릭키에 쌍에 맞는 프라이빗 키로 서명을 제시해서 나중에 비트코인을 꺼낼 수 있다.
인풋을 간단하게 보면 위와 같이 되어있다.
위 그림에서는 1개의 인풋만을 보여주고 있으며,
output이 들어있는 트랜잭션 id와 그 output의 인덱스 (0-index), 그리고 output script 에 제시할 서명으로서 scriptSig 가 들어가고, 2^31 보다 작은 값이 들어가므로 relative lock time 까지 시퀀스로 제시하고 있음을 알 수 있다.
이때 scriptSig 는 signature 또는 Witness 값이라고 부르는 값을 제시하여 내가 밥이라는 것을 입증하는데 사용한다.
(이 값이 input script 에 들어가는지 witness sturcture 에 들어가는지 아직은 모르겠다.)
결국 이 스크립트로 하려고 하는 것은 Authentication 과 Authorization 이다.
Authentication (인증) : 내가 output 에 해당하는 signature 를 제시하여 이 output의 주인이 나라는 것을 입증하는 것
Authorization (인가) : 나의 인증을 full node 들이 검증하는 것
Script
이제 스크립트에 대해서 본격적으로 살펴보자.
스크립트는 스택 기반의 언어로, 실행할 수 있는 범위가 제한되어있는, turing complete 하지 않은 언어이다.
예를 들면 스크립트에서는 반복문을 사용할 수 없기 때문에, 똑같은 코드를 여러번 적는 식으로 반복문을 흉내내야 한다.
(하지만 payment 입장에서는 그래도 충분히 많은 기능을 제공한다.)
스크립트가 실행될 때는 아무것도 없는, 비어있는 스택에서부터 시작한다.
그리고 스크립트의 실행이 끝난 후에는 무언가를 저장해둔다거나 하지도 않는다.
실행 과정에서 스택에 값이 남아있다면 그 안에서 다른 명령어를 실행할 때 이 값을 참조할 수 있을 뿐이다.
비록 스크립트 언어에 반복문은 없지만, 조건문은 존재하고 중첩해서 사용할 수도 있다.
그런데 반복문을 안 넣은 것은 매우 합리적이다.
만약 반복문이 존재하면 악의를 가진 사용자가 무한 루프를 만들 수 있고, 채굴자가 스크립트를 검증할 때 검증이 끝나지 않는 문제가 발생할 수 있기 때문이다. 그리고 이렇게 함으로써 스크립트 실행 시간을 어느정도 예측할 수 있는 이점도 있다.
Legacy Transaction validation Engine
비트코인 초창기에 사용되었던 트랜잭션 검증 엔진을 알아보자.
초창기에는 input script 와 output script 가 모두 필요했다.
앨리스가 밥에게 비트코인을 보내는 트랜잭션을 만들었고, 이게 컨펌되었다고 해보자.
이때 트랜잭션의 output script 에는 밥의 비트코인 주소가 들어있을 것이다.
정확히는 누가 이 UTXO 를 사용할 수 있는지를 스크립트로 기술해둔다.
밥이 비트코인을 사용하기 위해서는 이전 트랜잭션의 output script 에 대해 특정한 input script 를 제시하고,
full node는 밥이 제시한 input script (witness / signature) 와 output script 를 합쳐서 연산하고, 그 결과가 true 라면 밥이 해당 UTXO로부터 비트코인을 꺼내서 지불할 수 있다고 인정(검증)한다.
(SegWit 에서는 이 input script 가 존재하지 않고 별도로 빼냈다고 생각하면 된다.)
즉, 이전 트랜잭션의 output script 와 현재 트랜잭션의 input script 를 연결해서 실행해봄으로써 검증하는 것이다.
비트코인 트랜잭션 타입에는 여러가지가 있는데, 초창기에 사용한 트랜잭션 검증 엔진은 Pay-to-Public-Key-Hash (P2PKH) 라는 타입의 트랜잭션이 자주 사용되었다. 이름 그대로 public key 를 해싱한 것에 지불하겠다는 의미이다.
(비트코인 주소가 sha 256으로 두번 해싱해서 계산되었던 점을 상기해보자.)
스크립트를 실행했을 때 검증에 성공한다는 것은 스크립트의 실행결과가 일단 True 가 나오기만 하면 valid 한 것으로 본다.
즉, input script 와 output script 가 어떤 형태이든지, 일단 두 스크립트를 이어 붙여서 실행한 결과가 true 이면 이 트랜잭션은 valid 한 트랜잭션이 되는 것이다.
구체적인 예시를 가지고 살펴보자.
왼쪽의 녹색 부분은 input script 이고 오른쪽의 빨간색 부분은 output script 이다.
output script 해서 <PubKHash> 가 이전 트랜잭션에서 비트코인을 지불하는 주소이고, 이 주소에 들어있는 비트코인을 꺼내쓰기 위해 녹색 부분의 <sig> <PubK> 를 제시한 것이다.
full node 가 트랜잭션을 검증할 때는 input script 와 output script 코드를 이렇게 연결해서 이어 붙인 뒤, 검증 엔진에 넣고 실행해본다.
그리고 이 검증 엔진은 스택 기반으로 연산을 실행한다.
스택 기반의 연산을 이해하기 위해 간단한 예시를 먼저 살펴보자.
1. input script, output script 를 합친 스크립트가 2 3 ADD 5 EQUAL 이라고 해보자.
(연산자에는 OP_ 가 붙기도 하고 위 처럼 그냥 생략하기도 한다.)
화살표는 스택 포인터를 나타낸다.
2. 데이터를 만나면 스택에 푸쉬 한다.
2, 3 이 스택에 들어간다.
3. 연산자를 만나면 스택에서 2개의 값을 빼고 연산한 결과를 스택에 새로 푸쉬한다.
ADD 연산자는 스택에서 2개의 값을 빼고, 두 값을 더한 결과를 스택에 새로 푸쉬한다.
따라서 2 + 3 = 5가 스택에 들어간다.
4. 데이터로 5가 들어갔으니 push 한다.
5. EQUAL 연산자를 만났으니 스택에서 2개의 값을 빼고 연산한 결과를 스택에 새로 푸쉬한다.
EQUAL 연산자는 스택에서 2개의 값을 빼고, 두 값이 같다면 True 를 스택에 푸쉬한다.
두 값이 같으므로 스택에는 TRUE 가 남아있게 된다.
6. 최종적으로 연산이 끝났을 때 스택에 남아있는 값이 True 이므로 이 트랜잭션은 유효하다.
따라서 위와 같은 output script 가 있다면, 내가 2를 제시하기만 하면 누구든지 이 UTXO로부터 비트코인을 가져다가 사용할 수 있게 된다. 이제 진짜 예시로 돌아오자.
input script 와 output script 를 이어 붙인 스크립트에 대해 실행한다.
그림에서 < > 안에 들어있는 것은 데이터를 의미하고 꺽쇠가 없으면 연산자를 의미한다.
1. 먼저 <> 데이터를 모두 스택에 넣는다. <sig> <PubK> 가 스택에 들어간다.
2. DUP 연산자를 실행한다.
이 연산자는 stack top 에 있는 데이터를 복사한다.
<PubK> 가 복사되어 추가로 들어간다.
3. HASH 160
160bit의 결과를 내보내는 해시 연산을 수행한다.
해시 연산을 수행할 때 필요한 데이터는 1개 이므로 stack top 에서 데이터를 하나 꺼내서 해싱한 뒤 그 결과를 push 한다.
따라서 그림처럼 <PubKHash> 라는 데이터가 들어간다.
이 데이터가 비트코인 주소가 된다.
즉, 밥이 제시한 퍼블릭 키를 기반으로 비트코인 주소를 계산해본 것이다.
4. <PubKHash>
데이터를 만났으므로 push 한다.
5. EQUALVERIFY
2개의 데이터를 pop 해서 같은지 확인한다.
밥이 제시한 퍼블릭 키의 해시 연산 결과와 UTXO 에 들어있었던 해시 연산 결과가 같은지 확인한다.
같다면 같은 어드레스 주소를 나타내는 것이다.
이 연산자는 VERIFY 확인만 하는 것이므로, 2개를 빼서 체크만 하고 push 는 하지 않는다.
만약 같다면 그냥 넘어가고, 다르다면 여기서 멈추게 된다.
6. CHECK SIG
두 개의 데이터를 pop 해서 <sig> 데이터로 <PubK> 를 검증한다.
만약 이 PubK 가 밥의 것이 맞다면, 밥이 제시한 <sig> 에 의해 검증이 성공할 것이다.
검증에 성공하면 CHECKSIG 는 True 를 넣기 때문에 스크립트에는 최종적으로 TRUE 가 남게 된다.
따라서 이 트랜잭션은 유효하다.
트랜잭션 타입에는 P2PKH 말고도 다양한 종류의 타입이 존재하여 조금 더 복잡한 상황에 대한 트랜잭션을 처리할 수 있다.
- 여러개의 서명이 들어간 멀티 시그니처 스크립트
- 스크립트를 해싱하여 거기에 지불하는 스크립트
- if, else if, else 와 같은 조건문이 들어간 스크립트
- 시간에 대한 정보를 포함하는 스크립트
- SegWit 이 적용된 트랜잭션
하나씩 살펴보자.
Multisignature
먼저 여러 개의 서명을 사용하는 상황을 생각해보자.
예를 들어 모하마드가 인터넷에서 비트코인을 받고 물건을 파는 회사를 운영한다고 해보자.
모하마드는 혼자 일하지 않고 3명의 파트너와 업무를 도와주는 한 명의 변호사와 함께 일한다고 해보자.
이떄 이 회사가 만약 돈을 지불할 일이 생기면 기본적으로 모하마드의 동의가 필요하겠지만, 모하마드가 자리에 없을 경우를 대비해서 자기와 파트너 3명, 변호사 이렇게 5명 중에 2명의 동의가 있다면 돈을 지불할 수 있도록 하고 싶다고 해보자.
그러면 가능한 경우의 수가 5C2 = 10 이 나올 것이다.
이럴 때는 5명 중에 2명의 동의가 있으면 사용할 수 있는 주소를 별도로 만들고 (Multisig 주소)
이 주소에 들어있는 비트코인을 지불할 때는 5명 중에 2명의 서명이 있으면 지불할 수 있도록 만든다.
이런 상황에서 사용하는 output script 는 보통 이런 형태를 띄고 있다.
만약 5명 중에 2명의 서명이 필요하다면, 5명의 주소를 public key 로 만들어 나열해두고
M = 2 <5개의 pub 키> 5 OP_CHECKMULTISIG 와 같이 작성하는 것이다.
그러면 2개의 키를 제시했을 때, 5개 주소 중에 2개와 일치하면 통과하는 스크립트로 동작한다.
그러면 앨리스가 만약 이 회사의 물건을 사고 싶어한다면, 이 주소를 모두 알려주어 해당 주소로 payment 하도록 하면 된다.
(즉, 앨리스가 지불할 때는 output script 에 저렇게 여러개 주소를 나열해서 작성하는 것이다.)
이렇게 하면 output script 의 크기가 커지기 때문에 채굴자 입장에서는 이 트랜잭션 하나로 인해 다른 트랜잭션을 넣을 공간을 손해보게 되고, 이런 경우, 이 트랜잭션의 수수료는 높아지게 된다.
(이 문제를 해결하고자 이 스크립트 자체를 해싱해서 그 해시값에 지불하자는 아이디어도 있다.)
그래서 예시를 보면 첫번째 줄과 같은 output script 가 있으면, input script 로는 2개의 서명을 제시하고, 풀 노드가 검증할 때는 이를 이어붙어서 <Sig B> ... 와 같은 스크립트를 실행해보게 된다.
그런데 이 CHECKMULTISIG 연산자에는 버그(?) 가 있다.
바로 인풋 스크립트를 작성할 때 하나의 파라미터를 더 요구하는 것이다.
그래서 사람들은 이 버그를 피하기 위해 서명 앞에 0을 포함하여 전송하거나 OP_0 이라는 연산자를 포함하였다.
어떤 사람은 위 예시에서 3가지 pubkey 중에서 2개의 서명이 각각 어떤 것에 해당하는지 알 수 없으므로 어떤 조합에 해당하는 것인지 명시하기 위해 만든 필드라고 추측하기도 한다.
Pay-to-Script-Hash (P2SH)
최근에 자주 사용하는 트랜잭션 타입이다. (P2PKH 보다 더 많이 사용한다.)
이름 그대로 스크립트 코드를 해싱해서 거기에 페이먼트를 하겠다는 의미이다.
스크립트 코드를 가지고 어떻게 페이먼트를 할 수 있는걸까?
위에서 잠깐 언급했듯이 Multisignature 방식을 사용하다보면 output script 에 담는 key 가 매우 많아진다.
하나의 public key 길이는 타원 곡선을 사용한 크립토그래피 방식으로 생성하면 512 bit 인데, 이런 key 를 여러개 담다보면 ouptut script 의 크기가 커지게 된다. 512 bit = 64byte, 5개의 키를 담으면 320byte 라는 매우 큰 공간을 키를 담는데 사용하게 되고, 트랜잭션의 크기가 커지게 된다. 채굴자 입장에서는 트랜잭션 크기가 커지면 그만큼 다른 트랜잭션을 담을 수 없기 때문에 높은 수수료를 요구한다.
그리고 이 수수료를 물건을 사려는 사람, 지불자가 내야한다는 부담이 있다.
또한 비트코인 시스템 입장에서도 이 output 은 지불되기 전까지 계속 UTXO set 에 남아있게 된다.
모든 풀 노드들은 UTXO set 을 갖고 있어야 하는데, 이런 커다른 트랜잭션을 UTXO set에 가지고 있게 되면 부담스럽다.
그래서 이런 크고 복잡한 스크립트를 제시해야 하는 문제를 피하기 위해, 모하마드는 저 긴 output script 말고, 저 스크립트를 해싱한 값에 지불을 하라고 요구할 수도 있다.
그것이 Pay-to-Scrip-Hash 기법이다.
그리고 물건을 사는 사람 입장에서는 평범한 비트코인 주소 하나에 지불하는 것과 똑같이 사용할 수 있게 된다.
그러면 나중에 모하마드 회사에서 이 해시값에 지불된 코인을 사용하려고 할 때는 어떻게 사용할 수 있을까?
간단하다. 이 해시값을 만드는데 사용한 원본 스크립트를 해싱해서 제시하면 된다.
그리고 그 스크립트가 일치하는지를 먼저 확인한 후에, 그 다음에 원본 스크립트를 실행해서 검증에 들어가게 된다.
그림으로 보면 위와 같다.
P2SH 를 사용하지 않으면 Locking Script (output script) 에는 저렇게 긴 스크립트가 들어있을 것이고,
내가 비트코인을 꺼내 쓸 때는 Unlocking Script (input script) 에서는 0 Sig1 Sig2 를 제시해야 한다. (0은 버그로 인해 추가하는 것)
반면 P2SH 를 사용하게 되면 저 스크립트를 해싱한 값을 Locking Script 에 담도록 시키고,
그 UTXO에서 비트코인을 꺼낼 때는 0 Sig1 Sig2 <원본 스크립트> 를 담아서 실행하도록 시킨다.
(저렇게 하면 Input + output 했을 때 sig 를 검증하는게 먼저 이루어지고 , CHECKMULTISIG 가 끝난 이후에 True가 남을 텐데, 어떻게 다음에 HASH160 을 실행할 수 있을까? 교수님 설명으로는 redeem script 를 해싱하고 그거랑 사용자의 해시값이 같은지 확인 한 후에 redeem script 를 실행한다고 하셨는데, 구체적인 동작 순서상으로는 반대가 아닌가 싶어 헷갈린다.)
그리고 이렇게 하면 결국 redeem script 라는 긴 스크립트가 input 으로 들어가기 때문에 높은 트랜잭션 수수료를 지불하는 입장이 소비자에서 회사 (모하마드) 로 옮겨가게 된다.
동작 순서 역시 먼저 redeem script 를 체크하고, 그 다음에 redeem script를 실행하는 것으로 나온다.
위에서는 스크립트 전체를 < > 로 감싸서 데이터로 만들었다는 것에 주의하자.
아래 에서는 스크립트 전체에 대해 < > 를 풀고 스크립트로서 실행하고 있다.
CHECKMULTISIG 연산자가 동작할 때는 2개의 서명과 5개의 퍼블릭키를 매칭하는 5C2 조합을 모두 다 해본다.
그래서 하나라고 성공하면 유효하다고 판정을 내린다.
그리고 이걸 조금 응용하면, CHECKMULTISIG 가 아니라 여기에 어떠한 스크립트가 와도 상관없다는 것을 알 수 있다.
따라서 블록체인을 결제 용도가 아닌, 프로그램 코드를 담는 용도로 활용할 수도 있다.
P2SH 의 좋은 점
1. 복잡한 스크립트를 감출 수 있다. (센더에게 이게 스크립트인지 실제 주소인지 알려줄 필요가 없다.)
2. 월릿 개발이 단순해진다. (센더 입장에서는 스크립트인지 주소인지 구분할 필요가 없으므로)
3. UTXO set 의 크기가 줄어드므로 full node 의 부담이 줄어든다.
따라서 redeem script 는 블록 체인만 들어가면 된다. (UTXO에는 들어갈 필요가 없으므로)
-> UTXO는 output 만 관리하는데, redeem script 는 input 에 들어가서 실제 지불할 때 사용되므로 UTXO에는 들어갈 일이 없고, 블록으로 채굴되었을 때만 그 블록 안에 들어있게 된다.
4. 긴 스크립트에 지불하려는 sender 입장에서 트랜잭션 수수료를 높게 내지 않아도 괜찮다.
Redeem Script 를 검증할 때 문제점
P2SH 를 사용할 때 그 안에 재귀적으로 P2SH 를 넣는 것은 허용하지 않는다.
(다른 유효한 스크립트는 넣어도 괜찮기에 실험적인 시도를 한다거나 할 수는 있다.)
만약 유효하지 않은 redeem script 를 해싱한 결과에 지불한 경우, 그 비트코인은 영영 찾을 수 없게 된다.
이 경우, 이 UTXO는 절대 사용될 수 없음에도 모든 full node 가 들고다녀야 하기 때문에 비트코인 시스템 입장에서는 낭비이다.
(그리고 redeem script 해시값이 유효한 스크립틩 해시인지 검증하는 방법은 없다. 해시의 일방향성 때문이다.)
풀 노드들은 저 해시값을 일단 유효한 해시값이라고 간주할 수 밖에 없다.
(그리고 실제로는 꼭 이런 문제가 아니더라도 private key 를 분실해서 사용될 수 없는 채 묶여있는 비트코인도 꽤 있다.)
OP_RETURN
원래 비트코인은 '지불' 즉 페이먼트를 위한 용도로 만들어졌다.
그런데 비트코인을 페이먼트가 아니라 '특정한 시점에 무언가가 있었다' 라는 것을 증명하는 목적으로 비트코인을 사용하고자 하는 사람들이 생겨났다. 이게 가능한 이유는 트랜잭션이 한번 채굴되어 confirm 되고 오랜 시간이 지나면 그 뒤로 그 트랜잭션 값은 영구히 바뀌지 않는 값으로 보존되기 때문이다.
그래서 특허 같은 아이디어를 스크립트에 담아서 해싱한 뒤 트랜잭션으로 만들면 비트코인 블록에 영구히 언제 그 아이디어를 생각해냈는지 저장할 수도 있다. 이런 경우에는 output 을 아예 사용할 수 없는 트랜잭션으로 지정해서 보낸다.
그리고 그런 목적으로 사용하라고 판을 깔아주며 만든 연산자가 바로 OP_RETURN 이다.
이 부분은 꽤 많은 논란이 있었다.
순수주의자들은 사카시 나카모토가 처음 생각한 '지불'을 위한 비트코인이 다른 목적으로 사용되는 것,
그리고 그 과정에서 UTXO 집합에 쓸 수 없는 트랜잭션들이 쌓이는 것을 우려했고,
이런 다양한 활용을 환영하는 사람들도 있었다.
결국은 이 사람들이 이겨서 OP_RETURN 이라는 명령어도 만들어지게 되었다.
이 명령어는 순수주의자들이 말한 그 우려를 인정하고, 사용할 수 없는 트랜잭션을 식별할 수 있도록 명령어를 만든 뒤, full node 들은 이 트랜잭션에 대해서 UTXO set 에 저장하지 않을 수 있도록 해주었다.
그리고 output script 부분에는 이제 수신자가 존재하지 않으므로 내가 넣고 싶은 데이터를 80byte 까지 자유롭게 넣도록 하였다.
여기에 자기가 원하는 데이터를 넣고자 하는 사람들은 자신이 작성한 논문이나 발견한 내용의 해시값을 넣을 수도 있고, 내 웹사이트에 내가 발견한 내용을 올려둔 뒤, 그 웹사이트의 URL 을 넣기도 한다.
그리고 나중에는 이런 일을 대행해주는 회사도 등장했다.
일정 비용을 지불해주면 자신들이 직접 트랜잭션에 사용자가 원하는 데이터를 DOCPROOF 라는 prefix 를 붙여서 넣어주는 것이다.
그리고 이 과정에서 비록 지불할 주소는 없지만 input 주소에서 소량의 비트코인을 꺼내서 비트코인을 태우는 방향으로 지불하는 것도 가능은 하다. 트랜잭션 수수료를 만약 만든다면 output 2개를 만들어서 하나는 나의 주소로 보내고 하나는 OP_RETURN 스크립트를 사용해서 그 차액을 수수료로 지불하도록 할 수 있다.
'CS > 블록체인' 카테고리의 다른 글
[블록체인] 13. Mining (0) | 2024.10.25 |
---|---|
[블록체인] 12. 비트코인 script (2) (3) | 2024.10.25 |
[블록체인] 11. 비트코인 트랜잭션 (1) | 2024.10.25 |
[블록체인] 10. 비트코인이 해결하는 문제들 (1) | 2024.10.25 |
[블록체인] 9. 암호학 개념들 (0) | 2024.10.24 |