diff --git a/src/Neo/NeoSystem.cs b/src/Neo/NeoSystem.cs
index 5d28ff5264..108bb66275 100644
--- a/src/Neo/NeoSystem.cs
+++ b/src/Neo/NeoSystem.cs
@@ -20,6 +20,7 @@
using Neo.SmartContract;
using Neo.SmartContract.Native;
using Neo.VM;
+using Neo.Wallets;
using System.Collections.Immutable;
namespace Neo;
@@ -119,9 +120,9 @@ static NeoSystem()
/// The path of the storage.
/// If is the default in-memory storage engine, this parameter is ignored.
///
- public NeoSystem(ProtocolSettings settings, string? storageProvider = null, string? storagePath = null) :
- this(settings, StoreFactory.GetStoreProvider(storageProvider ?? nameof(MemoryStore))
- ?? throw new ArgumentException($"Can't find the storage provider {storageProvider}", nameof(storageProvider)), storagePath)
+ /// The of the node.
+ public NeoSystem(ProtocolSettings settings, string? storageProvider = null, string? storagePath = null, KeyPair? nodeKey = null)
+ : this(settings, StoreFactory.GetStoreProvider(storageProvider ?? nameof(MemoryStore)) ?? throw new ArgumentException($"Can't find the storage provider {storageProvider}", nameof(storageProvider)), storagePath, nodeKey)
{
}
@@ -134,7 +135,8 @@ public NeoSystem(ProtocolSettings settings, string? storageProvider = null, stri
/// The path of the storage.
/// If is the default in-memory storage engine, this parameter is ignored.
///
- public NeoSystem(ProtocolSettings settings, IStoreProvider storageProvider, string? storagePath = null)
+ /// The of the node.
+ public NeoSystem(ProtocolSettings settings, IStoreProvider storageProvider, string? storagePath = null, KeyPair? nodeKey = null)
{
Settings = settings;
GenesisBlock = CreateGenesisBlock(settings);
@@ -142,7 +144,7 @@ public NeoSystem(ProtocolSettings settings, IStoreProvider storageProvider, stri
_store = storageProvider.GetStore(storagePath);
MemPool = new MemoryPool(this);
Blockchain = ActorSystem.ActorOf(Ledger.Blockchain.Props(this));
- LocalNode = ActorSystem.ActorOf(Network.P2P.LocalNode.Props(this));
+ LocalNode = ActorSystem.ActorOf(Network.P2P.LocalNode.Props(this, nodeKey ?? new()));
TaskManager = ActorSystem.ActorOf(Network.P2P.TaskManager.Props(this));
TxRouter = ActorSystem.ActorOf(TransactionRouter.Props(this));
foreach (var plugin in Plugin.Plugins)
diff --git a/src/Neo/Network/P2P/Helper.cs b/src/Neo/Network/P2P/Helper.cs
index 8dd3713710..d12ca2e1ae 100644
--- a/src/Neo/Network/P2P/Helper.cs
+++ b/src/Neo/Network/P2P/Helper.cs
@@ -10,6 +10,8 @@
// modifications are permitted.
using Neo.Cryptography;
+using Neo.Cryptography.ECC;
+using Neo.Extensions.IO;
using Neo.Network.P2P.Payloads;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
@@ -92,4 +94,24 @@ public static byte[] GetSignData(this UInt256 messageHash, uint network)
return buffer;
}
+
+ ///
+ /// Computes a unique node identifier based on the specified public key and protocol settings.
+ ///
+ /// The returned node identifier is deterministic for a given public key and protocol settings.
+ /// This method is typically used to generate consistent node IDs for distributed hash table (DHT) operations in the
+ /// NEO network.
+ /// The public key of the node for which to generate the identifier.
+ /// The protocol settings that provide network-specific information used in the identifier calculation.
+ /// A 256-bit hash value that uniquely identifies the node within the specified network.
+ public static UInt256 GetNodeId(this ECPoint pubkey, ProtocolSettings protocol)
+ {
+ const string prefix = "NEO_DHT_NODEID";
+ using var ms = new MemoryStream();
+ using var writer = new BinaryWriter(ms);
+ writer.WriteFixedString(prefix, prefix.Length);
+ writer.Write(protocol.Network);
+ writer.Write(pubkey);
+ return Crypto.Hash256(ms.ToArray());
+ }
}
diff --git a/src/Neo/Network/P2P/LocalNode.cs b/src/Neo/Network/P2P/LocalNode.cs
index d53c27a997..210f1845e8 100644
--- a/src/Neo/Network/P2P/LocalNode.cs
+++ b/src/Neo/Network/P2P/LocalNode.cs
@@ -16,6 +16,7 @@
using Neo.Network.P2P.Capabilities;
using Neo.Network.P2P.Payloads;
using Neo.SmartContract.Native;
+using Neo.Wallets;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
@@ -65,9 +66,14 @@ public record GetInstance;
public int UnconnectedCount => UnconnectedPeers.Count;
///
- /// The random number used to identify the local node.
+ /// Gets the cryptographic key pair associated with this node.
///
- public static readonly uint Nonce;
+ public KeyPair NodeKey { get; }
+
+ ///
+ /// Gets the unique identifier for the node.
+ ///
+ public UInt256 NodeId { get; }
///
/// The identifier of the client software of the local node.
@@ -76,7 +82,6 @@ public record GetInstance;
static LocalNode()
{
- Nonce = RandomNumberFactory.NextUInt32();
UserAgent = $"/{Assembly.GetExecutingAssembly().GetName().Name}:{Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)}/";
}
@@ -84,9 +89,12 @@ static LocalNode()
/// Initializes a new instance of the class.
///
/// The object that contains the .
- public LocalNode(NeoSystem system)
+ /// The used to identify the local node.
+ public LocalNode(NeoSystem system, KeyPair nodeKey)
{
this.system = system;
+ NodeKey = nodeKey;
+ NodeId = nodeKey.PublicKey.GetNodeId(system.Settings);
SeedList = new IPEndPoint[system.Settings.SeedList.Length];
// Start dns resolution in parallel
@@ -156,11 +164,11 @@ private static IPEndPoint GetIPEndpointFromHostPort(string hostNameOrAddress, in
public bool AllowNewConnection(IActorRef actor, RemoteNode node)
{
if (node.Version!.Network != system.Settings.Network) return false;
- if (node.Version.Nonce == Nonce) return false;
+ if (node.Version.NodeId == NodeId) return false;
// filter duplicate connections
foreach (var other in RemoteNodes.Values)
- if (other != node && other.Remote.Address.Equals(node.Remote.Address) && other.Version?.Nonce == node.Version.Nonce)
+ if (other != node && other.Remote.Address.Equals(node.Remote.Address) && other.Version?.NodeId == node.Version.NodeId)
return false;
if (node.Remote.Port != node.ListenerTcpPort && node.ListenerTcpPort != 0)
@@ -281,10 +289,11 @@ protected override void OnTcpConnected(IActorRef connection)
/// Gets a object used for creating the actor.
///
/// The object that contains the .
+ /// The used to identify the local node.
/// The object used for creating the actor.
- public static Props Props(NeoSystem system)
+ public static Props Props(NeoSystem system, KeyPair nodeKey)
{
- return Akka.Actor.Props.Create(() => new LocalNode(system));
+ return Akka.Actor.Props.Create(() => new LocalNode(system, nodeKey));
}
protected override Props ProtocolProps(object connection, IPEndPoint remote, IPEndPoint local)
diff --git a/src/Neo/Network/P2P/Payloads/VersionPayload.cs b/src/Neo/Network/P2P/Payloads/VersionPayload.cs
index 43da22f973..57dd0ad577 100644
--- a/src/Neo/Network/P2P/Payloads/VersionPayload.cs
+++ b/src/Neo/Network/P2P/Payloads/VersionPayload.cs
@@ -10,11 +10,14 @@
// modifications are permitted.
using Neo;
+using Neo.Cryptography;
+using Neo.Cryptography.ECC;
using Neo.Extensions;
using Neo.Extensions.Collections;
using Neo.Extensions.IO;
using Neo.IO;
using Neo.Network.P2P.Capabilities;
+using Neo.Wallets;
namespace Neo.Network.P2P.Payloads;
@@ -44,9 +47,14 @@ public class VersionPayload : ISerializable
public uint Timestamp;
///
- /// A random number used to identify the node.
+ /// Represents the public key associated with this node as an elliptic curve point.
///
- public uint Nonce;
+ public required ECPoint NodeKey;
+
+ ///
+ /// Represents the unique identifier for the node as a 256-bit unsigned integer.
+ ///
+ public required UInt256 NodeId;
///
/// A used to identify the client software of the node.
@@ -63,36 +71,51 @@ public class VersionPayload : ISerializable
///
public required NodeCapability[] Capabilities;
+ ///
+ /// The digital signature of the payload.
+ ///
+ public required byte[] Signature;
+
public int Size =>
sizeof(uint) + // Network
sizeof(uint) + // Version
sizeof(uint) + // Timestamp
- sizeof(uint) + // Nonce
+ NodeKey.Size + // NodeKey
+ UInt256.Length + // NodeId
UserAgent.GetVarSize() + // UserAgent
- Capabilities.GetVarSize(); // Capabilities
+ Capabilities.GetVarSize() + // Capabilities
+ Signature.GetVarSize(); // Signature
///
/// Creates a new instance of the class.
///
- /// The magic number of the network.
- /// The random number used to identify the node.
+ /// The of the network.
+ /// The used to identify the node.
/// The used to identify the client software of the node.
/// The capabilities of the node.
///
- public static VersionPayload Create(uint network, uint nonce, string userAgent, params NodeCapability[] capabilities)
+ public static VersionPayload Create(ProtocolSettings protocol, KeyPair nodeKey, string userAgent, params NodeCapability[] capabilities)
{
var ret = new VersionPayload
{
- Network = network,
+ Network = protocol.Network,
Version = LocalNode.ProtocolVersion,
Timestamp = DateTime.UtcNow.ToTimestamp(),
- Nonce = nonce,
+ NodeKey = nodeKey.PublicKey,
+ NodeId = nodeKey.PublicKey.GetNodeId(protocol),
UserAgent = userAgent,
Capabilities = capabilities,
+ Signature = [],
// Computed
AllowCompression = !capabilities.Any(u => u is DisableCompressionCapability)
};
+ // Generate signature
+ using var ms = new MemoryStream();
+ using var writer = new BinaryWriter(ms);
+ ret.Serialize(writer, false);
+ ret.Signature = Crypto.Sign(ms.ToArray(), nodeKey.PrivateKey);
+
return ret;
}
@@ -101,7 +124,8 @@ void ISerializable.Deserialize(ref MemoryReader reader)
Network = reader.ReadUInt32();
Version = reader.ReadUInt32();
Timestamp = reader.ReadUInt32();
- Nonce = reader.ReadUInt32();
+ NodeKey = reader.ReadSerializable();
+ NodeId = reader.ReadSerializable();
UserAgent = reader.ReadVarString(1024);
// Capabilities
@@ -112,16 +136,33 @@ void ISerializable.Deserialize(ref MemoryReader reader)
if (capabilities.Select(p => p.Type).Distinct().Count() != capabilities.Count())
throw new FormatException("Duplicating capabilities are included");
+ Signature = reader.ReadVarMemory().ToArray();
AllowCompression = !capabilities.Any(u => u is DisableCompressionCapability);
}
void ISerializable.Serialize(BinaryWriter writer)
+ {
+ Serialize(writer, true);
+ }
+
+ void Serialize(BinaryWriter writer, bool withSignature)
{
writer.Write(Network);
writer.Write(Version);
writer.Write(Timestamp);
- writer.Write(Nonce);
+ writer.Write(NodeKey);
+ writer.Write(NodeId);
writer.WriteVarString(UserAgent);
writer.Write(Capabilities);
+ if (withSignature) writer.WriteVarBytes(Signature);
+ }
+
+ public bool Verify(ProtocolSettings protocol)
+ {
+ if (NodeId != NodeKey.GetNodeId(protocol)) return false;
+ using var ms = new MemoryStream();
+ using var writer = new BinaryWriter(ms);
+ Serialize(writer, false);
+ return Crypto.VerifySignature(ms.ToArray(), Signature, NodeKey);
}
}
diff --git a/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs b/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs
index 2fad8f5860..e96939e2a2 100644
--- a/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs
+++ b/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs
@@ -390,6 +390,7 @@ private void OnVerackMessageReceived()
private void OnVersionMessageReceived(VersionPayload payload)
{
+ if (!payload.Verify(_system.Settings)) throw new ProtocolViolationException();
Version = payload;
foreach (NodeCapability capability in payload.Capabilities)
{
diff --git a/src/Neo/Network/P2P/RemoteNode.cs b/src/Neo/Network/P2P/RemoteNode.cs
index 5e3248b309..63ce38008c 100644
--- a/src/Neo/Network/P2P/RemoteNode.cs
+++ b/src/Neo/Network/P2P/RemoteNode.cs
@@ -199,7 +199,7 @@ private void OnSend(IInventory inventory)
private void OnStartProtocol()
{
- SendMessage(Message.Create(MessageCommand.Version, VersionPayload.Create(_system.Settings.Network, LocalNode.Nonce, LocalNode.UserAgent, _localNode.GetNodeCapabilities())));
+ SendMessage(Message.Create(MessageCommand.Version, VersionPayload.Create(_system.Settings, _localNode.NodeKey, LocalNode.UserAgent, _localNode.GetNodeCapabilities())));
}
protected override void PostStop()
diff --git a/src/Neo/Wallets/KeyPair.cs b/src/Neo/Wallets/KeyPair.cs
index 1f5bca7328..d70d1b65bb 100644
--- a/src/Neo/Wallets/KeyPair.cs
+++ b/src/Neo/Wallets/KeyPair.cs
@@ -41,6 +41,16 @@ public class KeyPair : IEquatable
///
public UInt160 PublicKeyHash => PublicKey.EncodePoint(true).ToScriptHash();
+ ///
+ /// Initializes a new instance of the KeyPair class using a randomly generated 32-byte private key.
+ ///
+ /// This constructor generates a cryptographically secure random private key for the key pair.
+ /// Use this overload when you want to create a new key pair with a unique, unpredictable private key suitable for
+ /// cryptographic operations.
+ public KeyPair() : this(RandomNumberGenerator.GetBytes(32))
+ {
+ }
+
///
/// Initializes a new instance of the class.
///
diff --git a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_VersionPayload.cs b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_VersionPayload.cs
index 279ae09032..d5c533a762 100644
--- a/tests/Neo.UnitTests/Network/P2P/Payloads/UT_VersionPayload.cs
+++ b/tests/Neo.UnitTests/Network/P2P/Payloads/UT_VersionPayload.cs
@@ -23,39 +23,29 @@ public class UT_VersionPayload
[TestMethod]
public void SizeAndEndPoint_Get()
{
- var test = new VersionPayload() { Capabilities = Array.Empty(), UserAgent = "neo3" };
- Assert.AreEqual(22, test.Size);
+ var test = VersionPayload.Create(ProtocolSettings.Default, new(), "neo3");
+ Assert.AreEqual(148, test.Size);
- test = VersionPayload.Create(123, 456, "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) });
- Assert.AreEqual(25, test.Size);
+ test = VersionPayload.Create(ProtocolSettings.Default, new(), "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) });
+ Assert.AreEqual(151, test.Size);
}
[TestMethod]
public void DeserializeAndSerialize()
{
- var test = VersionPayload.Create(123, 456, "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) });
+ var test = VersionPayload.Create(ProtocolSettings.Default, new(), "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) });
var clone = test.ToArray().AsSerializable();
CollectionAssert.AreEqual(test.Capabilities.ToByteArray(), clone.Capabilities.ToByteArray());
Assert.AreEqual(test.UserAgent, clone.UserAgent);
- Assert.AreEqual(test.Nonce, clone.Nonce);
+ Assert.AreEqual(test.NodeId, clone.NodeId);
Assert.AreEqual(test.Timestamp, clone.Timestamp);
CollectionAssert.AreEqual(test.Capabilities.ToByteArray(), clone.Capabilities.ToByteArray());
- Assert.ThrowsExactly(() => _ = VersionPayload.Create(123, 456, "neo3",
+ Assert.ThrowsExactly(() => _ = VersionPayload.Create(ProtocolSettings.Default, new(), "neo3",
new NodeCapability[] {
new ServerCapability(NodeCapabilityType.TcpServer, 22) ,
new ServerCapability(NodeCapabilityType.TcpServer, 22)
}).ToArray().AsSerializable());
-
- var buf = test.ToArray();
- buf[buf.Length - 2 - 1 - 1] += 3; // We've got 1 capability with 2 bytes, this adds three more to the array size.
- buf = buf.Concat(new byte[] { 0xfe, 0x00 }).ToArray(); // Type = 0xfe, zero bytes of data.
- buf = buf.Concat(new byte[] { 0xfd, 0x02, 0x00, 0x00 }).ToArray(); // Type = 0xfd, two bytes of data.
- buf = buf.Concat(new byte[] { 0x10, 0x01, 0x00, 0x00, 0x00 }).ToArray(); // FullNode capability, 0x01 index.
-
- clone = buf.AsSerializable();
- Assert.HasCount(4, clone.Capabilities);
- Assert.AreEqual(2, clone.Capabilities.OfType().Count());
}
}
diff --git a/tests/Neo.UnitTests/Network/P2P/UT_RemoteNode.cs b/tests/Neo.UnitTests/Network/P2P/UT_RemoteNode.cs
index 7b47611d2f..281f7f1aea 100644
--- a/tests/Neo.UnitTests/Network/P2P/UT_RemoteNode.cs
+++ b/tests/Neo.UnitTests/Network/P2P/UT_RemoteNode.cs
@@ -39,20 +39,9 @@ public static void TestSetup(TestContext ctx)
public void RemoteNode_Test_Abort_DifferentNetwork()
{
var connectionTestProbe = CreateTestProbe();
- var remoteNodeActor = ActorOfAsTestActorRef(() => new RemoteNode(_system, new LocalNode(_system), connectionTestProbe, null!, null!, new ChannelsConfig()));
+ var remoteNodeActor = ActorOfAsTestActorRef(() => new RemoteNode(_system, new LocalNode(_system, new()), connectionTestProbe, null!, null!, new ChannelsConfig()));
- var msg = Message.Create(MessageCommand.Version, new VersionPayload
- {
- UserAgent = "".PadLeft(1024, '0'),
- Nonce = 1,
- Network = 2,
- Timestamp = 5,
- Version = 6,
- Capabilities =
- [
- new ServerCapability(NodeCapabilityType.TcpServer, 25)
- ]
- });
+ var msg = Message.Create(MessageCommand.Version, VersionPayload.Create(ProtocolSettings.Default with { Network = 2 }, new(), "".PadLeft(1024, '0'), new ServerCapability(NodeCapabilityType.TcpServer, 25)));
var testProbe = CreateTestProbe();
testProbe.Send(remoteNodeActor, new Tcp.Received((ByteString)msg.ToArray()));
@@ -66,22 +55,11 @@ public void RemoteNode_Test_Accept_IfSameNetwork()
var connectionTestProbe = CreateTestProbe();
var remoteNodeActor = ActorOfAsTestActorRef(() =>
new RemoteNode(_system,
- new LocalNode(_system),
+ new LocalNode(_system, new()),
connectionTestProbe,
new IPEndPoint(IPAddress.Parse("192.168.1.2"), 8080), new IPEndPoint(IPAddress.Parse("192.168.1.1"), 8080), new ChannelsConfig()));
- var msg = Message.Create(MessageCommand.Version, new VersionPayload()
- {
- UserAgent = "Unit Test".PadLeft(1024, '0'),
- Nonce = 1,
- Network = TestProtocolSettings.Default.Network,
- Timestamp = 5,
- Version = 6,
- Capabilities =
- [
- new ServerCapability(NodeCapabilityType.TcpServer, 25)
- ]
- });
+ var msg = Message.Create(MessageCommand.Version, VersionPayload.Create(TestProtocolSettings.Default, new(), "Unit Test".PadLeft(1024, '0'), new ServerCapability(NodeCapabilityType.TcpServer, 25)));
var testProbe = CreateTestProbe();
testProbe.Send(remoteNodeActor, new Tcp.Received((ByteString)msg.ToArray()));
diff --git a/tests/Neo.UnitTests/Network/P2P/UT_TaskSession.cs b/tests/Neo.UnitTests/Network/P2P/UT_TaskSession.cs
index b2cb3aeb87..6755caffd1 100644
--- a/tests/Neo.UnitTests/Network/P2P/UT_TaskSession.cs
+++ b/tests/Neo.UnitTests/Network/P2P/UT_TaskSession.cs
@@ -21,14 +21,14 @@ public class UT_TaskSession
[TestMethod]
public void CreateTest()
{
- var ses = new TaskSession(new VersionPayload() { Capabilities = new NodeCapability[] { new FullNodeCapability(123) }, UserAgent = "" });
+ var ses = new TaskSession(VersionPayload.Create(ProtocolSettings.Default, new(), "", new FullNodeCapability(123)));
Assert.IsFalse(ses.HasTooManyTasks);
Assert.AreEqual((uint)123, ses.LastBlockIndex);
Assert.IsEmpty(ses.IndexTasks);
Assert.IsTrue(ses.IsFullNode);
- ses = new TaskSession(new VersionPayload() { Capabilities = Array.Empty(), UserAgent = "" });
+ ses = new TaskSession(VersionPayload.Create(ProtocolSettings.Default, new(), ""));
Assert.IsFalse(ses.HasTooManyTasks);
Assert.AreEqual((uint)0, ses.LastBlockIndex);