Skip to content

Base58 encoder truncates (and throws) for inputs encoding to exactly 128 characters — regression in 5.6.3 #87

Description

@unsafePtr

Summary

In 5.6.3 the Base58 encoder is off by one character for inputs whose encoding is exactly 128 characters long. This shows up two ways:

  1. Silent truncationEncode returns a 127-character string where 128 are required, so the value no longer round-trips.
  2. IndexOutOfRangeException — when a leading zero byte pushes the same encoding to the 128-character boundary, the encoder throws.

5.6.2 handles both cases correctly, so this is a regression.

Repro

// 93 bytes of 0xFF  ->  correct encoding is 128 chars
var bare = new byte[93];
Array.Fill(bare, (byte)0xFF);
Console.WriteLine(SimpleBase.Base58.Bitcoin.Encode(bare).Length);
// 5.6.2: 128   (correct)
// 5.6.3: 127   (truncated, does not round-trip)

// same value with one leading zero byte  ->  correct encoding is 129 chars
var withZero = new byte[94];
Array.Fill(withZero, (byte)0xFF);
withZero[0] = 0x00;
Console.WriteLine(SimpleBase.Base58.Bitcoin.Encode(withZero).Length);
// 5.6.2: 129  (correct)
// 5.6.3: System.IndexOutOfRangeException
//        at SimpleBase.DividingCoder`1.Encode(ReadOnlySpan`1 bytes)

The neighbours are fine, which isolates it to the 128-character output length:

payload (0xFF) correct output 5.6.3 bare 5.6.3 with 1 leading zero
92 B 126 chars 126 ✅ 127 ✅
93 B 128 chars 127 ❌ throws
94 B 129 chars 129 ✅ 130 ✅

Note 94 bytes encodes to 129 characters, so a 128-character output is only reachable from a 93-byte payload — which is why the window is so narrow.

Expected

93 bytes of 0xFF is 2^744 - 1, and log58(2^744) = 127.0062, so 128 base58 digits are required.

Verified independently by three sources, all agreeing on 128 chars, first char '2', and round-tripping:

  • BigInteger reference implementation (repeated DivRem by 58)
  • Python (int.from_bytes + divmod loop)
  • the closed-form digit count above

Likely cause

The character-count calculation appears to be one too small for this size class; the throw is the same off-by-one hitting the output buffer edge once the leading-zero prefix is added. Possibly related to the count changes in #79 / #80 / #83.

Environment

  • SimpleBase 5.6.3 (5.6.2 is correct)
  • .NET 10.0.10, x64, Windows 11 and Linux (reproduces on both)

Found by a differential fuzz test that cross-checks against a BigInteger oracle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions