Skip to content

fix(Base58): preserve leading zeros for zero-valued and empty inputs#276

Open
spokodev wants to merge 1 commit into
wevm:mainfrom
spokodev:fix/base58-leading-zeros
Open

fix(Base58): preserve leading zeros for zero-valued and empty inputs#276
spokodev wants to merge 1 commit into
wevm:mainfrom
spokodev:fix/base58-leading-zeros

Conversation

@spokodev

Copy link
Copy Markdown

Problem

Base58 drops a leading 1 for all-zero inputs and throws on empty input, so round-trips are broken for zero-valued payloads:

Base58.fromHex('0x00')    // ''      (should be '1')
Base58.fromHex('0x0000')  // '1'     (should be '11')
Base58.fromHex('0x')      // throws  (should be '')
Base58.toHex('1')         // '0x000' (should be '0x00')
Base58.toHex('')          // '0x0'   (should be '0x')

Inputs with a non-zero tail were already correct (e.g. 0x0000287fb4cd1111233QC4), which is why the existing leading-zero vectors passed and this went unnoticed. It only surfaces when the value portion is entirely zero.

Impact: any all-zero or empty Base58 payload round-trips to the wrong byte length and is rejected/misread by other Base58 tools (bs58, ethers). Empty input throws an uncaught Cannot convert 0x to a BigInt instead of returning ''.

Cause

src/core/internal/base58.ts (encode): the leading-zero loop runs while bytes.length > 1, so the final zero byte is never counted (n zero bytes produce n−1 1s), and BigInt('0x') throws on empty input.

src/core/Base58.ts (decode): integer.toString(16) appends a stray 0 nibble when the decoded value is 0n.

Fix

  • Count every leading zero byte (> 0 instead of > 1).
  • Treat empty hex as 0n instead of throwing.
  • Emit no body nibble when the decoded integer is 0 (the zero bytes come entirely from the leading-1 count).

Non-zero inputs are untouched: both loops already stop at the first non-zero byte, and the decode body is unchanged for non-zero values.

Spec

The Base58 spec referenced in the Base58 docs (and Base58Check) maps each leading 0x00 byte to exactly one 1. Output verified to match bs58 and ethers across all-zero, leading-zero, empty and random inputs.

Tests

Added regression cases for the all-zero and empty paths on fromHex / toHex / toBytes. They fail on main and pass with the fix; the existing suite is unchanged (12/12 green).

Base58 dropped one leading "1" per all-zero input and threw on empty
input, so round-trips were broken for zero-valued payloads:

  Base58.fromHex('0x00')   // '' (should be '1')
  Base58.fromHex('0x0000') // '1' (should be '11')
  Base58.fromHex('0x')     // throws (should be '')
  Base58.toHex('1')        // '0x000' (should be '0x00')
  Base58.toHex('')         // '0x0' (should be '0x')

The encoder's leading-zero loop ran while `bytes.length > 1`, leaving
the final zero byte uncounted, and `BigInt('0x')` threw on empty input.
The decoder appended a stray "0" nibble whenever the decoded value was
zero. Each issue only surfaced when the value portion was entirely zero;
inputs with a non-zero tail were already correct, which is why the
existing leading-zero vectors (0x0000287fb4cd) passed.

Per the Base58 spec, each leading 0x00 byte maps to exactly one "1".
Output verified against bs58 and ethers across all-zero, leading-zero,
empty and random inputs.
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

@spokodev is attempting to deploy a commit to the Wevm Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant