Summary
Base58.Bitcoin.Decode(Base58.Bitcoin.Encode(x)) does not round-trip for some inputs: the decoded array comes back one byte short, missing the leading (most-significant) byte. Encode is correct — the failure is in Decode, which appears to under-allocate its output buffer by one byte when the decoded length lands on a size-estimate boundary.
Environment
- SimpleBase 5.6.2 (also reproduces on 5.6.0)
- .NET 10.0.9, x64
- Windows 11
Minimal reproduction
dotnet new console -o SimpleBaseRepro
cd SimpleBaseRepro
dotnet add package SimpleBase --version 5.6.2
# replace Program.cs with the code below, then:
dotnet run
using SimpleBase;
// Value = 256^134 — the smallest 135-byte integer: 0x01 followed by 134 zero bytes.
byte[] input = new byte[135];
input[0] = 1;
string encoded = Base58.Bitcoin.Encode(input);
byte[] decoded = Base58.Bitcoin.Decode(encoded);
Console.WriteLine($"input length : {input.Length}");
Console.WriteLine($"encoded length : {encoded.Length}");
Console.WriteLine($"decoded length : {decoded.Length}");
Console.WriteLine($"round-trips : {decoded.AsSpan().SequenceEqual(input)}");
Expected output
input length : 135
encoded length : 183
decoded length : 135
round-trips : True
Actual output
input length : 135
encoded length : 183
decoded length : 134 <-- one byte short
round-trips : False <-- leading 0x01 dropped
The decoded array is 134 bytes of all zeros; the leading 0x01 is gone.
Decode-only reproduction
The encoding is correct, so the bug can also be shown by decoding the string directly:
using SimpleBase;
const string encoded =
"zaZ9rvaCSMeX4tS1qfMDz5EUZd6uPZCCxRfbCsps3ozwjAUCfxioFEwfH8jECXa66kNrJu6Ntm6Gy1jq45DFasDNCoDZC5VpFGgJ27SpdZ8kmPyjAaynZ4538PvNhei1bqMbo9vnfG7fNtuLKMy9Ee7p61vdazm5WwwUGSyTawfkFemsUjUbLwR";
byte[] decoded = Base58.Bitcoin.Decode(encoded);
Console.WriteLine(decoded.Length); // 134, should be 135
It is periodic
The same construction (x[0] = 1, rest zero) fails at lengths 135, 176, 217, … — a period of +41 bytes:
using SimpleBase;
for (int len = 1; len <= 256; len++)
{
var x = new byte[len];
x[0] = 1;
var y = Base58.Bitcoin.Decode(Base58.Bitcoin.Encode(x));
if (!y.AsSpan().SequenceEqual(x))
Console.WriteLine($"FAIL at length {len}: decoded {y.Length} bytes (expected {len})");
}
FAIL at length 135: decoded 134 bytes (expected 135)
FAIL at length 176: decoded 175 bytes (expected 176)
FAIL at length 217: decoded 216 bytes (expected 217)
Only the smallest value at each of these lengths (i.e. 256^(len-1)) triggers it — larger values at the same length decode fine — which is why random round-trip testing rarely hits it.
Cross-verification (the encoding is correct)
The produced Base58 string was checked against two independent implementations, both of which agree with SimpleBase's Encode and decode the string back to the full 135 bytes:
- A
BigInteger reference implementation (repeated divide-by-58).
- The Python
base58 package (base58.b58decode(encoded) → 135 bytes, equal to the input).
So the string is a valid encoding of a 135-byte value; only SimpleBase's Decode truncates it.
Likely cause
Decode estimates the output byte count from the encoded string length and appears to under-allocate by one for encoded lengths that sit exactly on the estimate boundary (the shortest encodings for a given byte length). The high-order byte is then dropped.
Summary
Base58.Bitcoin.Decode(Base58.Bitcoin.Encode(x))does not round-trip for some inputs: the decoded array comes back one byte short, missing the leading (most-significant) byte.Encodeis correct — the failure is inDecode, which appears to under-allocate its output buffer by one byte when the decoded length lands on a size-estimate boundary.Environment
Minimal reproduction
Expected output
Actual output
The decoded array is 134 bytes of all zeros; the leading
0x01is gone.Decode-only reproduction
The encoding is correct, so the bug can also be shown by decoding the string directly:
It is periodic
The same construction (
x[0] = 1, rest zero) fails at lengths 135, 176, 217, … — a period of +41 bytes:Only the smallest value at each of these lengths (i.e.
256^(len-1)) triggers it — larger values at the same length decode fine — which is why random round-trip testing rarely hits it.Cross-verification (the encoding is correct)
The produced Base58 string was checked against two independent implementations, both of which agree with SimpleBase's
Encodeand decode the string back to the full 135 bytes:BigIntegerreference implementation (repeated divide-by-58).base58package (base58.b58decode(encoded)→ 135 bytes, equal to the input).So the string is a valid encoding of a 135-byte value; only SimpleBase's
Decodetruncates it.Likely cause
Decodeestimates the output byte count from the encoded string length and appears to under-allocate by one for encoded lengths that sit exactly on the estimate boundary (the shortest encodings for a given byte length). The high-order byte is then dropped.