diff --git a/All.slnx b/All.slnx index 7379cfb..d8655bc 100644 --- a/All.slnx +++ b/All.slnx @@ -6,6 +6,7 @@ + @@ -13,6 +14,7 @@ + diff --git a/src/Neo.Configuration/BlockchainStoreOptions.cs b/src/Neo.Configuration/BlockchainStoreOptions.cs new file mode 100644 index 0000000..adaaf5d --- /dev/null +++ b/src/Neo.Configuration/BlockchainStoreOptions.cs @@ -0,0 +1,35 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using Neo.Configuration.Json; +using System; +using System.IO; + +namespace Neo.Configuration +{ + public class BlockchainStoreOptions : JsonModel + { + public string DatabasePath { get; init; } = Path.Combine(AppContext.BaseDirectory, "data", "chain"); + + public bool CreateIfMissing { get; init; } = true; + } +} diff --git a/src/Neo.Core/Extensions/ByteExtensions.cs b/src/Neo.Core/Extensions/ByteExtensions.cs index bd7d01e..5264028 100644 --- a/src/Neo.Core/Extensions/ByteExtensions.cs +++ b/src/Neo.Core/Extensions/ByteExtensions.cs @@ -73,7 +73,7 @@ public static byte[] Xor(this byte[] x, byte[] y) /// The byte array to be converted. /// The offset into the byte array from which to begin using data. /// The converted object. - public static T? AsSerializable(this byte[] data, int startIndex = 0) + public static T AsSerializable(this byte[] data, int startIndex = 0) where T : class?, INeoSerializable? { using var ms = new MemoryStream(data, false); diff --git a/src/Neo.Core/Neo.Core.csproj b/src/Neo.Core/Neo.Core.csproj index 884b9c9..e708933 100644 --- a/src/Neo.Core/Neo.Core.csproj +++ b/src/Neo.Core/Neo.Core.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/Neo.IO/Hashing/Murmur128.cs b/src/Neo.IO/Hashing/Murmur128.cs index cbdd7b6..52ae2fe 100644 --- a/src/Neo.IO/Hashing/Murmur128.cs +++ b/src/Neo.IO/Hashing/Murmur128.cs @@ -43,10 +43,8 @@ public class Murmur128 : NonCryptographicHashAlgorithm private Murmur128(uint seed = DefaultSeed) : base(16) { - Reset(); - _seed = seed; - _h1 = _h2 = _seed; + Reset(); } public static byte[] Hash(byte[] data, uint seed = DefaultSeed) => @@ -65,22 +63,22 @@ public static byte[] Hash(string text, uint seed = DefaultSeed) return Hash(bytes, seed); } - public static ulong HashToUInt64(byte[] data, uint seed = DefaultSeed) => - HashToUInt64(data.AsSpan(), seed); + public static UInt128 HashToUInt128(byte[] data, uint seed = DefaultSeed) => + HashToUInt128(data.AsSpan(), seed); - public static ulong HashToUInt64(ReadOnlySpan data, uint seed = DefaultSeed) => - BitConverter.ToUInt64(Hash(data, seed), 0); + public static UInt128 HashToUInt128(ReadOnlySpan data, uint seed = DefaultSeed) => + BitConverter.ToUInt128(Hash(data, seed), 0); - public static ulong HashToUInt64(string text, uint seed = DefaultSeed) + public static UInt128 HashToUInt128(string text, uint seed = DefaultSeed) { var bytes = Encoding.UTF8.GetBytes(text); - return HashToUInt64(bytes, seed); + return HashToUInt128(bytes, seed); } public override void Reset() { - _h1 = 0; - _h2 = 0; + _h1 = _seed; + _h2 = _seed; _length = 0; } @@ -91,20 +89,12 @@ public override void Append(ReadOnlySpan source) var index = 0; var bytesRemaining = source.Length; - while (bytesRemaining >= 16) + for (; bytesRemaining >= 16; index += 16, bytesRemaining -= 16) { - unsafe - { - fixed (byte* ptr = &source[index]) - { - var k1 = Unsafe.ReadUnaligned(ptr); - var k2 = Unsafe.ReadUnaligned(ptr + 8); - - ProcessBlock(k1, k2); - } - } - index += 16; - bytesRemaining -= 16; + var k1 = Unsafe.ReadUnaligned(in source[index]); + var k2 = Unsafe.ReadUnaligned(in source[index + 8]); + + ProcessBlock(k1, k2); } // Process leftover bytes if the source isn't an exact multiple of 16 bytes @@ -150,40 +140,13 @@ protected override void GetCurrentHashCore(Span destination) _h1 += _h2; _h2 += _h1; - unsafe - { - fixed (byte* destPtr = destination) - { - Unsafe.WriteUnaligned(destPtr, _h1); - Unsafe.WriteUnaligned(destPtr + 8, _h2); - } - } + Unsafe.WriteUnaligned(ref destination[0], _h1); + Unsafe.WriteUnaligned(ref destination[8], _h2); } protected override void GetHashAndResetCore(Span destination) { - // Finalization - _h1 ^= _length; - _h2 ^= _length; - - _h1 += _h2; - _h2 += _h1; - - _h1 = MixFinal(_h1); - _h2 = MixFinal(_h2); - - _h1 += _h2; - _h2 += _h1; - - unsafe - { - fixed (byte* destPtr = destination) - { - Unsafe.WriteUnaligned(destPtr, _h1); - Unsafe.WriteUnaligned(destPtr + 8, _h2); - } - } - + GetCurrentHashCore(destination); Reset(); // Reset after reading } @@ -227,9 +190,9 @@ private static ulong MixK2(ulong k2) private static ulong MixFinal(ulong k) { k ^= k >> 33; - k *= 0xff51afd7ed558ccdUL; + k *= 0xff51afd7ed558ccdul; k ^= k >> 33; - k *= 0xc4ceb9fe1a85ec53UL; + k *= 0xc4ceb9fe1a85ec53ul; k ^= k >> 33; return k; } diff --git a/src/Neo.IO/Neo.IO.csproj b/src/Neo.IO/Neo.IO.csproj index 75b799f..e6d840e 100644 --- a/src/Neo.IO/Neo.IO.csproj +++ b/src/Neo.IO/Neo.IO.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Neo.Localization/Neo.Localization.csproj b/src/Neo.Localization/Neo.Localization.csproj index 8c0abdd..0f48a0e 100644 --- a/src/Neo.Localization/Neo.Localization.csproj +++ b/src/Neo.Localization/Neo.Localization.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Neo.Localization/Resources/VM/VirtualMachineLocalizer.resx b/src/Neo.Localization/Resources/VM/VirtualMachineLocalizer.resx index a4bc037..72d954b 100644 --- a/src/Neo.Localization/Resources/VM/VirtualMachineLocalizer.resx +++ b/src/Neo.Localization/Resources/VM/VirtualMachineLocalizer.resx @@ -121,9 +121,9 @@ L{0:D4} {1,-11:G}{2} - Starting execution | HardFork: {0:G} | GasLimit: {1:N0} + Starting execution | HardFork: {0:G} | GasLimit: {1:N8} - Execution completed successfully | GasConsumed: {0:N0} | GasLeft: {1:N0} | State: {2:G} + Execution completed successfully | GasConsumed: {0:N8} | GasLeft: {1:N8} | State: {2:G} \ No newline at end of file diff --git a/src/Neo.Platform.CLI/Neo.Platform.CLI.csproj b/src/Neo.Platform.CLI/Neo.Platform.CLI.csproj index 8c6ee08..87a7ff8 100644 --- a/src/Neo.Platform.CLI/Neo.Platform.CLI.csproj +++ b/src/Neo.Platform.CLI/Neo.Platform.CLI.csproj @@ -10,9 +10,9 @@ - - - + + + diff --git a/src/Neo.Platform.Hosting/Neo.Platform.Hosting.csproj b/src/Neo.Platform.Hosting/Neo.Platform.Hosting.csproj index 6225137..a8d8730 100644 --- a/src/Neo.Platform.Hosting/Neo.Platform.Hosting.csproj +++ b/src/Neo.Platform.Hosting/Neo.Platform.Hosting.csproj @@ -6,9 +6,9 @@ - - - + + + diff --git a/src/Neo.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs new file mode 100644 index 0000000..2fca4dc --- /dev/null +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -0,0 +1,263 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Neo.Configuration; +using Neo.Platform.Storage.Interface; +using Neo.Platform.Storage.Logging; +using RocksDbNet; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace Neo.Platform.Storage +{ + public sealed class BlockchainStore : IEnumerable>, IStore + { + public BlockchainStoreOptions Options => _options; + + private static readonly ColumnFamilyDescriptor[] s_columnFamilies = + [ + new(ColumnFamilyNames.Default), + new(ColumnFamilyNames.Checkpoints), + new(ColumnFamilyNames.Backups), + ]; + + private readonly Cache _blockSharedCache; + private readonly FilterPolicy _bloomFilter; + private readonly RocksDb _db; + + private readonly BlockchainStoreOptions _options; + + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public BlockchainStore( + IOptions options, + ILoggerFactory? loggerFactory = default) + { + _options = options.Value; + _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + _logger = _loggerFactory.CreateLogger(); + + var dirInfo = new DirectoryInfo(_options.DatabasePath); + + if (dirInfo.Exists == false) + dirInfo.Create(); + + var gcInfo = GC.GetGCMemoryInfo(); + var ram = (ulong)gcInfo.TotalAvailableMemoryBytes / 3ul; // 33% of all RAM + + _blockSharedCache = Cache.CreateLru(ram); + _bloomFilter = FilterPolicy.CreateBloom(10); + + var dbOptions = new DbOptions() + { + CreateIfMissing = _options.CreateIfMissing, + CreateMissingColumnFamilies = true, + + // Compression + Compression = Compression.Lz4, + BottommostCompression = Compression.Zstd, + + // Parallelism + MaxBackgroundJobs = Math.Max(2, Environment.ProcessorCount), + MaxSubcompactions = (uint)Environment.ProcessorCount / 2, + + // Write buffers + WriteBufferSize = 256 * 1024 * 1024, // 256MB + MaxWriteBufferNumber = 5, + MinWriteBufferNumberToMerge = 2, + + // Compaction + NumLevels = 7, + Level0FileNumCompactionTrigger = 4, + Level0SlowdownWritesTrigger = 20, + Level0StopWritesTrigger = 36, + TargetFileSizeBase = 128 * 1024 * 1024, // 128MB + MaxBytesForLevelBase = 256 * 1024 * 1024, // 256MB + LevelCompactionDynamicLevelBytes = true, + + // I/O + BytesPerSync = 0, + UseDirectReads = false, + AllowConcurrentMemtableWrite = true, + AtomicFlush = false, + + // Files + MaxOpenFiles = -1, // Keep all files open + + // WAL + WalRecoveryMode = WalRecoveryMode.PointInTime, + ManualWalFlush = false, + + // Cache + BlockBasedTableFactory = CreateBlockTableOptions(), + }; + + dbOptions.OptimizeForPointLookup(64); // 64MB + + _db = RocksDb.Open(dbOptions, _options.DatabasePath, s_columnFamilies); + } + + public void Dispose() + { + _db.Dispose(); + _blockSharedCache.Dispose(); + _bloomFilter.Dispose(); + } + + public IStoreSnapshot CreateSnapshot() + { + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogCreateSnapshotMessage(logLevel, "Creating snapshot of the store."); + + return new BlockchainStoreSnapshot(this, _db, _loggerFactory); + } + + public void Put(ReadOnlySpan key, ReadOnlySpan value) + { + _db.Put(key, value); + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogWriteMessage(logLevel, $"Put key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(value)}"); + } + + public void Delete(ReadOnlySpan key) + { + if (_db.KeyMayExist(key)) + { + _db.Delete(key); + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogDeleteMessage(logLevel, $"Deleted key: {Convert.ToHexStringLower(key)}"); + } + } + + public bool ContainsKey(ReadOnlySpan key) + { + if (_db.KeyMayExist(key)) + return TryGet(key, out _); + return false; + } + + public byte[]? Get(ReadOnlySpan key) + { + if (_db.KeyMayExist(key)) + { + var data = _db.Get(key); + if (data is not null) + { + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogReadMessage(logLevel, $"Get key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(data)}"); + + return data; + } + } + return default; + } + + public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value) + { + if (_db.KeyMayExist(key)) + { + var data = _db.Get(key); + if (data is not null) + { + value = data; + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogReadMessage(logLevel, $"TryGet key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(data)}"); + + return true; + } + } + + value = default; + return false; + } + + public IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false) + { + using var iter = _db.NewIterator(); + + for (iter.SeekForPrev(keyOrPrefix.Span); iter.IsValid();) + { + var key = iter.KeyToArray(); + var value = iter.ValueToArray(); + + yield return new(key, value); + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogReadMessage(logLevel, $"Seek key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(value)}"); + + if (seekFromEnd) + iter.Prev(); + else + iter.Next(); + } + } + + public IEnumerator GetEnumerator() => + GetEnumerator(); + + IEnumerator> IEnumerable>.GetEnumerator() + { + using var iter = _db.NewIterator(); + for (iter.SeekToFirst(); iter.IsValid(); iter.Next()) + yield return new(iter.KeyToArray(), iter.ValueToArray()); + } + + private BlockBasedTableOptions CreateBlockTableOptions() + { + var blockOptions = new BlockBasedTableOptions() + { + FormatVersion = 5, + BlockSize = 64 * 1024, // 64KB + CacheIndexAndFilterBlocks = true, + PinL0FilterAndIndexBlocksInCache = true, + WholeKeyFiltering = true, + }; + + blockOptions.SetBlockCache(_blockSharedCache); + blockOptions.SetFilterPolicy(_bloomFilter); + + return blockOptions; + } + } +} diff --git a/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs b/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs new file mode 100644 index 0000000..670fd1d --- /dev/null +++ b/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs @@ -0,0 +1,195 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Neo.Platform.Storage.Interface; +using Neo.Platform.Storage.Logging; +using RocksDbNet; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Platform.Storage +{ + public class BlockchainStoreSnapshot : IEnumerable>, IStoreSnapshot + { + public IStore Store => _store; + + private readonly IStore _store; + private readonly RocksDb _db; + private readonly Snapshot _snapshot; + private readonly ReadOptions _readOptions; + private readonly WriteBatch _writeBatch; + + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public BlockchainStoreSnapshot( + IStore store, + RocksDb db, + ILoggerFactory? loggerFactory = default) + { + _store = store; + _db = db; + _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + _logger = _loggerFactory.CreateLogger(); + + _snapshot = _db.NewSnapshot(); + _readOptions = new(); + _readOptions.SetSnapshot(_snapshot); + _writeBatch = new(); + } + + public void Dispose() + { + _snapshot.Dispose(); + _readOptions.Dispose(); + _writeBatch.Dispose(); + GC.SuppressFinalize(this); + } + + public void Commit() + { + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogCommitMessage(logLevel, $"Committing {_writeBatch.Count} change(s) to the database"); + + _db.Write(_writeBatch); + } + + public bool ContainsKey(ReadOnlySpan key) + { + if (_db.KeyMayExist(key)) + return TryGet(key, out _); + return false; + } + + public IStoreSnapshot CreateSnapshot() + { + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogCreateSnapshotMessage(logLevel, "Creating snapshot of the snapshot."); + + return new BlockchainStoreSnapshot(this, _db, _loggerFactory); + } + + public void Delete(ReadOnlySpan key) + { + if (_db.KeyMayExist(key)) + { + _writeBatch.Delete(key); + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogDeleteMessage(logLevel, $"Deleted key: {Convert.ToHexStringLower(key)}"); + } + } + + public byte[]? Get(ReadOnlySpan key) + { + if (_db.KeyMayExist(key)) + { + var data = _db.Get(key, _readOptions); + if (data is not null) + { + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogReadMessage(logLevel, $"Get key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(data)}"); + + return data; + } + } + return default; + } + + public IEnumerator> GetEnumerator() + { + using var iter = _db.NewIterator(_readOptions); + for (iter.SeekToFirst(); iter.IsValid(); iter.Next()) + yield return new(iter.KeyToArray(), iter.ValueToArray()); + } + + public void Put(ReadOnlySpan key, ReadOnlySpan value) + { + _writeBatch.Put(key, value); + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogWriteMessage(logLevel, $"Put key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(value)}"); + } + + public IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false) + { + using var iter = _db.NewIterator(_readOptions); + + for (iter.SeekForPrev(keyOrPrefix.Span); iter.IsValid();) + { + var key = iter.KeyToArray(); + var value = iter.ValueToArray(); + + yield return new(key, value); + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogReadMessage(logLevel, $"Seek key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(value)}"); + + if (seekFromEnd) + iter.Prev(); + else + iter.Next(); + } + } + + public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value) + { + if (_db.KeyMayExist(key)) + { + var data = _db.Get(key, _readOptions); + if (data is not null) + { + value = data; + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogReadMessage(logLevel, $"TryGet key: {Convert.ToHexStringLower(key)} value: {Convert.ToHexStringLower(data)}"); + + return true; + } + } + + value = default; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + } +} diff --git a/src/Neo.Platform.Storage/ColumnFamilyNames.cs b/src/Neo.Platform.Storage/ColumnFamilyNames.cs new file mode 100644 index 0000000..d46b04b --- /dev/null +++ b/src/Neo.Platform.Storage/ColumnFamilyNames.cs @@ -0,0 +1,31 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +namespace Neo.Platform.Storage +{ + public static class ColumnFamilyNames + { + public static readonly string Default = "default"; + public static readonly string Checkpoints = "checkpoints"; + public static readonly string Backups = "backups"; + } +} diff --git a/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs b/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs new file mode 100644 index 0000000..c911de2 --- /dev/null +++ b/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs @@ -0,0 +1,39 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Platform.Storage.Interface +{ + public interface IReadOnlyStore : IDisposable + { + bool ContainsKey(ReadOnlySpan key); + + byte[]? Get(ReadOnlySpan key); + + bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value); + + IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false); + } +} diff --git a/src/Neo.Platform.Storage/Interface/IStore.cs b/src/Neo.Platform.Storage/Interface/IStore.cs new file mode 100644 index 0000000..f7684bd --- /dev/null +++ b/src/Neo.Platform.Storage/Interface/IStore.cs @@ -0,0 +1,35 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using System; + +namespace Neo.Platform.Storage.Interface +{ + public interface IStore : IReadOnlyStore + { + IStoreSnapshot CreateSnapshot(); + + void Put(ReadOnlySpan key, ReadOnlySpan value); + + void Delete(ReadOnlySpan key); + } +} diff --git a/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs b/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs new file mode 100644 index 0000000..f9eaaed --- /dev/null +++ b/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs @@ -0,0 +1,33 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using System; + +namespace Neo.Platform.Storage.Interface +{ + public interface IStoreSnapshot : IStore, IDisposable + { + IStore Store { get; } + + void Commit(); + } +} diff --git a/src/Neo.Platform.Storage/Logging/LoggingExtensions.cs b/src/Neo.Platform.Storage/Logging/LoggingExtensions.cs new file mode 100644 index 0000000..5536de9 --- /dev/null +++ b/src/Neo.Platform.Storage/Logging/LoggingExtensions.cs @@ -0,0 +1,106 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using Microsoft.Extensions.Logging; +using System; + +namespace Neo.Platform.Storage.Logging +{ + internal static partial class LoggerExtensions + { + [LoggerMessage( + EventId = StoreEventId.Fault, + EventName = nameof(StoreEventId.Fault), + Message = "{Message}" + )] + public static partial void LogFaultMessage( + this ILogger logger, + LogLevel logLevel, + string message + ); + + [LoggerMessage( + EventId = StoreEventId.Fault, + Message = "" + )] + public static partial void LogFaultMessage( + this ILogger logger, + LogLevel logLevel, + Exception exception + ); + + [LoggerMessage( + EventId = StoreEventId.Read, + EventName = nameof(StoreEventId.Read), + Message = "{Message}" + )] + public static partial void LogReadMessage( + this ILogger logger, + LogLevel logLevel, + string message + ); + + [LoggerMessage( + EventId = StoreEventId.Write, + EventName = nameof(StoreEventId.Write), + Message = "{Message}" + )] + public static partial void LogWriteMessage( + this ILogger logger, + LogLevel logLevel, + string message + ); + + [LoggerMessage( + EventId = StoreEventId.Delete, + EventName = nameof(StoreEventId.Delete), + Message = "{Message}" + )] + public static partial void LogDeleteMessage( + this ILogger logger, + LogLevel logLevel, + string message + ); + + [LoggerMessage( + EventId = StoreEventId.CreateSnapshot, + EventName = nameof(StoreEventId.CreateSnapshot), + Message = "{Message}" + )] + public static partial void LogCreateSnapshotMessage( + this ILogger logger, + LogLevel logLevel, + string message + ); + + [LoggerMessage( + EventId = StoreEventId.Commit, + EventName = nameof(StoreEventId.Commit), + Message = "{Message}" + )] + public static partial void LogCommitMessage( + this ILogger logger, + LogLevel logLevel, + string message + ); + } +} diff --git a/src/Neo.Platform.Storage/Logging/StoreEventId.cs b/src/Neo.Platform.Storage/Logging/StoreEventId.cs new file mode 100644 index 0000000..4946a70 --- /dev/null +++ b/src/Neo.Platform.Storage/Logging/StoreEventId.cs @@ -0,0 +1,34 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +namespace Neo.Platform.Storage.Logging +{ + internal static class StoreEventId + { + public const int Fault = 100; + public const int Read = 200; + public const int Write = 300; + public const int Delete = 400; + public const int CreateSnapshot = 500; + public const int Commit = 600; + } +} diff --git a/src/Neo.Platform.Storage/Neo.Platform.Storage.csproj b/src/Neo.Platform.Storage/Neo.Platform.Storage.csproj new file mode 100644 index 0000000..86e14f3 --- /dev/null +++ b/src/Neo.Platform.Storage/Neo.Platform.Storage.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + + + + + + + + + + + + + diff --git a/src/Neo.Platform.Storage/StoreExtensions.cs b/src/Neo.Platform.Storage/StoreExtensions.cs new file mode 100644 index 0000000..25bbd6c --- /dev/null +++ b/src/Neo.Platform.Storage/StoreExtensions.cs @@ -0,0 +1,54 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using Neo.Core.Extensions; +using Neo.Core.Serialization; +using Neo.Platform.Storage.Interface; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Neo.Platform.Storage +{ + public static class StoreExtensions + { + public static void Put(this IStore store, ReadOnlySpan key, T value) + where T : INeoSerializable => + store.Put(key, value.ToArray()); + + public static TResult? Get(this IReadOnlyStore store, ReadOnlySpan key) + where TResult : class?, INeoSerializable => + store.Get(key)?.AsSerializable(); + + public static bool TryGet(this IReadOnlyStore store, ReadOnlySpan key, [NotNullWhen(true)] out T? value) + where T : class?, INeoSerializable + { + value = default; + if (store.TryGet(key, out var valueBytes)) + { + value = valueBytes.AsSerializable(); + return true; + } + + return false; + } + } +} diff --git a/src/Neo.VM/Extensions/ServiceCollectionExtensions.cs b/src/Neo.VM/Extensions/ServiceCollectionExtensions.cs index 68616b3..bfdfc72 100644 --- a/src/Neo.VM/Extensions/ServiceCollectionExtensions.cs +++ b/src/Neo.VM/Extensions/ServiceCollectionExtensions.cs @@ -32,18 +32,24 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddExecutionEngine(this IServiceCollection services) { // Register VirtualMachineEngine using the built pipeline - services.AddScoped(sp => - { - var middleware = sp.GetServices(); // NOTE: Get all custom middleware - var debugger = sp.GetRequiredService(); // NOTE: get the debugger middleware - var pipeline = VirtualMachinePipelineBuilder.Create() - .Use([debugger, .. middleware]) // NOTE: ORDER MATTERS HERE - .Build(); + services.AddScoped( + sp => + { + var middleware = sp.GetServices(); // NOTE: Get all custom middleware + var debugger = sp.GetService(); // NOTE: get the debugger middleware + + if (debugger is not null) + middleware = [debugger, .. middleware]; - return VirtualMachineBuilder.Create() - .UsePipeline(pipeline) - .Build(); - }); + var pipeline = VirtualMachinePipelineBuilder.Create() + .Use(middleware) // NOTE: ORDER MATTERS HERE + .Build(); + + return VirtualMachineBuilder.Create() + .UsePipeline(pipeline) + .Build(); + } + ); return services; } @@ -52,17 +58,18 @@ public static IServiceCollection AddEngineMiddleware(this IServiceC where TMiddleware : class, IEngineMiddleware { services.AddSingleton(); + return services; } - public static IServiceCollection AddEngineDebugger(this IServiceCollection services) + public static IServiceCollection AddEngineMiddlewareDebugger(this IServiceCollection services) { services.AddScoped(); return services; } - public static IServiceCollection AddExecuteLogger(this IServiceCollection services) + public static IServiceCollection AddEngineMiddlewareLogger(this IServiceCollection services) { services.AddScoped(); diff --git a/src/Neo.VM/Middleware/DebuggerMiddleware.cs b/src/Neo.VM/Middleware/DebuggerMiddleware.cs index f9eb260..d7453cc 100644 --- a/src/Neo.VM/Middleware/DebuggerMiddleware.cs +++ b/src/Neo.VM/Middleware/DebuggerMiddleware.cs @@ -36,6 +36,8 @@ public sealed class DebuggerMiddleware(IServiceProvider sp, ILogger sp.GetRequiredService(); private readonly HashSet _breakpoints = []; // script offset breakpoints @@ -87,8 +89,9 @@ public void PreExecute(ExecutionContext? context, ExecuteDelegate next) if (logger.IsEnabled(LogLevel.Debug)) { - var gasLeft = Engine.GasLimit - context.GasConsumed; - var message = $"Breakpoint hit | GasConsumed: {context.GasConsumed:N0} | GasLeft: {gasLeft:N0}"; + var gasLeft = (Engine.GasLimit - context.GasConsumed) / s_factor; + var gasConsumed = context.GasConsumed / s_factor; + var message = $"Breakpoint hit | GasConsumed: {gasConsumed:N8} | GasLeft: {gasLeft:N8}"; logger.LogBreakMessage(LogLevel.Debug, message); } diff --git a/src/Neo.VM/Middleware/ExecuteLoggerMiddleware.cs b/src/Neo.VM/Middleware/ExecuteLoggerMiddleware.cs index d84d961..7720941 100644 --- a/src/Neo.VM/Middleware/ExecuteLoggerMiddleware.cs +++ b/src/Neo.VM/Middleware/ExecuteLoggerMiddleware.cs @@ -31,6 +31,8 @@ namespace Neo.VM.Middleware { public sealed class ExecuteLoggerMiddleware(IServiceProvider sp, ILogger logger) : IEngineMiddleware { + private static readonly decimal s_factor = 1_00000000m; + private VirtualMachineEngine Engine => sp.GetRequiredService(); public void PostExecute(ExecutionContext? context, ExecuteDelegate next) @@ -47,7 +49,7 @@ public void PreExecution(ExecutionDelegate next) var message = VirtualMachineLocalizer.GetMessage( VirtualMachineMessageNames.ExecuteStartup, Engine.ActiveFork, - Engine.GasLimit + Engine.GasLimit / s_factor ); logger.LogExecuteMessage(logLevel, message); @@ -87,8 +89,8 @@ public void PostExecution(ExecutionDelegate next) { var message = VirtualMachineLocalizer.GetMessage( VirtualMachineMessageNames.ExecuteSuccessfully, - Engine.GasConsumed, - Engine.GasLeft, + Engine.GasConsumed / s_factor, + Engine.GasLeft / s_factor, Engine.State ); diff --git a/src/Neo.VM/Neo.VM.csproj b/src/Neo.VM/Neo.VM.csproj index ef12f3f..a4f16c3 100644 --- a/src/Neo.VM/Neo.VM.csproj +++ b/src/Neo.VM/Neo.VM.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/tests/Directory.Build.Props b/tests/Directory.Build.Props index 2e84aab..82df4df 100644 --- a/tests/Directory.Build.Props +++ b/tests/Directory.Build.Props @@ -17,7 +17,7 @@ - + diff --git a/tests/Neo.Configuration.Tests/Neo.Configuration.Tests.csproj b/tests/Neo.Configuration.Tests/Neo.Configuration.Tests.csproj index f00a051..49a4608 100644 --- a/tests/Neo.Configuration.Tests/Neo.Configuration.Tests.csproj +++ b/tests/Neo.Configuration.Tests/Neo.Configuration.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/Neo.Core.Tests/Blockchain/UT_BlockHeader.cs b/tests/Neo.Core.Tests/Blockchain/UT_BlockHeader.cs index 1bdd996..e09ca1c 100644 --- a/tests/Neo.Core.Tests/Blockchain/UT_BlockHeader.cs +++ b/tests/Neo.Core.Tests/Blockchain/UT_BlockHeader.cs @@ -73,9 +73,9 @@ public void TestDeserialize() Assert.AreEqual(expectedBlockHeader.Index, actualBlockHeader.Index); Assert.AreEqual(expectedBlockHeader.NextConsensus, actualBlockHeader.NextConsensus); - CollectionAssert.AreEqual(expectedBlockHeader.Witness.InvocationScript, actualBlockHeader.Witness.InvocationScript); - CollectionAssert.AreEqual(expectedBlockHeader.Witness.VerificationScript, actualBlockHeader.Witness.VerificationScript); - CollectionAssert.AreEqual(Convert.FromHexString(s_blockHeaderHexString), actualBlockHeaderBytes); + Assert.AreSequenceEqual(expectedBlockHeader.Witness.InvocationScript, actualBlockHeader.Witness.InvocationScript); + Assert.AreSequenceEqual(expectedBlockHeader.Witness.VerificationScript, actualBlockHeader.Witness.VerificationScript); + Assert.AreSequenceEqual(Convert.FromHexString(s_blockHeaderHexString), actualBlockHeaderBytes); } [TestMethod] diff --git a/tests/Neo.Core.Tests/Extensions/UT_ByteExtensions.cs b/tests/Neo.Core.Tests/Extensions/UT_ByteExtensions.cs index e59b350..3306b4a 100644 --- a/tests/Neo.Core.Tests/Extensions/UT_ByteExtensions.cs +++ b/tests/Neo.Core.Tests/Extensions/UT_ByteExtensions.cs @@ -46,13 +46,13 @@ public void TestXor() var actualXorByte3 = expectedBytes3.Xor(new byte[ushort.MaxValue]); Assert.HasCount(3, actualXorByte1); - CollectionAssert.AreEqual(expectedBytes1, actualXorByte1); + Assert.AreSequenceEqual(expectedBytes1, actualXorByte1); Assert.HasCount(3, actualXorByte2); - CollectionAssert.AreEqual(expectedBytes2, actualXorByte2); + Assert.AreSequenceEqual(expectedBytes2, actualXorByte2); Assert.HasCount(ushort.MaxValue, actualXorByte3); - CollectionAssert.AreEqual(expectedBytes3, actualXorByte3); + Assert.AreSequenceEqual(expectedBytes3, actualXorByte3); Assert.ThrowsExactly(() => new byte[0].Xor(new byte[ushort.MaxValue])); } diff --git a/tests/Neo.Core.Tests/Extensions/UT_StreamExtensions.cs b/tests/Neo.Core.Tests/Extensions/UT_StreamExtensions.cs index 8deed7d..89713b9 100644 --- a/tests/Neo.Core.Tests/Extensions/UT_StreamExtensions.cs +++ b/tests/Neo.Core.Tests/Extensions/UT_StreamExtensions.cs @@ -50,7 +50,7 @@ public void TestWrite() var actualData = ms.ToArray(); - CollectionAssert.AreEqual(expectedData, actualData); + Assert.AreSequenceEqual(expectedData, actualData); } [TestMethod] @@ -77,7 +77,7 @@ public void TestRead() var actualArrayData = ms.ReadDynamic(); Assert.HasCount(1, actualArrayData); - CollectionAssert.AreEqual(expectedArrayData, actualArrayData); + Assert.AreSequenceEqual(expectedArrayData, actualArrayData); } } } diff --git a/tests/Neo.Core.Tests/Neo.Core.Tests.csproj b/tests/Neo.Core.Tests/Neo.Core.Tests.csproj index 7413dff..835b575 100644 --- a/tests/Neo.Core.Tests/Neo.Core.Tests.csproj +++ b/tests/Neo.Core.Tests/Neo.Core.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/Neo.Core.Tests/Text/UT_Base58.cs b/tests/Neo.Core.Tests/Text/UT_Base58.cs index cf6f707..0669e7c 100644 --- a/tests/Neo.Core.Tests/Text/UT_Base58.cs +++ b/tests/Neo.Core.Tests/Text/UT_Base58.cs @@ -74,7 +74,7 @@ public void TestEncodeDecode() var actualDecodeBytes = Base58.Decode(entry.Value); Assert.AreEqual(entry.Value, actualEncodeBytes); - CollectionAssert.AreEqual(keyBytes, actualDecodeBytes); + Assert.AreSequenceEqual(keyBytes, actualDecodeBytes); } var invalidBase58 = new string[] { "0", "O", "I", "l", "+", "/" }; diff --git a/tests/Neo.Core.Tests/UT_UInt160.cs b/tests/Neo.Core.Tests/UT_UInt160.cs index 0077a15..96f3abd 100644 --- a/tests/Neo.Core.Tests/UT_UInt160.cs +++ b/tests/Neo.Core.Tests/UT_UInt160.cs @@ -20,7 +20,6 @@ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES -#pragma warning disable CS1718 // Comparison made to same variable #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. using System; diff --git a/tests/Neo.Core.Tests/UT_UInt256.cs b/tests/Neo.Core.Tests/UT_UInt256.cs index d1991a8..ec2621c 100644 --- a/tests/Neo.Core.Tests/UT_UInt256.cs +++ b/tests/Neo.Core.Tests/UT_UInt256.cs @@ -20,7 +20,6 @@ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES -#pragma warning disable CS1718 // Comparison made to same variable #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. using Neo.Core.Extensions; diff --git a/tests/Neo.IO.Tests/Data/UT_BloomFilter.cs b/tests/Neo.IO.Tests/Data/UT_BloomFilter.cs index 5d5df2f..6f4f3c8 100644 --- a/tests/Neo.IO.Tests/Data/UT_BloomFilter.cs +++ b/tests/Neo.IO.Tests/Data/UT_BloomFilter.cs @@ -46,7 +46,7 @@ public void TestAddContains() } [TestMethod] - public void TestBloomFIlterConstructorGetMTweak() + public void TestBloomFilterConstructorGetMTweak() { var expectedElements = 7; var nTweak = 123456u; diff --git a/tests/Neo.IO.Tests/Hashing/UT_Murmur128.cs b/tests/Neo.IO.Tests/Hashing/UT_Murmur128.cs index ec51fce..8548ede 100644 --- a/tests/Neo.IO.Tests/Hashing/UT_Murmur128.cs +++ b/tests/Neo.IO.Tests/Hashing/UT_Murmur128.cs @@ -32,7 +32,7 @@ public class UT_Murmur128 private const uint DefaultTestSeed = 123u; [TestMethod] - public void TestHashToUInt64() + public void TestHash() { var arrayData = Encoding.ASCII.GetBytes("hello"); var actualHash = Murmur128.Hash(arrayData, DefaultTestSeed); @@ -52,5 +52,31 @@ public void TestHashToUInt64() Assert.AreEqual("e0a0632d4f51302c55e3b3e48d28795d", actualHashString); } + + [TestMethod] + public void TestMoreThan16Characters() + { + var arrayData = Encoding.ASCII.GetBytes("Testing more than 16 characters."); + var actualHash = Murmur128.Hash(arrayData, DefaultTestSeed); + var actualHashString = Convert.ToHexStringLower(actualHash); + + Assert.AreEqual("6903e6d3d2fe781787819c89b2c11aa9", actualHashString); + + arrayData = Encoding.ASCII.GetBytes + ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Sed do eiusmod tempor incididunt ut labore et dolore magna " + + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation " + + "ullamco laboris nisi ut aliquip ex ea commodo consequat. " + + "Duis aute irure dolor in reprehenderit in voluptate velit " + + "esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " + + "occaecat cupidatat non proident, sunt in culpa qui officia " + + "deserunt mollit anim id est laborum." + ); + actualHash = Murmur128.Hash(arrayData, DefaultTestSeed); + actualHashString = Convert.ToHexStringLower(actualHash); + + Assert.AreEqual("dacfff46c267a7be8dbb73489c1d2678", actualHashString); + } } } diff --git a/tests/Neo.IO.Tests/Neo.IO.Tests.csproj b/tests/Neo.IO.Tests/Neo.IO.Tests.csproj index 43491ce..44c0458 100644 --- a/tests/Neo.IO.Tests/Neo.IO.Tests.csproj +++ b/tests/Neo.IO.Tests/Neo.IO.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/Neo.Platform.Storage.Tests/MSTestSettings.cs b/tests/Neo.Platform.Storage.Tests/MSTestSettings.cs new file mode 100644 index 0000000..7cde898 --- /dev/null +++ b/tests/Neo.Platform.Storage.Tests/MSTestSettings.cs @@ -0,0 +1,23 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj b/tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj new file mode 100644 index 0000000..d411cfd --- /dev/null +++ b/tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + latest + enable + + + + + + + + + + + + + + + + + diff --git a/tests/Neo.Platform.Storage.Tests/TestStorePool.cs b/tests/Neo.Platform.Storage.Tests/TestStorePool.cs new file mode 100644 index 0000000..55d048e --- /dev/null +++ b/tests/Neo.Platform.Storage.Tests/TestStorePool.cs @@ -0,0 +1,62 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using Microsoft.Extensions.Options; +using Neo.Configuration; +using System.Collections.Concurrent; +using System.IO; + +namespace Neo.Platform.Storage.Tests +{ + internal sealed class TestStorePool + { + public static TestStorePool Shared => s_storePool; + + private static readonly TestStorePool s_storePool = new(); + + private readonly ConcurrentDictionary _store = []; + + public BlockchainStore Rent() + { + var storeOptions = Options.Create(new BlockchainStoreOptions() + { + DatabasePath = Path.Combine(Path.GetRandomFileName()), + }); + + var store = new BlockchainStore(storeOptions, TestUtilities.TraceLoggerFactory); + + _store.TryAdd(storeOptions.Value.DatabasePath, store); + + return store; + } + + public void Return(BlockchainStore store) + { + if (_store.TryRemove(store.Options.DatabasePath, out var value)) + { + value.Dispose(); + new DirectoryInfo(store.Options.DatabasePath) + .Delete(true); + } + } + } +} diff --git a/tests/Neo.Platform.Storage.Tests/TestUtilities.cs b/tests/Neo.Platform.Storage.Tests/TestUtilities.cs new file mode 100644 index 0000000..82d8406 --- /dev/null +++ b/tests/Neo.Platform.Storage.Tests/TestUtilities.cs @@ -0,0 +1,40 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Neo.Platform.Storage.Tests +{ + internal class TestUtilities + { + public static ILoggerFactory TraceLoggerFactory => LoggerFactory.Create(logging => + { + var manger = new ConfigurationManager(); + logging.AddConfiguration(manger); + logging.ClearProviders(); + logging.AddSimpleConsole(options => options.SingleLine = true); + logging.AddDebug(); + logging.SetMinimumLevel(LogLevel.Trace); + }); + } +} diff --git a/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs new file mode 100644 index 0000000..ec101e0 --- /dev/null +++ b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs @@ -0,0 +1,97 @@ +// BSD 2-Clause License +// +// Copyright (c) 2026, Rapid Loop +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES + +using System.Linq; + +namespace Neo.Platform.Storage.Tests +{ + [TestClass] + public sealed class UT_BlockchainStore + { + [TestMethod] + public void TestSnapshot() + { + var store = TestStorePool.Shared.Rent(); + + store.Put([0xff, 0x00, 0x00], [0x00]); + + using (var snapshot = store.CreateSnapshot()) + { + var actualBytes = snapshot.Get([0xff, 0x00, 0x00]); + + Assert.IsTrue(snapshot.ContainsKey([0xff, 0x00, 0x00])); + Assert.IsNotNull(actualBytes); + Assert.AreEqual(1, actualBytes?.Length); + Assert.AreEqual(0x00, actualBytes?[0]); + + actualBytes = store.Get([0xff, 0x00, 0x00]); + + Assert.IsTrue(store.ContainsKey([0xff, 0x00, 0x00])); + Assert.IsNotNull(actualBytes); + Assert.AreEqual(1, actualBytes?.Length); + Assert.AreEqual(0x00, actualBytes?[0]); + + snapshot.Put([0xff, 0x00, 0x00], [0x01]); + snapshot.Commit(); + + actualBytes = store.Get([0xff, 0x00, 0x00]); + + Assert.IsTrue(store.ContainsKey([0xff, 0x00, 0x00])); + Assert.IsNotNull(actualBytes); + Assert.AreEqual(1, actualBytes?.Length); + Assert.AreEqual(0x01, actualBytes?[0]); + } + + TestStorePool.Shared.Return(store); + } + + [TestMethod] + public void TestSeek() + { + var store = TestStorePool.Shared.Rent(); + + store.Put([0x00, 0x00, 0x00], [0x00]); + store.Put([0x00, 0x00, 0x01], [0x01]); + store.Put([0x00, 0x00, 0x02], [0x02]); + store.Put([0x00, 0x00, 0x03], [0x03]); + store.Put([0x00, 0x00, 0x04], [0x04]); + + var actualResults = store.Seek((byte[])[0x00, 0x00, 0x01]).ToArray(); + + Assert.HasCount(4, actualResults); + Assert.AreSequenceEqual((byte[])[0x00, 0x00, 0x01], actualResults[0].Key); + Assert.AreSequenceEqual((byte[])[0x00, 0x00, 0x02], actualResults[1].Key); + Assert.AreSequenceEqual((byte[])[0x00, 0x00, 0x03], actualResults[2].Key); + Assert.AreSequenceEqual((byte[])[0x00, 0x00, 0x04], actualResults[3].Key); + + actualResults = [.. store.Seek((byte[])[0x00, 0x00, 0x03], seekFromEnd: true)]; + + Assert.HasCount(4, actualResults); + Assert.AreSequenceEqual((byte[])[0x00, 0x00, 0x03], actualResults[0].Key); + Assert.AreSequenceEqual((byte[])[0x00, 0x00, 0x02], actualResults[1].Key); + Assert.AreSequenceEqual((byte[])[0x00, 0x00, 0x01], actualResults[2].Key); + Assert.AreSequenceEqual((byte[])[0x00, 0x00, 0x00], actualResults[3].Key); + + TestStorePool.Shared.Return(store); + } + } +} diff --git a/tests/Neo.VM.Tests/Middleware/UT_DebuggerMiddleware.cs b/tests/Neo.VM.Tests/Middleware/UT_DebuggerMiddleware.cs index 3ace102..92a818d 100644 --- a/tests/Neo.VM.Tests/Middleware/UT_DebuggerMiddleware.cs +++ b/tests/Neo.VM.Tests/Middleware/UT_DebuggerMiddleware.cs @@ -51,7 +51,7 @@ public void TestJsonFiles() var script = OpCodeAssembler.Assemble(jsonTest.Script); using var scope = TestUtilities.Services.CreateScope(); - var vm = scope.ServiceProvider.GetRequiredService(); + using var vm = scope.ServiceProvider.GetRequiredService(); vm.LoadScript(script); @@ -78,7 +78,7 @@ public void TestBreakpoints() var script = sb.ToArray(); using var scope = TestUtilities.Services.CreateScope(); - var vm = scope.ServiceProvider.GetRequiredService(); + using var vm = scope.ServiceProvider.GetRequiredService(); vm.LoadScript(script); // Get the debugger that is inside the VM diff --git a/tests/Neo.VM.Tests/Neo.VM.Tests.csproj b/tests/Neo.VM.Tests/Neo.VM.Tests.csproj index 62edadf..61a4763 100644 --- a/tests/Neo.VM.Tests/Neo.VM.Tests.csproj +++ b/tests/Neo.VM.Tests/Neo.VM.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/tests/Neo.VM.Tests/TestUtilities.cs b/tests/Neo.VM.Tests/TestUtilities.cs index 98b313d..93a15ad 100644 --- a/tests/Neo.VM.Tests/TestUtilities.cs +++ b/tests/Neo.VM.Tests/TestUtilities.cs @@ -34,7 +34,7 @@ internal class TestUtilities private static readonly ServiceProvider s_serviceProvider; public static IServiceProvider Services => s_serviceProvider; - public static readonly ILoggerFactory TraceLoggerFactory = LoggerFactory.Create(logging => + public static ILoggerFactory TraceLoggerFactory => LoggerFactory.Create(logging => { var manger = new ConfigurationManager(); logging.AddConfiguration(manger); @@ -48,8 +48,8 @@ static TestUtilities() var services = new ServiceCollection(); services - .AddEngineDebugger() - .AddExecuteLogger() + .AddEngineMiddlewareDebugger() + .AddEngineMiddlewareLogger() .AddExecutionEngine() .AddScoped() .AddLogging( diff --git a/tests/Neo.VM.Tests/Types/UT_VMObject.cs b/tests/Neo.VM.Tests/Types/UT_VMObject.cs index ace465d..b8fae4f 100644 --- a/tests/Neo.VM.Tests/Types/UT_VMObject.cs +++ b/tests/Neo.VM.Tests/Types/UT_VMObject.cs @@ -280,7 +280,7 @@ public void TestCast() item = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 }; Assert.IsInstanceOfType(item); - CollectionAssert.AreEqual(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 }, item.AsSpan().ToArray()); + Assert.AreSequenceEqual(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 }, item.AsSpan().ToArray()); } [TestMethod] @@ -301,7 +301,7 @@ public void TestCloneSelfReference() var aa = (VMArray)a.Clone(); - Assert.AreEqual(a, aa); + Assert.AreSequenceEqual(a, aa); Assert.AreNotSame(a, aa); Assert.IsTrue(a[^2].Equals(aa[^2])); Assert.IsTrue(a.HasCircularReference()); diff --git a/tests/Neo.VM.Tests/UT_NeoVirtualMachine.cs b/tests/Neo.VM.Tests/UT_NeoVirtualMachine.cs index 7b41573..08a829f 100644 --- a/tests/Neo.VM.Tests/UT_NeoVirtualMachine.cs +++ b/tests/Neo.VM.Tests/UT_NeoVirtualMachine.cs @@ -63,7 +63,7 @@ public void TestSystemCall() var expectedGasPrice = 1_00000000L + GasTable.GetGasCost(OpCode.PUSHDATA1, HardFork.Gorgon); using var scope = TestUtilities.Services.CreateScope(); - var vm = scope.ServiceProvider.GetRequiredService(); + using var vm = scope.ServiceProvider.GetRequiredService(); var actualMethodDesc = vm.RegisterSystemCall( expectedSystemName, @@ -74,8 +74,8 @@ public void TestSystemCall() var actualFoundSystemCall = vm.SystemCallTable.TryGetValue(expectedSystemName, out var actualSystemCallAddress); using var sb = new ScriptBuilder() - .EmitSysCall(actualMethodDesc, expectedParamValue) - .EmitReturn(); + .EmitSysCall(actualMethodDesc, expectedParamValue) + .EmitReturn(); vm.LoadScript(sb.ToArray()); @@ -112,7 +112,7 @@ public void TestSimpleAdd() ]; using var scope = TestUtilities.Services.CreateScope(); - var vm = scope.ServiceProvider.GetRequiredService(); + using var vm = scope.ServiceProvider.GetRequiredService(); vm.LoadScript(script); var actualState = vm.Execute(); @@ -136,7 +136,7 @@ public void TestCircularReferenceCreateArray() .EmitReturn(); using var scope = TestUtilities.Services.CreateScope(); - var vm = scope.ServiceProvider.GetRequiredService(); + using var vm = scope.ServiceProvider.GetRequiredService(); vm.LoadScript(sb.ToArray()); var actualState = vm.Execute(); @@ -162,7 +162,7 @@ public void TestCreateArray() .EmitReturn(); using var scope = TestUtilities.Services.CreateScope(); - var vm = scope.ServiceProvider.GetRequiredService(); + using var vm = scope.ServiceProvider.GetRequiredService(); vm.LoadScript(sb.ToArray()); var actualState = vm.Execute(); @@ -203,7 +203,7 @@ public void TestPushingSimpleTypes() var script = sb.ToArray(); using var scope = TestUtilities.Services.CreateScope(); - var vm = scope.ServiceProvider.GetRequiredService(); + using var vm = scope.ServiceProvider.GetRequiredService(); vm.LoadScript(script); var actualState = vm.Execute(); diff --git a/tests/Neo.Wallet.Tests/Neo.Wallet.Tests.csproj b/tests/Neo.Wallet.Tests/Neo.Wallet.Tests.csproj index 19f638a..ae2202a 100644 --- a/tests/Neo.Wallet.Tests/Neo.Wallet.Tests.csproj +++ b/tests/Neo.Wallet.Tests/Neo.Wallet.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/Neo.Wallet.Tests/UT_ChainWallet.cs b/tests/Neo.Wallet.Tests/UT_ChainWallet.cs index a0ce0da..d9aebd9 100644 --- a/tests/Neo.Wallet.Tests/UT_ChainWallet.cs +++ b/tests/Neo.Wallet.Tests/UT_ChainWallet.cs @@ -41,7 +41,7 @@ public void TestGetKeyFromWifString() var actualKeyBytes = ChainWallet.GetKeyFromWifString("L3tgppXLgdaeqSGSFw1Go3skBiy8vQAM7YMXvTHsKQtE16PBncSU"); - CollectionAssert.AreEqual(expectedKeyBytes, actualKeyBytes); + Assert.AreSequenceEqual(expectedKeyBytes, actualKeyBytes); } [TestMethod] @@ -53,7 +53,7 @@ public void TestNep2String() var actualNep2String = ChainWallet.ToNep2String(expectedPrivateKeyBytes, "pwd", defaultSCryptParameters); var actualPrivateKeyBytes = ChainWallet.GetKeyFromNep2String(actualNep2String, "pwd", defaultSCryptParameters); - CollectionAssert.AreEqual(expectedPrivateKeyBytes, actualPrivateKeyBytes); + Assert.AreSequenceEqual(expectedPrivateKeyBytes, actualPrivateKeyBytes); } } } diff --git a/tests/Neo.Wallet.Tests/UT_DevWallet.cs b/tests/Neo.Wallet.Tests/UT_DevWallet.cs index 8a7a952..a0573bf 100644 --- a/tests/Neo.Wallet.Tests/UT_DevWallet.cs +++ b/tests/Neo.Wallet.Tests/UT_DevWallet.cs @@ -51,9 +51,9 @@ public void TestCreateAccount() Assert.AreEqual(expectedAccount.IsLocked, actualAccount.IsLocked); Assert.AreEqual(expectedAccount.Contract.ScriptHash, actualAccount.Contract.ScriptHash); - CollectionAssert.AreEqual(expectedAccount.Contract.Script, actualAccount.Contract.Script); - CollectionAssert.AreEqual(expectedAccount.Contract.ParameterList, actualAccount.Contract.ParameterList); - CollectionAssert.AreEqual(expectedAccount.GetPrivateKey(), actualAccount.GetPrivateKey()); + Assert.AreSequenceEqual(expectedAccount.Contract.Script, actualAccount.Contract.Script); + Assert.AreSequenceEqual(expectedAccount.Contract.ParameterList, actualAccount.Contract.ParameterList); + Assert.AreSequenceEqual(expectedAccount.GetPrivateKey(), actualAccount.GetPrivateKey()); } } } diff --git a/tests/Neo.Wallet.Tests/UT_Nep6Wallet.cs b/tests/Neo.Wallet.Tests/UT_Nep6Wallet.cs index 0fc7c02..e1a2f36 100644 --- a/tests/Neo.Wallet.Tests/UT_Nep6Wallet.cs +++ b/tests/Neo.Wallet.Tests/UT_Nep6Wallet.cs @@ -57,9 +57,9 @@ public void TestCreateAccount() Assert.AreEqual(expectedAccount.IsLocked, actualAccount.IsLocked); Assert.AreEqual(expectedAccount.Contract.ScriptHash, actualAccount.Contract.ScriptHash); - CollectionAssert.AreEqual(expectedAccount.Contract.Script, actualAccount.Contract.Script); - CollectionAssert.AreEqual(expectedAccount.Contract.ParameterList, actualAccount.Contract.ParameterList); - CollectionAssert.AreEqual(expectedAccount.GetPrivateKey(), actualAccount.GetPrivateKey()); + Assert.AreSequenceEqual(expectedAccount.Contract.Script, actualAccount.Contract.Script); + Assert.AreSequenceEqual(expectedAccount.Contract.ParameterList, actualAccount.Contract.ParameterList); + Assert.AreSequenceEqual(expectedAccount.GetPrivateKey(), actualAccount.GetPrivateKey()); Assert.IsTrue(expectedAccount.ChangePassword("abc123", "pwd")); Assert.IsTrue(actualAccount.ChangePassword("abc123", "pwd"));