§ 01

§ 01 · Bytes & numbers

CompactSize

A length that takes one byte, unless it really needs more

A transaction is full of lists: a list of inputs, a list of outputs, scripts of different lengths. A reader needs to know how long each one is before reading it.

So each list or script starts with a small number saying how many items or bytes follow. That number is a CompactSize, sometimes called a varint.

The trick is that it’s compact. Most counts are tiny, so most CompactSizes are one byte. Only a big number pays for more bytes.

Try it

Type a number to see its CompactSize bytes, or type bytes to decode them.

CompactSize

How Pearl writes a count in 1, 3, 5 or 9 bytes

Encoded 3 bytes
fdprefix 0302 number, little-endian

0xfd says “the number is in the next 2 bytes”. Read 03 02 backwards (little-endian): 0x0203 = 515.

First byte Then Numbers Size
00 – fc nothing: the byte is the number 0 – 252 1 B
fd 2 bytes, little-endian 253 – 65,535 3 B
fe 4 bytes, little-endian 65,536 – 4,294,967,295 5 B
ff 8 bytes, little-endian 4,294,967,296 – 2⁶⁴ − 1 9 B
Runs in your browser · nothing is sent src/lib/bytes.ts · same rule as Bitcoin

How does it work?

The first byte decides everything. If it’s below 0xfd (253), it is the number. If it’s one of the three marker bytes, it tells you how many bytes to read next.

First byte Then Range Total size
00 to fc nothing 0 to 252 1 byte
fd 2 bytes, little-endian 253 to 65,535 3 bytes
fe 4 bytes, little-endian 65,536 to 4,294,967,295 5 bytes
ff 8 bytes, little-endian 4,294,967,296 and up 9 bytes
Size
1, 3, 5 or 9 bytes
Type
Unsigned integer
Format
Marker byte, then little-endian
Range
0 to 2⁶⁴ − 1

A few worked examples, all computed with the codex’s own encoder:

Number CompactSize Why
2 02 below 253: one byte
252 fc the largest one-byte value
253 fd fd 00 the first value that needs a marker
1,000 fd e8 03 0x03e8, little-endian
70,000 fe 70 11 01 00 0x011170 needs four bytes

The bytes after the marker are little-endian, smallest byte first, like every other integer in Pearl.

Where are the CompactSizes in a real transaction?

Here is a real Pearl payment from block 130,825: one input, two outputs.

A typical payment: 1 input, 2 outputs 205 bytes
01000000 00 01 01 5939557091bae792be66f26cbd83fc78f16a690a854109e8a1d115e253812133 ab020000 00 ffffffff 02 b0359f1100000000 22 5120d7ab02c96a342bdddb5d07b9f3ebd6c04e077ff41418110b9a1e5997439416b2 00c2eb0b00000000 22 512062e9da3682aab288392cf7218bafc36287288abb3ce200c5d1a50aa234073b2b 01 40 4686c7014749649b25555874c7772ecb7a44e85454893ed5888db11ebcdc82a22d74ce1a1671d02810f5ba1dca3277409e95a653ba9644099f7da26be7a86ba2 00000000
Field Bytes Value
Version version 4 @0 1 int32 little-endian. Pearl wallets and pearld coinbases use version 1; relay policy accepts 1-2.
Marker marker 1 @4 0x00 Segwit marker (BIP-144): says witness data follows the outputs.
Flag flag 1 @5 0x01 Witness flag; 0x01 is the only value Pearl accepts.
Input count vin.length 1 @6 1 CompactSize number of inputs.
Previous txid vin[0].txid 32 @7 33218153e215d1a1e80941850a696af178fc83bd6cf266be92e7ba9170553959 The transaction being spent, stored byte-reversed (internal order); shown here in normal display order.
Output index vin[0].vout 4 @39 683 Which output of that transaction is spent (uint32 LE).
scriptSig length vin[0].scriptSig.length 1 @43 0 Always 0 for non-coinbase inputs in Pearl: every spend uses the witness instead.
Sequence vin[0].sequence 4 @44 0xffffffff Final (0xffffffff): no RBF signal, no relative locktime, and locktime is ignored for this input. Pearl wallets use this.
Output count vout.length 1 @48 2 CompactSize number of outputs.
Amount vout[0].value 8 @49 295,646,640 grains (2.95646640 PRL) int64 little-endian, in grains (1 PRL = 100,000,000 grains).
Script length vout[0].scriptPubKey.length 1 @57 34 CompactSize length of the locking script.
Locking script vout[0].scriptPubKey 34 @58 prl1p674s9jt2xs4amk6aq7ul867kcp8qwll5zsvpzzu6revewsu5z6eqv9d5z4 OP_1 OP_PUSHBYTES_32 <output key Q>: a Taproot output.
Amount vout[1].value 8 @92 200,000,000 grains (2.00000000 PRL) int64 little-endian, in grains (1 PRL = 100,000,000 grains).
Script length vout[1].scriptPubKey.length 1 @100 34 CompactSize length of the locking script.
Locking script vout[1].scriptPubKey 34 @101 prl1pvt5a5d5z42egswfv7uscht7rv2rj3z4m8n3qp3w35592ydq88v4seuhn70 OP_1 OP_PUSHBYTES_32 <output key Q>: a Taproot output.
Witness items witness[0].length 1 @135 1 Number of witness stack items for input 0.
Schnorr signature length witness[0][0].length 1 @136 64 CompactSize length of this witness item.
Schnorr signature witness[0][0] 64 @137 4686c7014749649b25555874c7772ecb7a44e85454893ed5888db11ebcdc82a22d74ce1a1671d02810f5ba1dca3277409e95a653ba9644099f7da26be7a86ba2 64-byte BIP-340 signature (R.x ‖ s) with SIGHASH_DEFAULT: a Taproot key-path spend.
Locktime locktime 4 @201 0 0: no time lock.
txid
d919e02a4adfd739ceef31d98803f0ddd24e1b1fa67b0fe572cdec42c1b384c9
Size
205 bytes · 154 vbytes
Block
130,825
Fee
7,915 grains

It contains seven CompactSizes, and every one of them fits in a single byte:

Offset Byte Means
6 01 1 input
43 00 the input’s scriptSig is empty (always, for Pearl spends)
48 02 2 outputs
57 22 the first output’s script is 34 bytes
100 22 the second output’s script is 34 bytes
135 01 the input’s witness has 1 item
136 40 that item is 64 bytes: a Schnorr signature

The 34-byte scripts are Taproot locks (51 20 followed by a 32-byte key), so 22 appears in almost every Pearl output.

Make a guess

The coinbase of the genesis block carries a 234-byte message in its script. Is its length prefix one byte or three?

Think of your answer first, then check it.

Answer One byte: ea.

234 is below 253, so it fits in the single byte 0xea. Only a script of 253 bytes or more needs the fd marker. The coinbase of block 130,825 is shorter: its script length is 27, 39 bytes.

Why not just use a fixed size?

Because most counts are tiny, and every byte is stored by every full node and paid for in fees.

If every count and length in the payment above were a fixed 4-byte number, its seven CompactSizes would take 28 bytes instead of 7, and the 205-byte transaction would grow to 226 bytes, about 10% bigger, to carry no extra information. CompactSize spends the extra bytes only on the rare big number.

Where else does Pearl use them?

Anywhere something has a variable length:

  • The transaction count in a block. After the certificate and the 108-byte header, block 130,825 has the byte 73: 115 transactions.
  • Witness data. Each input’s witness starts with an item count, and each item with its length (BIP-144, same as Bitcoin).
  • Taproot and P2MR script trees. A leaf hash covers the leaf version, then a CompactSize of the script length, then the script. A 34-byte leaf script gets the prefix 22.

What does “non-canonical” mean?

Nothing stops you writing 5 as fd 05 00. It decodes to 5. But it’s wasteful, and worse, it means the same data could be written two ways, giving two different transaction IDs.

So Pearl’s decoder, like Bitcoin’s, rejects any CompactSize that could have been shorter. Each number has exactly one valid encoding:

  • after fd, the value must be at least 253;
  • after fe, at least 65,536;
  • after ff, at least 4,294,967,296.
Code: Pearl's encoder, and a JavaScript one

pearld writes a CompactSize in WriteVarIntBuf (node/wire/common.go:571-602 at v1.5.2, on GitHub), inherited unchanged from btcd:

node/wire/common.go Go
switch {
case val < 0xfd:
	buf[0] = uint8(val)
	_, err := w.Write(buf[:1])
	return err

case val <= math.MaxUint16:
	buf[0] = 0xfd
	littleEndian.PutUint16(buf[1:3], uint16(val))
	_, err := w.Write(buf[:3])
	return err

case val <= math.MaxUint32:
	buf[0] = 0xfe
	littleEndian.PutUint32(buf[1:5], uint32(val))
	_, err := w.Write(buf[:5])
	return err
// default: 0xff, then 8 bytes
}

The reader, ReadVarIntBuf (node/wire/common.go:500-552 at v1.5.2, on GitHub), returns an error whenever the value is smaller than the minimum for its marker.

JavaScript
function compactSize(n) {
  if (n < 0xfd) return [n];
  if (n <= 0xffff) return [0xfd, n & 0xff, n >> 8];
  if (n <= 0xffffffff) return [0xfe, n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, n >>> 24];
  throw new Error('use BigInt for 9-byte values');
}
compactSize(1000); // [253, 232, 3] = fd e8 03

The codex

Suggested

Up and down arrows move through the results, Enter opens one, Escape closes. Anywhere on the site, slash or Command K opens this search, and the square brackets [ and ] go to the previous and next page.