Skip to content

Commit 61ec194

Browse files
committed
add EncodeCheckSkipZeroes and new EncodeCheck and TryDecodeCheck variants with variable length prefix strings
1 parent f320802 commit 61ec194

File tree

4 files changed

+126
-16
lines changed

4 files changed

+126
-16
lines changed

src/Base58.cs

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System;
77
using System.Diagnostics;
8+
using System.Runtime.Serialization;
89
using System.Security.Cryptography;
910

1011
namespace SimpleBase;
@@ -28,9 +29,9 @@ public sealed class Base58(Base58Alphabet alphabet) : DividingCoder<Base58Alphab
2829
const int reductionFactor = 733;
2930

3031
const int maxCheckPayloadLength = 256;
32+
const int minCheckDecodedBufferSize = 5;
3133
const int sha256Bytes = 32;
3234
const int sha256DigestBytes = 4;
33-
3435
static readonly Lazy<Base58> bitcoin = new(() => new Base58(Base58Alphabet.Bitcoin));
3536
static readonly Lazy<Base58> ripple = new(() => new Base58(Base58Alphabet.Ripple));
3637
static readonly Lazy<Base58> flickr = new(() => new Base58(Base58Alphabet.Flickr));
@@ -87,17 +88,57 @@ public string EncodeCheck(ReadOnlySpan<byte> payload, byte version)
8788
throw new ArgumentException($"Payload length {payload.Length} is greater than {maxCheckPayloadLength}", nameof(payload));
8889
}
8990

90-
int versionPlusPayloadLen = payload.Length + 1;
91-
int outputLen = versionPlusPayloadLen + sha256DigestBytes;
91+
ReadOnlySpan<byte> prefix = [version];
92+
return EncodeCheck(payload, prefix);
93+
}
94+
95+
/// <summary>
96+
/// Generate a Base58Check string out of a prefix buffer and payload.
97+
/// </summary>
98+
/// <param name="payload">Address data.</param>
99+
/// <param name="prefix">Prefix buffer.</param>
100+
/// <returns>Base58Check address.</returns>
101+
/// <exception cref="ArgumentException">If <paramref name="payload"/> is empty.</exception>
102+
public string EncodeCheck(ReadOnlySpan<byte> payload, ReadOnlySpan<byte> prefix)
103+
{
104+
int totalLength = prefix.Length + payload.Length;
105+
int outputLen = totalLength + sha256DigestBytes;
92106
Span<byte> output = (outputLen < Bits.SafeStackMaxAllocSize) ? stackalloc byte[outputLen] : new byte[outputLen];
93-
output[0] = version;
94-
payload.CopyTo(output[1..]);
107+
prefix.CopyTo(output);
108+
payload.CopyTo(output[prefix.Length..]);
95109
Span<byte> sha256 = stackalloc byte[sha256Bytes];
96-
computeDoubleSha256(output[..versionPlusPayloadLen], sha256);
97-
sha256[..sha256DigestBytes].CopyTo(output[versionPlusPayloadLen..]);
110+
computeDoubleSha256(output[..totalLength], sha256);
111+
sha256[..sha256DigestBytes].CopyTo(output[totalLength..]);
98112
return Encode(output);
99113
}
100114

115+
/// <summary>
116+
/// Generate a Base58Check string out of a prefix buffer and payload
117+
/// by skipping leading zeroes in <paramref name="payload"/>.
118+
/// Platforms like Tezos expects this behavior.
119+
/// </summary>
120+
/// <param name="payload">Address data.</param>
121+
/// <param name="prefix">Prefix buffer.</param>
122+
/// <returns>Base58Check address.</returns>
123+
/// <exception cref="ArgumentException">
124+
/// If <paramref name="payload"/> is empty or only contains zeroes.
125+
/// </exception>
126+
public string EncodeCheckSkipZeroes(ReadOnlySpan<byte> payload, ReadOnlySpan<byte> prefix)
127+
{
128+
int firstNonZeroIndex = payload.IndexOfAnyExcept((byte)0);
129+
if (firstNonZeroIndex < 0)
130+
{
131+
throw new ArgumentException("Payload cannot be empty or all zeroes", nameof(payload));
132+
}
133+
134+
if (firstNonZeroIndex > 0)
135+
{
136+
payload = payload[firstNonZeroIndex..];
137+
}
138+
139+
return EncodeCheck(payload, prefix);
140+
}
141+
101142
/// <summary>
102143
/// Try to decode and verify a Base58Check address.
103144
/// </summary>
@@ -111,28 +152,47 @@ public bool TryDecodeCheck(
111152
Span<byte> payload,
112153
out byte version,
113154
out int bytesWritten)
155+
{
156+
Span<byte> versionBuffer = stackalloc byte[1];
157+
bool result = TryDecodeCheck(address, payload, versionBuffer, out bytesWritten);
158+
version = versionBuffer[0];
159+
return result;
160+
}
161+
162+
/// <summary>
163+
/// Try to decode and verify a Base58Check address.
164+
/// </summary>
165+
/// <param name="address">Address string.</param>
166+
/// <param name="payload">Output address buffer.</param>
167+
/// <param name="prefix">Prefix decoded, must have the exact size of the expected prefix.</param>
168+
/// <param name="payloadBytesWritten">Number of bytes written in the output payload.</param>
169+
/// <returns>True if address was decoded successfully and passed validation. False, otherwise.</returns>
170+
public bool TryDecodeCheck(
171+
ReadOnlySpan<char> address,
172+
Span<byte> payload,
173+
Span<byte> prefix,
174+
out int payloadBytesWritten)
114175
{
115176
Span<byte> buffer = stackalloc byte[maxCheckPayloadLength + sha256DigestBytes + 1];
116-
if (!TryDecode(address, buffer, out bytesWritten) || bytesWritten < 5)
177+
if (!TryDecode(address, buffer, out int decodedBufferSize) || decodedBufferSize < minCheckDecodedBufferSize)
117178
{
118-
version = 0;
179+
payloadBytesWritten = 0;
119180
return false;
120181
}
121182

122-
buffer = buffer[..bytesWritten];
123-
version = buffer[0];
183+
buffer = buffer[..decodedBufferSize];
184+
buffer[..prefix.Length].CopyTo(prefix);
124185
Span<byte> sha256 = stackalloc byte[sha256Bytes];
125186
computeDoubleSha256(buffer[..^sha256DigestBytes], sha256);
126187
if (!sha256[..sha256DigestBytes].SequenceEqual(buffer[^sha256DigestBytes..]))
127188
{
128-
version = 0;
189+
payloadBytesWritten = 0;
129190
return false;
130191
}
131192

132-
var finalBuffer = buffer[1..^sha256DigestBytes];
133-
version = buffer[0];
193+
var finalBuffer = buffer[prefix.Length..^sha256DigestBytes];
134194
finalBuffer.CopyTo(payload);
135-
bytesWritten = finalBuffer.Length;
195+
payloadBytesWritten = finalBuffer.Length;
136196
return true;
137197
}
138198

src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-

1+
SimpleBase.Base58.EncodeCheck(System.ReadOnlySpan<byte> payload, System.ReadOnlySpan<byte> prefix) -> string!
2+
SimpleBase.Base58.EncodeCheckSkipZeroes(System.ReadOnlySpan<byte> payload, System.ReadOnlySpan<byte> prefix) -> string!
3+
SimpleBase.Base58.TryDecodeCheck(System.ReadOnlySpan<char> address, System.Span<byte> payload, System.Span<byte> prefix, out int payloadBytesWritten) -> bool

test/Base58/Base58CheckTest.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ public class Base58CheckTest
2525
new object[] { 20, "00000000000000000000000000000000000000000000000000000000000000", "bi1EWXwJay2udZVxLJozuTb8Meg4W9c6xnmJaRDjg6pri5MBAxb9XwrpQXbtnqEoRV5U2pixnFfwyXC8tRAVC8XxnjK"},
2626
];
2727

28+
// test vector taken from gotezos at https://github.com/goat-systems/go-tezos/blob/800cc714fad7313e92a5068407c23e0e397f5323/keys/key_test.go#L127
29+
// see LICENSE.gotezos.txt for details
30+
static object[][] zeroPrefixedLongVersionTestData = [
31+
[ "0000861299624c9a3b52be10762c64bac282b1c02316", new byte[] { 6, 161, 159 }, "tz1XrwX7i9Nzh8e6UmG3VnFkAeoyWdTqDf3U" ],
32+
];
33+
34+
static object[][] longVersionTestData = [
35+
[ "861299624c9a3b52be10762c64bac282b1c02316", new byte[] { 6, 161, 159 }, "tz1XrwX7i9Nzh8e6UmG3VnFkAeoyWdTqDf3U" ],
36+
];
37+
2838
[Test]
2939
[TestCaseSource(nameof(testData))]
3040
public void EncodeCheck_ValidInput_ReturnsExpectedResult(int version, string payload, string expectedOutput)
@@ -34,6 +44,37 @@ public void EncodeCheck_ValidInput_ReturnsExpectedResult(int version, string pay
3444
Assert.That(result, Is.EqualTo(expectedOutput));
3545
}
3646

47+
[Test]
48+
[TestCaseSource(nameof(zeroPrefixedLongVersionTestData))]
49+
public void EncodeCheckSkipZeroes_ZeroPrefixes_ReturnsExpectedResult(string hexPayload, byte[] version, string expectedResult)
50+
{
51+
var payload = Convert.FromHexString(hexPayload);
52+
string result = Base58.Bitcoin.EncodeCheckSkipZeroes(payload, version);
53+
Assert.That(result, Is.EqualTo(expectedResult));
54+
}
55+
56+
[Test]
57+
[TestCaseSource(nameof(zeroPrefixedLongVersionTestData))]
58+
public void TryDecodeCheck_ZeroPrefixedLongVersion_ValidInput_ReturnsExpectedResult(string expectedHexPayload, byte[] expectedVersion, string encoded)
59+
{
60+
var versionBuffer = new byte[expectedVersion.Length];
61+
var outputBuffer = new byte[256];
62+
bool result = Base58.Bitcoin.TryDecodeCheck(encoded, outputBuffer, versionBuffer, out int bytesWritten);
63+
Assert.That(result, Is.True);
64+
Assert.That(versionBuffer, Is.EquivalentTo(expectedVersion));
65+
var expectedBuffer = Convert.FromHexString(expectedHexPayload.TrimStart('0'));
66+
Assert.That(outputBuffer.AsSpan(0, bytesWritten).ToArray(), Is.EquivalentTo(expectedBuffer));
67+
}
68+
69+
[Test]
70+
[TestCaseSource(nameof(longVersionTestData))]
71+
public void EncodeCheck_LongVersionOverload_DoesNotStripPrefixZeroes(string hexPayload, byte[] version, string expectedResult)
72+
{
73+
var payload = Convert.FromHexString(hexPayload);
74+
string result = Base58.Bitcoin.EncodeCheck(payload, version);
75+
Assert.That(result, Is.EqualTo(expectedResult));
76+
}
77+
3778
[Test]
3879
[TestCaseSource(nameof(testData))]
3980
public void TryDecodeCheck_ValidInput_ReturnsExpectedResult(int expectedVersion, string expectedOutput, string input)

test/LICENSE.gotezos.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2018 DefinitelyNotAGoat/MagicAglet
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

0 commit comments

Comments
 (0)