Skip to content

Commit e0433a1

Browse files
authored
Allow for extensible BitcoinNetwork (#57)
* allow for extensible `BitcoinNetwork` --------- Signed-off-by: Níckolas Goline <nickolas.goline+github@gmail.com>
1 parent 9850142 commit e0433a1

File tree

4 files changed

+186
-9
lines changed

4 files changed

+186
-9
lines changed

src/NLightning.Domain/CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## v1.1.0
6+
7+
This version introduces the ability to register and manage custom Bitcoin-like networks at runtime, enhancing the
8+
flexibility and extensibility of the `BitcoinNetwork` class. This allows developers to define and work with networks
9+
beyond the standard `mainnet`, `testnet`, and `regtest`, making the library more adaptable to various use cases and
10+
testing scenarios.
11+
12+
### Added
13+
14+
- **Extensible Custom Bitcoin Networks:**
15+
`BitcoinNetwork` now supports registration and management of custom network names and corresponding `ChainHash` values
16+
at runtime.
17+
- Implementers can register any custom Bitcoin-like network and have it fully supported throughout the domain layer.
18+
- Provides methods to register and unregister custom networks and retrieve their `ChainHash` from name, with all
19+
conversions and equality logic preserved.
20+
- Built-in networks (`mainnet`, `testnet`, `regtest`) remain unchanged.
21+
522
## v1.0.0
623

724
This version's focus has been on enhancing Bitcoin integration, refining channel management, standardizing protocol

src/NLightning.Domain/NLightning.Domain.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<LangVersion>latest</LangVersion>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8-
<Version>1.0.0</Version>
8+
<Version>1.1.0</Version>
99
<Configurations>Debug;Release;Debug.Native;Debug.Wasm;Release.Native;Release.Wasm</Configurations>
1010
<Platforms>AnyCPU</Platforms>
1111
</PropertyGroup>
@@ -16,8 +16,8 @@
1616
<Copyright>Copyright © Níckolas Goline 2024-2025</Copyright>
1717
<RepositoryUrl>https://github.com/ipms-io/nlightning</RepositoryUrl>
1818
<RepositoryType>git</RepositoryType>
19-
<AssemblyVersion>1.0.0</AssemblyVersion>
20-
<FileVersion>1.0.0</FileVersion>
19+
<AssemblyVersion>1.1.0</AssemblyVersion>
20+
<FileVersion>1.1.0</FileVersion>
2121
<NeutralLanguage>en</NeutralLanguage>
2222
<Icon>logo.png</Icon>
2323
<PackageTags>lightning-network,bitcoin,crypto,infrastructure,dotnet,cryptography,cross-platform,libsodium,blazor,webassembly,aot</PackageTags>
@@ -26,7 +26,7 @@
2626
<PackageLicenseFile>LICENSE</PackageLicenseFile>
2727
<PackageReadmeFile>README.md</PackageReadmeFile>
2828
<PackageId>NLightning.Domain</PackageId>
29-
<PackageReleaseNotes>This version's focus has been on enhancing Bitcoin integration, refining channel management, standardizing protocol message handling, and improving core domain primitives. These changes aim to solidify the domain logic and provide a richer, more robust foundation for building Lightning Network features.</PackageReleaseNotes>
29+
<PackageReleaseNotes>This version introduces the ability to register and manage custom Bitcoin-like networks at runtime, enhancing the flexibility and extensibility of the `BitcoinNetwork` class. This allows developers to define and work with networks beyond the standard `mainnet`, `testnet`, and `regtest`, making the library more adaptable to various use cases and testing scenarios.</PackageReleaseNotes>
3030
</PropertyGroup>
3131

3232
<PropertyGroup Condition="$(Configuration.Contains('Debug'))">

src/NLightning.Domain/Protocol/ValueObjects/BitcoinNetwork.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ namespace NLightning.Domain.Protocol.ValueObjects;
44

55
public readonly struct BitcoinNetwork : IEquatable<BitcoinNetwork>
66
{
7+
private static Dictionary<string, ChainHash> CustomChainHashes { get; } = new();
8+
79
public static readonly BitcoinNetwork Mainnet = new(NetworkConstants.Mainnet);
810
public static readonly BitcoinNetwork Testnet = new(NetworkConstants.Testnet);
911
public static readonly BitcoinNetwork Regtest = new(NetworkConstants.Regtest);
@@ -25,19 +27,49 @@ public ChainHash ChainHash
2527
NetworkConstants.Mainnet => ChainConstants.Main,
2628
NetworkConstants.Testnet => ChainConstants.Testnet,
2729
NetworkConstants.Regtest => ChainConstants.Regtest,
28-
_ => throw new Exception("Chain not supported.")
30+
_ => CustomChainHashes.TryGetValue(Name.ToLowerInvariant(), out var hash)
31+
? hash
32+
: throw new InvalidOperationException($"Chain not supported: {Name}")
2933
};
3034
}
3135
}
3236

3337
public override string ToString() => Name;
3438

39+
/// <summary>
40+
/// Register a custom network mapping
41+
/// </summary>
42+
public void Register(string name, ChainHash chainHash)
43+
{
44+
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
45+
46+
if (!CustomChainHashes.TryAdd(name.ToLowerInvariant(), chainHash))
47+
{
48+
if (CustomChainHashes.ContainsKey(name.ToLowerInvariant()))
49+
throw new InvalidOperationException($"Chain hash already registered: {name}");
50+
51+
throw new InvalidOperationException($"Failed to register chain hash: {name}");
52+
}
53+
}
54+
55+
/// <summary>
56+
/// Unregister a custom network mapping
57+
/// </summary>
58+
public static void Unregister(string name)
59+
{
60+
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
61+
CustomChainHashes.Remove(name.ToLowerInvariant());
62+
}
63+
3564
#region Implicit Conversions
65+
3666
public static implicit operator string(BitcoinNetwork bitcoinNetwork) => bitcoinNetwork.Name.ToLowerInvariant();
3767
public static implicit operator BitcoinNetwork(string value) => new(value.ToLowerInvariant());
68+
3869
#endregion
3970

4071
#region Equality
72+
4173
public override bool Equals(object? obj)
4274
{
4375
return obj is BitcoinNetwork network && ChainHash.Equals(network.ChainHash);
@@ -62,5 +94,6 @@ public override int GetHashCode()
6294
{
6395
return !(left == right);
6496
}
97+
6598
#endregion
6699
}

test/NLightning.Domain.Tests/ValueObjects/BitcoinNetworkTests.cs

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
using NLightning.Domain.Protocol.ValueObjects;
2-
31
namespace NLightning.Domain.Tests.ValueObjects;
42

53
using Domain.Protocol.Constants;
4+
using Domain.Protocol.ValueObjects;
65

76
public class BitcoinNetworkTests
87
{
@@ -60,7 +59,7 @@ public void Given_NetworkInstance_When_ChainHashAccessed_Then_ReturnsCorrectHash
6059
NetworkConstants.Mainnet => ChainConstants.Main,
6160
NetworkConstants.Testnet => ChainConstants.Testnet,
6261
NetworkConstants.Regtest => ChainConstants.Regtest,
63-
_ => throw new Exception("Chain not supported.")
62+
_ => throw new InvalidOperationException("Chain not supported.")
6463
};
6564

6665
// When
@@ -77,7 +76,7 @@ public void Given_UnsupportedNetworkName_When_ChainHashAccessed_Then_ThrowsExcep
7776
var network = new BitcoinNetwork("unsupported");
7877

7978
// When & Then
80-
Assert.Throws<Exception>(() => network.ChainHash);
79+
Assert.Throws<InvalidOperationException>(() => network.ChainHash);
8180
}
8281

8382
[Fact]
@@ -120,4 +119,132 @@ public void Given_TwoDifferentNetworkInstances_When_Compared_Then_AreNotEqual()
120119
Assert.False(network1.Equals(network2));
121120
Assert.False(network2.Equals(network1));
122121
}
122+
123+
[Fact]
124+
public void Builtin_Networks_ChainHash_Matches()
125+
{
126+
Assert.Equal(ChainConstants.Main, BitcoinNetwork.Mainnet.ChainHash);
127+
Assert.Equal(ChainConstants.Testnet, BitcoinNetwork.Testnet.ChainHash);
128+
Assert.Equal(ChainConstants.Regtest, BitcoinNetwork.Regtest.ChainHash);
129+
}
130+
131+
[Fact]
132+
public void Unregistered_CustomNetwork_Throws()
133+
{
134+
var net = new BitcoinNetwork("mycustomnet");
135+
Assert.Throws<InvalidOperationException>(() =>
136+
{
137+
var _ = net.ChainHash;
138+
});
139+
}
140+
141+
[Fact]
142+
public void RegisterCustomNetwork_ReturnsExpectedChainHash()
143+
{
144+
var customChainHash = DummyChainHash(0x42);
145+
// Register
146+
var tmpNet = new BitcoinNetwork("mycustomnet");
147+
tmpNet.Register("mycustomnet", customChainHash);
148+
149+
var net = new BitcoinNetwork("mycustomnet");
150+
Assert.Equal(customChainHash, net.ChainHash);
151+
}
152+
153+
[Fact]
154+
public void Register_IsCaseInsensitive()
155+
{
156+
var customChainHash = DummyChainHash(0xDD);
157+
var lower = "lowercase";
158+
var upper = "LOWERCASE";
159+
160+
var tmpNet = new BitcoinNetwork(lower);
161+
tmpNet.Register(upper, customChainHash);
162+
163+
var net1 = new BitcoinNetwork(lower);
164+
var net2 = new BitcoinNetwork(upper);
165+
Assert.Equal(customChainHash, net1.ChainHash);
166+
Assert.Equal(customChainHash, net2.ChainHash);
167+
}
168+
169+
[Fact]
170+
public void Unregister_RemovesChainHash()
171+
{
172+
var customChainHash = DummyChainHash(0xAB);
173+
var name = "toRemove";
174+
var net = new BitcoinNetwork(name);
175+
net.Register(name, customChainHash);
176+
177+
var useNet = new BitcoinNetwork(name);
178+
Assert.Equal(customChainHash, useNet.ChainHash);
179+
180+
BitcoinNetwork.Unregister(name);
181+
182+
var afterRemove = new BitcoinNetwork(name);
183+
Assert.Throws<InvalidOperationException>(() =>
184+
{
185+
var _ = afterRemove.ChainHash;
186+
});
187+
}
188+
189+
[Fact]
190+
public void ToString_And_Conversions_Work_For_Custom()
191+
{
192+
var customChainHash = DummyChainHash(0x5A);
193+
var name = "foo_bar";
194+
var bnet = new BitcoinNetwork(name);
195+
bnet.Register(name, customChainHash);
196+
197+
Assert.Equal(name, bnet.ToString());
198+
199+
string str = bnet;
200+
Assert.Equal(name.ToLowerInvariant(), str);
201+
202+
BitcoinNetwork net2 = name;
203+
Assert.Equal(name, net2.Name);
204+
Assert.Equal(customChainHash, net2.ChainHash);
205+
}
206+
207+
[Fact]
208+
public void Register_Throws_For_AlreadyExistingCustomNetwork()
209+
{
210+
var chainHash1 = DummyChainHash(0x13);
211+
var chainHash2 = DummyChainHash(0x53);
212+
var name = "networkdup";
213+
214+
// Ensure clean slate
215+
BitcoinNetwork.Unregister(name);
216+
217+
var net = new BitcoinNetwork(name);
218+
net.Register(name, chainHash1);
219+
220+
// Attempting to add again (any value) must throw
221+
var ex = Assert.Throws<InvalidOperationException>(() => net.Register(name, chainHash2));
222+
Assert.Contains("already registered", ex.Message, StringComparison.OrdinalIgnoreCase);
223+
224+
// Clean up for other tests
225+
BitcoinNetwork.Unregister(name);
226+
}
227+
228+
[Fact]
229+
public void Register_Throws_For_Null_Or_Whitespace_Name()
230+
{
231+
var net = new BitcoinNetwork("foo");
232+
var ch = DummyChainHash(0x77);
233+
234+
Assert.Throws<ArgumentNullException>(() => net.Register(null, ch));
235+
Assert.Throws<ArgumentNullException>(() => net.Register("", ch));
236+
Assert.Throws<ArgumentNullException>(() => net.Register(" ", ch));
237+
}
238+
239+
private static ChainHash DummyChainHash(byte fill)
240+
{
241+
// Create a 32-byte chainhash with a pattern to differentiate
242+
return new ChainHash(new[]
243+
{
244+
fill, fill, fill, fill, fill, fill, fill, fill, fill, fill, fill, fill, fill, fill,
245+
fill, fill,
246+
fill, fill, fill, fill, fill, fill, fill, fill, fill, fill, fill, fill, fill, fill,
247+
fill, fill
248+
});
249+
}
123250
}

0 commit comments

Comments
 (0)