Skip to content

Commit 29904d0

Browse files
committed
feat: add solution
1 parent 9600947 commit 29904d0

14 files changed

Lines changed: 1044 additions & 0 deletions

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Base58 Encoding Library
2+
3+
A .NET 9.0 Base58 encoding and decoding library with support for multiple alphabet variants.
4+
5+
## Features
6+
7+
- **Multiple Alphabets**: Built-in support for Bitcoin(IFPS/Sui), Ripple, and Flickr alphabets
8+
- **Memory Efficient**: Uses span operations to minimize allocations
9+
- **Type Safe**: Leverages ReadOnlySpan and ReadOnlyMemory for safe operations
10+
- **Intrinsics**: Uses SIMD `Vector128/Vector256` and unrolled loop for counting leading zeros
11+
12+
## Usage
13+
14+
```csharp
15+
using Base58Encoding;
16+
17+
// Encode bytes to Base58 Bitcoin(IFPS/Sui) alphabet
18+
byte[] data = { 0x01, 0x02, 0x03, 0x04 };
19+
string encoded = Base58.Bitcoin.Encode(data);
20+
21+
// Decode Base58 string back to bytes
22+
byte[] decoded = Base58.Bitcoin.Decode(encoded);
23+
24+
// Ripple / Flickr
25+
Base58.Ripple.Encode(data);
26+
Base58.Flickr.Encode(data);
27+
28+
// Custom
29+
new Base58(Base58Alphabet.Custom(""));
30+
```
31+
32+
## Benchmarks
33+
34+
```
35+
BenchmarkDotNet v0.13.12, Windows 11 (10.0.26100.4946)
36+
13th Gen Intel Core i7-13700KF, 1 CPU, 24 logical and 16 physical cores
37+
.NET SDK 9.0.304
38+
[Host] : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
39+
.NET 9.0 : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
40+
41+
Job=.NET 9.0 Runtime=.NET 9.0
42+
```
43+
| Method | DataSize | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
44+
|--------------------------- |--------- |------------:|-----------:|-------------:|------------:|------:|--------:|-------:|----------:|------------:|
45+
| **'Our Base58 Encode'** | **8** | **51.62 ns** | **0.557 ns** | **0.521 ns** | **51.62 ns** | **1.81** | **0.04** | **0.0030** | **48 B** | **1.50** |
46+
| 'SimpleBase Base58 Encode' | 8 | 55.59 ns | 0.251 ns | 0.234 ns | 55.59 ns | 1.95 | 0.03 | 0.0030 | 48 B | 1.50 |
47+
| 'NBitcoin Base58 Encode' | 8 | 63.25 ns | 1.229 ns | 1.366 ns | 63.07 ns | 2.23 | 0.05 | 0.0030 | 48 B | 1.50 |
48+
| 'Our Base58 Decode' | 8 | 28.57 ns | 0.528 ns | 0.494 ns | 28.54 ns | 1.00 | 0.00 | 0.0020 | 32 B | 1.00 |
49+
| 'SimpleBase Base58 Decode' | 8 | 62.70 ns | 0.597 ns | 0.558 ns | 62.45 ns | 2.20 | 0.04 | 0.0020 | 32 B | 1.00 |
50+
| | | | | | | | | | | |
51+
| **'Our Base58 Encode'** | **13** | **125.03 ns** | **1.721 ns** | **1.525 ns** | **124.68 ns** | **2.11** | **0.03** | **0.0041** | **64 B** | **1.60** |
52+
| 'SimpleBase Base58 Encode' | 13 | 143.07 ns | 1.342 ns | 1.256 ns | 143.22 ns | 2.41 | 0.05 | 0.0041 | 64 B | 1.60 |
53+
| 'NBitcoin Base58 Encode' | 13 | 152.49 ns | 1.776 ns | 1.661 ns | 153.13 ns | 2.57 | 0.04 | 0.0041 | 64 B | 1.60 |
54+
| 'Our Base58 Decode' | 13 | 59.34 ns | 0.876 ns | 0.776 ns | 59.35 ns | 1.00 | 0.00 | 0.0025 | 40 B | 1.00 |
55+
| 'SimpleBase Base58 Decode' | 13 | 158.13 ns | 0.529 ns | 0.495 ns | 158.19 ns | 2.66 | 0.04 | 0.0024 | 40 B | 1.00 |
56+
| | | | | | | | | | | |
57+
| **'Our Base58 Encode'** | **32** | **900.00 ns** | **13.132 ns** | **11.641 ns** | **899.01 ns** | **3.05** | **0.04** | **0.0067** | **112 B** | **2.00** |
58+
| 'SimpleBase Base58 Encode' | 32 | 956.71 ns | 6.189 ns | 5.789 ns | 955.69 ns | 3.24 | 0.02 | 0.0057 | 112 B | 2.00 |
59+
| 'NBitcoin Base58 Encode' | 32 | 1,215.07 ns | 11.678 ns | 10.923 ns | 1,216.22 ns | 4.11 | 0.04 | 0.0057 | 112 B | 2.00 |
60+
| 'Our Base58 Decode' | 32 | 295.31 ns | 1.387 ns | 1.297 ns | 295.56 ns | 1.00 | 0.00 | 0.0033 | 56 B | 1.00 |
61+
| 'SimpleBase Base58 Decode' | 32 | 693.05 ns | 5.673 ns | 5.306 ns | 691.92 ns | 2.35 | 0.02 | 0.0029 | 56 B | 1.00 |
62+
| | | | | | | | | | | |
63+
| **'Our Base58 Encode'** | **69** | **4,515.29 ns** | **18.892 ns** | **15.776 ns** | **4,519.64 ns** | **2.41** | **0.82** | **0.0076** | **216 B** | **2.25** |
64+
| 'SimpleBase Base58 Encode' | 69 | 6,924.91 ns | 137.444 ns | 251.323 ns | 7,064.97 ns | 3.08 | 0.74 | 0.0076 | 216 B | 2.25 |
65+
| 'NBitcoin Base58 Encode' | 69 | 7,536.47 ns | 150.542 ns | 282.753 ns | 7,613.02 ns | 3.36 | 0.79 | 0.0076 | 216 B | 2.25 |
66+
| 'Our Base58 Decode' | 69 | 2,403.76 ns | 87.230 ns | 257.200 ns | 2,503.34 ns | 1.00 | 0.00 | 0.0057 | 96 B | 1.00 |
67+
| 'SimpleBase Base58 Decode' | 69 | 4,697.46 ns | 447.955 ns | 1,320.805 ns | 3,896.45 ns | 1.98 | 0.58 | - | 96 B | 1.00 |
68+
69+
70+
## License
71+
72+
This project is available under the MIT License.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Diagnosers;
3+
using BenchmarkDotNet.Jobs;
4+
using NBitcoin.DataEncoders;
5+
6+
namespace Base58Encoding.Benchmarks;
7+
8+
[SimpleJob(RuntimeMoniker.Net90)]
9+
[MemoryDiagnoser]
10+
public class Base58ComparisonBenchmark
11+
{
12+
private byte[] _testData = null!;
13+
private string _ourBase58Encoded = null!;
14+
private string _simpleBase58Encoded = null!;
15+
16+
[Params(8, 13, 32, 69)]
17+
public int DataSize { get; set; }
18+
19+
[GlobalSetup]
20+
public void Setup()
21+
{
22+
_testData = new byte[DataSize];
23+
Random.Shared.NextBytes(_testData);
24+
25+
// Add some leading zeros for realistic Base58 scenarios
26+
if (DataSize >= 2)
27+
{
28+
_testData[0] = 0;
29+
}
30+
if (DataSize > 32)
31+
_testData[1] = 0;
32+
33+
_ourBase58Encoded = Base58.Bitcoin.Encode(_testData);
34+
_simpleBase58Encoded = Base58.Bitcoin.Encode(_testData.AsSpan());
35+
}
36+
37+
[Benchmark(Description = "Our Base58 Encode")]
38+
public string Encode_OurBase58()
39+
{
40+
return Base58.Bitcoin.Encode(_testData);
41+
}
42+
43+
[Benchmark(Description = "SimpleBase Base58 Encode")]
44+
public string Encode_SimpleBase58()
45+
{
46+
return SimpleBase.Base58.Bitcoin.Encode(_testData.AsSpan());
47+
}
48+
49+
[Benchmark(Description = "NBitcoin Base58 Encode")]
50+
public string Encode_NBitcoin()
51+
{
52+
var bcEncoded = Encoders.Base58.EncodeData(_testData);
53+
return bcEncoded;
54+
}
55+
56+
[Benchmark(Description = "Our Base58 Decode", Baseline = true)]
57+
public byte[] Decode_OurBase58()
58+
{
59+
return Base58.Bitcoin.Decode(_ourBase58Encoded);
60+
}
61+
62+
[Benchmark(Description = "SimpleBase Base58 Decode")]
63+
public byte[] Decode_SimpleBase58()
64+
{
65+
return SimpleBase.Base58.Bitcoin.Decode(_simpleBase58Encoded.AsSpan());
66+
}
67+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
12+
<PackageReference Include="NBitcoin" Version="9.0.0" />
13+
<PackageReference Include="SimpleBase" Version="5.0.0" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\Base58Encoding\Base58Encoding.csproj" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using BenchmarkDotNet.Attributes;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace Base58Encoding.Benchmarks;
5+
6+
public class CountLeadingZerosBenchmark
7+
{
8+
private static Lazy<byte[][]> _lazyTestData = new(() =>
9+
{
10+
// Decode the provided base58 addresses to get byte arrays with leading zeros
11+
var addresses = new[]
12+
{
13+
"1111111111111111111114oLvT2",
14+
"1111113LrYkRba5STgvA5UoLqzLxwP6XhtN6f",
15+
"1BoatSLRHtKNngkdXEeobR76b53LETtpyT",
16+
"11bJr6xq2UhxDqkdqNKoGPYYEVBy6cd3M"
17+
};
18+
var data = new byte[addresses.Length][];
19+
for (int i = 0; i < addresses.Length; i++)
20+
{
21+
data[i] = Base58.Bitcoin.Decode(addresses[i]);
22+
}
23+
return data;
24+
});
25+
26+
public byte[][] TestData;
27+
28+
29+
[GlobalSetup]
30+
public void Setup()
31+
{
32+
TestData = _lazyTestData.Value;
33+
}
34+
35+
[Benchmark(Description = "Simple Array")]
36+
public int SimpleArray()
37+
{
38+
int totalCount = 0;
39+
foreach (var data in TestData)
40+
{
41+
totalCount += CountLeadingZerosArray(data);
42+
}
43+
return totalCount;
44+
}
45+
46+
[Benchmark(Description = "Scalar (Current)")]
47+
public int Scalar()
48+
{
49+
int totalCount = 0;
50+
foreach (var data in TestData)
51+
{
52+
totalCount += Base58.CountLeadingZerosScalar(data);
53+
}
54+
return totalCount;
55+
}
56+
57+
[Benchmark(Description = "SIMD+Scalar")]
58+
public int SimdOnly()
59+
{
60+
int totalCount = 0;
61+
foreach (var data in TestData)
62+
{
63+
int count = Base58.CountLeadingZerosSimd(data, out int processed);
64+
if (count < processed)
65+
return count;
66+
67+
var total = count + Base58.CountLeadingZerosScalar(data.AsSpan(count));
68+
69+
totalCount += total;
70+
}
71+
72+
return totalCount;
73+
}
74+
75+
[Benchmark(Baseline = true, Description = "Combined (Current)")]
76+
public int Combined()
77+
{
78+
int totalCount = 0;
79+
foreach (var data in TestData)
80+
{
81+
totalCount += Base58.CountLeadingZeros(data);
82+
}
83+
return totalCount;
84+
}
85+
86+
// Simple array-based approach
87+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
88+
private static int CountLeadingZerosArray(ReadOnlySpan<byte> data)
89+
{
90+
int count = 0;
91+
for (int i = 0; i < data.Length; i++)
92+
{
93+
if (data[i] != 0)
94+
break;
95+
count++;
96+
}
97+
return count;
98+
}
99+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using BenchmarkDotNet.Running;
2+
using Base58Encoding.Benchmarks;
3+
4+
// Uncomment the benchmark you want to run
5+
//BenchmarkRunner.Run<Base58ComparisonBenchmark>();
6+
BenchmarkRunner.Run<CountLeadingZerosBenchmark>();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
<IsTestProject>true</IsTestProject>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
13+
<PackageReference Include="SimpleBase" Version="5.4.1" />
14+
<PackageReference Include="xunit" Version="2.9.2" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
<PackageReference Include="coverlet.collector" Version="6.0.2">
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
<PrivateAssets>all</PrivateAssets>
22+
</PackageReference>
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\Base58Encoding\Base58Encoding.csproj" />
27+
</ItemGroup>
28+
29+
</Project>

0 commit comments

Comments
 (0)