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:
- Silent truncation —
Encode returns a 127-character string where 128 are required, so the value no longer round-trips.
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.
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:
Encodereturns a 127-character string where 128 are required, so the value no longer round-trips.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
The neighbours are fine, which isolates it to the 128-character output length:
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
0xFFis2^744 - 1, andlog58(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:BigIntegerreference implementation (repeatedDivRemby 58)int.from_bytes+divmodloop)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
Found by a differential fuzz test that cross-checks against a
BigIntegeroracle.