From f03fd5921ab4490359e02e5a9a5f88d93f474326 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Wed, 1 Jul 2026 06:51:56 -0400 Subject: [PATCH 1/9] Added `BlockchainStore` class --- All.slnx | 2 + .../BlockchainStoreOptions.cs | 35 +++++ src/Neo.Platform.Storage/BlockchainStore.cs | 148 ++++++++++++++++++ src/Neo.Platform.Storage/ColumnFamilyNames.cs | 33 ++++ .../Neo.Platform.Storage.csproj | 18 +++ .../MSTestSettings.cs | 23 +++ .../Neo.Platform.Storage.Tests.csproj | 21 +++ .../UT_BlockchainStore.cs | 59 +++++++ 8 files changed, 339 insertions(+) create mode 100644 src/Neo.Configuration/BlockchainStoreOptions.cs create mode 100644 src/Neo.Platform.Storage/BlockchainStore.cs create mode 100644 src/Neo.Platform.Storage/ColumnFamilyNames.cs create mode 100644 src/Neo.Platform.Storage/Neo.Platform.Storage.csproj create mode 100644 tests/Neo.Platform.Storage.Tests/MSTestSettings.cs create mode 100644 tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj create mode 100644 tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs 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.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs new file mode 100644 index 0000000..32ae19a --- /dev/null +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -0,0 +1,148 @@ +// 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.Options; +using Neo.Configuration; +using RocksDbNet; +using System; +using System.IO; + +namespace Neo.Platform.Storage +{ + public sealed class BlockchainStore : IDisposable + { + private static readonly ColumnFamilyDescriptor[] s_columnFamilies = + [ + new(ColumnFamilyNames.Default), + new(ColumnFamilyNames.Meta), + new(ColumnFamilyNames.Indexes), + new(ColumnFamilyNames.Blocks), + new(ColumnFamilyNames.Transactions), + ]; + + private readonly Cache _blockSharedCache; + private readonly FilterPolicy _bloomFilter; + private readonly RocksDb _db; + + public BlockchainStore( + IOptions options, + ILogger? logger = default) + { + var configuration = options.Value; + + var dirInfo = new DirectoryInfo(configuration.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 = configuration.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, configuration.DatabasePath, s_columnFamilies); + } + + public void Dispose() + { + _db.Dispose(); + _blockSharedCache.Dispose(); + _bloomFilter.Dispose(); + } + + public ReadOnlySpan Get(ReadOnlySpan key, string? columnFamilyName = default) => + _db.Get(key, GetColumnFamilyHandle(columnFamilyName)); + + public void Put(ReadOnlySpan key, ReadOnlySpan value, string? columnFamilyName = default) => + _db.Put(key, value, GetColumnFamilyHandle(columnFamilyName)); + + private ColumnFamilyHandle GetColumnFamilyHandle(string? columnFamilyName = default) => + string.IsNullOrEmpty(columnFamilyName) ? + _db.GetDefaultColumnFamily() : + _db.GetColumnFamily(columnFamilyName); + + 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/ColumnFamilyNames.cs b/src/Neo.Platform.Storage/ColumnFamilyNames.cs new file mode 100644 index 0000000..258c135 --- /dev/null +++ b/src/Neo.Platform.Storage/ColumnFamilyNames.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 + +namespace Neo.Platform.Storage +{ + public static class ColumnFamilyNames + { + public static readonly string Default = "default"; + public static readonly string Meta = "meta"; + public static readonly string Indexes = "indexes"; + public static readonly string Blocks = "blocks"; + public static readonly string Transactions = "transactions"; + } +} 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/tests/Neo.Platform.Storage.Tests/MSTestSettings.cs b/tests/Neo.Platform.Storage.Tests/MSTestSettings.cs new file mode 100644 index 0000000..b7fb1e6 --- /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..82b3dd0 --- /dev/null +++ b/tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + latest + enable + + + + + + + + + + + + + + + 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..9b17e37 --- /dev/null +++ b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs @@ -0,0 +1,59 @@ +// 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.IO; + +namespace Neo.Platform.Storage.Tests +{ + [TestClass] + public sealed class UT_BlockchainStore + { + [TestMethod] + public void TestColumnFamilyNames() + { + var options = Options.Create(new BlockchainStoreOptions()); + var store = new BlockchainStore(options); + + store.Put([0xff, 0x00, 0x00], [0x00]); + store.Put([0xff, 0x00, 0x01], [0x01], ColumnFamilyNames.Blocks); + + var actualBytes = store.Get([0xff, 0x00, 0x01]); + + Assert.IsTrue(actualBytes.IsEmpty); + Assert.AreEqual(0, actualBytes.Length); + + actualBytes = store.Get([0xff, 0x00, 0x01], ColumnFamilyNames.Blocks); + + Assert.IsFalse(actualBytes.IsEmpty); + Assert.AreEqual(1, actualBytes.Length); + Assert.AreEqual(1, actualBytes[0]); + + store.Dispose(); + + var dirInfo = new DirectoryInfo(options.Value.DatabasePath); + + dirInfo.Delete(true); + } + } +} From 48789f2eb5debd8e728f2e8436ba5392dd573b98 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Mon, 6 Jul 2026 09:27:22 -0400 Subject: [PATCH 2/9] Updated BlockchainStore --- src/Neo.Platform.Storage/BlockchainStore.cs | 76 +++++++++++++++++-- .../Interface/IReadOnlyStore.cs | 39 ++++++++++ src/Neo.Platform.Storage/Interface/IStore.cs | 35 +++++++++ .../Interface/IStoreSnapshot.cs | 31 ++++++++ .../UT_BlockchainStore.cs | 16 ++-- 5 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs create mode 100644 src/Neo.Platform.Storage/Interface/IStore.cs create mode 100644 src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs diff --git a/src/Neo.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs index 32ae19a..a248ca9 100644 --- a/src/Neo.Platform.Storage/BlockchainStore.cs +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -23,13 +23,17 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Neo.Configuration; +using Neo.Platform.Storage.Interface; 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 : IDisposable + public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, IEnumerable, IStore, IDisposable { private static readonly ColumnFamilyDescriptor[] s_columnFamilies = [ @@ -117,14 +121,76 @@ public void Dispose() _bloomFilter.Dispose(); } - public ReadOnlySpan Get(ReadOnlySpan key, string? columnFamilyName = default) => - _db.Get(key, GetColumnFamilyHandle(columnFamilyName)); + public IStoreSnapshot CreateSnapshot() + { + throw new NotImplementedException(); + } - public void Put(ReadOnlySpan key, ReadOnlySpan value, string? columnFamilyName = default) => + public void Put(in ReadOnlySpan key, in ReadOnlySpan value, string? columnFamilyName = default) => _db.Put(key, value, GetColumnFamilyHandle(columnFamilyName)); + public void Delete(in ReadOnlySpan key, string? columnFamilyName = default) + { + //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) + _db.Delete(key, GetColumnFamilyHandle(columnFamilyName)); + } + + public bool ContainsKey(in ReadOnlySpan key, string? columnFamilyName = default) + { + //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) + return TryGet(key, out _, columnFamilyName); + //return false; + } + + public byte[]? Get(in ReadOnlySpan key, string? columnFamilyName = default) + { + //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) + return _db.Get(key, GetColumnFamilyHandle(columnFamilyName)); + //return default; + } + + public bool TryGet(in ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default) + { + //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) + //{ + var data = _db.Get(key, GetColumnFamilyHandle(columnFamilyName)); + if (data is not null) + { + value = data; + return true; + } + //} + + value = null; + return false; + } + + public IEnumerable<(byte[] Key, byte[] Value)> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default) + { + using var iter = _db.NewIterator(GetColumnFamilyHandle(columnFamilyName)); + + for (iter.SeekForPrev(keyOrPrefix.Span); iter.IsValid();) + { + yield return new(iter.KeyToArray(), iter.ValueToArray()); + if (seekFromEnd) + iter.Prev(); + else + iter.Next(); + } + } + + public IEnumerator GetEnumerator() => + GetEnumerator(); + + IEnumerator<(byte[] Key, byte[] Value)> IEnumerable<(byte[] Key, byte[] Value)>.GetEnumerator() + { + using var iter = _db.NewIterator(); + for (iter.SeekToFirst(); iter.IsValid(); iter.Next()) + yield return new(iter.KeyToArray(), iter.ValueToArray()); + } + private ColumnFamilyHandle GetColumnFamilyHandle(string? columnFamilyName = default) => - string.IsNullOrEmpty(columnFamilyName) ? + string.IsNullOrWhiteSpace(columnFamilyName) ? _db.GetDefaultColumnFamily() : _db.GetColumnFamily(columnFamilyName); diff --git a/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs b/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs new file mode 100644 index 0000000..1a3fbc9 --- /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 + { + bool ContainsKey(in ReadOnlySpan key, string? columnFamilyName = default); + + byte[]? Get(in ReadOnlySpan key, string? columnFamilyName = default); + + bool TryGet(in ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default); + + IEnumerable<(byte[] Key, byte[] Value)> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default); + } +} diff --git a/src/Neo.Platform.Storage/Interface/IStore.cs b/src/Neo.Platform.Storage/Interface/IStore.cs new file mode 100644 index 0000000..f43f60d --- /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(in ReadOnlySpan key, in ReadOnlySpan value, string? columnFamilyName = default); + + void Delete(in ReadOnlySpan key, string? columnFamilyName = default); + } +} diff --git a/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs b/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs new file mode 100644 index 0000000..4a65037 --- /dev/null +++ b/src/Neo.Platform.Storage/Interface/IStoreSnapshot.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 + +using System; + +namespace Neo.Platform.Storage.Interface +{ + public interface IStoreSnapshot : IStore, IDisposable + { + void Commit(); + } +} diff --git a/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs index 9b17e37..b7fdb90 100644 --- a/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs +++ b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs @@ -32,7 +32,11 @@ public sealed class UT_BlockchainStore [TestMethod] public void TestColumnFamilyNames() { - var options = Options.Create(new BlockchainStoreOptions()); + var options = Options.Create(new BlockchainStoreOptions() + { + DatabasePath = Path.Combine(Path.GetRandomFileName()), + }); + var store = new BlockchainStore(options); store.Put([0xff, 0x00, 0x00], [0x00]); @@ -40,14 +44,14 @@ public void TestColumnFamilyNames() var actualBytes = store.Get([0xff, 0x00, 0x01]); - Assert.IsTrue(actualBytes.IsEmpty); - Assert.AreEqual(0, actualBytes.Length); + Assert.IsFalse(store.ContainsKey([0xff, 0x00, 0x01])); + Assert.IsNull(actualBytes); actualBytes = store.Get([0xff, 0x00, 0x01], ColumnFamilyNames.Blocks); - Assert.IsFalse(actualBytes.IsEmpty); - Assert.AreEqual(1, actualBytes.Length); - Assert.AreEqual(1, actualBytes[0]); + Assert.IsNotNull(actualBytes); + Assert.AreEqual(1, actualBytes?.Length); + Assert.AreEqual(0x01, actualBytes?[0]); store.Dispose(); From 032b8d2fa2f39e4d283a23539deafe61fa58ef75 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Mon, 6 Jul 2026 10:57:09 -0400 Subject: [PATCH 3/9] Added test utils --- src/Neo.Platform.Storage/BlockchainStore.cs | 11 ++-- .../TestStorePool.cs | 62 +++++++++++++++++++ .../UT_BlockchainStore.cs | 17 +---- 3 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 tests/Neo.Platform.Storage.Tests/TestStorePool.cs diff --git a/src/Neo.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs index a248ca9..6bc9149 100644 --- a/src/Neo.Platform.Storage/BlockchainStore.cs +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -44,17 +44,20 @@ public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, I new(ColumnFamilyNames.Transactions), ]; + public BlockchainStoreOptions Options => _options; + private readonly Cache _blockSharedCache; private readonly FilterPolicy _bloomFilter; private readonly RocksDb _db; + private readonly BlockchainStoreOptions _options; public BlockchainStore( IOptions options, ILogger? logger = default) { - var configuration = options.Value; + _options = options.Value; - var dirInfo = new DirectoryInfo(configuration.DatabasePath); + var dirInfo = new DirectoryInfo(_options.DatabasePath); if (dirInfo.Exists == false) dirInfo.Create(); @@ -67,7 +70,7 @@ public BlockchainStore( var dbOptions = new DbOptions() { - CreateIfMissing = configuration.CreateIfMissing, + CreateIfMissing = _options.CreateIfMissing, CreateMissingColumnFamilies = true, // Compression @@ -111,7 +114,7 @@ public BlockchainStore( dbOptions.OptimizeForPointLookup(64); // 64MB - _db = RocksDb.Open(dbOptions, configuration.DatabasePath, s_columnFamilies); + _db = RocksDb.Open(dbOptions, _options.DatabasePath, s_columnFamilies); } public void Dispose() diff --git a/tests/Neo.Platform.Storage.Tests/TestStorePool.cs b/tests/Neo.Platform.Storage.Tests/TestStorePool.cs new file mode 100644 index 0000000..690e49f --- /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); + + _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/UT_BlockchainStore.cs b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs index b7fdb90..1629a3d 100644 --- a/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs +++ b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs @@ -20,10 +20,6 @@ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES -using Microsoft.Extensions.Options; -using Neo.Configuration; -using System.IO; - namespace Neo.Platform.Storage.Tests { [TestClass] @@ -32,12 +28,7 @@ public sealed class UT_BlockchainStore [TestMethod] public void TestColumnFamilyNames() { - var options = Options.Create(new BlockchainStoreOptions() - { - DatabasePath = Path.Combine(Path.GetRandomFileName()), - }); - - var store = new BlockchainStore(options); + var store = TestStorePool.Shared.Rent(); store.Put([0xff, 0x00, 0x00], [0x00]); store.Put([0xff, 0x00, 0x01], [0x01], ColumnFamilyNames.Blocks); @@ -53,11 +44,7 @@ public void TestColumnFamilyNames() Assert.AreEqual(1, actualBytes?.Length); Assert.AreEqual(0x01, actualBytes?[0]); - store.Dispose(); - - var dirInfo = new DirectoryInfo(options.Value.DatabasePath); - - dirInfo.Delete(true); + TestStorePool.Shared.Return(store); } } } From 1f7851469524572e17673a3f76796ba5efee7556 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt <8141309+cschuchardt88@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:20:12 -0400 Subject: [PATCH 4/9] fixed order --- src/Neo.Platform.Storage/BlockchainStore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Neo.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs index 6bc9149..8187fdc 100644 --- a/src/Neo.Platform.Storage/BlockchainStore.cs +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -35,6 +35,8 @@ namespace Neo.Platform.Storage { public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, IEnumerable, IStore, IDisposable { + public BlockchainStoreOptions Options => _options; + private static readonly ColumnFamilyDescriptor[] s_columnFamilies = [ new(ColumnFamilyNames.Default), @@ -44,8 +46,6 @@ public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, I new(ColumnFamilyNames.Transactions), ]; - public BlockchainStoreOptions Options => _options; - private readonly Cache _blockSharedCache; private readonly FilterPolicy _bloomFilter; private readonly RocksDb _db; From c74e5fbe5581f38911e3aed50d6c198ef82f859d Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt <8141309+cschuchardt88@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:28:52 -0400 Subject: [PATCH 5/9] Removed `in` keyword from interfaces --- src/Neo.Platform.Storage/BlockchainStore.cs | 12 ++++++------ src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs | 8 ++++---- src/Neo.Platform.Storage/Interface/IStore.cs | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Neo.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs index 8187fdc..d8e481d 100644 --- a/src/Neo.Platform.Storage/BlockchainStore.cs +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -33,7 +33,7 @@ namespace Neo.Platform.Storage { - public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, IEnumerable, IStore, IDisposable + public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, IEnumerable, IStore { public BlockchainStoreOptions Options => _options; @@ -129,30 +129,30 @@ public IStoreSnapshot CreateSnapshot() throw new NotImplementedException(); } - public void Put(in ReadOnlySpan key, in ReadOnlySpan value, string? columnFamilyName = default) => + public void Put(ReadOnlySpan key, ReadOnlySpan value, string? columnFamilyName = default) => _db.Put(key, value, GetColumnFamilyHandle(columnFamilyName)); - public void Delete(in ReadOnlySpan key, string? columnFamilyName = default) + public void Delete(ReadOnlySpan key, string? columnFamilyName = default) { //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) _db.Delete(key, GetColumnFamilyHandle(columnFamilyName)); } - public bool ContainsKey(in ReadOnlySpan key, string? columnFamilyName = default) + public bool ContainsKey(ReadOnlySpan key, string? columnFamilyName = default) { //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) return TryGet(key, out _, columnFamilyName); //return false; } - public byte[]? Get(in ReadOnlySpan key, string? columnFamilyName = default) + public byte[]? Get(ReadOnlySpan key, string? columnFamilyName = default) { //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) return _db.Get(key, GetColumnFamilyHandle(columnFamilyName)); //return default; } - public bool TryGet(in ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default) + public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default) { //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) //{ diff --git a/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs b/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs index 1a3fbc9..01b289c 100644 --- a/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs +++ b/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs @@ -26,13 +26,13 @@ namespace Neo.Platform.Storage.Interface { - public interface IReadOnlyStore + public interface IReadOnlyStore : IDisposable { - bool ContainsKey(in ReadOnlySpan key, string? columnFamilyName = default); + bool ContainsKey(ReadOnlySpan key, string? columnFamilyName = default); - byte[]? Get(in ReadOnlySpan key, string? columnFamilyName = default); + byte[]? Get(ReadOnlySpan key, string? columnFamilyName = default); - bool TryGet(in ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default); + bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default); IEnumerable<(byte[] Key, byte[] Value)> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default); } diff --git a/src/Neo.Platform.Storage/Interface/IStore.cs b/src/Neo.Platform.Storage/Interface/IStore.cs index f43f60d..4d3e58f 100644 --- a/src/Neo.Platform.Storage/Interface/IStore.cs +++ b/src/Neo.Platform.Storage/Interface/IStore.cs @@ -28,8 +28,8 @@ public interface IStore : IReadOnlyStore { IStoreSnapshot CreateSnapshot(); - void Put(in ReadOnlySpan key, in ReadOnlySpan value, string? columnFamilyName = default); + void Put(ReadOnlySpan key, ReadOnlySpan value, string? columnFamilyName = default); - void Delete(in ReadOnlySpan key, string? columnFamilyName = default); + void Delete(ReadOnlySpan key, string? columnFamilyName = default); } } From c1b3c206673ee52b1cb8da5f854a83646ea7114a Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt <8141309+cschuchardt88@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:49:16 -0400 Subject: [PATCH 6/9] Fixed `Murmur128` unsafe pointers --- src/Neo.IO/Hashing/Murmur128.cs | 75 ++++++--------------- src/Neo.Platform.Storage/BlockchainStore.cs | 2 +- tests/Neo.IO.Tests/Hashing/UT_Murmur128.cs | 28 +++++++- 3 files changed, 47 insertions(+), 58 deletions(-) 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.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs index d8e481d..150d3b4 100644 --- a/src/Neo.Platform.Storage/BlockchainStore.cs +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -33,7 +33,7 @@ namespace Neo.Platform.Storage { - public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, IEnumerable, IStore + public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, IStore { public BlockchainStoreOptions Options => _options; 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); + } } } From 13a45f01f18f06bf99401f078f0065cbe647b8aa Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt <8141309+cschuchardt88@users.noreply.github.com> Date: Sun, 12 Jul 2026 00:12:25 -0400 Subject: [PATCH 7/9] Added Store Snapshots --- src/Neo.Core/Extensions/ByteExtensions.cs | 2 +- src/Neo.Platform.Storage/BlockchainStore.cs | 14 +- .../BlockchainStoreSnapshot.cs | 134 ++++++++++++++++++ .../Interface/IReadOnlyStore.cs | 2 +- .../Interface/IStoreSnapshot.cs | 2 + src/Neo.Platform.Storage/StoreExtensions.cs | 54 +++++++ tests/Neo.IO.Tests/Data/UT_BloomFilter.cs | 2 +- .../UT_BlockchainStore.cs | 37 +++++ 8 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs create mode 100644 src/Neo.Platform.Storage/StoreExtensions.cs 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.Platform.Storage/BlockchainStore.cs b/src/Neo.Platform.Storage/BlockchainStore.cs index 150d3b4..3c52e07 100644 --- a/src/Neo.Platform.Storage/BlockchainStore.cs +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -33,7 +33,7 @@ namespace Neo.Platform.Storage { - public sealed class BlockchainStore : IEnumerable<(byte[] Key, byte[] Value)>, IStore + public sealed class BlockchainStore : IEnumerable>, IStore { public BlockchainStoreOptions Options => _options; @@ -124,10 +124,8 @@ public void Dispose() _bloomFilter.Dispose(); } - public IStoreSnapshot CreateSnapshot() - { - throw new NotImplementedException(); - } + public IStoreSnapshot CreateSnapshot() => + new BlockchainStoreSnapshot(this, _db); public void Put(ReadOnlySpan key, ReadOnlySpan value, string? columnFamilyName = default) => _db.Put(key, value, GetColumnFamilyHandle(columnFamilyName)); @@ -164,11 +162,11 @@ public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value } //} - value = null; + value = default; return false; } - public IEnumerable<(byte[] Key, byte[] Value)> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default) + public IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default) { using var iter = _db.NewIterator(GetColumnFamilyHandle(columnFamilyName)); @@ -185,7 +183,7 @@ public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value public IEnumerator GetEnumerator() => GetEnumerator(); - IEnumerator<(byte[] Key, byte[] Value)> IEnumerable<(byte[] Key, byte[] Value)>.GetEnumerator() + IEnumerator> IEnumerable>.GetEnumerator() { using var iter = _db.NewIterator(); for (iter.SeekToFirst(); iter.IsValid(); iter.Next()) diff --git a/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs b/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs new file mode 100644 index 0000000..10b7917 --- /dev/null +++ b/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs @@ -0,0 +1,134 @@ +// 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.Platform.Storage.Interface; +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; + + public BlockchainStoreSnapshot(IStore store, RocksDb db) + { + _store = store; + _db = db; + _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() => + _db.Write(_writeBatch); + + public bool ContainsKey(ReadOnlySpan key, string? columnFamilyName = default) + { + //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) + return TryGet(key, out _, columnFamilyName); + //return false; + } + + public IStoreSnapshot CreateSnapshot() => + new BlockchainStoreSnapshot(this, _db); + + public void Delete(ReadOnlySpan key, string? columnFamilyName = default) + { + //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) + _writeBatch.Delete(key, GetColumnFamilyHandle(columnFamilyName)); + } + + public byte[]? Get(ReadOnlySpan key, string? columnFamilyName = default) + { + //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) + return _db.Get(key, GetColumnFamilyHandle(columnFamilyName), _readOptions); + //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, string? columnFamilyName = default) => + _writeBatch.Put(key, value, GetColumnFamilyHandle(columnFamilyName)); + + public IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default) + { + using var iter = _db.NewIterator(GetColumnFamilyHandle(columnFamilyName), _readOptions); + + for (iter.SeekForPrev(keyOrPrefix.Span); iter.IsValid();) + { + yield return new(iter.KeyToArray(), iter.ValueToArray()); + if (seekFromEnd) + iter.Prev(); + else + iter.Next(); + } + } + + public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default) + { + //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) + //{ + var data = _db.Get(key, GetColumnFamilyHandle(columnFamilyName), _readOptions); + if (data is not null) + { + value = data; + return true; + } + //} + + value = default; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + private ColumnFamilyHandle GetColumnFamilyHandle(string? columnFamilyName = default) => + string.IsNullOrWhiteSpace(columnFamilyName) ? + _db.GetDefaultColumnFamily() : + _db.GetColumnFamily(columnFamilyName); + } +} diff --git a/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs b/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs index 01b289c..892d0cb 100644 --- a/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs +++ b/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs @@ -34,6 +34,6 @@ public interface IReadOnlyStore : IDisposable bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default); - IEnumerable<(byte[] Key, byte[] Value)> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default); + IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default); } } diff --git a/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs b/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs index 4a65037..f9eaaed 100644 --- a/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs +++ b/src/Neo.Platform.Storage/Interface/IStoreSnapshot.cs @@ -26,6 +26,8 @@ namespace Neo.Platform.Storage.Interface { public interface IStoreSnapshot : IStore, IDisposable { + IStore Store { get; } + void Commit(); } } 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/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.Platform.Storage.Tests/UT_BlockchainStore.cs b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs index 1629a3d..25b62d8 100644 --- a/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs +++ b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs @@ -46,5 +46,42 @@ public void TestColumnFamilyNames() TestStorePool.Shared.Return(store); } + + [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); + } } } From 55b0377eecd5a7cd62f37d9a0d89dd79a6842d82 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt <8141309+cschuchardt88@users.noreply.github.com> Date: Sun, 12 Jul 2026 00:36:44 -0400 Subject: [PATCH 8/9] Added Seek Tests --- .../UT_BlockchainStore.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs index 25b62d8..5769c90 100644 --- a/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs +++ b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs @@ -20,6 +20,8 @@ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES +using System.Linq; + namespace Neo.Platform.Storage.Tests { [TestClass] @@ -47,6 +49,31 @@ public void TestColumnFamilyNames() TestStorePool.Shared.Return(store); } + [TestMethod] + public void TestSnapshotColumnFamilyNames() + { + var store = TestStorePool.Shared.Rent(); + + store.Put([0xff, 0x00, 0x00], [0x00]); + store.Put([0xff, 0x00, 0x01], [0x01], ColumnFamilyNames.Blocks); + + var actualBytes = store.Get([0xff, 0x00, 0x01]); + + Assert.IsFalse(store.ContainsKey([0xff, 0x00, 0x01])); + Assert.IsNull(actualBytes); + + using (var snapshot = store.CreateSnapshot()) + { + actualBytes = snapshot.Get([0xff, 0x00, 0x01], ColumnFamilyNames.Blocks); + + Assert.IsNotNull(actualBytes); + Assert.AreEqual(1, actualBytes?.Length); + Assert.AreEqual(0x01, actualBytes?[0]); + } + + TestStorePool.Shared.Return(store); + } + [TestMethod] public void TestSnapshot() { @@ -83,5 +110,35 @@ public void TestSnapshot() 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); + CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x01], actualResults[0].Key); + CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x02], actualResults[1].Key); + CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x03], actualResults[2].Key); + CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x04], actualResults[3].Key); + + actualResults = [.. store.Seek((byte[])[0x00, 0x00, 0x03], seekFromEnd: true)]; + + Assert.HasCount(4, actualResults); + CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x03], actualResults[0].Key); + CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x02], actualResults[1].Key); + CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x01], actualResults[2].Key); + CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x00], actualResults[3].Key); + + TestStorePool.Shared.Return(store); + } } } From 09fef5a869da00c06b2b2e5342b41f51b83c34ff Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt <8141309+cschuchardt88@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:33:00 -0400 Subject: [PATCH 9/9] Updated packages and code to reflect changes --- src/Neo.Core/Neo.Core.csproj | 6 +- src/Neo.IO/Neo.IO.csproj | 2 +- src/Neo.Localization/Neo.Localization.csproj | 2 +- .../Resources/VM/VirtualMachineLocalizer.resx | 4 +- src/Neo.Platform.CLI/Neo.Platform.CLI.csproj | 6 +- .../Neo.Platform.Hosting.csproj | 6 +- src/Neo.Platform.Storage/BlockchainStore.cs | 120 ++++++++++++----- .../BlockchainStoreSnapshot.cs | 127 +++++++++++++----- src/Neo.Platform.Storage/ColumnFamilyNames.cs | 6 +- .../Interface/IReadOnlyStore.cs | 8 +- src/Neo.Platform.Storage/Interface/IStore.cs | 4 +- .../Logging/LoggingExtensions.cs | 106 +++++++++++++++ .../Logging/StoreEventId.cs | 34 +++++ .../Extensions/ServiceCollectionExtensions.cs | 33 +++-- src/Neo.VM/Middleware/DebuggerMiddleware.cs | 7 +- .../Middleware/ExecuteLoggerMiddleware.cs | 8 +- src/Neo.VM/Neo.VM.csproj | 4 +- tests/Directory.Build.Props | 2 +- .../Neo.Configuration.Tests.csproj | 2 +- .../Blockchain/UT_BlockHeader.cs | 6 +- .../Extensions/UT_ByteExtensions.cs | 6 +- .../Extensions/UT_StreamExtensions.cs | 4 +- tests/Neo.Core.Tests/Neo.Core.Tests.csproj | 2 +- tests/Neo.Core.Tests/Text/UT_Base58.cs | 2 +- tests/Neo.Core.Tests/UT_UInt160.cs | 1 - tests/Neo.Core.Tests/UT_UInt256.cs | 1 - tests/Neo.IO.Tests/Neo.IO.Tests.csproj | 2 +- .../MSTestSettings.cs | 2 +- .../Neo.Platform.Storage.Tests.csproj | 4 +- .../TestStorePool.cs | 2 +- .../TestUtilities.cs | 40 ++++++ .../UT_BlockchainStore.cs | 63 ++------- .../Middleware/UT_DebuggerMiddleware.cs | 4 +- tests/Neo.VM.Tests/Neo.VM.Tests.csproj | 2 +- tests/Neo.VM.Tests/TestUtilities.cs | 6 +- tests/Neo.VM.Tests/Types/UT_VMObject.cs | 4 +- tests/Neo.VM.Tests/UT_NeoVirtualMachine.cs | 14 +- .../Neo.Wallet.Tests/Neo.Wallet.Tests.csproj | 2 +- tests/Neo.Wallet.Tests/UT_ChainWallet.cs | 4 +- tests/Neo.Wallet.Tests/UT_DevWallet.cs | 6 +- tests/Neo.Wallet.Tests/UT_Nep6Wallet.cs | 6 +- 41 files changed, 461 insertions(+), 209 deletions(-) create mode 100644 src/Neo.Platform.Storage/Logging/LoggingExtensions.cs create mode 100644 src/Neo.Platform.Storage/Logging/StoreEventId.cs create mode 100644 tests/Neo.Platform.Storage.Tests/TestUtilities.cs 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/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 index 3c52e07..2fca4dc 100644 --- a/src/Neo.Platform.Storage/BlockchainStore.cs +++ b/src/Neo.Platform.Storage/BlockchainStore.cs @@ -21,9 +21,11 @@ // 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; @@ -40,22 +42,26 @@ public sealed class BlockchainStore : IEnumerable>, private static readonly ColumnFamilyDescriptor[] s_columnFamilies = [ new(ColumnFamilyNames.Default), - new(ColumnFamilyNames.Meta), - new(ColumnFamilyNames.Indexes), - new(ColumnFamilyNames.Blocks), - new(ColumnFamilyNames.Transactions), + 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, - ILogger? logger = default) + ILoggerFactory? loggerFactory = default) { _options = options.Value; + _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + _logger = _loggerFactory.CreateLogger(); var dirInfo = new DirectoryInfo(_options.DatabasePath); @@ -124,55 +130,102 @@ public void Dispose() _bloomFilter.Dispose(); } - public IStoreSnapshot CreateSnapshot() => - new BlockchainStoreSnapshot(this, _db); + public IStoreSnapshot CreateSnapshot() + { + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogCreateSnapshotMessage(logLevel, "Creating snapshot of the store."); - public void Put(ReadOnlySpan key, ReadOnlySpan value, string? columnFamilyName = default) => - _db.Put(key, value, GetColumnFamilyHandle(columnFamilyName)); + return new BlockchainStoreSnapshot(this, _db, _loggerFactory); + } - public void Delete(ReadOnlySpan key, string? columnFamilyName = default) + public void Put(ReadOnlySpan key, ReadOnlySpan value) { - //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) - _db.Delete(key, GetColumnFamilyHandle(columnFamilyName)); + _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 bool ContainsKey(ReadOnlySpan key, string? columnFamilyName = default) + public void Delete(ReadOnlySpan key) { - //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) - return TryGet(key, out _, columnFamilyName); - //return false; + if (_db.KeyMayExist(key)) + { + _db.Delete(key); + + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogDeleteMessage(logLevel, $"Deleted key: {Convert.ToHexStringLower(key)}"); + } } - public byte[]? Get(ReadOnlySpan key, string? columnFamilyName = default) + public bool ContainsKey(ReadOnlySpan key) { - //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) - return _db.Get(key, GetColumnFamilyHandle(columnFamilyName)); - //return default; + if (_db.KeyMayExist(key)) + return TryGet(key, out _); + return false; } - public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default) + public byte[]? Get(ReadOnlySpan key) { - //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) - //{ - var data = _db.Get(key, GetColumnFamilyHandle(columnFamilyName)); - if (data is not null) + if (_db.KeyMayExist(key)) { - value = data; - return true; + 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, string? columnFamilyName = default) + public IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false) { - using var iter = _db.NewIterator(GetColumnFamilyHandle(columnFamilyName)); + using var iter = _db.NewIterator(); for (iter.SeekForPrev(keyOrPrefix.Span); iter.IsValid();) { - yield return new(iter.KeyToArray(), iter.ValueToArray()); + 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 @@ -190,11 +243,6 @@ IEnumerator> IEnumerable - string.IsNullOrWhiteSpace(columnFamilyName) ? - _db.GetDefaultColumnFamily() : - _db.GetColumnFamily(columnFamilyName); - private BlockBasedTableOptions CreateBlockTableOptions() { var blockOptions = new BlockBasedTableOptions() diff --git a/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs b/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs index 10b7917..670fd1d 100644 --- a/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs +++ b/src/Neo.Platform.Storage/BlockchainStoreSnapshot.cs @@ -20,7 +20,10 @@ // 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; @@ -39,10 +42,19 @@ public class BlockchainStoreSnapshot : IEnumerable> private readonly ReadOptions _readOptions; private readonly WriteBatch _writeBatch; - public BlockchainStoreSnapshot(IStore store, RocksDb db) + 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); @@ -57,30 +69,62 @@ public void Dispose() GC.SuppressFinalize(this); } - public void Commit() => + 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, string? columnFamilyName = default) + public bool ContainsKey(ReadOnlySpan key) { - //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) - return TryGet(key, out _, columnFamilyName); - //return false; + if (_db.KeyMayExist(key)) + return TryGet(key, out _); + return false; } - public IStoreSnapshot CreateSnapshot() => - new BlockchainStoreSnapshot(this, _db); + public IStoreSnapshot CreateSnapshot() + { + var logLevel = LogLevel.Debug; + + if (_logger.IsEnabled(logLevel)) + _logger.LogCreateSnapshotMessage(logLevel, "Creating snapshot of the snapshot."); - public void Delete(ReadOnlySpan key, string? columnFamilyName = default) + return new BlockchainStoreSnapshot(this, _db, _loggerFactory); + } + + public void Delete(ReadOnlySpan key) { - //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) - _writeBatch.Delete(key, GetColumnFamilyHandle(columnFamilyName)); + 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, string? columnFamilyName = default) + public byte[]? Get(ReadOnlySpan key) { - //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) - return _db.Get(key, GetColumnFamilyHandle(columnFamilyName), _readOptions); - //return default; + 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() @@ -90,16 +134,32 @@ public IEnumerator> GetEnumerator() yield return new(iter.KeyToArray(), iter.ValueToArray()); } - public void Put(ReadOnlySpan key, ReadOnlySpan value, string? columnFamilyName = default) => - _writeBatch.Put(key, value, GetColumnFamilyHandle(columnFamilyName)); + public void Put(ReadOnlySpan key, ReadOnlySpan value) + { + _writeBatch.Put(key, value); + + var logLevel = LogLevel.Debug; - public IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default) + 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(GetColumnFamilyHandle(columnFamilyName), _readOptions); + using var iter = _db.NewIterator(_readOptions); for (iter.SeekForPrev(keyOrPrefix.Span); iter.IsValid();) { - yield return new(iter.KeyToArray(), iter.ValueToArray()); + 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 @@ -107,17 +167,23 @@ public IEnumerable> Seek(ReadOnlyMemory keyOr } } - public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default) + public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value) { - //if (_db.KeyMayExist(key, GetColumnFamilyHandle(columnFamilyName))) - //{ - var data = _db.Get(key, GetColumnFamilyHandle(columnFamilyName), _readOptions); - if (data is not null) + if (_db.KeyMayExist(key)) { - value = data; - return true; + 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; @@ -125,10 +191,5 @@ public bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private ColumnFamilyHandle GetColumnFamilyHandle(string? columnFamilyName = default) => - string.IsNullOrWhiteSpace(columnFamilyName) ? - _db.GetDefaultColumnFamily() : - _db.GetColumnFamily(columnFamilyName); } } diff --git a/src/Neo.Platform.Storage/ColumnFamilyNames.cs b/src/Neo.Platform.Storage/ColumnFamilyNames.cs index 258c135..d46b04b 100644 --- a/src/Neo.Platform.Storage/ColumnFamilyNames.cs +++ b/src/Neo.Platform.Storage/ColumnFamilyNames.cs @@ -25,9 +25,7 @@ namespace Neo.Platform.Storage public static class ColumnFamilyNames { public static readonly string Default = "default"; - public static readonly string Meta = "meta"; - public static readonly string Indexes = "indexes"; - public static readonly string Blocks = "blocks"; - public static readonly string Transactions = "transactions"; + 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 index 892d0cb..c911de2 100644 --- a/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs +++ b/src/Neo.Platform.Storage/Interface/IReadOnlyStore.cs @@ -28,12 +28,12 @@ namespace Neo.Platform.Storage.Interface { public interface IReadOnlyStore : IDisposable { - bool ContainsKey(ReadOnlySpan key, string? columnFamilyName = default); + bool ContainsKey(ReadOnlySpan key); - byte[]? Get(ReadOnlySpan key, string? columnFamilyName = default); + byte[]? Get(ReadOnlySpan key); - bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value, string? columnFamilyName = default); + bool TryGet(ReadOnlySpan key, [NotNullWhen(true)] out byte[]? value); - IEnumerable> Seek(ReadOnlyMemory keyOrPrefix, bool seekFromEnd = false, string? columnFamilyName = default); + 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 index 4d3e58f..f7684bd 100644 --- a/src/Neo.Platform.Storage/Interface/IStore.cs +++ b/src/Neo.Platform.Storage/Interface/IStore.cs @@ -28,8 +28,8 @@ public interface IStore : IReadOnlyStore { IStoreSnapshot CreateSnapshot(); - void Put(ReadOnlySpan key, ReadOnlySpan value, string? columnFamilyName = default); + void Put(ReadOnlySpan key, ReadOnlySpan value); - void Delete(ReadOnlySpan key, string? columnFamilyName = default); + void Delete(ReadOnlySpan key); } } 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.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/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 index b7fb1e6..7cde898 100644 --- a/tests/Neo.Platform.Storage.Tests/MSTestSettings.cs +++ b/tests/Neo.Platform.Storage.Tests/MSTestSettings.cs @@ -1,4 +1,4 @@ -// BSD 2-Clause License +// BSD 2-Clause License // // Copyright (c) 2026, Rapid Loop // diff --git a/tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj b/tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj index 82b3dd0..d411cfd 100644 --- a/tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj +++ b/tests/Neo.Platform.Storage.Tests/Neo.Platform.Storage.Tests.csproj @@ -7,7 +7,9 @@ - + + + diff --git a/tests/Neo.Platform.Storage.Tests/TestStorePool.cs b/tests/Neo.Platform.Storage.Tests/TestStorePool.cs index 690e49f..55d048e 100644 --- a/tests/Neo.Platform.Storage.Tests/TestStorePool.cs +++ b/tests/Neo.Platform.Storage.Tests/TestStorePool.cs @@ -42,7 +42,7 @@ public BlockchainStore Rent() DatabasePath = Path.Combine(Path.GetRandomFileName()), }); - var store = new BlockchainStore(storeOptions); + var store = new BlockchainStore(storeOptions, TestUtilities.TraceLoggerFactory); _store.TryAdd(storeOptions.Value.DatabasePath, store); 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 index 5769c90..ec101e0 100644 --- a/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs +++ b/tests/Neo.Platform.Storage.Tests/UT_BlockchainStore.cs @@ -27,53 +27,6 @@ namespace Neo.Platform.Storage.Tests [TestClass] public sealed class UT_BlockchainStore { - [TestMethod] - public void TestColumnFamilyNames() - { - var store = TestStorePool.Shared.Rent(); - - store.Put([0xff, 0x00, 0x00], [0x00]); - store.Put([0xff, 0x00, 0x01], [0x01], ColumnFamilyNames.Blocks); - - var actualBytes = store.Get([0xff, 0x00, 0x01]); - - Assert.IsFalse(store.ContainsKey([0xff, 0x00, 0x01])); - Assert.IsNull(actualBytes); - - actualBytes = store.Get([0xff, 0x00, 0x01], ColumnFamilyNames.Blocks); - - Assert.IsNotNull(actualBytes); - Assert.AreEqual(1, actualBytes?.Length); - Assert.AreEqual(0x01, actualBytes?[0]); - - TestStorePool.Shared.Return(store); - } - - [TestMethod] - public void TestSnapshotColumnFamilyNames() - { - var store = TestStorePool.Shared.Rent(); - - store.Put([0xff, 0x00, 0x00], [0x00]); - store.Put([0xff, 0x00, 0x01], [0x01], ColumnFamilyNames.Blocks); - - var actualBytes = store.Get([0xff, 0x00, 0x01]); - - Assert.IsFalse(store.ContainsKey([0xff, 0x00, 0x01])); - Assert.IsNull(actualBytes); - - using (var snapshot = store.CreateSnapshot()) - { - actualBytes = snapshot.Get([0xff, 0x00, 0x01], ColumnFamilyNames.Blocks); - - Assert.IsNotNull(actualBytes); - Assert.AreEqual(1, actualBytes?.Length); - Assert.AreEqual(0x01, actualBytes?[0]); - } - - TestStorePool.Shared.Return(store); - } - [TestMethod] public void TestSnapshot() { @@ -125,18 +78,18 @@ public void TestSeek() var actualResults = store.Seek((byte[])[0x00, 0x00, 0x01]).ToArray(); Assert.HasCount(4, actualResults); - CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x01], actualResults[0].Key); - CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x02], actualResults[1].Key); - CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x03], actualResults[2].Key); - CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x04], actualResults[3].Key); + 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); - CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x03], actualResults[0].Key); - CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x02], actualResults[1].Key); - CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x01], actualResults[2].Key); - CollectionAssert.AreEqual((byte[])[0x00, 0x00, 0x00], actualResults[3].Key); + 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"));