diff --git a/src/Neo/Network/P2P/Connection.cs b/src/Neo/Network/P2P/Connection.cs
index c213df60b6..41c16fb007 100644
--- a/src/Neo/Network/P2P/Connection.cs
+++ b/src/Neo/Network/P2P/Connection.cs
@@ -75,6 +75,7 @@ public void Disconnect(bool abort = false)
{
disconnected = true;
tcp?.Tell(abort ? Tcp.Abort.Instance : Tcp.Close.Instance);
+ OnDisconnect(abort);
Context.Stop(Self);
}
@@ -85,6 +86,16 @@ protected virtual void OnAck()
{
}
+ ///
+ /// Invoked when a disconnect operation occurs, allowing derived classes to handle cleanup or custom logic.
+ ///
+ /// Override this method in a derived class to implement custom behavior when a disconnect
+ /// occurs. This method is called regardless of whether the disconnect is graceful or due to an abort.
+ /// true to indicate the disconnect is due to an abort operation; otherwise, false.
+ protected virtual void OnDisconnect(bool abort)
+ {
+ }
+
///
/// Called when data is received.
///
diff --git a/src/Neo/Network/P2P/KBucket.cs b/src/Neo/Network/P2P/KBucket.cs
new file mode 100644
index 0000000000..4262752201
--- /dev/null
+++ b/src/Neo/Network/P2P/KBucket.cs
@@ -0,0 +1,195 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// KBucket.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Neo.Network.P2P;
+
+///
+/// A Kademlia-style k-bucket: stores up to contacts in LRU order.
+///
+sealed class KBucket
+{
+ private readonly LinkedList _lru = new();
+ private readonly Dictionary> _index = new();
+
+ // Replacement cache: best-effort candidates when the bucket is full.
+ private readonly LinkedList _replacements = new();
+ private readonly Dictionary> _repIndex = new();
+
+ public int Capacity { get; }
+ public int ReplacementCapacity { get; }
+ public int BadThreshold { get; }
+ public int Count => _lru.Count;
+ public IReadOnlyCollection Contacts => _lru;
+
+ public KBucket(int capacity, int replacementCapacity, int badThreshold)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
+ ArgumentOutOfRangeException.ThrowIfNegative(replacementCapacity);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(badThreshold);
+ Capacity = capacity;
+ ReplacementCapacity = replacementCapacity;
+ BadThreshold = badThreshold;
+ }
+
+ public bool TryGet(UInt256 nodeId, [NotNullWhen(true)] out NodeContact? contact)
+ {
+ if (_index.TryGetValue(nodeId, out var node))
+ {
+ contact = node.Value;
+ return true;
+ }
+ contact = null;
+ return false;
+ }
+
+ ///
+ /// Updates LRU position and contact metadata. If bucket is full and the node is new,
+ /// the node is placed into replacement cache.
+ ///
+ ///
+ /// True if the contact ended up in the main bucket; false if it was cached as a replacement.
+ ///
+ public bool Update(NodeContact incoming)
+ {
+ if (_index.TryGetValue(incoming.NodeId, out var existingNode))
+ {
+ Merge(existingNode.Value, incoming);
+ Touch(existingNode);
+ return true;
+ }
+
+ if (_lru.Count < Capacity)
+ {
+ var node = _lru.AddLast(incoming);
+ _index[incoming.NodeId] = node;
+ return true;
+ }
+
+ // Bucket full: keep as replacement candidate.
+ AddOrUpdateReplacement(incoming);
+ return false;
+ }
+
+ public void MarkSuccess(UInt256 nodeId)
+ {
+ if (_index.TryGetValue(nodeId, out var node))
+ {
+ node.Value.FailCount = 0;
+ node.Value.LastSeen = TimeProvider.Current.UtcNow;
+ Touch(node);
+ return;
+ }
+
+ // If it was only a replacement, promote its freshness.
+ if (_repIndex.TryGetValue(nodeId, out var repNode))
+ {
+ repNode.Value.FailCount = 0;
+ repNode.Value.LastSeen = TimeProvider.Current.UtcNow;
+ Touch(repNode);
+ }
+ }
+
+ public void MarkFailure(UInt256 nodeId)
+ {
+ if (_index.TryGetValue(nodeId, out var node))
+ {
+ node.Value.FailCount++;
+ if (node.Value.FailCount < BadThreshold) return;
+
+ // Evict bad node and promote best replacement (if any).
+ RemoveFrom(node, _index);
+ PromoteReplacementIfAny();
+ }
+ else if (_repIndex.TryGetValue(nodeId, out var repNode))
+ {
+ // If it is a replacement, decay it and possibly drop.
+ repNode.Value.FailCount++;
+ if (repNode.Value.FailCount >= BadThreshold)
+ RemoveFrom(repNode, _repIndex);
+ }
+ }
+
+ public void Remove(UInt256 nodeId)
+ {
+ if (_index.TryGetValue(nodeId, out var node))
+ {
+ RemoveFrom(node, _index);
+ PromoteReplacementIfAny();
+ }
+ else if (_repIndex.TryGetValue(nodeId, out var repNode))
+ {
+ RemoveFrom(repNode, _repIndex);
+ }
+ }
+
+ void AddOrUpdateReplacement(NodeContact incoming)
+ {
+ if (_repIndex.TryGetValue(incoming.NodeId, out var existing))
+ {
+ Merge(existing.Value, incoming);
+ Touch(existing);
+ return;
+ }
+
+ if (ReplacementCapacity == 0) return;
+
+ var node = _replacements.AddLast(incoming);
+ _repIndex[incoming.NodeId] = node;
+
+ if (_replacements.Count > ReplacementCapacity)
+ {
+ // Drop oldest replacement.
+ var first = _replacements.First;
+ if (first is not null)
+ RemoveFrom(first, _repIndex);
+ }
+ }
+
+ void PromoteReplacementIfAny()
+ {
+ if (_lru.Count >= Capacity) return;
+ if (_replacements.Last is null) return;
+
+ // Promote the most recently seen replacement.
+ var rep = _replacements.Last;
+ RemoveFrom(rep, _repIndex);
+ var main = _lru.AddLast(rep.Value);
+ _index[main.Value.NodeId] = main;
+ }
+
+ static void Merge(NodeContact dst, NodeContact src)
+ {
+ // Merge endpoints (promote the first src endpoint if present).
+ if (src.Endpoints.Count > 0)
+ dst.AddOrPromoteEndpoint(src.Endpoints[0]);
+ for (int i = 1; i < src.Endpoints.Count; i++)
+ dst.AddOrPromoteEndpoint(src.Endpoints[i]);
+
+ // Prefer latest seen & features.
+ if (src.LastSeen > dst.LastSeen) dst.LastSeen = src.LastSeen;
+ dst.Features |= src.Features;
+ }
+
+ static void Touch(LinkedListNode node)
+ {
+ var list = node.List!;
+ list.Remove(node);
+ list.AddLast(node);
+ }
+
+ static void RemoveFrom(LinkedListNode node, Dictionary> index)
+ {
+ index.Remove(node.Value.NodeId);
+ node.List!.Remove(node);
+ }
+}
diff --git a/src/Neo/Network/P2P/LocalNode.cs b/src/Neo/Network/P2P/LocalNode.cs
index 210f1845e8..d1ef29f4ee 100644
--- a/src/Neo/Network/P2P/LocalNode.cs
+++ b/src/Neo/Network/P2P/LocalNode.cs
@@ -75,6 +75,11 @@ public record GetInstance;
///
public UInt256 NodeId { get; }
+ ///
+ /// Routing table used by the DHT overlay network.
+ ///
+ public RoutingTable RoutingTable { get; }
+
///
/// The identifier of the client software of the local node.
///
@@ -95,6 +100,7 @@ public LocalNode(NeoSystem system, KeyPair nodeKey)
this.system = system;
NodeKey = nodeKey;
NodeId = nodeKey.PublicKey.GetNodeId(system.Settings);
+ RoutingTable = new RoutingTable(NodeId);
SeedList = new IPEndPoint[system.Settings.SeedList.Length];
// Start dns resolution in parallel
diff --git a/src/Neo/Network/P2P/NodeContact.cs b/src/Neo/Network/P2P/NodeContact.cs
new file mode 100644
index 0000000000..1841b5ff82
--- /dev/null
+++ b/src/Neo/Network/P2P/NodeContact.cs
@@ -0,0 +1,77 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// NodeContact.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Net;
+
+namespace Neo.Network.P2P;
+
+///
+/// Represents a reachability hint for a DHT node (NOT a live connection).
+///
+public sealed class NodeContact
+{
+ ///
+ /// The verified DHT node identifier.
+ ///
+ public UInt256 NodeId { get; }
+
+ ///
+ /// Known endpoints for contacting the node. The first item is the preferred endpoint.
+ ///
+ public List Endpoints { get; } = new();
+
+ ///
+ /// Last time we successfully communicated with this node (handshake or DHT message).
+ ///
+ public DateTime LastSeen { get; internal set; }
+
+ ///
+ /// Consecutive failures when trying to contact this node.
+ ///
+ public int FailCount { get; internal set; }
+
+ ///
+ /// Optional capability flags (reserved).
+ ///
+ public ulong Features { get; internal set; }
+
+ ///
+ /// Initializes a new instance of the NodeContact class with the specified node identifier, optional endpoints, and
+ /// feature flags.
+ ///
+ /// The unique identifier for the node. This value is used to distinguish the node within the network.
+ /// A collection of network endpoints associated with the node. If not specified, the contact will have no initial
+ /// endpoints.
+ /// A bit field representing the features supported by the node. The default is 0, indicating no features.
+ public NodeContact(UInt256 nodeId, IEnumerable? endpoints = null, ulong features = 0)
+ {
+ NodeId = nodeId;
+ if (endpoints is not null)
+ foreach (var ep in endpoints)
+ AddOrPromoteEndpoint(ep);
+ LastSeen = TimeProvider.Current.UtcNow;
+ Features = features;
+ }
+
+ internal void AddOrPromoteEndpoint(IPEndPoint endpoint)
+ {
+ // Keep unique endpoints; promote to the front when we learn it's good.
+ int index = Endpoints.IndexOf(endpoint);
+ if (index == 0) return;
+ if (index > 0) Endpoints.RemoveAt(index);
+ Endpoints.Insert(0, endpoint);
+ }
+
+ public override string ToString()
+ {
+ return $"{NodeId} ({(Endpoints.Count > 0 ? Endpoints[0].ToString() : "no-endpoint")})";
+ }
+}
diff --git a/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs b/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs
index e96939e2a2..2b195c7aae 100644
--- a/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs
+++ b/src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs
@@ -385,6 +385,15 @@ private void OnVerackMessageReceived()
{
_verack = true;
_system.TaskManager.Tell(new TaskManager.Register(Version!));
+
+ // DHT: a verack means the handshake is complete and the remote identity (NodeId) has been verified.
+ // Feed the remote contact into the local RoutingTable.
+ var nodeId = Version!.NodeId;
+ // Prefer the advertised TCP server endpoint (Listener) when available, otherwise fall back to the connected Remote endpoint.
+ var ep = ListenerTcpPort > 0 ? Listener : Remote;
+ _localNode.RoutingTable.Update(nodeId, ep);
+ _localNode.RoutingTable.MarkSuccess(nodeId);
+
CheckMessageQueue();
}
diff --git a/src/Neo/Network/P2P/RemoteNode.cs b/src/Neo/Network/P2P/RemoteNode.cs
index 63ce38008c..db7c35836c 100644
--- a/src/Neo/Network/P2P/RemoteNode.cs
+++ b/src/Neo/Network/P2P/RemoteNode.cs
@@ -137,6 +137,16 @@ protected override void OnAck()
CheckMessageQueue();
}
+ protected override void OnDisconnect(bool abort)
+ {
+ if (abort)
+ {
+ // DHT: connection dropped. Penalize the contact (do not immediately delete; allow churn).
+ if (Version != null)
+ _localNode.RoutingTable.MarkFailure(Version.NodeId);
+ }
+ }
+
protected override void OnData(ByteString data)
{
_messageBuffer = _messageBuffer.Concat(data);
diff --git a/src/Neo/Network/P2P/RoutingTable.cs b/src/Neo/Network/P2P/RoutingTable.cs
new file mode 100644
index 0000000000..5ff0fb220f
--- /dev/null
+++ b/src/Neo/Network/P2P/RoutingTable.cs
@@ -0,0 +1,225 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// RoutingTable.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Net;
+
+namespace Neo.Network.P2P;
+
+///
+/// Kademlia-style routing table built from 256 k-buckets.
+///
+public sealed class RoutingTable
+{
+ const int IdBits = UInt256.Length * 8;
+
+ readonly UInt256 _selfId;
+ readonly KBucket[] _buckets;
+
+ ///
+ /// Max contacts per bucket (K in Kademlia).
+ ///
+ public int BucketSize { get; }
+
+ ///
+ /// Initializes a new instance of the RoutingTable class with the specified node identifier and bucket configuration
+ /// parameters.
+ ///
+ /// The routing table is organized into buckets based on the distance from the local node
+ /// identifier. Adjusting bucketSize, replacementSize, or badThreshold can affect the table's resilience and
+ /// performance in peer-to-peer network scenarios.
+ /// The unique identifier of the local node for which the routing table is constructed.
+ /// The maximum number of entries allowed in each bucket. Must be positive.
+ /// The maximum number of replacement entries maintained for each bucket. Must be non-negative.
+ /// The number of failed contact attempts after which a node is considered bad and eligible for replacement. Must be
+ /// positive.
+ public RoutingTable(UInt256 selfId, int bucketSize = 20, int replacementSize = 10, int badThreshold = 3)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bucketSize);
+ ArgumentOutOfRangeException.ThrowIfNegative(replacementSize);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(badThreshold);
+
+ _selfId = selfId;
+ BucketSize = bucketSize;
+
+ _buckets = new KBucket[IdBits];
+ for (int i = 0; i < _buckets.Length; i++)
+ _buckets[i] = new KBucket(bucketSize, replacementSize, badThreshold);
+ }
+
+ ///
+ /// Adds or refreshes a contact in the routing table.
+ ///
+ /// The unique identifier of the node whose contact information is to be updated.
+ /// The network endpoint associated with the node. Must not be null.
+ /// An optional set of feature flags describing the node's capabilities. The default is 0, indicating no features.
+ /// true if the node contact information was updated successfully; otherwise, false.
+ public bool Update(UInt256 nodeId, IPEndPoint endpoint, ulong features = 0)
+ {
+ return Update(new(nodeId, [endpoint], features));
+ }
+
+ ///
+ /// Adds or refreshes a contact in the routing table.
+ ///
+ /// If the specified contact represents the local node, the update is ignored and the method
+ /// returns false.
+ /// The contact information for the node to update. Must not represent the local node.
+ /// true if the contact was successfully updated; otherwise, false.
+ public bool Update(NodeContact contact)
+ {
+ int bucket = GetBucketIndex(contact.NodeId);
+ if (bucket < 0) return false; // ignore self
+ lock (_buckets[bucket])
+ return _buckets[bucket].Update(contact);
+ }
+
+ ///
+ /// Marks a contact as recently successful (e.g., handshake OK, DHT request succeeded).
+ ///
+ public void MarkSuccess(UInt256 nodeId)
+ {
+ int bucket = GetBucketIndex(nodeId);
+ if (bucket < 0) return;
+ lock (_buckets[bucket])
+ _buckets[bucket].MarkSuccess(nodeId);
+ }
+
+ ///
+ /// Marks a contact as failed (e.g., connection timeout). May evict it if it becomes bad.
+ ///
+ public void MarkFailure(UInt256 nodeId)
+ {
+ int bucket = GetBucketIndex(nodeId);
+ if (bucket < 0) return;
+ lock (_buckets[bucket])
+ _buckets[bucket].MarkFailure(nodeId);
+ }
+
+ ///
+ /// Removes a contact from the routing table.
+ ///
+ public void Remove(UInt256 nodeId)
+ {
+ int bucket = GetBucketIndex(nodeId);
+ if (bucket < 0) return;
+ lock (_buckets[bucket])
+ _buckets[bucket].Remove(nodeId);
+ }
+
+ ///
+ /// Returns up to contacts closest to .
+ ///
+ public IReadOnlyList FindClosest(UInt256 targetId, int count)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(count);
+ if (count == 0) return Array.Empty();
+
+ // Start from the bucket corresponding to target distance, then expand outward.
+ int start = GetBucketIndex(targetId);
+ if (start < 0) start = 0;
+
+ var candidates = new List(Math.Min(count * 3, BucketSize * 8));
+ CollectFromBuckets(start, candidates, hardLimit: Math.Max(count * 8, BucketSize * 8));
+
+ // Sort by XOR distance to target.
+ candidates.Sort((a, b) => CompareDistance(a.NodeId, b.NodeId, targetId));
+
+ if (candidates.Count <= count) return candidates;
+ return candidates.GetRange(0, count);
+ }
+
+ ///
+ /// Returns a sample of contacts across buckets (useful for bootstrap / gossip / health checks).
+ ///
+ public IReadOnlyList Sample(int count)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(count);
+ if (count == 0) return Array.Empty();
+
+ var list = new List(count);
+ // Prefer spread: take one from each bucket, round-robin.
+ int index = 0;
+ while (list.Count < count && index < IdBits)
+ {
+ lock (_buckets[index])
+ {
+ var bucket = _buckets[index].Contacts;
+ if (bucket.Count > 0)
+ {
+ // take most recently seen (tail)
+ list.Add(bucket.Last());
+ }
+ }
+ index++;
+ }
+
+ // If still short, just flatten and take.
+ if (list.Count < count)
+ {
+ foreach (var c in EnumerateAllContacts())
+ {
+ if (list.Count >= count) break;
+ if (!list.Contains(c)) list.Add(c);
+ }
+ }
+ return list;
+ }
+
+ void CollectFromBuckets(int start, List output, int hardLimit)
+ {
+ void AddRange(int bucketIndex)
+ {
+ lock (_buckets[bucketIndex])
+ foreach (var c in _buckets[bucketIndex].Contacts)
+ {
+ output.Add(c);
+ if (output.Count >= hardLimit) return;
+ }
+ }
+
+ AddRange(start);
+ if (output.Count >= hardLimit) return;
+
+ for (int step = 1; step < IdBits; step++)
+ {
+ int left = start - step;
+ int right = start + step;
+
+ if (left >= 0) AddRange(left);
+ if (output.Count >= hardLimit) break;
+ if (right < IdBits) AddRange(right);
+ if (output.Count >= hardLimit) break;
+ if (left < 0 && right >= IdBits) break;
+ }
+ }
+
+ IEnumerable EnumerateAllContacts()
+ {
+ for (int i = 0; i < IdBits; i++)
+ lock (_buckets[i])
+ foreach (var c in _buckets[i].Contacts)
+ yield return c;
+ }
+
+ int GetBucketIndex(UInt256 nodeId)
+ {
+ if (nodeId == _selfId) return -1;
+ int msb = (_selfId ^ nodeId).MostSignificantBit;
+ return msb; // -1..255
+ }
+
+ static int CompareDistance(UInt256 a, UInt256 b, UInt256 target)
+ {
+ var da = a ^ target;
+ var db = b ^ target;
+ return da.CompareTo(db);
+ }
+}
diff --git a/src/Neo/UInt256.cs b/src/Neo/UInt256.cs
index f50097c2c6..8cea5aab30 100644
--- a/src/Neo/UInt256.cs
+++ b/src/Neo/UInt256.cs
@@ -12,6 +12,7 @@
using Neo.IO;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -40,6 +41,27 @@ public class UInt256 : IComparable, IComparable, IEquatable, I
public int Size => Length;
+ ///
+ /// Gets the index of the most significant set bit in the current value.
+ ///
+ /// The most significant bit is the highest-order bit that is set to 1. If no bits are set, the
+ /// property returns -1.
+ public int MostSignificantBit
+ {
+ get
+ {
+ if (_value4 != 0)
+ return 192 + BitOperations.Log2(_value4);
+ if (_value3 != 0)
+ return 128 + BitOperations.Log2(_value3);
+ if (_value2 != 0)
+ return 64 + BitOperations.Log2(_value2);
+ if (_value1 != 0)
+ return BitOperations.Log2(_value1);
+ return -1;
+ }
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -252,4 +274,21 @@ public static implicit operator UInt256(byte[] b)
{
return left.CompareTo(right) <= 0;
}
+
+ ///
+ /// Performs a bitwise XOR operation on two values.
+ ///
+ /// The first operand.
+ /// The second operand.
+ /// The result of the bitwise XOR operation.
+ public static UInt256 operator ^(UInt256 left, UInt256 right)
+ {
+ return new UInt256
+ {
+ _value1 = left._value1 ^ right._value1,
+ _value2 = left._value2 ^ right._value2,
+ _value3 = left._value3 ^ right._value3,
+ _value4 = left._value4 ^ right._value4
+ };
+ }
}
diff --git a/tests/Neo.UnitTests/UT_UInt256.cs b/tests/Neo.UnitTests/UT_UInt256.cs
index dd6536ab4e..9c12ce3d6b 100644
--- a/tests/Neo.UnitTests/UT_UInt256.cs
+++ b/tests/Neo.UnitTests/UT_UInt256.cs
@@ -208,4 +208,57 @@ public void TestSpanAndSerializeLittleEndian()
Assert.ThrowsExactly(() => value.Serialize(shortBuffer.AsSpan()));
Assert.ThrowsExactly(() => value.SafeSerialize(shortBuffer.AsSpan()));
}
+
+ [TestMethod]
+ public void TestXorWithZeroIsIdentity()
+ {
+ var a = CreateSequential(0x10);
+ Assert.AreEqual(a, a ^ UInt256.Zero);
+ Assert.AreEqual(a, UInt256.Zero ^ a);
+ }
+
+ [TestMethod]
+ public void TestXorWithSelfIsZero()
+ {
+ var a = CreateSequential(0x42);
+ Assert.AreEqual(UInt256.Zero, a ^ a);
+ }
+
+ [TestMethod]
+ public void TestXorAssociative()
+ {
+ var a = CreateSequential(0x10);
+ var b = CreateSequential(0x20);
+ var c = CreateSequential(0x30);
+ var left = (a ^ b) ^ c;
+ var right = a ^ (b ^ c);
+
+ Assert.AreEqual(left, right);
+ }
+
+ [TestMethod]
+ public void TestXorCommutativeAndMatchesManual()
+ {
+ var a = CreateSequential(0x00);
+ var b = CreateSequential(0xF0);
+
+ var ab = a.ToArray();
+ var bb = b.ToArray();
+
+ var rb = new byte[UInt256.Length];
+ for (int i = 0; i < rb.Length; i++)
+ rb[i] = (byte)(ab[i] ^ bb[i]);
+
+ var expectedValue = new UInt256(rb);
+ Assert.AreEqual(expectedValue, a ^ b);
+ Assert.AreEqual(expectedValue, b ^ a);
+ }
+
+ private static UInt256 CreateSequential(byte start)
+ {
+ var bytes = new byte[UInt256.Length];
+ for (var i = 0; i < bytes.Length; i++)
+ bytes[i] = unchecked((byte)(start + i));
+ return new UInt256(bytes);
+ }
}