용어집 / 개발자 참고 자료
CompactSize
별칭은 Compact size unsigned integer, VarInt, 컴팩트사이즈입니다.
- CompactSize란 무엇입니까?
- CompactSize는 비트코인이 직렬화된 데이터에서 개수와 길이를 앞에 붙일 때 사용하는 가변 길이 정수로, 253 미만의 값에는 1바이트가 들고 나머지 값에는 최대 9바이트가 듭니다.
비트코인의 이진 형식에는 필드 이름이 없으므로, 파서는 뒤에 입력이 몇 개 이어지는지, 각 스크립트가 얼마나 긴지를 미리 알려 주어야 합니다. CompactSize는 최소 1바이트로 답합니다. 0부터 252까지는 그 1바이트만으로 표현되고, 더 큰 값은 표식 1바이트에 2바이트, 4바이트 또는 8바이트가 더해집니다. 보내는 모든 결제에는 이 구조가 적어도 4개 들어 있습니다.
작동 방식
CompactSize는 자기 첫 바이트를 읽어 남은 부분이 얼마인지 결정합니다.
첫 바이트가 0xfd 미만이면 그 값 자체가 값이므로, 0부터 252까지의 개수는 1바이트만 차지하고 다른 바이트는 필요하지 않습니다. 첫 바이트가 0xfd이면 값은 다음 2바이트에, 0xfe이면 다음 4바이트에, 0xff이면 다음 8바이트에 들어 있습니다. 이렇게 뒤따르는 숫자는 모두 리틀 엔디언입니다. 가능한 가장 긴 인코딩은 9바이트이며 18,000,000조를 넘는 값도 표현할 수 있습니다. 실제 어떤 구조도 그 정도까지 필요로 하지 않습니다.
인코딩은 최소여야 합니다. 숫자 5를 0xfd 0x05 0x00으로 쓰는 것은 0x05로 쓰는 것과 산술적으로 동일하며, Bitcoin Core는 역직렬화할 때 이를 거부하고 비정규(non-canonical) 오류를 발생시킵니다. 그 규칙이 없다면 같은 트랜잭션을 여러 방식으로 쓸 수 있고 각각 다른 식별자를 갖게 되는데, 이는 정돈의 문제가 아니라 가변성(malleability) 문제입니다.
산술적 한계보다 훨씬 낮은 실용적 상한도 있습니다. Bitcoin Core는 0x02000000바이트, 약 33.5메가바이트보다 크다고 주장하는 역직렬화 객체에는 메모리를 할당하지 않으며, 피어 프로토콜은 별도로 메시지 하나를 4,000,000바이트로 제한합니다. 따라서 악의적인 피어가 입력을 백경 개라고 알리는 트랜잭션을 보내도 사용자의 노드가 기가바이트 단위의 메모리를 예약하게 만들 수는 없습니다.
이름에 관한 함정이 하나 있어 짚어 둘 만합니다. 전송 문서는 이를 var_int라고 부르고, 지갑 라이브러리는 보통 VarInt라고 부르며, Bitcoin Core 자체 소스 코드는 VarInt라는 이름을 완전히 다르고 무관한 인코딩에 사용하는데 그것은 자체 데이터베이스와 되돌리기(undo) 파일에만 나타납니다. 코드를 읽을 때 둘 중 어느 것을 보고 있는지 확인해야 합니다.
나타나는 곳
CompactSize는 비트코인에서 사용자에게 보이는 이름이 전혀 없는 구조 가운데 가장 자주 마주치는 구조입니다.
원시 트랜잭션에서는 4바이트 버전 바로 뒤, 또는 세그윗 표식과 플래그가 있을 때는 그 뒤에 나타나 입력 개수를 담습니다. 각 스크립트 앞에 다시 나타나 길이를 알려 주고, 출력 개수 앞에도 다시 나타납니다. 직렬화된 블록에서는 트랜잭션 개수를 담습니다. 피어 프로토콜에서는 inv, getdata, addr, headers 메시지의 페이로드를 엽니다.
headers 메시지는 이 인코딩이 가장 이상하게 쓰이는 모습을 보여 줍니다. 80바이트 블록 헤더마다 트랜잭션 개수를 나타내는 CompactSize 0이 뒤따르므로, 항목 하나가 80바이트가 아니라 81바이트를 차지하고, 2,000개 헤더의 한 묶음 전체는 전송 시 162,000바이트가 됩니다. headers 메시지는 트랜잭션을 절대 담지 않으므로 이 개수는 항상 0입니다. 이 필드가 존재하는 이유는 뒤에 블록 본문이 오든 오지 않든 같은 파싱 코드가 헤더를 읽을 수 있게 하기 위해서입니다.
원시 트랜잭션을 직접 디코딩하다가 중간부터 필드가 말이 되지 않는다면, 거의 항상 원인은 CompactSize를 잘못 읽은 것입니다. 1바이트를 덜 소비하면 그 뒤의 모든 것이 밀립니다.