Compact Size

O campo de tamanho variável usado nas mensagens da rede

Diagrama mostrando como o campo compact size indica o tamanho ou a contagem de itens a seguir.

Um campo compact size é usado em mensagens da rede para indicar o tamanho de um campo a seguir ou o número de campos a seguir.

Ele pode armazenar números entre 0 e 18446744073709551615.

O tamanho do campo aumenta conforme o número que ele contém aumenta. Ou seja, números menores ocupam menos espaço. Isso significa que você não precisa usar um campo de tamanho fixo maior o tempo todo para acomodar o maior número aceitável.

Compact Size

Converta entre um número e sua codificação compact size (o varint usado no Bitcoin).

0d
0 bytes
Prefixo

O primeiro byte indica quais bytes codificam o inteiro:

Nota: os bytes que codificam o inteiro estão em little endian.

Estrutura

Um campo compact size é uma estrutura de bytes de tamanho variável. O byte inicial indica o tamanho do campo e também indica os bytes que contêm o número.

Diagrama mostrando os prefixos usados para campos compact size e os tamanhos de campo correspondentes.
Prefixos do campo compact size e seus tamanhos de campo correspondentes
Byte InicialNúmeroIntervaloTamanho do CampoExemplo
FC (e abaixo)O byte atual0 - 2521 byte64 (100)
FDPróximos 2 bytes253 - 655353 bytesFDE803 (1.000)
FEPróximos 4 bytes65536 - 42949672955 bytesFEA0860100 (100.000)
FFPróximos 8 bytes4294967296 - 184467440737095516159 bytesFF00E40B5402000000 (10.000.000.000)

Nota: Os bytes que contêm o número estão em little-endian.

Então, para números pequenos de 252 ou menos, você usa um único byte. Mas para números maiores você usa um prefixo de FD, FE ou FF, e o inteiro fica contido nos próximos 2, 4 ou 8 bytes.

O valor máximo que um campo compact size pode conter é 18446744073709551615, que é FFFFFFFFFFFFFFFFFF (um prefixo FF seguido de FFFFFFFFFFFFFFFFFF).

Na maioria das vezes você verá campos compact size contendo números de 252 ou menos. Então, à primeira vista, você pode supor que está olhando um campo simples de 1 byte e não perceber que está olhando um tipo especial de campo que pode variar de tamanho.

Um campo compact size começando com FF (para números de 8 bytes) é totalmente exagerado e nunca é usado no Bitcoin. Ele indicaria mais de 4 GB de dados a seguir, muito mais do que jamais caberia dentro de um bloco real.

Você pode, na verdade, usar o prefixo FF e então usar os próximos 8 bytes 0000000000000001 para indicar um valor de 1. Seria um desperdício de espaço, mas ainda seria tão válido quanto usar o único byte 01.

Exemplos

Aqui estão alguns exemplos dos diferentes prefixos compact size encontrados em dados de transação:

FC (e abaixo)

Um único byte de FC ou abaixo é de longe o mais comum:

Este é só um exemplo rápido. Você pode procurar qualquer transação na blockchain e encontrará campos compact size simples de 1 byte. Eles estão em todo lugar.

FD

Você vai esbarrar no prefixo FD de vez em quando. Isso acontece quando há uma contagem de entradas/saídas acima da média, ou se um scriptsig/scriptpubkey for excepcionalmente grande:

FE e FF

Você vai esbarrar muito raramente nos prefixos FE ou FF (para números maiores que 65.535). Isso porque o tamanho máximo de scriptpubkey/scriptsig é 10.000 bytes (veja script.h). Além disso, devido ao limite de tamanho do bloco de 4.000.000 unidades de peso, seria impossível ter mais de 65.535 entradas em uma única transação, e seria incrivelmente difícil ter mais de 65.535 saídas.

A única vez que você encontra um prefixo FE ou FF na prática é quando ele foi usado incorretamente para armazenar um número que poderia ter sido colocado em um campo compact size menor.

Onde aparece

Os campos compact size são usados em vários lugares nos dados do Bitcoin. Aqui estão os mais comuns:

Dados de Transação

Campos compact size são usados em todo o conteúdo das transações brutas. Eles são usados para indicar:

Aqui está um exemplo de transação legada (414719d592b73341b77497165d9f46f6eff6c243469265f95d920b779c7a0492). Dividi os dados brutos da transação em campos individuais e destaquei os campos compact size em verde:

01000000
  01 <- contagem de entradas
    79fe743502ff8cd181121572fececac3feee5ef3034edfb3ccd2bfaa24537dae00000000
    6b <- tamanho do scriptsig
      483045022100d39e64d275f0e69d5a2722ad93e3e206e98bf03584525cec05b5fcb75dc3e5a8022071fc39e3784be3a76d8469ed13ade270d8da25677fc5a226c5e7223a85701c7c012102b0453d54d1e0c0b41a63b3ca898afc4cc4243ed0241a9cc116e37854969a2270
    ffffffff
  01 <- contagem de saídas
    72c9000000000000
    19 <- tamanho do scriptpubkey
      76a91400bafac9185e183c1203025fbdac30a4be5af91088ac
00000000

Aqui está um exemplo de transação segwit (672d9428242a097e57c5def8b300d05068e0d85a1028ac3e93c9a487561f36c9), novamente com os campos compact size destacados em verde:

01000000
    0001
    01 <- contagem de entradas
        53baeaeed4799240f2a48e99fcc6e504672120764d622e4e5af9fd04b37a8293
        05000000
        00 <- tamanho do scriptsig
        ffffffff
    01 <- contagem de saídas
      4ac7010000000000
      17 <- tamanho do scriptpubkey
        a914f314b4ac619e1d3f96a5ffac796b17e0a47b52b987
    02 <- contagem de elementos da testemunha
      47 <- tamanho do elemento da testemunha
        3044022064576f10eee1b679648965b72081a636ac46b21be3e36558585775fc523dbcdf0220440b31af77adcbc75cf79679406d8ba1e2c14ff03d02606725d29ffdaa028a5f01
      21 <- tamanho do elemento da testemunha
        021ce981c19e4f998b62091ffd960549ead5f8ced3de7fc919d5d4a25e6edf42cd
00000000

Dados de Bloco

Um campo compact size é usado uma vez dentro de um bloco bruto. Ele indica:

Por exemplo, este é o bloco gênesis (000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f), e ele tem apenas uma transação:

01000000
0000000000000000000000000000000000000000000000000000000000000000
3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a
29ab5f49
ffff001d
1dac2b7c
01 <- contagem de transações
01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000

Ele fica entre o cabeçalho do bloco e as transações. Esta é a única vez que o campo compact size aparece dentro de um bloco bruto (sem contar o interior das transações).

Mensagens da Rede

O campo compact size é usado nas várias mensagens que os nós enviam uns aos outros na rede bitcoin.

Por exemplo, o payload de uma mensagem "inv" usa um campo compact size para indicar o número de itens a seguir:

01 01000000aa325e9122aa39ca18c75aabe2a3ceaf9802acd1a40720925bfd77fff58ed821

Esta mensagem indica que há um item na lista, e é este TXID: 21d88ef5ff77fd5b922007a4d1ac0298afcea3e2ab5ac718ca39aa22915e32aa (ordem de bytes inversa).

Transações e blocos também são mensagens que trafegam pela rede bitcoin. Então o campo compact size ajuda a economizar espaço nas mensagens serializadas que são enviadas entre os nós. Você sempre quer enviar a menor quantidade de dados possível pela rede (por eficiência), e é por isso que o compact size é útil.

Benefícios

Por que campos compact size são usados no Bitcoin?

O campo compact size economiza alguns bytes extras de espaço.

Por exemplo, você consegue colocar confortavelmente alguns milhares de saídas em uma transação, mas na maioria das vezes você só cria uma ou duas. Para o campo contagem de saídas, uma solução básica seria torná-lo um campo fixo de 2 bytes o tempo todo para permitir um número grande de saídas em raras ocasiões, mesmo que na vasta maioria das vezes isso não seja necessário. Então, por exemplo, em 10 transações você teria os seguintes campos:

Campo Fixo de 2 Bytes:

Número  | Bytes
--------|------
 2      | 0002
 2      | 0002
 1      | 0001
 2      | 0002
 27     | 001B
 3      | 0003
 3000   | 0BB8
 2      | 0002
 1      | 0001
 2      | 0002

 TOTAL = 40 bytes (em 10 transações)

Mas, usando um campo compact size flexível, podemos usar 1 byte na maioria das vezes e expandir até 3 bytes (1 byte de prefixo + 2 bytes de número) para acomodar números maiores nas raras ocasiões em que precisamos. Nas mesmas 10 transações, por exemplo:

Campo Compact Size:

Número  | Bytes
--------|------
 2      | 02
 2      | 02
 1      | 01
 2      | 02
 27     | 1B
 3      | 03
 3000   | FD0BB8
 2      | 02
 1      | 01
 2      | 02

 TOTAL = 24 bytes (em 10 transações)

É uma técnica de economia de espaço pequena. Mas, quando você tem vários desses campos em uma transação, e centenas de milhares de transações viajando entre computadores todos os dias (e bilhões de transações armazenadas na blockchain), os bytes se somam.

Código

Aqui está um exemplo rápido de código mostrando como converter entre um inteiro e compact size em Ruby:

# pack()       - converte um inteiro para bytes brutos de um comprimento e ordem de byte específicos (ex.: little-endian) de acordo com a diretiva dada
# unpack("H*") - converte bytes brutos em uma string hexadecimal

# Diretivas:
#
# C  =  inteiro de 8 bits
# S< = inteiro de 16 bits, little-endian
# L< = inteiro de 32 bits, little-endian
# Q< = inteiro de 64 bits, little-endian

def encode(i)
    # converte inteiro para string hex com o prefixo correto dependendo do tamanho do inteiro
    if (                     i <= 252)                  then compactsize =        [i].pack("C").unpack("H*")[0]
    elsif (i > 252        && i <= 65535)                then compactsize = 'fd' + [i].pack("S<").unpack("H*")[0]
    elsif (i > 65535      && i <= 4294967295)           then compactsize = 'fe' + [i].pack("L<").unpack("H*")[0]
    elsif (i > 4294967295 && i <= 18446744073709551615) then compactsize = 'ff' + [i].pack("Q<").unpack("H*")[0]
    end

    return compactsize
end

def decode(compactsize)
    # pega o primeiro byte
    first = compactsize[0...2]

    # pega a quantidade certa de bytes da string hex, então converte esta string hex para inteiro
    if    (first == "fd") then i = [compactsize[2...6]].pack("H*").unpack("S<")[0]
    elsif (first == "fe") then i = [compactsize[2...10]].pack("H*").unpack("L<")[0]
    elsif (first == "ff") then i = [compactsize[2...18]].pack("H*").unpack("Q<")[0]
    else                       i = [compactsize[0...2]].pack("H*").unpack("C")[0]
    end

    return i
end


# Exemplos de Codificação
puts encode(0)                    #=> 00
puts encode(252)                  #=> fc

puts encode(253)                  #=> fdfd00
puts encode(65535)                #=> fdffff

puts encode(65536)                #=> fe00000100
puts encode(4294967295)           #=> feffffffff

puts encode(4294967296)           #=> ff0000000001000000
puts encode(18446744073709551615) #=> ffffffffffffffffff

# Exemplos de Decodificação
puts decode("00")                 #=> 0
puts decode("fc")                 #=> 252

puts decode("fdfd00")             #=> 253
puts decode("fdffff")             #=> 65535

puts decode("fe00000100")         #=> 65536
puts decode("feffffffff")         #=> 4294967295

puts decode("ff0000000001000000") #=> 4294967296
puts decode("ffffffffffffffffff") #=> 18446744073709551615

Resumo

Um campo compact size é usado para indicar um número de itens a seguir ou o tamanho de alguns dados a seguir, em mensagens da rede (ex.: transações brutas). Normalmente tem 1 byte, mas pode se expandir até 9 bytes quando necessário para codificar números maiores.

Ele faz parte do protocolo desde o primeiro lançamento do Bitcoin (v0.1.0) e pode ser encontrado em serialize.h. Acredito que essa codificação compact size é algo que o Satoshi criou ao programar o Bitcoin, pois não a vi sendo usada em nenhum outro lugar.

Recursos