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: 9 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ jobs:
env:
TEST_OPTS: -c ${{ env.BUILD_CONFIG }} --no-restore
run: |
dotnet test --project Libp2p.Core.Tests/Libp2p.Core.Tests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.Protocols.Multistream.Tests/Libp2p.Protocols.Multistream.Tests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.Protocols.Noise.Tests/Libp2p.Protocols.Noise.Tests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.Protocols.Pubsub.Tests/Libp2p.Protocols.Pubsub.Tests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.Protocols.Quic.Tests/Libp2p.Protocols.Quic.Tests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.Protocols.Yamux.Tests/Libp2p.Protocols.Yamux.Tests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.E2eTests/Libp2p.E2eTests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.Protocols.Pubsub.E2eTests/Libp2p.Protocols.Pubsub.E2eTests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.Protocols.PubsubPeerDiscovery.E2eTests/Libp2p.Protocols.PubsubPeerDiscovery.E2eTests.csproj ${{ env.PACK_OPTS }}
dotnet test --project Libp2p.Core.Tests/Libp2p.Core.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test --project Libp2p.Protocols.Multistream.Tests/Libp2p.Protocols.Multistream.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test --project Libp2p.Protocols.Noise.Tests/Libp2p.Protocols.Noise.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test --project Libp2p.Protocols.Pubsub.Tests/Libp2p.Protocols.Pubsub.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test --project Libp2p.Protocols.Quic.Tests/Libp2p.Protocols.Quic.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test --project Libp2p.Protocols.Yamux.Tests/Libp2p.Protocols.Yamux.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test --project Libp2p.E2eTests/Libp2p.E2eTests.csproj ${{ env.TEST_OPTS }}
dotnet test --project Libp2p.Protocols.Pubsub.E2eTests/Libp2p.Protocols.Pubsub.E2eTests.csproj ${{ env.TEST_OPTS }}
dotnet test --project Libp2p.Protocols.PubsubPeerDiscovery.E2eTests/Libp2p.Protocols.PubsubPeerDiscovery.E2eTests.csproj ${{ env.TEST_OPTS }}
38 changes: 23 additions & 15 deletions src/libp2p/Libp2p.Core.Tests/MultiaddrResolverTests.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: MIT

using System.Collections.Generic;
using System.Linq;
using NSubstitute;
using NUnit.Framework;
using Multiformats.Address;

namespace Nethermind.Libp2p.Core.Tests;

public class MultiaddrResolverTests
{
[Explicit("DNS may change")]
[Test]
public async Task Test()
public async Task Test_Resolve_Dnsaddr_UsesInjectedResolver()
{
Multiaddress[] addrs = [
"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
];
// Arrange: mock DNS TXT records for _dnsaddr.bootstrap.libp2p.io
var dns = NSubstitute.Substitute.For<IDnsLookup>();
string dnsName = "_dnsaddr.bootstrap.libp2p.io";
var txtRecords = new[] {
"dnsaddr=/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
"dnsaddr=/ip4/1.2.3.4/tcp/4001/p2p/QmNLei78zWmzUdbeRB3CiUfAizWUrbeeZh5K1rhAQKCh51"
};
dns.QueryTxtAsync(dnsName).Returns(Task.FromResult((IEnumerable<string>)txtRecords));

foreach (Multiaddress addr in addrs)
var resolver = new MultiaddrResolver(dns);

// Act
var results = new List<Multiaddress>();
Multiaddress input = "/dnsaddr/bootstrap.libp2p.io/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ";
await foreach (var item in resolver.Resolve(input))
{
await foreach (var item in new MultiaddrResolver().Resolve(addr))
{
TestContext.Out.WriteLine(item);
}
results.Add(item);
}

Assert.Pass();
// Assert: expect to get the first address (matching the p2p filter)
Assert.That(results.Count, Is.GreaterThan(0));
Assert.That(results[0].ToString(), Is.EqualTo("/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"));
}
}
98 changes: 50 additions & 48 deletions src/libp2p/Libp2p.Core.Tests/VarintTests.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,58 @@
namespace Nethermind.Libp2p.Core.Tests;

public class VarintTests
{
[Test]
public async Task Test_VarInt_Assumption_And_Roundtrip_Encoding()
{
Assert.That(5, Is.EqualTo(VarInt.GetSizeInBytes(System.Int32.MinValue)));
Assert.That(1, Is.EqualTo(VarInt.GetSizeInBytes(System.UInt64.MinValue)));
Assert.That(5, Is.EqualTo(VarInt.GetSizeInBytes(System.Int32.MaxValue)));
Assert.That(10, Is.EqualTo(VarInt.GetSizeInBytes(System.UInt64.MaxValue)));
Assert.That(1, Is.EqualTo(VarInt.GetSizeInBytes(1UL)));
Assert.That(1, Is.EqualTo(VarInt.GetSizeInBytes(0UL)));
Assert.That(1, Is.EqualTo(VarInt.GetSizeInBytes(1)));
Assert.That(1, Is.EqualTo(VarInt.GetSizeInBytes(0)));
Span<byte> memoryA = stackalloc byte[10];
int offset = 0;
VarInt.Encode(System.UInt64.MaxValue, memoryA, ref offset);
offset = 0;
Assert.That(System.UInt64.MaxValue, Is.EqualTo(VarInt.Decode(memoryA, ref offset)));
offset = 0;
memoryA.Clear();
[Test]
public async Task Test_VarInt_Assumption_And_Roundtrip_Encoding()
{
Assert.That(VarInt.GetSizeInBytes(System.Int32.MinValue), Is.EqualTo(5));
Assert.That(VarInt.GetSizeInBytes(System.UInt64.MinValue), Is.EqualTo(1));
Assert.That(VarInt.GetSizeInBytes(System.Int32.MaxValue), Is.EqualTo(5));
Assert.That(VarInt.GetSizeInBytes(System.UInt64.MaxValue), Is.EqualTo(10));
Assert.That(VarInt.GetSizeInBytes(1UL), Is.EqualTo(1));
Assert.That(VarInt.GetSizeInBytes(0UL), Is.EqualTo(1));
Assert.That(VarInt.GetSizeInBytes(1), Is.EqualTo(1));
Assert.That(VarInt.GetSizeInBytes(0), Is.EqualTo(1));
Span<byte> memoryA = stackalloc byte[10];
int offset = 0;
VarInt.Encode(System.UInt64.MaxValue, memoryA, ref offset);
offset = 0;
Assert.That(VarInt.Decode(memoryA, ref offset), Is.EqualTo(System.UInt64.MaxValue));
offset = 0;
memoryA.Clear();

VarInt.Encode(System.UInt64.MinValue, memoryA, ref offset);
offset = 0;
Assert.That(VarInt.Decode(memoryA, ref offset), Is.EqualTo(System.UInt64.MinValue));
offset = 0;
memoryA.Clear();

VarInt.Encode(System.UInt64.MinValue, memoryA, ref offset);
offset = 0;
Assert.That(System.UInt64.MinValue, Is.EqualTo(VarInt.Decode(memoryA, ref offset)));
offset = 0;
memoryA.Clear();
System.UInt64 roundtrip_ulong_target = (ulong)System.Random.Shared.NextInt64();
VarInt.Encode(roundtrip_ulong_target, memoryA, ref offset);
offset = 0;
Assert.That(roundtrip_ulong_target, Is.EqualTo(VarInt.Decode(memoryA, ref offset)));
offset = 0;
memoryA.Clear();

System.UInt64 roundtrip_ulong_target = (ulong) System.Random.Shared.NextInt64();
VarInt.Encode(roundtrip_ulong_target, memoryA, ref offset);
offset = 0;
Assert.That(roundtrip_ulong_target, Is.EqualTo(VarInt.Decode(memoryA, ref offset)));
offset = 0;
memoryA.Clear();
Span<byte> memoryB = stackalloc byte[5];
VarInt.Encode(System.Int32.MaxValue, memoryB, ref offset);
offset = 0;
Assert.That(VarInt.Decode(memoryB, ref offset), Is.EqualTo(System.Int32.MaxValue));
offset = 0;
memoryB.Clear();

Span<byte> memoryB = stackalloc byte[5];
VarInt.Encode(System.Int32.MaxValue, memoryB, ref offset);
offset = 0;
Assert.That(System.Int32.MaxValue, Is.EqualTo(VarInt.Decode(memoryB, ref offset)));
offset = 0;
memoryB.Clear();
VarInt.Encode(System.Int32.MinValue, memoryB, ref offset);
offset = 0;
Assert.That((int)VarInt.Decode(memoryB, ref offset), Is.EqualTo(System.Int32.MinValue));
offset = 0;
memoryB.Clear();

VarInt.Encode(System.Int32.MinValue, memoryB, ref offset);
offset = 0;
Assert.That(System.Int32.MinValue, Is.EqualTo((int) VarInt.Decode(memoryB, ref offset)));
offset = 0;
memoryB.Clear();
System.Int32 roundtrip_int_target = System.Random.Shared.Next();
VarInt.Encode(roundtrip_int_target, memoryB, ref offset);
offset = 0;
Assert.That(roundtrip_int_target, Is.EqualTo((int)VarInt.Decode(memoryB, ref offset)));
offset = 0;
memoryB.Clear();
}
}

System.Int32 roundtrip_int_target = System.Random.Shared.Next();
VarInt.Encode(roundtrip_int_target, memoryB, ref offset);
offset = 0;
Assert.That(roundtrip_int_target, Is.EqualTo((int) VarInt.Decode(memoryB, ref offset)));
offset = 0;
memoryB.Clear();
}
}
33 changes: 33 additions & 0 deletions src/libp2p/Libp2p.Core/DnsClientLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Net;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DnsClient;
using DnsClient.Protocol;

namespace Nethermind.Libp2p.Core;

public class DnsClientLookup : IDnsLookup
{
private readonly LookupClient _lookup;

public DnsClientLookup() => _lookup = new LookupClient();

public async Task<IEnumerable<string>> QueryTxtAsync(string name)
{
IDnsQueryResponse result = await _lookup.QueryAsync(name, QueryType.TXT);
return result.Answers.TxtRecords().SelectMany(r => r.Text ?? Enumerable.Empty<string>());
}

public async Task<IEnumerable<System.Net.IPAddress>> QueryAAsync(string name)
{
IDnsQueryResponse result = await _lookup.QueryAsync(name, QueryType.A);
return result.Answers.ARecords().Select(r => r.Address);
}

public async Task<IEnumerable<System.Net.IPAddress>> QueryAaaaAsync(string name)
{
IDnsQueryResponse result = await _lookup.QueryAsync(name, QueryType.AAAA);
return result.Answers.AaaaRecords().Select(r => r.Address);
}
}
13 changes: 13 additions & 0 deletions src/libp2p/Libp2p.Core/IDnsLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Net;
using System.Collections.Generic;
using System.Threading.Tasks;
using DnsClient;

namespace Nethermind.Libp2p.Core;

public interface IDnsLookup
{
Task<IEnumerable<string>> QueryTxtAsync(string name);
Task<IEnumerable<IPAddress>> QueryAAsync(string name);
Task<IEnumerable<IPAddress>> QueryAaaaAsync(string name);
}
46 changes: 23 additions & 23 deletions src/libp2p/Libp2p.Core/MultiaddrResolver.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: MIT

using System.Net;
using DnsClient;
using Multiformats.Address;
using Multiformats.Address.Protocols;
Expand All @@ -10,7 +11,12 @@ namespace Nethermind.Libp2p.Core;

public class MultiaddrResolver
{
LookupClient? lookup = null;
private readonly IDnsLookup _dns;

public MultiaddrResolver(IDnsLookup? dns = null)
{
_dns = dns ?? new DnsClientLookup();
}

/// <summary>
/// Converts DNS/DNS4/DNS6/dnsaddr to non-unique IP4/IP6-based addresses
Expand All @@ -25,21 +31,17 @@ public async IAsyncEnumerable<Multiaddress> Resolve(Multiaddress addr)
{
async IAsyncEnumerable<string> GetRecords(string dnsAddr)
{
lookup ??= new();
IDnsQueryResponse result = await lookup.QueryAsync(dnsAddr, QueryType.TXT);
foreach (DnsClient.Protocol.TxtRecord? record in result.Answers.TxtRecords())
IEnumerable<string> records = await _dns.QueryTxtAsync(dnsAddr);
foreach (string text in records)
{
foreach (string? text in record.Text)
{
const string prefix = "dnsaddr=";
const string prefix = "dnsaddr=";

if (text.StartsWith(prefix))
if (text.StartsWith(prefix))
{
Multiaddress addr = text[prefix.Length..];
if (p2p is null || (addr.Has<P2P>() && addr.Get<P2P>().Value.Equals(p2p)))
{
Multiaddress addr = text[prefix.Length..];
if (p2p is null || (addr.Has<P2P>() && addr.Get<P2P>().Value.Equals(p2p)))
{
yield return text[prefix.Length..];
}
yield return text[prefix.Length..];
}
}
}
Expand All @@ -59,36 +61,34 @@ async IAsyncEnumerable<string> GetRecords(string dnsAddr)
if (addr.Has<DNS6>() || addr.Has<DNS>())
{
resolved = true;
lookup ??= new();
IDnsQueryResponse result = await lookup.QueryAsync(addr.Get<DNS6>()?.ToString() ?? addr.Get<DNS>().ToString(), QueryType.AAAA);
IEnumerable<IPAddress> addrs = await _dns.QueryAaaaAsync(addr.Get<DNS6>()?.ToString() ?? addr.Get<DNS>().ToString());

foreach (DnsClient.Protocol.AaaaRecord? record in result.Answers.AaaaRecords())
foreach (IPAddress record in addrs)
{
if (addr.Has<DNS6>())
{
yield return addr.Clone().Replace<DNS6, IP6>(record.Address);
yield return addr.Clone().Replace<DNS6, IP6>(record);
}
else
{
yield return addr.Clone().Replace<DNS, IP6>(record.Address);
yield return addr.Clone().Replace<DNS, IP6>(record);
}
}
}
if (addr.Has<DNS4>() || addr.Has<DNS>())
{
resolved = true;
lookup ??= new();
IDnsQueryResponse result = await lookup.QueryAsync(addr.Get<DNS4>()?.ToString() ?? addr.Get<DNS>().ToString(), QueryType.A);
IEnumerable<IPAddress> addrs4 = await _dns.QueryAAsync(addr.Get<DNS4>()?.ToString() ?? addr.Get<DNS>().ToString());

foreach (DnsClient.Protocol.ARecord? record in result.Answers.ARecords())
foreach (IPAddress record in addrs4)
{
if (addr.Has<DNS4>())
{
yield return addr.Clone().Replace<DNS4, IP4>(record.Address);
yield return addr.Clone().Replace<DNS4, IP4>(record);
}
else
{
yield return addr.Clone().Replace<DNS, IP4>(record.Address);
yield return addr.Clone().Replace<DNS, IP4>(record);
}
}
}
Expand Down
14 changes: 10 additions & 4 deletions src/libp2p/Libp2p.Core/PeerId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,16 @@ public PeerId(byte[] bytes)

public string ToCidString()
{
byte[] encodedPeerIdBytes = new byte[Bytes.Length + 2];
encodedPeerIdBytes[0] = (byte)Cid.Cidv1;
encodedPeerIdBytes[1] = (byte)Ipld.Libp2pKey;
Array.Copy(Bytes, 0, encodedPeerIdBytes, 2, Bytes.Length);
// CID format: <multibase><cid-version><multicodec><multihash>
// Calculate required buffer size
int codecSize = VarInt.GetSizeInBytes((int)Ipld.Libp2pKey);
byte[] encodedPeerIdBytes = new byte[1 + codecSize + Bytes.Length];

int offset = 0;
encodedPeerIdBytes[offset++] = (byte)Cid.Cidv1;
VarInt.Encode((int)Ipld.Libp2pKey, encodedPeerIdBytes, ref offset);
Array.Copy(Bytes, 0, encodedPeerIdBytes, offset, Bytes.Length);

return Multibase.Encode(MultibaseEncoding.Base32Lower, encodedPeerIdBytes);
}

Expand Down
14 changes: 9 additions & 5 deletions src/libp2p/Libp2p.Core/VarInt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// SPDX-License-Identifier: MIT

namespace Nethermind.Libp2p.Core;

using System.Runtime.CompilerServices;
public static class VarInt
{
//(ulong)((uint) number) casts number to uint first then ulong because apparently I had to
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(int number, Span<byte> buf, ref int offset) => Encode((ulong)((uint) number), buf, ref offset);

public static void Encode(int number, Span<byte> buf, ref int offset) =>
Encode((ulong)((uint)number), buf, ref offset);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ulong number, Span<byte> buf, ref int offset)
{
Expand All @@ -33,7 +35,8 @@ public static void Encode(ulong number, Span<byte> buf, ref int offset)

//(ulong)((uint) number) casts number to uint first then ulong because apparently I had to
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetSizeInBytes(int number) => GetSizeInBytes((ulong)((uint) number));
public static int GetSizeInBytes(int number) =>
GetSizeInBytes((ulong)((uint)number));

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetSizeInBytes(ulong number)
Expand All @@ -59,12 +62,12 @@ public static ulong Decode(Span<byte> source, ref int offset)
byte @byte = source[offset + bytesRead++];
// Use the AND operator (& 0x7F) to get the 7 bits of data
// Use the OR operator (|=) to add them to the result
result |= ((ulong) (@byte & 0x7F)) << shift;
result |= ((ulong)(@byte & 0x7F)) << shift;

// Check the 8th bit: If it is 0, return the result
if ((@byte & 0x80) == 0)
{
offset = offset + (shift / 7);
offset = offset + bytesRead;
return result;
}
shift += 7;
Expand Down Expand Up @@ -110,3 +113,4 @@ public static async Task<int> Decode(IReader buf, CancellationToken token = defa
throw new FormatException("Invalid 7-bit encoding");
}
}

Loading
Loading