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);