diff --git a/benchmarks/Neo.Benchmarks/Benchmarks.Cache.cs b/benchmarks/Neo.Benchmarks/Benchmarks.Cache.cs new file mode 100644 index 0000000000..f31cedd079 --- /dev/null +++ b/benchmarks/Neo.Benchmarks/Benchmarks.Cache.cs @@ -0,0 +1,69 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.Cache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using Neo.Persistence; +using Neo.Persistence.Providers; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using System.Numerics; + +namespace Neo.Benchmark +{ + public class Benchmarks_Cache + { + readonly MemoryStore _store; + readonly StoreCache _snapshot; + + public Benchmarks_Cache() + { + _store = new MemoryStore(); + _snapshot = new(_store.GetSnapshot()); + + // Ledger.CurrentIndex + + _snapshot.GetAndChange(new KeyBuilder(NativeContract.Ledger.Id, 12), () => new StorageItem(new HashIndexState() { Hash = UInt256.Zero, Index = 2 })); + + // Gas Per block + + _snapshot.GetAndChange(new KeyBuilder(NativeContract.NEO.Id, 29).AddBigEndian(0), () => new StorageItem(0)); + _snapshot.GetAndChange(new KeyBuilder(NativeContract.NEO.Id, 29).AddBigEndian(1), () => new StorageItem(1)); + _snapshot.GetAndChange(new KeyBuilder(NativeContract.NEO.Id, 29).AddBigEndian(2), () => new StorageItem(2)); + } + + [Benchmark] + public void WithCache() + { + for (var x = 0; x < 1_000; x++) + { + var ret = NativeContract.NEO.GetGasPerBlock(_snapshot); + if (ret != 2) throw new Exception("Test error"); + } + } + + [Benchmark] + public void WithoutCache() + { + for (var x = 0; x < 1_000; x++) + { + var ret = OldCode(); + if (ret != 2) throw new Exception("Test error"); + } + } + + private BigInteger OldCode() + { + var end = NativeContract.Ledger.CurrentIndex(_snapshot) + 1; + var last = NativeContract.NEO.GetSortedGasRecords(_snapshot, end).First(); + return last.GasPerBlock; + } + } +} diff --git a/src/Neo/Neo.csproj b/src/Neo/Neo.csproj index 53864a963c..55fea79dda 100644 --- a/src/Neo/Neo.csproj +++ b/src/Neo/Neo.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Neo/Persistence/ClonedCache.cs b/src/Neo/Persistence/ClonedCache.cs index 3bb3b18b5c..b298064a43 100644 --- a/src/Neo/Persistence/ClonedCache.cs +++ b/src/Neo/Persistence/ClonedCache.cs @@ -20,7 +20,7 @@ class ClonedCache : DataCache { private readonly DataCache _innerCache; - public ClonedCache(DataCache innerCache) + public ClonedCache(DataCache innerCache) : base(innerCache.SerializedCacheChanges) { _innerCache = innerCache; } diff --git a/src/Neo/Persistence/DataCache.cs b/src/Neo/Persistence/DataCache.cs index 54382faa22..2c76e7b3f3 100644 --- a/src/Neo/Persistence/DataCache.cs +++ b/src/Neo/Persistence/DataCache.cs @@ -24,7 +24,7 @@ namespace Neo.Persistence /// /// Represents a cache for the underlying storage of the NEO blockchain. /// - public abstract class DataCache : IReadOnlyStore + public abstract class DataCache : ICacheableReadOnlyStore { /// /// Represents an entry in the cache. @@ -45,6 +45,25 @@ public class Trackable(StorageItem item, TrackState state) private readonly Dictionary _dictionary = []; private readonly HashSet _changeSet = []; + /// + /// Serialized cache + /// + public SerializedCache SerializedCache { get; } + + /// + /// This is where the cache changes are stored + /// + internal SerializedCache SerializedCacheChanges { get; } = new(); + + /// + /// Constructor + /// + /// Serialized cache + protected DataCache(SerializedCache serializedCache) + { + SerializedCache = serializedCache; + } + /// /// Reads a specified entry from the cache. If the entry is not in the cache, it will be automatically loaded from the underlying storage. /// @@ -101,6 +120,51 @@ public void Add(StorageKey key, StorageItem value) } } + /// + /// Adds a new entry to the cache. + /// + /// The key of the entry. + /// The data of the entry. + /// The entry has already been cached. + /// Note: This method does not read the internal storage to check whether the record already exists. + public void Add(StorageKey key, T value) where T : IStorageCacheEntry + { + lock (_dictionary) + { + Add(key, value.GetStorageItem()); + AddToCache(value); + } + } + + /// + /// Adds a new entry to the cache. + /// + /// The data of the entry. + /// The entry has already been cached. + /// Note: This method does not read the internal storage to check whether the record already exists. + public void AddToCache(T? value = default) where T : IStorageCacheEntry + { + var type = typeof(T); + SerializedCacheChanges.Set(type, value); + } + + /// + /// Tries to get the entry from cache. + /// + /// Cache type + /// The entry if found, null otherwise. + public T? GetFromCache() where T : IStorageCacheEntry + { + var value = SerializedCacheChanges.Get(); + + if (value != null) + { + return value; + } + + return SerializedCache.Get(); + } + /// /// Adds a new entry to the underlying storage. /// @@ -134,6 +198,8 @@ public virtual void Commit() break; } } + SerializedCache.CopyFrom(SerializedCacheChanges); + SerializedCacheChanges.Clear(); _changeSet.Clear(); } } @@ -365,6 +431,19 @@ public bool Contains(StorageKey key) } } + /// + /// Reads a specified entry from the cache, and mark it as . + /// If the entry is not in the cache, it will be automatically loaded from the underlying storage. + /// + /// The key of the entry. + /// Serialized cache + public void Upsert(StorageKey key, T serializedCache) where T : IStorageCacheEntry + { + var ret = GetAndChange(key, serializedCache.GetStorageItem); + ret!.FromReplica(serializedCache.GetStorageItem()); + AddToCache(serializedCache); + } + /// /// Reads a specified entry from the cache. /// If the entry is not in the cache, it will be automatically loaded from the underlying storage. diff --git a/src/Neo/Persistence/ICacheableReadOnlyStore.cs b/src/Neo/Persistence/ICacheableReadOnlyStore.cs new file mode 100644 index 0000000000..8da45120ea --- /dev/null +++ b/src/Neo/Persistence/ICacheableReadOnlyStore.cs @@ -0,0 +1,44 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ICacheableReadOnlyStore.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#nullable enable + +using Neo.SmartContract; +using System; + +namespace Neo.Persistence +{ + /// + /// This interface provides methods to read from the database. + /// + public interface ICacheableReadOnlyStore : ICacheableReadOnlyStore, IReadOnlyStore { } + + /// + /// This interface provides methods to read from the database. + /// + public interface ICacheableReadOnlyStore : IReadOnlyStore + { + /// + /// Tries to get the entry from cache. + /// + /// Cache type + /// The entry if found, null otherwise. + public T? GetFromCache() where T : IStorageCacheEntry; + + /// + /// Adds a new entry to the cache. + /// + /// The data of the entry. + /// The entry has already been cached. + /// Note: This method does not read the internal storage to check whether the record already exists. + public void AddToCache(T? value = default) where T : IStorageCacheEntry; + } +} diff --git a/src/Neo/Persistence/IStorageCacheEntry.cs b/src/Neo/Persistence/IStorageCacheEntry.cs new file mode 100644 index 0000000000..ddddef0711 --- /dev/null +++ b/src/Neo/Persistence/IStorageCacheEntry.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IStorageCacheEntry.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.SmartContract; + +namespace Neo.Persistence +{ + public interface IStorageCacheEntry + { + public StorageItem GetStorageItem(); + } +} diff --git a/src/Neo/Persistence/IStore.cs b/src/Neo/Persistence/IStore.cs index 7cc413a7aa..af24a6ea80 100644 --- a/src/Neo/Persistence/IStore.cs +++ b/src/Neo/Persistence/IStore.cs @@ -23,6 +23,11 @@ public interface IStore : IWriteStore, IDisposable { + /// + /// Serialized cache + /// + SerializedCache SerializedCache { get; } + /// /// Creates a snapshot of the database. /// diff --git a/src/Neo/Persistence/Providers/MemoryStore.cs b/src/Neo/Persistence/Providers/MemoryStore.cs index af639db851..f6a68e21c2 100644 --- a/src/Neo/Persistence/Providers/MemoryStore.cs +++ b/src/Neo/Persistence/Providers/MemoryStore.cs @@ -26,6 +26,7 @@ namespace Neo.Persistence.Providers public class MemoryStore : IStore { private readonly ConcurrentDictionary _innerData = new(ByteArrayEqualityComparer.Default); + public SerializedCache SerializedCache { get; } = new(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Delete(byte[] key) diff --git a/src/Neo/Persistence/SerializedCache.cs b/src/Neo/Persistence/SerializedCache.cs new file mode 100644 index 0000000000..0142fffa3f --- /dev/null +++ b/src/Neo/Persistence/SerializedCache.cs @@ -0,0 +1,108 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// SerializedCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Neo.Persistence +{ + public class SerializedCache + { + private readonly Dictionary _cache = []; + + /// + /// Get cached entry + /// + /// Type + /// The cached entry. If not cached, the default value will be returned + public T? Get() + { + if (_cache.TryGetValue(typeof(T), out var ret)) + { + return (T)ret; + } + + return default; + } + + /// + /// Set entry + /// + /// Type + /// Value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(T? value) where T : IStorageCacheEntry + { + Set(typeof(T), value); + } + + /// + /// Set entry + /// + /// Type + /// Value + public void Set(Type type, IStorageCacheEntry? value) + { + if (value == null) + { + Remove(type); + } + else + { + lock (_cache) + { + _cache[type] = value; + } + } + } + + /// + /// Remove entry + /// + /// Type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Remove(Type type) + { + lock (_cache) + { + _cache.Remove(type, out _); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + lock (_cache) + { + _cache.Clear(); + } + } + + /// + /// Copy from + /// + /// Value + public void CopyFrom(SerializedCache value) + { + if (ReferenceEquals(this, value)) return; + lock (_cache) lock (value._cache) + { + foreach (var serialized in value._cache) + { + _cache[serialized.Key] = serialized.Value; + } + } + } + } +} diff --git a/src/Neo/Persistence/StoreCache.cs b/src/Neo/Persistence/StoreCache.cs index 04084a0a11..1b4e0c3bb8 100644 --- a/src/Neo/Persistence/StoreCache.cs +++ b/src/Neo/Persistence/StoreCache.cs @@ -30,8 +30,8 @@ public class StoreCache : DataCache, IDisposable /// /// Initializes a new instance of the class. /// - /// An to create a readonly cache. - public StoreCache(IStore store) + /// An to create a readonly cache. + public StoreCache(IStore store) : base(store.SerializedCache) { _store = store; } @@ -40,7 +40,7 @@ public StoreCache(IStore store) /// Initializes a new instance of the class. /// /// An to create a snapshot cache. - public StoreCache(IStoreSnapshot snapshot) + public StoreCache(IStoreSnapshot snapshot) : base(snapshot.Store.SerializedCache) { _store = snapshot; _snapshot = snapshot; diff --git a/src/Neo/SmartContract/Native/ContractMethodMetadata.cs b/src/Neo/SmartContract/Native/ContractMethodMetadata.cs index e4d17827a4..c2aaa4e638 100644 --- a/src/Neo/SmartContract/Native/ContractMethodMetadata.cs +++ b/src/Neo/SmartContract/Native/ContractMethodMetadata.cs @@ -54,7 +54,7 @@ public ContractMethodMetadata(MemberInfo member, ContractMethodAttribute attribu if (parameterInfos.Length > 0) { NeedApplicationEngine = parameterInfos[0].ParameterType.IsAssignableFrom(typeof(ApplicationEngine)); - // snapshot is a DataCache instance, and DataCache implements IReadOnlyStoreView + // snapshot is a DataCache instance, and DataCache implements IReadOnlyStore NeedSnapshot = parameterInfos[0].ParameterType.IsAssignableFrom(typeof(DataCache)); } if (NeedApplicationEngine || NeedSnapshot) diff --git a/src/Neo/SmartContract/Native/HashIndexState.cs b/src/Neo/SmartContract/Native/HashIndexState.cs index 06bc5704cb..7ceeeee72e 100644 --- a/src/Neo/SmartContract/Native/HashIndexState.cs +++ b/src/Neo/SmartContract/Native/HashIndexState.cs @@ -15,7 +15,7 @@ namespace Neo.SmartContract.Native { - class HashIndexState : IInteroperable + internal class HashIndexState : IInteroperable { public UInt256 Hash; public uint Index; diff --git a/src/Neo/SmartContract/Native/NeoToken.cs b/src/Neo/SmartContract/Native/NeoToken.cs index 4d7198358f..3012fda35c 100644 --- a/src/Neo/SmartContract/Native/NeoToken.cs +++ b/src/Neo/SmartContract/Native/NeoToken.cs @@ -59,6 +59,14 @@ public sealed class NeoToken : FungibleToken private readonly StorageKey _votersCount; private readonly StorageKey _registerPrice; + private class LastGasPerBlock(BigInteger gasPerBlock, long index) : IStorageCacheEntry + { + public readonly BigInteger GasPerBlock = gasPerBlock; + public readonly long Index = index; + + public StorageItem GetStorageItem() => new(GasPerBlock); + } + [ContractEvent(1, name: "CandidateStateChanged", "pubkey", ContractParameterType.PublicKey, "registered", ContractParameterType.Boolean, @@ -202,10 +210,11 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor { if (hardfork == ActiveIn) { + var initIndex = engine.PersistingBlock?.Index ?? 0u; var cachedCommittee = new CachedCommittee(engine.ProtocolSettings.StandbyCommittee.Select(p => (p, BigInteger.Zero))); engine.SnapshotCache.Add(CreateStorageKey(Prefix_Committee), new StorageItem(cachedCommittee)); - engine.SnapshotCache.Add(_votersCount, new StorageItem(Array.Empty())); - engine.SnapshotCache.Add(CreateStorageKey(Prefix_GasPerBlock, 0u), new StorageItem(5 * GAS.Factor)); + engine.SnapshotCache.Add(_votersCount, new StorageItem([])); + engine.SnapshotCache.Add(CreateStorageKey(Prefix_GasPerBlock, initIndex), new LastGasPerBlock(5 * GAS.Factor, initIndex)); engine.SnapshotCache.Add(_registerPrice, new StorageItem(1000 * GAS.Factor)); return Mint(engine, Contract.GetBFTAddress(engine.ProtocolSettings.StandbyValidators), TotalAmount, false); } @@ -285,8 +294,7 @@ private void SetGasPerBlock(ApplicationEngine engine, BigInteger gasPerBlock) if (!CheckCommittee(engine)) throw new InvalidOperationException(); var index = engine.PersistingBlock.Index + 1; - var entry = engine.SnapshotCache.GetAndChange(CreateStorageKey(Prefix_GasPerBlock, index), () => new StorageItem(gasPerBlock)); - entry.Set(gasPerBlock); + engine.SnapshotCache.Upsert(CreateStorageKey(Prefix_GasPerBlock, index), new LastGasPerBlock(gasPerBlock, index)); } /// @@ -297,7 +305,16 @@ private void SetGasPerBlock(ApplicationEngine engine, BigInteger gasPerBlock) [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] public BigInteger GetGasPerBlock(DataCache snapshot) { - return GetSortedGasRecords(snapshot, Ledger.CurrentIndex(snapshot) + 1).First().GasPerBlock; + var end = Ledger.CurrentIndex(snapshot) + 1; + var cached = snapshot.GetFromCache(); + if (cached != null && cached.Index < end) + { + return cached.GasPerBlock; + } + + var last = GetSortedGasRecords(snapshot, end).First(); + snapshot.AddToCache(new LastGasPerBlock(last.GasPerBlock, last.Index)); + return last.GasPerBlock; } [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] @@ -321,7 +338,7 @@ public long GetRegisterPrice(IReadOnlyStore snapshot) return (long)(BigInteger)snapshot[_registerPrice]; } - private IEnumerable<(uint Index, BigInteger GasPerBlock)> GetSortedGasRecords(DataCache snapshot, uint end) + internal IEnumerable<(uint Index, BigInteger GasPerBlock)> GetSortedGasRecords(DataCache snapshot, uint end) { var key = CreateStorageKey(Prefix_GasPerBlock, end).ToArray(); var boundary = CreateStorageKey(Prefix_GasPerBlock).ToArray(); diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs index cba5694ddb..95c8707206 100644 --- a/src/Neo/SmartContract/Native/PolicyContract.cs +++ b/src/Neo/SmartContract/Native/PolicyContract.cs @@ -69,6 +69,23 @@ public sealed class PolicyContract : NativeContract private readonly StorageKey _execFeeFactor; private readonly StorageKey _storagePrice; + private class LastFeePerByte(long feePerByte) : IStorageCacheEntry + { + public readonly long FeePerByte = feePerByte; + public StorageItem GetStorageItem() => new(FeePerByte); + } + + private class LastStorageFee(uint storagePrice) : IStorageCacheEntry + { + public readonly uint StoragePrice = storagePrice; + public StorageItem GetStorageItem() => new(StoragePrice); + } + + private class LastExecFee(uint execFeeFactor) : IStorageCacheEntry + { + public readonly uint ExecFeeFactor = execFeeFactor; + public StorageItem GetStorageItem() => new(ExecFeeFactor); + } internal PolicyContract() : base() { @@ -81,9 +98,9 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor { if (hardfork == ActiveIn) { - engine.SnapshotCache.Add(_feePerByte, new StorageItem(DefaultFeePerByte)); - engine.SnapshotCache.Add(_execFeeFactor, new StorageItem(DefaultExecFeeFactor)); - engine.SnapshotCache.Add(_storagePrice, new StorageItem(DefaultStoragePrice)); + engine.SnapshotCache.Add(_feePerByte, new LastFeePerByte(DefaultFeePerByte)); + engine.SnapshotCache.Add(_execFeeFactor, new LastExecFee(DefaultExecFeeFactor)); + engine.SnapshotCache.Add(_storagePrice, new LastStorageFee(DefaultStoragePrice)); } return ContractTask.CompletedTask; } @@ -94,9 +111,16 @@ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfor /// The snapshot used to read data. /// The network fee per transaction byte. [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] - public long GetFeePerByte(IReadOnlyStore snapshot) + public long GetFeePerByte(ICacheableReadOnlyStore snapshot) { - return (long)(BigInteger)snapshot[_feePerByte]; + var cached = snapshot.GetFromCache(); + if (cached != null) + { + return cached.FeePerByte; + } + var fee = (long)(BigInteger)snapshot[_feePerByte]; + snapshot.AddToCache(new LastFeePerByte(fee)); + return fee; } /// @@ -105,9 +129,16 @@ public long GetFeePerByte(IReadOnlyStore snapshot) /// The snapshot used to read data. /// The execution fee factor. [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] - public uint GetExecFeeFactor(IReadOnlyStore snapshot) + public uint GetExecFeeFactor(ICacheableReadOnlyStore snapshot) { - return (uint)(BigInteger)snapshot[_execFeeFactor]; + var cached = snapshot.GetFromCache(); + if (cached != null) + { + return cached.ExecFeeFactor; + } + var fee = (uint)(BigInteger)snapshot[_execFeeFactor]; + snapshot.AddToCache(new LastExecFee(fee)); + return fee; } /// @@ -116,9 +147,16 @@ public uint GetExecFeeFactor(IReadOnlyStore snapshot) /// The snapshot used to read data. /// The storage price. [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] - public uint GetStoragePrice(IReadOnlyStore snapshot) + public uint GetStoragePrice(ICacheableReadOnlyStore snapshot) { - return (uint)(BigInteger)snapshot[_storagePrice]; + var cached = snapshot.GetFromCache(); + if (cached != null) + { + return cached.StoragePrice; + } + var fee = (uint)(BigInteger)snapshot[_storagePrice]; + snapshot.AddToCache(new LastStorageFee(fee)); + return fee; } /// @@ -163,7 +201,7 @@ private void SetFeePerByte(ApplicationEngine engine, long value) { if (value < 0 || value > 1_00000000) throw new ArgumentOutOfRangeException(nameof(value)); if (!CheckCommittee(engine)) throw new InvalidOperationException(); - engine.SnapshotCache.GetAndChange(_feePerByte).Set(value); + engine.SnapshotCache.Upsert(_feePerByte, new LastFeePerByte(value)); } [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] @@ -171,7 +209,7 @@ private void SetExecFeeFactor(ApplicationEngine engine, uint value) { if (value == 0 || value > MaxExecFeeFactor) throw new ArgumentOutOfRangeException(nameof(value)); if (!CheckCommittee(engine)) throw new InvalidOperationException(); - engine.SnapshotCache.GetAndChange(_execFeeFactor).Set(value); + engine.SnapshotCache.Upsert(_execFeeFactor, new LastExecFee(value)); } [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] @@ -179,7 +217,7 @@ private void SetStoragePrice(ApplicationEngine engine, uint value) { if (value == 0 || value > MaxStoragePrice) throw new ArgumentOutOfRangeException(nameof(value)); if (!CheckCommittee(engine)) throw new InvalidOperationException(); - engine.SnapshotCache.GetAndChange(_storagePrice).Set(value); + engine.SnapshotCache.Upsert(_storagePrice, new LastStorageFee(value)); } [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] diff --git a/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs b/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs index c1173e3b5f..6995f4a44a 100644 --- a/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs +++ b/src/Plugins/LevelDBStore/Plugins/Storage/Store.cs @@ -25,6 +25,8 @@ internal class Store : IStore, IEnumerable> private readonly DB _db; private readonly Options _options; + public SerializedCache SerializedCache { get; } = new(); + public Store(string path) { _options = new Options diff --git a/src/Plugins/RocksDBStore/Plugins/Storage/Snapshot.cs b/src/Plugins/RocksDBStore/Plugins/Storage/Snapshot.cs index ad20efdb7b..06584bd604 100644 --- a/src/Plugins/RocksDBStore/Plugins/Storage/Snapshot.cs +++ b/src/Plugins/RocksDBStore/Plugins/Storage/Snapshot.cs @@ -42,7 +42,6 @@ internal Snapshot(Store store, RocksDb db) _db = db; _snapshot = db.CreateSnapshot(); _batch = new WriteBatch(); - _options = new ReadOptions(); _options.SetFillCache(false); _options.SetSnapshot(_snapshot); diff --git a/src/Plugins/RocksDBStore/Plugins/Storage/Store.cs b/src/Plugins/RocksDBStore/Plugins/Storage/Store.cs index 184c53eaa2..54a9b319fc 100644 --- a/src/Plugins/RocksDBStore/Plugins/Storage/Store.cs +++ b/src/Plugins/RocksDBStore/Plugins/Storage/Store.cs @@ -22,6 +22,8 @@ internal class Store : IStore { private readonly RocksDb _db; + public SerializedCache SerializedCache { get; } = new(); + public Store(string path) { _db = RocksDb.Open(Options.Default, Path.GetFullPath(path)); diff --git a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs index 4f6a00ea85..b6d3704765 100644 --- a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs +++ b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs @@ -9,12 +9,15 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. +#nullable enable + using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Extensions; using Neo.Persistence; using Neo.Persistence.Providers; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -22,9 +25,8 @@ namespace Neo.Cryptography.MPTTrie.Tests { class TestSnapshot : IStoreSnapshot { - public Dictionary store = new Dictionary(ByteArrayEqualityComparer.Default); - - private byte[] StoreKey(byte[] key) + public Dictionary store = new(ByteArrayEqualityComparer.Default); + private static byte[] StoreKey(byte[] key) { return [.. key]; } @@ -45,16 +47,16 @@ public void Delete(byte[] key) public bool Contains(byte[] key) { throw new NotImplementedException(); } - public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] key, SeekDirection direction) { throw new NotImplementedException(); } + public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[]? key, SeekDirection direction) { throw new NotImplementedException(); } - public byte[] TryGet(byte[] key) + public byte[]? TryGet(byte[] key) { - var result = store.TryGetValue(StoreKey(key), out byte[] value); + var result = store.TryGetValue(StoreKey(key), out var value); if (result) return value; return null; } - public bool TryGet(byte[] key, out byte[] value) + public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value) { return store.TryGetValue(StoreKey(key), out value); } @@ -67,16 +69,15 @@ public bool TryGet(byte[] key, out byte[] value) [TestClass] public class UT_Trie { - private Node root; - private IStore mptdb; + private readonly Node root; + private readonly IStore mptdb; - private void PutToStore(IStore store, Node node) + private static void PutToStore(IStore store, Node node) { store.Put([.. new byte[] { 0xf0 }, .. node.Hash.ToArray()], node.ToArray()); } - [TestInitialize] - public void TestInit() + public UT_Trie() { var b = Node.NewBranch(); var r = Node.NewExtension("0a0c".HexToBytes(), b); @@ -85,9 +86,9 @@ public void TestInit() var v3 = Node.NewLeaf(Encoding.ASCII.GetBytes("existing"));//key=acae var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing")); var h3 = Node.NewHash(v3.Hash); - var e1 = Node.NewExtension(new byte[] { 0x01 }, v1); - var e3 = Node.NewExtension(new byte[] { 0x0e }, h3); - var e4 = Node.NewExtension(new byte[] { 0x01 }, v4); + var e1 = Node.NewExtension([0x01], v1); + var e3 = Node.NewExtension([0x0e], h3); + var e4 = Node.NewExtension([0x01], v4); b.Children[0] = e1; b.Children[10] = e3; b.Children[16] = v2; diff --git a/tests/Neo.UnitTests/Persistence/UT_SerializedCache.cs b/tests/Neo.UnitTests/Persistence/UT_SerializedCache.cs new file mode 100644 index 0000000000..b65cd5c25a --- /dev/null +++ b/tests/Neo.UnitTests/Persistence/UT_SerializedCache.cs @@ -0,0 +1,239 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_SerializedCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Persistence; +using Neo.Persistence.Providers; +using Neo.SmartContract; +using System; + +namespace Neo.UnitTests.Persistence +{ + // Dummy implementations for IStorageCacheEntry for testing purposes + public class TestCacheEntry(int value) : IStorageCacheEntry + { + public int Value { get; } = value; + public StorageItem GetStorageItem() => new() { Value = BitConverter.GetBytes(Value) }; + } + + public class TestCacheEntry2(string text) : IStorageCacheEntry + { + public string Text { get; } = text; + public StorageItem GetStorageItem() => new() { Value = System.Text.Encoding.UTF8.GetBytes(Text) }; + } + + [TestClass] + [TestCategory("SerializedCache")] + public class UT_SerializedCache + { + [TestMethod] + public void TestGetReturnsDefaultWhenNotSet() + { + var cache = new SerializedCache(); + Assert.IsNull(cache.Get(), "Expected null when cache does not contain the type"); + } + + [TestMethod] + public void TestSnapstot() + { + var store = new MemoryStore(); + var entry = new TestCacheEntry(42); + store.SerializedCache.Set(entry); + var retrieved = store.SerializedCache.Get(); + Assert.AreEqual(42, retrieved.Value, "Retrieved entry does not match the set value"); + + var snapshot = new StoreCache(store.GetSnapshot()); + entry = new TestCacheEntry(43); + snapshot.Upsert(new StorageKey(new byte[10]), entry); + + retrieved = store.SerializedCache.Get(); + Assert.AreEqual(42, retrieved.Value, "Retrieved entry does not match the set value"); + retrieved = snapshot.GetFromCache(); + Assert.AreEqual(43, retrieved.Value, "Retrieved entry does not match the set value"); + + var clone = snapshot.CloneCache(); + + entry = new TestCacheEntry(44); + clone.Upsert(new StorageKey(new byte[10]), entry); + retrieved = store.SerializedCache.Get(); + Assert.AreEqual(42, retrieved.Value, "Retrieved entry does not match the set value"); + retrieved = snapshot.GetFromCache(); + Assert.AreEqual(43, retrieved.Value, "Retrieved entry does not match the set value"); + retrieved = clone.GetFromCache(); + Assert.AreEqual(44, retrieved.Value, "Retrieved entry does not match the set value"); + + clone.Commit(); + retrieved = store.SerializedCache.Get(); + Assert.AreEqual(42, retrieved.Value, "Retrieved entry does not match the set value"); + retrieved = snapshot.GetFromCache(); + Assert.AreEqual(44, retrieved.Value, "Retrieved entry does not match the set value"); + + snapshot.Commit(); + retrieved = store.SerializedCache.Get(); + Assert.AreEqual(44, retrieved.Value, "Retrieved entry does not match the set value"); + } + + [TestMethod] + public void TestSetAndGet() + { + var entry = new TestCacheEntry(42); + var cache = new SerializedCache(); + cache.Set(entry); + var retrieved = cache.Get(); + Assert.IsNotNull(retrieved, "Entry was not set correctly"); + Assert.AreEqual(42, retrieved.Value, "Retrieved entry does not match the set value"); + } + + [TestMethod] + public void TestRemove() + { + var entry = new TestCacheEntry(42); + var cache = new SerializedCache(); + cache.Set(entry); + cache.Remove(typeof(TestCacheEntry)); + Assert.IsNull(cache.Get(), "Entry should be null after removal"); + } + + [TestMethod] + public void TestClear() + { + var cache = new SerializedCache(); + cache.Set(new TestCacheEntry(1)); + cache.Set(new TestCacheEntry2("one")); + cache.Clear(); + Assert.IsNull(cache.Get(), "Cache should be cleared for first type"); + Assert.IsNull(cache.Get(), "Cache should be cleared for second type"); + } + + [TestMethod] + public void TestCopyFrom() + { + var source = new SerializedCache(); + source.Set(new TestCacheEntry(99)); + + var target = new SerializedCache(); + target.Set(new TestCacheEntry2("hello")); + + target.CopyFrom(source); + + var entryA = target.Get(); + var entryB = target.Get(); + + Assert.IsNotNull(entryA, "Copied entry should exist in target cache"); + Assert.AreEqual(99, entryA.Value, "Copied entry value mismatch"); + Assert.IsNotNull(entryB, "Existing entry in target cache should not be overwritten if types differ"); + Assert.AreEqual("hello", entryB.Text, "Existing entry text mismatch"); + } + + [TestMethod] + public void TestSetNullValueRemovesEntry() + { + var cache = new SerializedCache(); + var entry = new TestCacheEntry(42); + cache.Set(entry); + Assert.IsNotNull(cache.Get(), "Entry should be set initially"); + + cache.Set(null); + Assert.IsNull(cache.Get(), "Entry should be removed after setting null"); + } + + [TestMethod] + public void TestMultipleTypesStorage() + { + var cache = new SerializedCache(); + var entry1 = new TestCacheEntry(42); + var entry2 = new TestCacheEntry2("test"); + + cache.Set(entry1); + cache.Set(entry2); + + var retrieved1 = cache.Get(); + var retrieved2 = cache.Get(); + + Assert.IsNotNull(retrieved1, "First entry should be retrievable"); + Assert.IsNotNull(retrieved2, "Second entry should be retrievable"); + Assert.AreEqual(42, retrieved1.Value, "First entry value mismatch"); + Assert.AreEqual("test", retrieved2.Text, "Second entry text mismatch"); + } + + [TestMethod] + public void TestOverwriteExistingEntry() + { + var cache = new SerializedCache(); + cache.Set(new TestCacheEntry(42)); + cache.Set(new TestCacheEntry(99)); + + var retrieved = cache.Get(); + Assert.IsNotNull(retrieved, "Entry should exist"); + Assert.AreEqual(99, retrieved.Value, "Entry should be overwritten with new value"); + } + + [TestMethod] + public void TestCopyFromEmptyCache() + { + var source = new SerializedCache(); + var target = new SerializedCache(); + target.Set(new TestCacheEntry(42)); + + target.CopyFrom(source); + + var entry = target.Get(); + Assert.IsNotNull(entry, "Existing entry should remain after copying from empty cache"); + Assert.AreEqual(42, entry.Value, "Existing entry value should be unchanged"); + } + + [TestMethod] + public void TestThreadSafety() + { + var cache = new SerializedCache(); + var tasks = new System.Threading.Tasks.Task[10]; + + for (int i = 0; i < tasks.Length; i++) + { + var value = i; + tasks[i] = System.Threading.Tasks.Task.Run(() => + { + cache.Set(new TestCacheEntry(value)); + cache.Get(); + if (value % 2 == 0) + cache.Remove(typeof(TestCacheEntry)); + }); + } + + System.Threading.Tasks.Task.WaitAll(tasks); + // If we get here without exceptions, the test passes + // The final state is non-deterministic due to race conditions, + // but the operations should be thread-safe + } + + [TestMethod] + public void TestConcurrentCopyFrom() + { + var source = new SerializedCache(); + var target = new SerializedCache(); + source.Set(new TestCacheEntry(42)); + + var tasks = new System.Threading.Tasks.Task[5]; + for (int i = 0; i < tasks.Length; i++) + { + tasks[i] = System.Threading.Tasks.Task.Run(() => + { + target.CopyFrom(source); + }); + } + + System.Threading.Tasks.Task.WaitAll(tasks); + var entry = target.Get(); + Assert.IsNotNull(entry, "Entry should be copied successfully under concurrent operations"); + Assert.AreEqual(42, entry.Value, "Copied entry should have correct value"); + } + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_ContractMethodAttribute.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_ContractMethodAttribute.cs index 7337507696..34fc2273db 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_ContractMethodAttribute.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_ContractMethodAttribute.cs @@ -37,6 +37,9 @@ class NeedSnapshot [ContractMethod] public bool MethodReadOnlyStoreView(IReadOnlyStore view) => view is null; + [ContractMethod] + public bool MethodCachedReadOnlyStoreView(ICacheableReadOnlyStore view) => view is null; + [ContractMethod] public bool MethodDataCache(DataCache dataCache) => dataCache is null; }