Jason Dev
About
Bitcoin Transaction 이해하기 (2)
published: 2023-10-15
bitcoin
transaction
utxo

이번 글에서도 특정 트랜잭션에 대해 blockstream.info에서 보여주는 내용을 기준으로 알아보자.

아래는 해당 트랜잭션에서의 비트코인 입출력 정보이다.

4곳에서 비트코인을 모아서 2곳으로 보내고 있다.

트랜잭션의 입출력 정보

비트코인을 하나의 주소에서 다른 하나의 주소로 보낼 수도 있지만 위 그림처럼 입력과 출력이 여러 개 일 수도 있다. 그렇다고 위 그림을 보고 4개의 주소로부터 비트코인을 모았다고 단정지을 수는 없다.

위 그림의 첫 번째 입력 소스는 아래와 같다.

afe25ae18e3bb5aacfbdbd073d984b20f03c0c6e6fb1b17f30b5b167313c7f9e:1

얼핏 보면 비트코인 주소로 착각할 수 있겠지만 입력 소스는 아래의 구조를 갖고 있다.

{트랜잭션 hash}:{출력 index}

즉, 특정 트랜잭션의 출력을 이 트랜잭션의 입력으로 사용하고 있다. 비트코인은 이전 트랜잭션의 출력을 새로운 트랜잭션의 입력으로 사용한다. 당연한 이야기지만 입력으로 사용된 출력은 나중에 다시 사용될 수 없다.

비트코인에서는 아직 입력으로 사용되지 않은 출력을 UTXO(Unspent Transaction Output)라고 부른다. 특정 지갑의 비트코인 잔액을 하나의 숫자로 관리하는 것이 아니라 UTXO 목록으로 관리한다. UTXO 방식으로 관리하기 때문에 각 코인이 채굴된 순간부터 현재까지 어떤 경로를 통해 이동했는지 추적할 수 있다.

잠금/해제 스크립트

아래는 이 트랜잭션의 첫 번째 출력 내용이다. 나중에 이 출력을 입력으로 사용하기 위해서는 SCRIPTPUBKEY (ASM)의 내용을 해제하는 스크립트가 필요하다. 스크립트에 대한 자세한 내용은 다음 글에서 다룰 예정이다.

보통은 공개키로 잠금 스크립트(scriptPubKey)를 만들고 개인키를 이용해서 해제 스크립트를 만든다. 따라서 잠금 스크립트를 만든 지갑의 소유자가 해제 스크립트도 만들 수 있다.

이 출력은 나중에 입력으로 사용됐는데 아래는 입력으로 사용된 내용이다.

SCRIPTSIG (ASM) 부분이 해제 스크립트이다.

Raw 트랜잭션 파싱

Raw 트랜잭션은 비트코인의 getrawtransaction API를 이용해서 얻을 수 있다. 앞에서 다루던 트랜잭션은 입출력이 많아서 복잡하므로 입출력이 모두 1개인 트랜잭션의 Raw 데이터를 파싱해보자.

getrawtransaction API에 해시값 177cf2cd4b388ed2fb92e40640ba8f4a638cadce6739bd71c2f394fab01cf519를 입력하면 다음 출력값을 얻을 수 있다. getrawtransaction API를 직접 사용해보고 싶다면 온라인 사이트를 이용해보자.

020000000001012cb48eb4805d85b8e87cb7acc53a2565d213fceb256bdb5ed28f07bfc0b386b40000000000ffffffff01f1f6e845000000001600145082a50325fdfe2545a6c70f2d872b15951d7eda02483045022100fbea269bc485078c486752c6158ad707c1dd2a5bd06b6cbbaa57bb783fe9bb8d02206ed562648f5a222ac2d96d74955f5e7f51e3d72435c5d6a49292d183436bc79001210276ad91b3a31a81f5021ba46e79af22b7034816538df3d0cc392d94ae4c5713a900000000

각 필드의 바이트는 little-endian 방식(작은 단위의 바이트가 앞에 오는 방식)으로 읽어야 한다.

Version

처음 4바이트는 트랜잭션의 버전을 나타낸다. 이 경우 02000000이며 little-endian으로는 00000002이므로 이 트랜잭션의 버전은 2이다.

Marker and Flag

다음 바이트가 00으로 시작한다면 Marker를 의미한다. Marker 다음 바이트는 Flag를 의미하며 01이면 SegWit 트랜잭션을 의미한다. 따라서 이 트랜잭션은 SegWit 트랜잭션이라는 것을 알 수 있다.

이후 Non-SegWit 트랜잭션은 파싱 방법이 다르며 여기서는 SegWit 트랜잭션만 설명한다.

Input Count

비트코인에서는 숫자 데이터의 용량 최적화를 위해 VarInt(Variable Integer)라는 숫자 타입을 사용한다. 필요한 만큼의 바이트만 사용하는데, 최소 1바이트부터 최대 8바이트까지 사용하게 된다. 자세한 내용은 링크에서 확인할 수 있다.

첫 바이트가 252 이하이면 그 숫자 그대로 사용하고, 252 초과이면 여러 바이트를 사용한다. 이 경우 01이므로 1개의 입력이 있음을 나타낸다.

Input

입력 파싱은 다음 단계로 진행된다.

  1. 첫 32바이트를 가져와 previous transaction hash로 해석
  2. 그 다음 4바이트를 previous output index로 해석
  3. 그 후 VarInt를 읽어 script length를 해석
  4. 지정된 script length만큼의 바이트를 읽어 scriptSig로 해석
  5. 마지막으로 4바이트를 읽어 nSequence로 해석
  6. 이 과정을 Input Count 수 만큼 반복

입력 파싱 후 지금까지 처리된 결과는 다음과 같다.

const 직전_남은_데이터 =
"012cb48eb4805d85b8e87cb7acc53a2565d213fceb256bdb5ed28f07bfc0b386b40000000000ffffffff01f1f6e845000000001600145082a50325fdfe2545a6c70f2d872b15951d7eda02483045022100fbea269bc485078c486752c6158ad707c1dd2a5bd06b6cbbaa57bb783fe9bb8d02206ed562648f5a222ac2d96d74955f5e7f51e3d72435c5d6a49292d183436bc79001210276ad91b3a31a81f5021ba46e79af22b7034816538df3d0cc392d94ae4c5713a900000000";

const Input_Count = "01";
const Previous_Transaction_Hash =
"2cb48eb4805d85b8e87cb7acc53a2565d213fceb256bdb5ed28f07bfc0b386b4";
const Previous_Output_Index = "00000000";
const Script_Length = "00";
const nSequence = "ffffffff";

const 남은_데이터 =
"01f1f6e845000000001600145082a50325fdfe2545a6c70f2d872b15951d7eda02483045022100fbea269bc485078c486752c6158ad707c1dd2a5bd06b6cbbaa57bb783fe9bb8d02206ed562648f5a222ac2d96d74955f5e7f51e3d72435c5d6a49292d183436bc79001210276ad91b3a31a81f5021ba46e79af22b7034816538df3d0cc392d94ae4c5713a900000000";

nSequence는 초기에 트랜잭션 수정을 위해 추가되었으나 현재는 다양한 용도로 사용되고 있다. 예를 들어, 여기서처럼 nSequence가 ffffffff이면 RBF(Replace by Fee) 기능을 사용할 수 없다. 참고로 RBF는 트랜잭션 수수료를 너무 낮게 설정한 경우 수수료를 더 높여서 트랜잭션이 빨리 처리될 수 있도록 하는 기능이다.

nSequence 값이 ffffffff가 아니면 상대적인 잠금 시간을 의미한다. 만약 nSequence가 3이고 해당 입력에서 사용하는 출력이 100번 블록에 있다면, 이 입력은 103번 이후 블록에만 포함될 수 있다. 참고로 22 bit가 1이면 블록이 아니라 타임스탬프를 의미한다.

Output Count

Input Count와 같은 방식으로 파싱하며 이 경우 01이므로 1개의 출력이 있음을 나타낸다.

Output

출력 파싱은 다음 단계로 진행된다.

  1. 첫 8바이트를 가져와 Value(단위는 사토시)로 해석
  2. VarInt 형식으로 읽어서 Script Length로 해석
  3. 그 후 Script Length 만큼 읽어서 잠금 스크립트(scriptPubKey)로 해석
  4. 이 과정을 Output Count 수 만큼 반복

출력 파싱 결과는 다음과 같다.

const 직전_남은_데이터 =
"01f1f6e845000000001600145082a50325fdfe2545a6c70f2d872b15951d7eda02483045022100fbea269bc485078c486752c6158ad707c1dd2a5bd06b6cbbaa57bb783fe9bb8d02206ed562648f5a222ac2d96d74955f5e7f51e3d72435c5d6a49292d183436bc79001210276ad91b3a31a81f5021ba46e79af22b7034816538df3d0cc392d94ae4c5713a900000000";

const Output_Count = "01";
const Output_Value = "f1f6e84500000000";
const Script_Length = "16";
const scriptPubKey = "00145082a50325fdfe2545a6c70f2d872b15951d7eda";

const 남은_데이터 =
"02483045022100fbea269bc485078c486752c6158ad707c1dd2a5bd06b6cbbaa57bb783fe9bb8d02206ed562648f5a222ac2d96d74955f5e7f51e3d72435c5d6a49292d183436bc79001210276ad91b3a31a81f5021ba46e79af22b7034816538df3d0cc392d94ae4c5713a900000000";

Witness

스크립트 유형에 따라 Witness 파싱 방법이 다르다. 비트코인 스크립트는 다음과 같이 다양한 유형이 있다.

P2PK, P2PKH, P2SH, P2WPKH, P2WSH, P2TR

스크립트 유형은 트랜잭션에 직접적으로 표현되지는 않는다. 대신 잠금 스크립트(scriptPubKey)의 구조를 통해 그 유형을 간접적으로 확인할 수 있다.

이 글에서 분석하고 있는 트랜잭션의 입력과 출력은 모두 P2WPKH 유형을 따르고 있다. P2WPKH 유형의 파싱 순서는 다음과 같다.

첫 번째 정보는 VarInt 형식이며 항목 개수로 해석한다. 이 경우 02이므로 2개의 항목이 있다. 각 항목은 다음과 같이 파싱한다.

  1. 첫 번째 정보는 VarInt 형식이며 해당 항목 데이터의 바이트 크기로 해석한다.
  2. 다음은 그 크기 만큼의 항목 데이터이다.

Witness 파싱 결과는 다음과 같다.

const 직전_남은_데이터 =
"02483045022100fbea269bc485078c486752c6158ad707c1dd2a5bd06b6cbbaa57bb783fe9bb8d02206ed562648f5a222ac2d96d74955f5e7f51e3d72435c5d6a49292d183436bc79001210276ad91b3a31a81f5021ba46e79af22b7034816538df3d0cc392d94ae4c5713a900000000";

const 항목_개수 = "02";
const 항목_1_크기 = "48";
const 항목_1_데이터 =
"3045022100fbea269bc485078c486752c6158ad707c1dd2a5bd06b6cbbaa57bb783fe9bb8d02206ed562648f5a222ac2d96d74955f5e7f51e3d72435c5d6a49292d183436bc79001";
const 항목_2_크기 = "21";
const 항목_2_데이터 =
"0276ad91b3a31a81f5021ba46e79af22b7034816538df3d0cc392d94ae4c5713a9";

const 남은_데이터 = "00000000";

항목_1_데이터는 서명(signature)을 항목_2_데이터는 공개키(pubkey) 정보를 담고있다.

Locktime

다음 4바이트는 Locktime을 의미하며 여기서는 0이므로 특별한 의미를 갖지 않는다.

트랜잭션 수수료

수수료는 Raw 트랜잭션에 직접적으로 나타나지 않는다. 모든 입력에서 모든 출력을 뺀 나머지가 수수료가 된다.

이 경우 입력에 사용된 비트코인은 11.73017393 이다. 이 정보는 현재 트랜잭션에는 없고 입력에서 사용된 이전 트랜잭션의 출력을 통해 알 수 있다. 따라서 이 트랜잭션을 블록에 포함시키기 위해 지불한 비트코인은 다음과 같다.

11.73017393 - 11.72895473 = 0.0012192

댓글 삭제 시 메일 주소가 필요합니다