Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.cs]
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true

dotnet_separate_import_directive_groups = true
dotnet_sort_system_usings_first = true
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* text=auto

*.cs text diff=csharp
18 changes: 14 additions & 4 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ on:
required: false
type: string

permissions:
id-token: write # required for GitHub OIDC

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -42,7 +45,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x

- name: Restore dependencies
run: dotnet restore src/Base58Encoding.slnx
Expand Down Expand Up @@ -71,6 +74,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.version != '')
permissions:
contents: write
id-token: write # enable GitHub OIDC token issuance for this job

steps:
- name: Download artifacts
Expand All @@ -80,12 +84,18 @@ jobs:
path: ./artifacts

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
dotnet-version: 9.0.x
user: ${{ secrets.NUGET_USER }}

- name: Publish to NuGet
run: dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate
run: dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
Expand Down
92 changes: 56 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Base58 Encoding Library

A .NET 9.0 Base58 encoding and decoding library with support for multiple alphabet variants.
A .NET 10.0 Base58 encoding and decoding library with support for multiple alphabet variants.

## Features

- **Multiple Alphabets**: Built-in support for Bitcoin(IFPS/Sui), Ripple, and Flickr alphabets
- **Multiple Alphabets**: Built-in support for Bitcoin(IFPS/Sui/Solana), Ripple, and Flickr alphabets
- **Memory Efficient**: Uses stackalloc operations when possible to minimize allocations
- **Type Safe**: Leverages ReadOnlySpan and ReadOnlyMemory for safe memory operations
- **Intrinsics**: Uses SIMD `Vector128/Vector256` and unrolled loop for counting leading zeros
- **Optimized Hot Paths**: Fast fixed-length encode/decode for 32-byte and 64-byte inputs using Firedancer-like optimizations

## Usage

Expand All @@ -24,50 +25,69 @@ byte[] decoded = Base58.Bitcoin.Decode(encoded);
// Ripple / Flickr
Base58.Ripple.Encode(data);
Base58.Flickr.Encode(data);
```

## Performance

The library automatically uses optimized fast paths for common fixed-size inputs:
- **32-byte inputs** (Bitcoin/Solana addresses, SHA-256 hashes): 8.5x faster encoding
- **64-byte inputs** (SHA-512 hashes): Similar performance improvements

These optimizations are based on Firedancer's specialized Base58 algorithms and are transparent to the user. Unlike Firedancer however, we fallback to the generic approach in case of edge-cases.

**Algorithm Details:**
- Uses **Mixed Radix Conversion (MRC)** with intermediate base 58^5 representation
- Precomputed multiplication tables replace expensive division operations
- Converts binary data to base 58^5 limbs, then to raw base58 digits
- Matrix multiplication approach processes 5 base58 digits simultaneously
- Separate encode/decode tables for 32-byte and 64-byte fixed sizes
- Achieves ~2.5x speedup through table-based optimizations vs iterative division

**References:**
- [Firedancer C implementation](https://github.com/firedancer-io/firedancer/tree/main/src/ballet/base58)

// Custom
new Base58(Base58Alphabet.Custom(""));
```

## Benchmarks

```
BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4946/24H2/2024Update/HudsonValley)
BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700KF 3.40GHz, 1 CPU, 24 logical and 16 physical cores
.NET SDK 9.0.304
[Host] : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
.NET 9.0 : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
.NET SDK 10.0.101
[Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3
DefaultJob : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3

Job=DefaultJob

Job=.NET 9.0 Runtime=.NET 9.0
```
| Method | VectorType | Mean | Error | StdDev | Median |
|--------------------------- |--------------- |------------:|----------:|----------:|------------:|
| **'Our Base58 Encode'** | **BitcoinAddress** | **519.2 ns** | **1.99 ns** | **1.76 ns** | **519.2 ns** |
| 'SimpleBase Base58 Encode' | BitcoinAddress | 770.1 ns | 4.93 ns | 4.61 ns | 769.8 ns |
| 'Our Base58 Decode' | BitcoinAddress | 187.5 ns | 2.06 ns | 1.93 ns | 187.8 ns |
| 'SimpleBase Base58 Decode' | BitcoinAddress | 502.1 ns | 6.95 ns | 5.43 ns | 502.4 ns |
| | | | | | |
| **'Our Base58 Encode'** | **SolanaAddress** | **1,375.9 ns** | **36.83 ns** | **108.59 ns** | **1,410.7 ns** |
| 'SimpleBase Base58 Encode' | SolanaAddress | 2,546.5 ns | 112.91 ns | 329.36 ns | 2,661.5 ns |
| 'Our Base58 Decode' | SolanaAddress | 536.5 ns | 24.89 ns | 73.39 ns | 564.3 ns |
| 'SimpleBase Base58 Decode' | SolanaAddress | 1,210.2 ns | 31.14 ns | 90.33 ns | 1,236.8 ns |
| | | | | | |
| **'Our Base58 Encode'** | **SolanaTx** | **4,185.4 ns** | **48.51 ns** | **37.87 ns** | **4,173.0 ns** |
| 'SimpleBase Base58 Encode' | SolanaTx | 10,844.0 ns | 337.01 ns | 988.40 ns | 11,158.2 ns |
| 'Our Base58 Decode' | SolanaTx | 2,159.0 ns | 116.99 ns | 344.95 ns | 2,294.0 ns |
| 'SimpleBase Base58 Decode' | SolanaTx | 5,357.9 ns | 177.28 ns | 494.18 ns | 5,506.1 ns |
| | | | | | |
| **'Our Base58 Encode'** | **IPFSHash** | **1,285.9 ns** | **80.51 ns** | **237.37 ns** | **1,081.6 ns** |
| 'SimpleBase Base58 Encode' | IPFSHash | 1,654.3 ns | 4.14 ns | 3.88 ns | 1,653.9 ns |
| 'Our Base58 Decode' | IPFSHash | 347.0 ns | 1.19 ns | 0.99 ns | 347.0 ns |
| 'SimpleBase Base58 Decode' | IPFSHash | 883.4 ns | 16.93 ns | 15.01 ns | 883.2 ns |
| | | | | | |
| **'Our Base58 Encode'** | **MoneroAddress** | **4,907.0 ns** | **13.36 ns** | **11.84 ns** | **4,907.9 ns** |
| 'SimpleBase Base58 Encode' | MoneroAddress | 8,998.7 ns | 25.07 ns | 20.94 ns | 8,998.8 ns |
| 'Our Base58 Decode' | MoneroAddress | 1,367.1 ns | 4.38 ns | 3.89 ns | 1,366.5 ns |
| 'SimpleBase Base58 Decode' | MoneroAddress | 3,809.8 ns | 59.58 ns | 55.73 ns | 3,797.3 ns |
| Method | VectorType | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
|--------------------------- |--------------- |------------:|------:|-------:|----------:|------------:|
| **'Our Base58 Encode'** | **BitcoinAddress** | **537.07 ns** | **1.00** | **0.0057** | **96 B** | **1.00** |
| 'SimpleBase Base58 Encode' | BitcoinAddress | 782.31 ns | 1.46 | 0.0057 | 96 B | 1.00 |
| 'Our Base58 Decode' | BitcoinAddress | 168.95 ns | 0.31 | 0.0033 | 56 B | 0.58 |
| 'SimpleBase Base58 Decode' | BitcoinAddress | 352.63 ns | 0.66 | 0.0033 | 56 B | 0.58 |
| | | | | | | |
| **'Our Base58 Encode'** | **SolanaAddress** | **93.41 ns** | **1.00** | **0.0070** | **112 B** | **1.00** |
| 'SimpleBase Base58 Encode' | SolanaAddress | 1,430.37 ns | 15.31 | 0.0057 | 112 B | 1.00 |
| 'Our Base58 Decode' | SolanaAddress | 181.71 ns | 1.95 | 0.0035 | 56 B | 0.50 |
| 'SimpleBase Base58 Decode' | SolanaAddress | 837.03 ns | 8.96 | 0.0019 | 56 B | 0.50 |
| | | | | | | |
| **'Our Base58 Encode'** | **SolanaTx** | **252.31 ns** | **1.00** | **0.0124** | **200 B** | **1.00** |
| 'SimpleBase Base58 Encode' | SolanaTx | 7,247.09 ns | 28.73 | 0.0076 | 200 B | 1.00 |
| 'Our Base58 Decode' | SolanaTx | 178.05 ns | 0.71 | 0.0055 | 88 B | 0.44 |
| 'SimpleBase Base58 Decode' | SolanaTx | 2,379.54 ns | 9.43 | 0.0038 | 88 B | 0.44 |
| | | | | | | |
| **'Our Base58 Encode'** | **IPFSHash** | **1,096.58 ns** | **1.00** | **0.0076** | **120 B** | **1.00** |
| 'SimpleBase Base58 Encode' | IPFSHash | 1,644.83 ns | 1.50 | 0.0076 | 120 B | 1.00 |
| 'Our Base58 Decode' | IPFSHash | 287.87 ns | 0.26 | 0.0038 | 64 B | 0.53 |
| 'SimpleBase Base58 Decode' | IPFSHash | 643.63 ns | 0.59 | 0.0038 | 64 B | 0.53 |
| | | | | | | |
| **'Our Base58 Encode'** | **MoneroAddress** | **4,998.35 ns** | **1.00** | **0.0076** | **216 B** | **1.00** |
| 'SimpleBase Base58 Encode' | MoneroAddress | 8,585.92 ns | 1.72 | - | 216 B | 1.00 |
| 'Our Base58 Decode' | MoneroAddress | 1,173.48 ns | 0.23 | 0.0057 | 96 B | 0.44 |
| 'SimpleBase Base58 Decode' | MoneroAddress | 3,716.38 ns | 0.74 | 0.0038 | 96 B | 0.44 |


## License

This project is available under the MIT License.
This project is available under the MIT License.
10 changes: 3 additions & 7 deletions src/Base58Encoding.Benchmarks/Base58ComparisonBenchmark.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
using Base58Encoding.Benchmarks.Common;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using Base58Encoding.Benchmarks.Common;

namespace Base58Encoding.Benchmarks;

[SimpleJob(RuntimeMoniker.Net90)]
[SimpleJob(RuntimeMoniker.Net10_0)]
[MemoryDiagnoser]
[DisassemblyDiagnoser(exportCombinedDisassemblyReport: true)]
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
public class Base58ComparisonBenchmark
{
Expand Down Expand Up @@ -55,4 +51,4 @@ public byte[] Decode_SimpleBase58()
{
return SimpleBase.Base58.Bitcoin.Decode(_base58Encoded);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
<PackageReference Include="SimpleBase" Version="5.4.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="SimpleBase" Version="5.6.0" />
</ItemGroup>

<ItemGroup>
Expand Down
13 changes: 6 additions & 7 deletions src/Base58Encoding.Benchmarks/BoundsCheckComparisonBenchmark.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

using Base58Encoding.Benchmarks.Common;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;

namespace Base58Encoding.Benchmarks;

[SimpleJob(RuntimeMoniker.Net90)]
Expand All @@ -17,7 +18,6 @@ public class BoundsCheckComparisonBenchmark
private const int Base = 58;
private const int MaxStackallocByte = 512;
private byte[] _testData = null!;
private Base58 _base58 = null!;

[Params(TestVectors.VectorType.BitcoinAddress, TestVectors.VectorType.SolanaAddress, TestVectors.VectorType.SolanaTx, TestVectors.VectorType.IPFSHash, TestVectors.VectorType.MoneroAddress, TestVectors.VectorType.FlickrTestData, TestVectors.VectorType.RippleTestData)]
public TestVectors.VectorType VectorType { get; set; }
Expand All @@ -26,7 +26,6 @@ public class BoundsCheckComparisonBenchmark
public void Setup()
{
_testData = TestVectors.GetVector(VectorType);
_base58 = Base58.Bitcoin;
}

[Benchmark(Baseline = true)]
Expand Down Expand Up @@ -146,4 +145,4 @@ public unsafe byte[] EncodeFixed()
digits.Slice(0, digitCount).CopyTo(result);
return result;
}
}
}
2 changes: 1 addition & 1 deletion src/Base58Encoding.Benchmarks/Common/TestVectors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ public static byte[] GetVector(VectorType type)
_ => BitcoinAddress
};
}
}
}
10 changes: 4 additions & 6 deletions src/Base58Encoding.Benchmarks/CountLeadingCharactersBenchmark.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Jobs;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static System.Net.Mime.MediaTypeNames;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace Base58Encoding.Benchmarks;

Expand Down Expand Up @@ -128,4 +126,4 @@ internal static int CountLeadingCharacters(ReadOnlySpan<char> text, char target)

return count;
}
}
}
9 changes: 4 additions & 5 deletions src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Jobs;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace Base58Encoding.Benchmarks;

[SimpleJob(RuntimeMoniker.Net90)]
Expand Down Expand Up @@ -219,4 +218,4 @@ private static int CountLeadingZerosCombinedImpl(ReadOnlySpan<byte> data)

return count + CountLeadingZerosScalarImpl(data.Slice(count));
}
}
}
45 changes: 45 additions & 0 deletions src/Base58Encoding.Benchmarks/FastVsRegularEncodeBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using BenchmarkDotNet.Attributes;

namespace Base58Encoding.Benchmarks;

[MemoryDiagnoser]
public class FastVsRegularEncodeBenchmark
{
private byte[] _data = default!;
private string _encodedBase58 = default!;

[GlobalSetup]
public void Setup()
{
_data = new byte[32];

Random.Shared.NextBytes(_data);

_encodedBase58 = SimpleBase.Base58.Bitcoin.Encode(_data);
}

[Benchmark]
public string RegularEncode()
{
return Base58.Bitcoin.EncodeGeneric(_data);
}

[Benchmark]
public string FastEncode()
{
return Base58.EncodeBitcoin32Fast(_data);
}


[Benchmark]
public byte[] RegularDecode()
{
return Base58.Bitcoin.DecodeGeneric(_encodedBase58);
}

[Benchmark]
public byte[] FastDecode()
{
return Base58.DecodeBitcoin32Fast(_encodedBase58)!;
}
}
Loading