diff --git a/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs b/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs
index 6d31165ba..5edff4002 100644
--- a/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs
+++ b/plugins/LevelDBStore/Plugins/Storage/Snapshot.cs
@@ -25,7 +25,8 @@ internal class Snapshot : IStoreSnapshot, IEnumerable
public IEnumerable<(byte[] Key, byte[] Value)> Find(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward)
{
- return _db.Seek(_readOptions, keyOrPrefix, direction);
+ return _db.Seek(_scanReadOptions, keyOrPrefix, direction);
}
public bool Contains(byte[] key)
{
- return _db.Contains(_readOptions, key);
+ return _db.Contains(_pointReadOptions, key);
}
public byte[]? TryGet(byte[] key)
{
- return _db.Get(_readOptions, key);
+ return _db.Get(_pointReadOptions, key);
}
public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value)
{
- value = _db.Get(_readOptions, key);
+ value = _db.Get(_pointReadOptions, key);
return value != null;
}
public IEnumerator> GetEnumerator()
{
- using var iterator = _db.CreateIterator(_readOptions);
+ using var iterator = _db.CreateIterator(_scanReadOptions);
for (iterator.SeekToFirst(); iterator.Valid(); iterator.Next())
yield return new KeyValuePair(iterator.Key()!, iterator.Value()!);
}
diff --git a/plugins/RocksDBStore/Plugins/Storage/Snapshot.cs b/plugins/RocksDBStore/Plugins/Storage/Snapshot.cs
index 9c3fc170e..cdfa5deb5 100644
--- a/plugins/RocksDBStore/Plugins/Storage/Snapshot.cs
+++ b/plugins/RocksDBStore/Plugins/Storage/Snapshot.cs
@@ -23,7 +23,8 @@ internal class Snapshot : IStoreSnapshot
private readonly RocksDb _db;
private readonly RocksDbSharp.Snapshot _snapshot;
private readonly WriteBatch _batch;
- private readonly ReadOptions _options;
+ private readonly ReadOptions _scanOptions;
+ private readonly ReadOptions _pointOptions;
private readonly Lock _lock = new();
public IStore Store { get; }
@@ -35,9 +36,12 @@ internal Snapshot(Store store, RocksDb db)
_snapshot = db.CreateSnapshot();
_batch = new WriteBatch();
- _options = new ReadOptions();
- _options.SetFillCache(false);
- _options.SetSnapshot(_snapshot);
+ _scanOptions = new ReadOptions();
+ _scanOptions.SetFillCache(false);
+ _scanOptions.SetSnapshot(_snapshot);
+
+ _pointOptions = new ReadOptions();
+ _pointOptions.SetSnapshot(_snapshot);
}
public void Commit()
@@ -63,7 +67,7 @@ public void Put(byte[] key, byte[] value)
{
keyOrPrefix ??= [];
- using var it = _db.NewIterator(readOptions: _options);
+ using var it = _db.NewIterator(readOptions: _scanOptions);
if (direction == SeekDirection.Forward)
for (it.Seek(keyOrPrefix); it.Valid(); it.Next())
@@ -75,17 +79,17 @@ public void Put(byte[] key, byte[] value)
public bool Contains(byte[] key)
{
- return _db.Get(key, Array.Empty(), 0, 0, readOptions: _options) >= 0;
+ return _db.Get(key, Array.Empty(), 0, 0, readOptions: _pointOptions) >= 0;
}
public byte[]? TryGet(byte[] key)
{
- return _db.Get(key, readOptions: _options);
+ return _db.Get(key, readOptions: _pointOptions);
}
public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value)
{
- value = _db.Get(key, readOptions: _options);
+ value = _db.Get(key, readOptions: _pointOptions);
return value != null;
}
diff --git a/tests/Neo.Plugins.Storage.Tests/SnapshotReadOptionsTest.cs b/tests/Neo.Plugins.Storage.Tests/SnapshotReadOptionsTest.cs
new file mode 100644
index 000000000..564c9031c
--- /dev/null
+++ b/tests/Neo.Plugins.Storage.Tests/SnapshotReadOptionsTest.cs
@@ -0,0 +1,102 @@
+// Copyright (C) 2015-2026 The Neo Project.
+//
+// SnapshotReadOptionsTest.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System.Reflection;
+
+namespace Neo.Plugins.Storage.Tests;
+
+[TestClass]
+public class SnapshotReadOptionsTest
+{
+ [TestMethod]
+ public void LevelDbSnapshotUsesDedicatedReadOptionsForScanAndPointReads()
+ {
+ var snapshotType = GetSnapshotType(typeof(LevelDBStore));
+ var scanReadOptions = GetField(snapshotType, "_scanReadOptions");
+ var pointReadOptions = GetField(snapshotType, "_pointReadOptions");
+
+ AssertMethodUsesOnly(snapshotType, "Find", scanReadOptions, pointReadOptions);
+ AssertMethodUsesOnly(snapshotType, "GetEnumerator", scanReadOptions, pointReadOptions);
+ AssertMethodUsesOnly(snapshotType, "Contains", pointReadOptions, scanReadOptions);
+ AssertMethodUsesOnly(snapshotType, "TryGet", pointReadOptions, scanReadOptions);
+ }
+
+ [TestMethod]
+ public void RocksDbSnapshotUsesDedicatedReadOptionsForScanAndPointReads()
+ {
+ var snapshotType = GetSnapshotType(typeof(RocksDBStore));
+ var scanOptions = GetField(snapshotType, "_scanOptions");
+ var pointOptions = GetField(snapshotType, "_pointOptions");
+
+ AssertMethodUsesOnly(snapshotType, "Find", scanOptions, pointOptions);
+ AssertMethodUsesOnly(snapshotType, "Contains", pointOptions, scanOptions);
+ AssertMethodUsesOnly(snapshotType, "TryGet", pointOptions, scanOptions);
+ }
+
+ private static Type GetSnapshotType(Type storeProviderType)
+ {
+ return storeProviderType.Assembly.GetType("Neo.Plugins.Storage.Snapshot", throwOnError: true)!;
+ }
+
+ private static FieldInfo GetField(Type type, string fieldName)
+ {
+ var field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.IsNotNull(field, $"{type.FullName} should declare {fieldName}.");
+ return field;
+ }
+
+ private static void AssertMethodUsesOnly(Type type, string methodName, FieldInfo expectedField, FieldInfo unexpectedField)
+ {
+ var implementations = GetImplementations(type, methodName).ToArray();
+ Assert.IsNotEmpty(implementations, $"{type.FullName}.{methodName} should exist.");
+ Assert.IsTrue(
+ implementations.Any(method => UsesField(method, expectedField)),
+ $"{type.FullName}.{methodName} should use {expectedField.Name}.");
+ Assert.IsFalse(
+ implementations.Any(method => UsesField(method, unexpectedField)),
+ $"{type.FullName}.{methodName} should not use {unexpectedField.Name}.");
+ }
+
+ private static IEnumerable GetImplementations(Type type, string methodName)
+ {
+ const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
+
+ foreach (var method in type.GetMethods(flags).Where(method => method.Name == methodName))
+ yield return method;
+
+ foreach (var nestedType in type.GetNestedTypes(BindingFlags.NonPublic))
+ {
+ if (!nestedType.Name.Contains($"<{methodName}>")) continue;
+
+ var moveNext = nestedType.GetMethod("MoveNext", flags);
+ if (moveNext != null)
+ yield return moveNext;
+ }
+ }
+
+ private static bool UsesField(MethodBase method, FieldInfo field)
+ {
+ var body = method.GetMethodBody()?.GetILAsByteArray();
+ if (body is null) return false;
+
+ var token = BitConverter.GetBytes(field.MetadataToken);
+ for (var i = 0; i <= body.Length - token.Length; i++)
+ {
+ if (body[i] == token[0]
+ && body[i + 1] == token[1]
+ && body[i + 2] == token[2]
+ && body[i + 3] == token[3])
+ return true;
+ }
+
+ return false;
+ }
+}