§ 01 · Bytes & numbers
CompactSize
A length that takes one byte, unless it really needs more
On this page 6 sections
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 CompactSizeCompactSize: A space-saving way to write a count or a length: numbers below 253 take one byte, bigger ones take 3, 5 or 9 bytes. Same as Bitcoin., 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.
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.
| 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. |
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:
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.
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