Foundations & Encodings
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Convert small integers between decimal, hexadecimal, and little-endian bytes.
What you will practice
- Distinguish canonical CompactSize from base-128 encodings and reject redundant forms.
- Preserve leading zero bytes through Base58Check and explain the checksum boundary.
In this lesson
Lesson
5 min readBytes Before Blocks
A blockchain identifier commits to bytes. Before building consensus machinery, learn to distinguish a number, its encoding, the raw hash of those bytes, and text used to display that hash. You will implement two small codecs, then use these exact decisions when transactions and blocks acquire identities.
Readiness check
Write 258 in hexadecimal, then as a two-byte little-endian value. Explain why
those bytes are not the ASCII characters in the string "0102".
Check your representation
258 is 0x0102; its little-endian bytes are 02 01. The text "0102" consists
of four ASCII bytes 30 31 30 32. Hashing text instead of decoded bytes changes
the input and therefore normally the digest.
One value, one canonical count
CompactSize stores counts and script lengths. It is not the base-128 varint used in other protocols. A leading byte chooses the representation:
| Value | Representation | Total bytes |
|---|---|---|
| 0 through 252 | The value itself | 1 |
| 253 through 65535 | fd then uint16 little-endian |
3 |
| 65536 through 4294967295 | fe then uint32 little-endian |
5 |
| Larger uint64 values | ff then uint64 little-endian |
9 |
Trace 300: it exceeds 252, fits uint16, and equals 0x012c. Emit fd 2c 01.
A decoder sees fd, requires two more bytes, reconstructs 0x012c, checks that
300 actually belongs in this branch, and reports (300, 3, true).
The branch check matters. fd fc 00 contains enough bytes and reconstructs 252,
but 252 must be encoded as fc. Reject the redundant representation. Otherwise
two encodings of a count could produce different transaction bytes and IDs.
fd 2c is a different failure: incomplete input. Both failures return
(0, 0, false) in this exercise.
Complete the decoder trace
For fe 00 00 01 00 aa, the prefix selects a four-byte payload. Fill in the
value, bytes consumed, and whether aa is an error. Then repeat with
fe ff ff 00 00.
Check the remaining decisions
The first value is 65536, the decoder consumes five bytes, and aa remains
for the caller. A single-field decoder must not consume the whole stream.
The second value is 65535: complete but noncanonical, because the fd branch
would suffice. The full transaction parser later imposes a separate rule that
no bytes may remain after its final field.
Implement CompactSize. Test each boundary on both sides, every truncated prefix form, redundant encodings, and a valid count followed by another field. Encoding and decoding your own result is useful, but two implementations can share the same mistake; keep explicit byte vectors.
A checksum around an exact byte string
Base58Check wraps a one-byte version and an arbitrary payload. Construct
body = version || payload, double-SHA256 the raw body, and append the first
four digest bytes. Encode the complete result as Base58. This task accepts
arbitrary payload lengths; it does not enforce a particular address type.
For version 00 and payload 00 01, the body is 00 00 01, the checksum is
ab fb 08 44, and the resulting string is 11BwW2qR. This is an exercise
vector, not a recommendation to use it as a real payment address.
The alphabet begins 123456789... and omits easily confused characters such
as 0, O, I, and l. Convert the byte string to an unsigned big-endian
integer and repeatedly divide by 58. Remainders give digits from right to left.
Count leading zero bytes separately; integer conversion discards them.
A smaller Base58-only example makes that last step visible. Bytes 00 00 3a
represent integer 58. Division gives remainders 0 then 1; reversing their alphabet
digits gives 21. Preserve the two leading zero bytes as 11, giving 1121.
This example omits a checksum deliberately; it is not itself a Base58Check value.
Diagnose before you retry
Decoding reverses the digit accumulation, restores leading zeros, then requires at least five bytes for version plus checksum. Split the last four bytes, recompute the checksum over the remaining body, compare, and only then return the version and payload. Reject invalid characters instead of skipping them.
If leading zeros disappear, inspect the integer-to-byte boundary. If round-trip encoding works but an external vector fails, check the alphabet, digit reversal, and whether you hashed raw bytes or hexadecimal text. If a damaged string is accepted, inspect the comparison with the supplied checksum, not just whether you computed a new one.
The checksum detects many accidental changes. It cannot authenticate a sender: an attacker can replace the payload and calculate another checksum. Encryption, signature verification, and value ownership are separate mechanisms.
Independent reconstruction
Before implementing Base58Check, explain why adding a leading zero to a payload must survive a decode/encode cycle. Then write tests for empty payload, multiple leading zeros, invalid alphabet symbols, and a changed checksum. Take the checkpoint without the tables open; return to the specific representation you mixed up.
For comparison with the original protocol, consult the Bitcoin transaction reference and Bitcoin Core Base58 implementation. The worked vectors above and this exercise's input contracts are the basis of our tests; this module does not cover all current Bitcoin address formats.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.