이 그림은 비트코인의 동작을 한 페이지에 정리한 모습을 나타낸다.
맨 왼쪽에는 블록체인이 존재한다.
제네시스 블록 (높이가 0인 블록) 부터 그 위로 하나씩 블록을 차곡 차곡 쌓으면서 블록 체인이 형성된다.
비트코인을 사용한 거래가 발생하여 그에 대한 트랜잭션이 발생하면, 채굴자는 그 트랜잭션을 모아서 블록으로 만들고, 작업 증명에 성공하면 채굴된 블록을 발표한다.
만약 이 블록이 다수의 사용자에 의해 받아들여지면 그 블록에 포함된 트랜잭션들은 다수에 의해 인정받는 유효한 트랜잭션이 된다.
비트코인의 트랜잭션은 위와 같이 생겼다.
먼저 트랜잭션에는 input, output 부분이 있고, input 에는 비트코인이 빠져나갈 위치가 들어간다.
(정확히는 나의 계좌의 최종 잔고를 나타내는 트랜잭션들이 들어간다.)
output 에는 내가 지불할 위치가 기술되며, input 부분의 총합과 output 부분의 총합을 뺀 차액이 이 트랜잭션을 채굴한 채굴자에게 지불할 수수료가 된다.
이 그림은 비트코인 월렛에서 일어나는 과정을 보여준다.
만약 David 이 Sandra 에게 5BTC를 지불하는 트랜잭션을 생성하고자 한다면,
월렛 소프트웨어는 트랜잭션 요청 메세지를 만들고, 이를 David 의 private key 로 서명해서 비트코인 네트워크에 broadcast 한다.
그러면 이 트랜잭션을 수신한 노드는 이 트랜잭션이 유효한지 검증한다.
만약 이 트랜잭션을 포함하여 블록을 만들기를 원하면 아직 지불되지 않은 여러 트랜잭션을 가지고와서 블록으로 만들어 작업증명을 통해 블록을 채굴한다.
지불을 위한 트랜잭션
UTXO vs STXO
UTXO = unspent transaction output
STXO = spent transaction output
STXO는 이미 다른 블록에 채굴되어서 이미 확정된 트랜잭션을 말한다.
따라서 이런 트랜잭션은 채굴자 입장에서는 신경쓸 필요가 없다.
모든 트랜잭션은 input 으로 들어온 모든 비트코인을 털어내기 때문이다.
하나의 트랜잭션에서 지불하고 남은 금액같은 것이 존재하지 않으므로 이전 트랜잭션은 신경쓸 필요가 없다.
(만약 내 계좌에 남아있는 5 비트코인 중 3 비트코인을 지불한다면, 트랜잭션을 생성할 때는 남은 2 비트코인을 나의 또다른 계좌로 보내는 트랜잭션을 작성해야 잔액으로서 관리된다. 안쓰면 남은 2 비트코인이 고스란히 수수료로 나간다.)
따라서 채굴자들의 관심사항은 UTXO 이고, 그렇기에 모든 full node는 전체 UTXO를 다 가지고 있어야 한다.
왜냐하면 언제 이 UTXO 로부터 지불하는 트랜잭션이 들어올지 모르는데, 트랜잭션을 블록에 포함하려면 그 트랜잭션을 검증해야 하고, 그 과정에서 그 트랜잭션이 돈을 빼내려는 트랜잭션 (UTXO) 이 내가 갖고 있는 UTXO 데이터 셋에 들어있는지 확인해야 하기 때문이다.
따라서 STXO 를 사용하면 그 트랜잭션은 유효하지 않다. 또 UTXO 라고 해도, 그 트랜잭션의 주인이 자신이 맞다는 것을 입증하지 못하면 유효하지 않다.
만약 비트코인 월렛 소프트웨어가 비트코인을 '받았다' 라고 한다면, 그 말은 어떤 UTXO로부터 이 월렛에 있는 주소 중 하나로 비트코인을 보내는 UTXO가 존재한다는 것과 같다.
월렛은 이 월렛에 들어있는 모든 비트코인의 총 합을 보여주므로 사용자는 자신이 가지고 있는 비트코인을 한눈에 확인할 수 있다.
지불을 할 때는 특정 UTXO의 output 에 들어있는 여러 output 중에서 인덱스로 특정 아웃풋을 지정해서 이 주소로 비트코인을 보내는 그 output 에서부터 비트코인을 꺼내겠다는 트랜잭션을 만들면 된다.
이때 내가 만들려는 트랜잭션 output 에는 어디에 얼마를 지불할지 명시하는데, 지불 금액의 단위는 사토시 단위를 사용한다.
(1 BTC = 10**8 SATOSHI) 그래서 거래소에서 비트코인을 살 때는 비트코인을 사토시 단위로 쪼갠 만큼의 금액으로도 소액으로 살 수 있다.)
트랜잭션에는 script 라는 스택 기반의 작은 코드 조각을 넣을 수 있다.
그리고 이를 통해서 내가 지불한 비트코인의 수신자가 합당한 수신자임을 증명하기 전까지 그 수신 주소에서 비트코인을 꺼내지 못하도록 막을 수 있다.
(그래서 output 에서 특정 주소를 가리켜 비트코인을 전송할 때, 해당 주소에 락을 건다고 표현하기도 한다.)
그래서 내가 이 주소로 받은 비트코인을 사용하려면 언락하는 스크립트 코드를 제시해야만 쓸 수 있으며, 이 부분을 witness 라고 부른다.
(또는 소유권이라고도 말한다.)
비트코인은 genesis block 이후에 블록이 생성될 때마다 block height 가 증가한다.
만약 alice 가 만든 트랜잭션이 277316 높이의 블록에 존재하고, 현재 최신 블록이 277319 블록이라면, 현재 앨리스의 트랜잭션은 자신 포함 4번의 confirmation 이 진행되었다고 말한다.
그리고 이렇게 confirmation 이 계속 진행될 수록, 이 트랜잭션이 유효하지 않게 될 가능성은 점점 낮아진다.
컨펌이 충분히 되었다는 것은 이 트랜잭션이 들어있는 블록이 대세 블록일 가능성이 높다는 뜻과 같기 때문이다.
그림은 여러 트랜잭션이 연결된 모습을 보여준다.
먼저 Joe 의 지갑에서 자신이 갖고 있는 비트코인을 앨리스에게 지불했다고 해보자.
앨리스는 자신의 주소로 받은 비트코인 중 일부를 밥에게 지불하고, 나머지는 자신의 계좌로 옮겼다.
이떄 밥이 다시 자신이 받은 비트코인 중 일부를 조셉에게 지불하고 나머지를 자신의 계좌로 보낸다면,
최종적으로 앨리스가 자신의 계좌로 잔액을 옮기는 트랜잭션, 밥이 자신의 계좌로 잔액을 옮기는 트랜잭션, 조셉이 비트코인을 받은 트랜잭션 3가지만이 UTXO로 존재하고 나머지는 STXO가 된다.
만약 앨리스가 나중에 추가적으로 비트코인을 지불한다면 자신의 UTXO를 input 으로 가지는 트랜잭션을 만들어서 지불하면 된다.
이 그림은 블록이 채굴되면서 트랜잭션이 확정될 때마다 UTXO 집합이 어떻게 변화하는지를 보여준다.
먼저 최초에 조가 앨리스에게 지불하는 트랜잭션을 생성했고, 그 트랜잭션이 277298 높이의 블록에서 컨펌되었다면
기존의 조가 끌어다쓴 0번 트랜잭션 STXO 가 되어 UTXO 집합에서 사라지고, 앨리스가 비트코인을 받는 1번 트랜잭션이 UTXO 셋에 남아있는다.
이때 앨리스가 자신이 가진 비트코인 일부를 밥에게 지불하고, 남은 잔액을 자신에게 보내는 트랜잭션을 생성하고, 이 트랜잭션이 277316 높이의 블록에서 컨펌되었다면, 기존의 앨리스가 갖고 있던 UTXO 는 STXO가 되어 사라지고, 밥에게 전송하는 UTXO와 잔액을 다시 앨리스에게 보내는 UTXO 2개가 UTXO 집합에 저장된다.
마지막으로 밥이 조셉에게 지불하는 트랜잭션을 생성하였고 277317 높이의 블록에서 이것이 확정되면 UTXO 집합에는 기존의 밥의 UTXO가 사라지고, 조셉이 비트코인을 받는 output 과 지불하고 남은 잔액을 bob 이 받는 output 2개가 UTXO 집합에 남아있게 된다.
이때 UTXO 집합에는 앨리스가 잔액을 받는 트랜잭션이 여전히 남아있다.
이렇게 UTXO 집합에는 UTXO가 계속해서 생성될 수도 있고, 내가 나의 UTXO 를 묶어서 하나의 주소로 보내면 UTXO 집합에 있는 UTXO 개수가 줄어들 수도 있다.
이 그림은 실제 트랜잭션의 모습을 보여준다.
각 초록박스는 하나의 트랜잭션을 나타내고, 박스 맨 위의 굵은 글씨는 트랜잭션 데이터를 해싱하여 나오는 트랜잭션 ID이다.
맨 위에서부터 다시 보면 조가 앨리스에게 지불하는 트랜잭션이 있고,
이 UTXO가 다음 트랜잭션에서 input 으로 그대로 들어와서 앨리스가 밥에게 지불하는 트랜잭션에 사용된다.
그러면 기존의 조가 앨리스에게 지불하는 트랜잭션에서 앨리스가 받는, 0번 output 은 STXO 가 된다.
마찬가지로 밥이 비트코인을 받은 그 output (0번 output) 을 input 으로 하여 새로운 트랜잭션을 생성하면 기존 output은 STXO가 된다.
또 다른 예시는 위와 같다.
트랜잭션에는 input 으로 코인을 가져올 UTXO 를 여러개 지정하고, Output 에는 가져온 코인을 보낼 곳을 여러개 지정할 수 있다.
이때 코인을 가졍로 트랜잭션은 이전 트랜잭션의 ID 와, 그 트랜잭션의 output 중 몇 번째 output을 사용할 것인지 인덱스를 지정한다.
위 그림애서 Tx0:0 은 Tx0 의 0번째 인덱스에 있는 UTXO를 사용하겠다는 뜻과 같다.
그래서 Tx1:0, Tx2:1 이 가리키는 화살표가 다른 것이다.
트랜잭션의 여러가지 형태를 보면, 보통 트랜잭션은 하나의 output 에서 한 곳으로 지불하고, 남은 잔액을 자신의 다른 계좌에 보내는 식으로 사용한다.
이렇게 트랜잭션을 쓰기도 한다.
N 개의 이전 트랜잭션 output을 모아서 하나의 output으로 만드는 경우이다.
예시로는 스포츠카 한 대를 사려고 하는데, 내가 가지고 있는 여러 계좌에 있는 비트코인을 합해야 돈이 나온다면 여러 개의 계좌 속 비트코인을 모아서 스포츠카 판매점에 지불하는 것이다.
거꾸로 하나의 output 에서 여러 곳에 동시에 지불하는 트랜잭션도 만들 수 있다.
트랜잭션에 대한 이해
앨리스가 밥에게 비트코인을 지불하는 상황을 가정해보자.
이를 위해서는 앨리스가 밥에게 비트코인을 지불하는 트랜잭션이 다수의 노드에 의해 받아들여져야 한다.
즉, 비트코인 네트워크에 24시간 상주하면서 트랜잭션을 검증하고 채굴하는 full node 에게 인정을 받아야만 정말 지불했다고 받아들여지는 것이다. 만약 이 트랜잭션이 대세 블록에 포함되지 않으면 지불이 되지 않은 것과 마찬가지이다.
만약 이 트랜잭션이 대세 노드에 포함이 되어 인정을 받게 되면, 그때부터는 밥이 앨리스가 보낸 비트코인을 자신 것으로 인정받고 사용할 수 있게 된다. 이 과정에서 실제 앨리스와 밥의 신분증이나 활동이 들어가지 않고, 오직 비트코인 주소에서 주소로만 이동할 뿐이라서 특정 비트코인 주소의 실 소유자가 누구인지 알기가 힘들다. 그래서 범죄자들이 비트코인을 거래 목적으로 활용하기도 한다.
앨리스가 밥에게 비트코인을 지불하는 트랜잭션을 만들 때는 자신의 비트코인 주소로 비트코인이 들어오는 UTXO 를 input에 담아서 여기에서 비트코인을 꺼내겠다고 해야 한다. 이때 이 주소가 자신의 주소가 맞음을 입증하기 위해 앨리스는 서명을 통해 이 주소가 자신의 주소임을 입증한다.
UTXO 는 32bit의 이전 트랜잭션의 ID 와 그 트랜잭션의 output 에서 어떤 주소를 가리키는지를 나타내는 인덱스를 지정한다.
풀 노드는 트랜잭션을 받으면 그 트랜잭션의 input 계좌들의 소유주를 제시한 서명을 통해 검증한 뒤, 이 금액들을 합해서 지불하려는 금액이 지불할 수 있는 금액인지 확인하고, 제시한 트랜잭션 output 이 UTXO 인지 확인한다.
또한 트랜잭션에는 time lock 이 존재해서, 일정 시간동안 블록에 포함되지 않도록 할 수 있는데, full node는 이것도 체크해서 포함 여부를 결정한다.
트랜잭션의 구조
트랜잭션을 byte map 으로 보면 위와 같이 되어있다.
version : backward compatable 을 위해 사용된다. 버전을 보고 과거 버전이면 과거의 방식대로 처리하고, 높은 버전이면 그 버전에 추가된 기능에 맞게 처리한다.
Marker, Flag : 세그윗을 위해 지정한 비트
Inputs, output : 비트코인을 끌어올 UTXO 와 지불할 비트코인 주소를 지정한다.
Witness structure : 2017년에 비트코인에 새로 추가된 변경점이다. 트랜잭션에서 witness (목격자) 를 분리하는 seg wit 개념이 나왔다. witness 는 signature (서명)을 위해 존재하는 필드라고 생각하고 뒤에서 더 자세히 정리한다.
Lock time : 이 트랜잭션이 일정 시간동안 블록에 포함되지 않도록 제한하는 시간
Input 부분의 바이트 맵만 따로 가져오면 위와 같이 되어있다.
여러개 input 을 담는다면 이 input 을 여러개 작성하면 된다(?)
앨리스의 트랜잭션에서는 1개의 input 만 존재한다. (?)
count : 몇 개의 input 이 있는지
previous output txid : 이전 트랜잭션 ID (32bit 아닌가? 왜 byte map 이지?)
previous output index : 해당 트랜잭션에서 몇 번째 output을 사용할 것인지
length : Sequence 영역에 들어있는 데이터의 길이 (스크립트의 길이, 세그윗 이후로는 0으로 설정)
과거에는 이 길이 필드 뒤에 스크립트가 들어갔으나 seg wit 이후에는 스크립트를 넣지 않는다.
Sequence : 트랜잭션에 대해 번호가 누적해서 증가하면서 이 트랜잭션이 과거의 트랜잭션인지 최신의 트랜잭션인지 구분하는데 사용한다. 시퀀스에서는 그 아래 보이는 것처럼 32bit 가 있는데, 31번째 비트와 22번째 비트에 의미가 있다.
이 비트가 어떻게 설정되어있는지에 따라서 시퀀스 넘버를 해석하는 방법이 달라진다.
시퀀스는 원래 같은 트랜잭션에 대해 그 트랜잭션을 덮어써서 수정하려는 목적으로 만들어졌다.
앨리스가 밥에게 0.1 비트코인을 지불하는 트랜잭션을 만들어서 뿌렸는데, 이를 덮어쓰고 싶다면 sequence 넘버가 증가한 동일 트랜잭션을 다시 뿌리면 된다.
또는 만약 앨리스가 밥이 운영하는 카페에 매일 가서 커피를 먹는다고 하자.
이때 매번 트랜잭션을 발생시켜서 결제하기보다, 1달치 커피값을 미리 내놓고 매일 지불하도록 하면 더 편리할 것이다.
그런데 앨리스만 1달치 돈을 미리 내두면 밥이 갑자기 카페 문을 닫고 야반도주했을 때 앨리스는 억울할 수 있으므로, 밥도 상응하는 돈을 미리 넣어두어 펀드를 만든다. (initial payment)
그리고 0번 트랜잭션으로는 앨리스는 자신의 원금을 회수하고, 밥은 자신의 원금을 회수하는 트랜잭션을 걸어둔다.
이 트랜잭션은 아직 뿌리지 않고, 앨리스가 커피를 사 마실 때마다 이 트랜잭션에 앨리스의 몫을 줄이고 밥의 몫을 늘리도록 수정을 가하고 sequence 를 증가시켜 서명만 해둔다. 만약 1달동안 성공적으로 커피를 마셨다면 최종 sequence 번호를 가진 트랜잭션 하나를 뿌린다.
하지만 지금은 이런 식으로 사용하지 않는다.
시퀀스 번호가 제일 큰 것만 뿌리자는 것은 둘 사이의 약속일 뿐이기에 상대방이 이를 무시하고 그냥 트랜잭션을 뿌리면 매번 수정된 버전의 시퀀스 번호를 가진 트랜잭션이 네트워크에 돌아다니면서 네트워크에 혼란이 올 수 있기 때문이다.
지금은 시퀀스를 relative time lock 을 위해 사용한다.
time lock 에는 absolute time lock 과 relative time lock 이 있다.
time lock 은 상대방이 이 output 주소로부터 비트코인을 꺼내가지 못하도록 막는 시간을 지정한다.
(일종의 약속 어음 개념과 비슷하다)
relative timelock 은 이 시간을 각각의 input 마다 지정할 수 있다.
각각의 input은 output 을 끌어오는 역할을 하는데, 그 output 으로부터 input을 끌어올 때까지의 시간을 relative timelock 으로 정해두는 것이다.
예를 들어 이전 트랜잭션이 200 번 블록에서 컨펌되었고, relative time lock 이 300 이 걸려있다면, 현재 트랜잭션은 300 블록이 지난 500 블록에서 이 input 을 넣어야 유효하게 사용할 수 있다.
(그 이전 블록에서는 이 input 값을 사용할 수 없다고 보기 때문에 이 트랜잭션을 포함해서 채굴하지 않는다.?)
(UTXO에서 일정기간 지불을 못하도록 막는건 output 에서 락을 걸어야 하지 않나? 왜 끌어다 쓰는 입장인 input 에서 락을 걸어두지..? 그렇게 락을 걸 거면 내가 그만큼 블록이 지난 다음에 트랜잭션을 생성하면 되는 것 아닌가? 그걸 매번 확인하지 않고 특정 시간이 지난 다음에 꺼내지도록 제한을 걸어두면서 트랜잭션을 만드는게 목적인건가?)
다시 예를 들어 100번 블록에서 컨펌된 트랜잭션을 끌어다가 사용하려고 하는데, 그 끌어다 쓰는 인풋에 (이전 트랜잭션 기준으로 output) 100 블록의 timelock 이 걸려있었다고 해보자. 그러면 이 트랜잭션이 실제로 실행되려면 200블록에 포함될 때까지 기다렸다가 실행이 된다. 그 전에는 실행이 안된다. 풀 노드들이 거부하는 것이다.
(실행이라는게 컨펌을 얘기해서, input 에 들어있는 relative timelock 을 보고 충분한 시간이 안지났으면 얘는 아직 블록에 포함시킬 수 없는 트랜잭션이라고 보는 것 같다.)
만약 relative timelock 이 30이 걸려있었다면, 이 인풋에 담긴 output이 들어갔던 트랜잭션을 담는 블록은 최소한 29 블록 이전에는 컨펌이 되어있어야 한다. (현재가 30번째라면 현재부터는 가능)
sequence 값은 2^31 보다 작으면 (= 최상위 비트인 31번째 비트가 0) relative timelock 으로 간주하고, 그보다 크면 (보통 모든 비트를 1로 세팅한다) timelock 이 없으니 바로 가져다가 사용할 수 있도록 한다.
22번 비트가 1이고 31번째 비트가 0이면 파란색 영역 (value) 은 파란색 영역의 값 x 512 초만큼 기다리라는 뜻이고,
22번 비트가 0이고 31번째 비트가 0이면 파란색 영역은 그 개수의 블록이 채굴될 때까지 기다리라는 뜻이다.
이런 식으로 트랜잭션 인풋에 상대적으로 타임 락을 거는 것이 가능하다.
이번에는 ouput 영역을 떼와서 자세히 살펴보자.
count : output 의 개수
Amount : 지불할 액수 (사토시 단위, 10**8 사토시 = 1 비트코인)
이 값은 재밌게도 0이 될 수 있다. 최대 2100만 비트코인까지 설정할 수 있다. (총 발행량)
물론 실제로 이렇게 설정할 일은 없을 것이다.
Length : output script 길이
output script
(amount, length, script) 가 반복되는 구조로 되어있다.
input에서는 input이 하나라서 한번만 적었던 것이다.
마지막으로 Witness Structure 에 대해 알아보자.
사카시 나카모토가 맨 처음 비트코인을 설계했을 때는 Witness Structure 가 없었다.
하지만 2015년에 처음 이 개념이 등장하고 2017년에 받아들여져서 실제 구현이 되었다.
2015년에는 비트코인의 인지도가 늘어서 점점 트랜잭션이 늘어나기 시작하던 때이다.
그런데 이 때는 블록의 크기가 1MB로 제한이 되어있다보니 트랜잭션을 담는데 한계가 있었고, 내가 생성한 트랜잭션이 블록에 포함되기를 한참 기다려야 하는 문제가 발생했다.
이에 답답함을 느껴서 떨어져 나간 프로젝트가 '비트코인 캐시' 이고, 여기서는 블록의 크기 제한을 8MB로 두었다.
비트코인은 사카시 나카모토가 생각했던 1MB 를 최대한 존중하면서 트랜잭션을 보다 많이 담을 수 있도록 하기 위해 다른 방법을 적용했는데, 이것이 Witness Structure 이다.
Witness 의 목적은 2가지 이다.
첫 번째는 트랜잭션이 늘어났으니 블록 사이즈 제한을 늘려보자는 것이고 (결과적으로 4MB로 증가했다.)
두 번째는 malleability 문제를 해결하는 것이다. (사이드 이펙트로 얻게된 이점)
그러면 기존 방식처럼 Witness (Signatrure) 를 Input Script 에 넣었을 때는 어떤 문제가 발생했을까?
1. Circular Dependencies
앨리스와 밥이 스크립트에 공동 자금을 넣어두고 앨리스와 밥 두 명의 서명이 있어야만 펀드에서 돈을 꺼낼 수 있다고 해보자.
그런데 만약 상대방이 잠수를 타버리면 내가 돈을 꺼낼 수 없기 때문에 안전 장치가 필요하다.
따라서 펀드를 만들 때 먼저 0번 트랜잭션으로 각자 자신의 돈을 펀드에 넣는 트랜잭션을 생성하고,
1번 트랜잭션으로는 0번 트랜잭션의 output을 가져다가 각자가 자신의 몫을 꺼낼 수 있도록 하는 트랜잭션을 만들어둔다.
그러면 상대방이 사라지더라도 이 트랜잭션을 뿌려서 컨펌받으면 적어도 내 몫은 돌려받을 수 있다.
그런데 앨리스와 밥 입장에서는 tx0에 서명하기 전에 먼저 tx1에 서명하고 싶을 것이다.
왜냐하면 tx0 에 먼저 서명을 했는데, tx1에 서명하기 전에 밥이 앨리스가 골탕먹어보라고 tx0을 뿌려버린 뒤 사라지면, 앨리스는 이 펀드에서 돈을 꺼낼 수 없게 되기 때문이다.
따라서 이 둘 모두 서로를 믿지 못하기 때문에 tx1 에 먼저 서명하고 싶어한다.
그런데 tx1을 서명하려면 tx0 의 트랜잭션 아이디가 필요하고, 이를 얻으려면 tx0을 서명해서 해싱해야만 한다.
tx0 을 서명하려면 tx1 이 먼저 서명되어있기를 원하는데, 아이러니하게 tx1을 서명하려면 tx0이 서명되어야만 tx1을 서명할 수 있는 문제가 circular dependencies 문제이다.
만약 서명(signature) 부분을 따로 빼서 트랜잭션을 만들 때 사용하지 않는다면 이 문제가 해결될 것이고,
이 아이디어가 witness 이다.
2. third-party malleability
malleability 는 쉽게 얘기해서 내가 조물조물해서 바꿀 수 있는 것을 말한다.
third-party malleability 라고 하면 이 트랜잭션의 sender, reciever 가 아닌 제 3자가 이 트랜잭션을 조작할 수 있는 가능성을 말한다.
먼저 이를 설명하려면 비트코인 스크립트를 알아야 한다.
비트코인 스크립트는 스택 기반으로 동작한다.
예를 들어 위와 같은 스크립트가 있다면 2, OP_ADD, 4, OP_EQUAL 순으로 스택에 차곡차곡 쌓는 것이다.
OP_ 가 붙은 문자열은 연산자를 나타내고 경우에 따라서 생략하기도 한다.
결국 이 스크립트를 통해 하고 싶은 것은, 이 연산 결과를 만족시키는 무언가를 제시하고 싶은 것이 목적이다.
예를 들어 이 스크립트가 output script 에 들어있고, 앨리스가 이를 끌어다가 사용하려고 하면, '2' 를 제시해야 한다.
왜냐하면 2와 더해져서 4가 되도록 하는 값은 2 이기 때문이다.
그래서 아웃풋 스크립트에 저런 스크립트가 있다면, 인풋 스크립트에서는 2를 제시하는 것이다. (2가 일종의 witness 역할이다.)
비트코인 스크립트는 데이터 (operand) 가 들어오면 스택에 push 하고, operator 가 나오면 2개를 pop 해서 연산한 결과를 push 한다.
2 OP_ADD 4 OP_EQUAL
이제 위 스크립트를 따라 2라는 데이터를 제시해보자.
1. 스택에 2를 넣는다.
[ 2 ]
2. 스크립트 왼쪽부터 차례대로 스택에 넣는다. 데이터는 push, 연산자는 top에서 2개를 빼고 결과를 push 한다.
[ 2 2 ]
OP_ADD 를 수행하면 [ 4 ]
4를 넣으면 [ 4 4 ]
OP_EQUAL 을 실행하면 [ true ]
따라서 이 스크립트의 실행 결과가 true 라면 성공적으로 스크립트를 끝까지 실행한 것이 된다.
즉, 누군가 output 에 이런 스크립트를 만들었다면, 여기에 2를 제시해야 이 output을 끌어다가 사용할 수 있다.
(이건 그냥 예시일 뿐이다. 실제로 이렇게 하면 누구나 2를 제시하는 순간 가져다가 사용할 수 있으므로 오픈된 payment가 되어버린다.)
2를 제시할 때는 input script 에서 위와 같이 제시할 수 있다.
3가지 문장 다 2를 스택에 push 하는 문법이다.
(OP_PUSH1 은 1바이트 데이터를 푸쉬한다는 뜻이다.)
그런데 만약 앨리스가 이 output 을 가져다가 input 에 2를 제시하는 트랜잭션을 만들었는데, 앨리스는 OP _2 로 넣었지만 다른 사람은 OP_PUSH1 0x02로 넣을 수 있다. 그런데 이 데이터는 같은 기능을 해도 엄연히 다른 글자라서 트랜잭션 해시값이 다르고 다른 트랜잭션으로 여겨진다. 이때 만약 공격자가 만든 다른 버전이 confirm 되어버리면 앨리스의 트랜잭션이 invalid가 되어서 문제가 발생할 수 있다.
(이게 가능한가? 앨리스의 서명을 제시해야만 앨리스에서 빼낼 수 있는 거 아닌가? 이 스크립트가 서명을 검증하는 용도로 쓰이는 건가?
=> 스크립트의 역할이 서명 검증인 것 같다.)
그래서 이 문제를 해결하고자 input 쪽에는 아예 witness 에 해당하는 스크립트를 넣지 않기로 했다.
3. second-party malleability
이번에도 앨리스와 밥이 각자 100씩 내서 200만원짜리 공동 펀드를 만들었다고 해보자.
이때 앨리스가 자신의 돈을 5만원 사용하고 싶어서 트랜잭션을 생성했다고 해보자.
그러면 0번 트랜잭션에서 첫번째 output 에는 앨리스가 돈을 일부 지불하고, 두번째 ouput 에는 남은 잔액을 다시 공동 펀드로 내보내도록 했다고 해보자. 이 공동펀드는 지불하려면 둘 모두의 서명이 필요하기 때문에 나중에 한명이 사라지는 것을 방지하기 위해 이 트랜잭션 이후에는 각자가 자신의 돈을 돌려받을 수 있는 안전장치용 트랜잭션을 추가적으로 만들어야 한다.
이 경우에는 앨리스가 95를 돌려받고, 밥이 100을 돌려받는 트랜잭션이 될 것이다.
이때도 당연히 tx1 을 먼저 서명하기를 원하는데, tx0 가 있어야 tx1을 서명할 수 있는 서큘러 디펜던시 문제가 발생할 수 있다.
그렇지만 둘이 서로 신뢰를 해서 tx0 을 서명하고, tx1 까지 성공적으로 서명했다고 해보자.
그런데 여전히 다른 문제가 발생할 수 있다.
타원 곡선 기반 서명의 특징이, 서명을 할 때 특정한 랜덤 넘버를 지정하여 서명에 포함시키도록 할 수 있다.
그리고 이 랜덤 넘버를 바꾸면 같은 내용에 대해서 서로 다른 유효한 서명을 만들어낼 수 있다.
이때 앨리스가 밥이 기존에 사용하는 서명을 통해 생성한 0번 트랜잭션을 브로드캐스트 했다고 해보자.
그런데 밥이 여기에서 tx0 에 대해 기존의 앨리스 서명은 놔두고 밥의 서명 부분만 랜덤 넘버를 바꾼 또 다른 서명으로 바꿔서 브로드캐스트를 했고, 이게 컨펌되는 경우, 앨리스가 기존에 알고 있던 tx1 은 더 이상 안전장치가 될 수 없다. (tx0 에서 꺼내와야 하는데, 서명이 바뀌었으니 트랜잭션 아이디의 해시값도 바뀌었기 때문이다.)
이 처럼 트랜잭션과 관련된 당사자가 조작을 가해서 문제를 일으키는 것을 second-party mallebility 라고 한다.
그러면 Witness 를 분리하게 되면 어떻게 될까?
witness 는 서명과 같다고 생각하면 된다.
서명이 분리된다는 것은 트랜잭션 ID를 해싱하는데 서명이 관여하지 않는다는 뜻이고, 밥의 조작, 제 삼자의 조작이 더 이상 통하지 않는다.
따라서 위 문제들이 자연스럽게 해결 된다.
SegWit 이전에는 인풋 스크립트에 witness 에 해당하는 서명을 포함해서 트랜잭션 ID를 계산했지만,
SegWit 이후에는 인풋 스크립트가 비이었고, witness 가 빠져서 트랜잭션 ID를 계산하는데 관여하지 않는다.
그리고 witness를 뺀 트랜잭션들의 합이 1MB가 되도록 유지함으로써 사카시 나카모토의 기존 아이디어를 존중하고, 블록의 크기를 최대 4MB까지 늘릴 수 있게 되었다. (블록에 트랜잭션을 저장할 때 witness 를 빼고 저장하는건가?)
분리된 Witness 는 그것만 가지고 별도로 아이디를 만들기도 하며, 이를 Witness ID 라고 한다.
2017년에 세그윗을 분리할 때, 90%가 넘는 찬성률이 있었지만, 소수의 반대자를 위해서 세그윗 이전의 트랜잭션도 사용할 수는 있도록 backward compatability를 제공하여 소프트 포크로 구현하였다.
(비트코인 캐시와 같이 별개의 길을 걷는 것이 하드포크고, 이전의 참여자를 지원하면서 변경점을 추가하는 것이 소프트 포크이다.)
그리고 기존 트랜잭션 형식을 사용가능하도록 하기 위해서, 기존 트랜잭션에서 invalid 라고 생각하는 것은 segwit 에서도 invalid 하다. 하지만 기존 트랜잭션에서 valid 한 것이 segwit 에서 항상 valid 하지는 않을 수 있다는 아이디어로 기존 아이디어를 함께 끌고간다.
(즉 segwit의 valid transacton 은 기존보다 더 엄격한 조건을 따르는 것이다.)
이러면 극 소수의 사람들이 기존 방식으로 트랜잭션을 만들어서 valid 하다고 보냈는데, segwit 방식을 사용하는 대다수의 사람들이 그에 대해 invalid 하다고 해서 거부할 수 있다보니, 결국엔 segwit을 사용할 수 밖에 없다.
Segwit 은 Anyone-Can-Spend output script 를 사용한다.
(즉, 이전 버전의 트랜잭션을 사용하는 노드들은 세그윗 아웃풋으로 보내는 페이먼트에 대해 누구나 가져다가 쓸 수 있는 페이먼트로 여긴다.)
세그윗 아웃풋은 위와 같은 구조로 되어있다.
먼저 세그윗 버전이 0~16 사이로 들어가고, 20~40 바이트의 데이터가 witness program 으로 들어간다.
이 둘은 비트코인 아웃풋 스크립트 입장에서는 '데이터' 이고, 세그윗을 지원하지 않는 노드 입장에서는 2개의 데이터가 그냥 통으로 왔기 때문에 왜 스크립트를 통해서 검증해야만 쓸 수 있게 하지 않고 바로 데이터 덩어리만 보냈는지 의문을 가진다. (데이터 덩어리만 보내면 누구나 가져다가 쓸 수 있는 것으로 여겨진다. 계산할 게 없기 때문이다.)
그래서 세그윗을 지원하는 노드들이 만든 output 을 세그윗을 지원하지 않는 노드들에게 보내도, 그 노드들은 이 트랜잭션에 대해 특이하다고 생각하면서도 유효는 하니까 받아들이게 된다.
(세그윗을 지원하지 않는 노드 입장에서는 output 에 검증 수식이 없으니 마치 공공장소에 돈을 그냥 두고 아무나 가져가라고 한 것처럼 보이게 된다.)
결론적으로 이전 버전 노드에서는 input script을 넣는 것을 '허용' 하므로 비어있어도 괜찮다고 하지만, (may)
세그윗을 지원하는 노드에서는 input script 를 '비워놔야만' 한다. (must)
그리고 세그윗을 지원하는 입장에서는 output 에 들어있는 데이터 자체를 분석해보면 그 안에 돈을 가져가기 위해 인증을 해야하는 수단을 준비해두었으므로 인증을 해야만 해당 output 에서 지불을 할 수 있게 된다.
기본적으로는 output 스크립트에 들어있는 수식을 만족하도록 하는 데이터를 input script 에서 제시를 해야 가져다 쓸 수 있는 것이 대세였으나 (지금도 쓰이긴 한다. 안 쓰이는 건 아니다!), 지금은 witness 프로그램에게 지불을 하고, 이 witness 프로그램이 나라는 것을 witness structure 영역에서 제시하도록 한다.
witness structure 는 위와 같은 구조를 가지며, 비트코인 스크립트를 다룰 때 자세히 정리한다.
Lock Time
앨리스의 트랜잭션 맨 마지막에는 Lock Time 이 들어있다.
이 락타임은 트랜잭션 전체에 적용되는 락타임이다. (input 에 들어있는 sequence는 input 마다 적용되는 lock time)
제일 많이 사용되는 것은 트랜잭션 락타임을 0으로 세팅하는 것이다.
이 말은 이 트랜잭션이 언제든 블록에 포함될 수 있음을 의미한다.
만약 락 타임을 5억보다 작게 잡으면 ( < ) 그때는 락 타임의 값을 block height 로 본다.
만약 락타임이 98520 이라면, 이 트랜잭션은 98521 의 높이를 갖는 블록부터 들어갈 수 있다.
(98520, 98521, 98522, ...)
만약 락 타임을 5억보다 크거나 같게 잡으면 그때는 락타임의 값을 시간으로 본다.
그래서 유닉스 시스템의 시간 체계를 따라 1970년 1월 1일 이후에 경과한 초 수를 나타내는 값으로 보고, 지정한 시간이 되었을 때부터 블록에 포함되도록 한다.
그런데 각 비트코인 채굴자마다 컴퓨터에서 생각하는 시간이 달라서 블록이 채굴된 시간이 다르게 측정될 수 있다.
이를 보정하기 위해 지난 11개의 채굴된 블록의 채굴 시간을 줄세워두고, 그 중에 중앙값 (평균이 아니다) 보다 이후의 시간이라면 허용하는 것으로 한다. 이를 가리켜 MTP 라고 한다.
Coinbase Transaction
코인 베이스 트랜잭션은 매우 독특한 트랜잭션이다.
비트코인을 생성하는 트랜잭션이기 때문에, input 에 비트코인을 가져오는 주소가 없기 때문이다.
정확히는 아무런 값을 넣지 않는 것이 아니라 null TX ID 를 넣는다. (tx id 값을 모두 1로 설정한 것)
그리고 output index 도 지정할 필요가 없기 때문에 그냥 최댓값을 지정한다.
input script 도 input 주소를 입증하는 과정이 필요하지 않기 때문에 그 공간에 자유롭게 아무 데이터나 넣을 수 있고, 사카시 나카모토는 그곳에 뉴스 헤드라인 제목을 넣었다.
이 공간에는 2바이트부터 최대 100바이트의 데이터를 넣을 수 있따.
output을 지정할 때는 보통의 경우 1개 output을 지정한다.
(보통 채굴에 성공하면 내 계좌 하나에 채굴 리워드와 수수료 합을 보내는 것이 일반적이다.)
물론 원하면 여러 곳에 나눠서 받을 수도 있다.
그리고 채굴에 성공한 이후에는 그 리워드를 지급하기까지 100번의 comfirmation을 기다린다.
(longest chain rule 에 의해 동시에 채굴된 블록에 대해서 경합이 발생할 수 있고, 100번의 comfirmation이 지나면 그 경합의 결과가 어느정도 나온다고 생각했기 때문이다.) 시간으로 따지면 하루가 조금 넘는 시간이다.
그래서 100 confirmation 이 지나지 않으면 아직 성숙하지 않았다고 해서 immature 라고 부르고, 100 confirmation이 지나면 mature 이라고 해서 그때부터는 이 주소로 받은 비트코인을 사용할 수 있게 된다.
Weight 와 Byte
세그윗 이전에는 비트코인 블록의 크기가 1MB 제한이 있어서 담을 수 있는 트랜잭션 수에 한계가 있었다.
하지만 세그윗 이후에는 트랜잭션 제한을 더 넉넉하게 풀게 되었다.
그리고 이때부터는 트랜잭션에 대해 '바이트' 개념이 아니라 'weight' 즉 무게 개념을 사용하기 시작했다.
그래서 명목상의 byte 제약은 1MB를 지키도록 유지하면서, weight 라는 새로운 개념으로 트랜잭션을 측정해서 4 million weight 의 무게까지 허용한다.
그래서 트랜잭션의 특정 부분에 대해 weight 를 다르게 줌으로써 어떤 필드는 1byte = 1weight, 어떤 필드는 1byte = 4weight 가 되도록 조정해서 사실상 1MB의 제약을 없앴다.
'CS > 블록체인' 카테고리의 다른 글
[블록체인] 12. 비트코인 script (2) (3) | 2024.10.25 |
---|---|
[블록체인] 12. 비트코인 script (1) (0) | 2024.10.25 |
[블록체인] 10. 비트코인이 해결하는 문제들 (1) | 2024.10.25 |
[블록체인] 9. 암호학 개념들 (0) | 2024.10.24 |
[블록체인] 8. 비트코인 스크립트 (0) | 2024.10.17 |