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
12 changes: 7 additions & 5 deletions src/Neo/NeoSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Neo.SmartContract;
using Neo.SmartContract.Native;
using Neo.VM;
using Neo.Wallets;
using System.Collections.Immutable;

namespace Neo;
Expand Down Expand Up @@ -119,9 +120,9 @@ static NeoSystem()
/// The path of the storage.
/// If <paramref name="storageProvider"/> is the default in-memory storage engine, this parameter is ignored.
/// </param>
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)
/// <param name="nodeKey">The <see cref="KeyPair"/> of the node.</param>
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)
{
}

Expand All @@ -134,15 +135,16 @@ public NeoSystem(ProtocolSettings settings, string? storageProvider = null, stri
/// The path of the storage.
/// If <paramref name="storageProvider"/> is the default in-memory storage engine, this parameter is ignored.
/// </param>
public NeoSystem(ProtocolSettings settings, IStoreProvider storageProvider, string? storagePath = null)
/// <param name="nodeKey">The <see cref="KeyPair"/> of the node.</param>
public NeoSystem(ProtocolSettings settings, IStoreProvider storageProvider, string? storagePath = null, KeyPair? nodeKey = null)
{
Settings = settings;
GenesisBlock = CreateGenesisBlock(settings);
StorageProvider = storageProvider;
_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)
Expand Down
22 changes: 22 additions & 0 deletions src/Neo/Network/P2P/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,4 +94,24 @@ public static byte[] GetSignData(this UInt256 messageHash, uint network)

return buffer;
}

/// <summary>
/// Computes a unique node identifier based on the specified public key and protocol settings.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="pubkey">The public key of the node for which to generate the identifier.</param>
/// <param name="protocol">The protocol settings that provide network-specific information used in the identifier calculation.</param>
/// <returns>A 256-bit hash value that uniquely identifies the node within the specified network.</returns>
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());
}
}
25 changes: 17 additions & 8 deletions src/Neo/Network/P2P/LocalNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,9 +66,14 @@ public record GetInstance;
public int UnconnectedCount => UnconnectedPeers.Count;

/// <summary>
/// The random number used to identify the local node.
/// Gets the cryptographic key pair associated with this node.
/// </summary>
public static readonly uint Nonce;
public KeyPair NodeKey { get; }

/// <summary>
/// Gets the unique identifier for the node.
/// </summary>
public UInt256 NodeId { get; }

/// <summary>
/// The identifier of the client software of the local node.
Expand All @@ -76,17 +82,19 @@ public record GetInstance;

static LocalNode()
{
Nonce = RandomNumberFactory.NextUInt32();
UserAgent = $"/{Assembly.GetExecutingAssembly().GetName().Name}:{Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)}/";
}

/// <summary>
/// Initializes a new instance of the <see cref="LocalNode"/> class.
/// </summary>
/// <param name="system">The <see cref="NeoSystem"/> object that contains the <see cref="LocalNode"/>.</param>
public LocalNode(NeoSystem system)
/// <param name="nodeKey">The <see cref="KeyPair"/> used to identify the local node.</param>
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -281,10 +289,11 @@ protected override void OnTcpConnected(IActorRef connection)
/// Gets a <see cref="Akka.Actor.Props"/> object used for creating the <see cref="LocalNode"/> actor.
/// </summary>
/// <param name="system">The <see cref="NeoSystem"/> object that contains the <see cref="LocalNode"/>.</param>
/// <param name="nodeKey">The <see cref="KeyPair"/> used to identify the local node.</param>
/// <returns>The <see cref="Akka.Actor.Props"/> object used for creating the <see cref="LocalNode"/> actor.</returns>
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)
Expand Down
63 changes: 52 additions & 11 deletions src/Neo/Network/P2P/Payloads/VersionPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -44,9 +47,14 @@ public class VersionPayload : ISerializable
public uint Timestamp;

/// <summary>
/// A random number used to identify the node.
/// Represents the public key associated with this node as an elliptic curve point.
/// </summary>
public uint Nonce;
public required ECPoint NodeKey;

/// <summary>
/// Represents the unique identifier for the node as a 256-bit unsigned integer.
/// </summary>
public required UInt256 NodeId;

/// <summary>
/// A <see cref="string"/> used to identify the client software of the node.
Expand All @@ -63,36 +71,51 @@ public class VersionPayload : ISerializable
/// </summary>
public required NodeCapability[] Capabilities;

/// <summary>
/// The digital signature of the payload.
/// </summary>
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

/// <summary>
/// Creates a new instance of the <see cref="VersionPayload"/> class.
/// </summary>
/// <param name="network">The magic number of the network.</param>
/// <param name="nonce">The random number used to identify the node.</param>
/// <param name="protocol">The <see cref="ProtocolSettings"/> of the network.</param>
/// <param name="nodeKey">The <see cref="ECPoint"/> used to identify the node.</param>
/// <param name="userAgent">The <see cref="string"/> used to identify the client software of the node.</param>
/// <param name="capabilities">The capabilities of the node.</param>
/// <returns></returns>
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;
}

Expand All @@ -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<ECPoint>();
NodeId = reader.ReadSerializable<UInt256>();
UserAgent = reader.ReadVarString(1024);

// Capabilities
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove private keyword? It needs to be clear Identification of access modifier for the method visibility. Whether it be public, private, internal, and protected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private is the default one.

Copy link
Member

@cschuchardt88 cschuchardt88 Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on the compilers and/or IDE environment and default configurations for them. Well I just going to saying it. I fought ever since I started working with neo to get to some kind of standard for our code. So it eliminates down time for developers, whether they be new or existing for this open source project. So everything is clean, neat, all the same and simple to understand/debug/test and add new features. So, I created an .editorconfig file with restrictions for safe pointing and stopping messy and error proven code or lazy coding from sneaking in the repo. But its seems everything that was configured and setup is all now removed. I don't think its smart to have half the code look like spaghetti or using different coding styles.

I came up with these coding rules for developers. So anyone can easy jump in and understand what is going in the code. Which we all agreed upon.
https://github.com/neo-project/.github/blob/d83897afb4fd5bd3c107a8dfd845e6dcf1e8d713/docs/neo-coding-rules.md

Copy link
Member Author

@erikzhang erikzhang Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers#class-and-struct-accessibility

According to Microsoft's official documentation, by default, class and struct members, including nested classes and structs, have private access. So it doesn't depend on the compilers and/or IDE environment.

But if you strongly insist, we can add private later. It's not a big problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior is consistent across all compilers and .NET versions because it is part of the C# Language Specification enforced by the Roslyn compiler.

{
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);
}
}
1 change: 1 addition & 0 deletions src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Neo/Network/P2P/RemoteNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 10 additions & 0 deletions src/Neo/Wallets/KeyPair.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ public class KeyPair : IEquatable<KeyPair>
/// </summary>
public UInt160 PublicKeyHash => PublicKey.EncodePoint(true).ToScriptHash();

/// <summary>
/// Initializes a new instance of the KeyPair class using a randomly generated 32-byte private key.
/// </summary>
/// <remarks>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.</remarks>
public KeyPair() : this(RandomNumberGenerator.GetBytes(32))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="KeyPair"/> class.
/// </summary>
Expand Down
24 changes: 7 additions & 17 deletions tests/Neo.UnitTests/Network/P2P/Payloads/UT_VersionPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,39 +23,29 @@ public class UT_VersionPayload
[TestMethod]
public void SizeAndEndPoint_Get()
{
var test = new VersionPayload() { Capabilities = Array.Empty<NodeCapability>(), 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<VersionPayload>();

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<FormatException>(() => _ = VersionPayload.Create(123, 456, "neo3",
Assert.ThrowsExactly<FormatException>(() => _ = VersionPayload.Create(ProtocolSettings.Default, new(), "neo3",
new NodeCapability[] {
new ServerCapability(NodeCapabilityType.TcpServer, 22) ,
new ServerCapability(NodeCapabilityType.TcpServer, 22)
}).ToArray().AsSerializable<VersionPayload>());

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<VersionPayload>();
Assert.HasCount(4, clone.Capabilities);
Assert.AreEqual(2, clone.Capabilities.OfType<UnknownCapability>().Count());
}
}
Loading