using System.Numerics;
using System.Collections.Concurrent;
using System.Diagnostics;
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Services;
using Neo.Sol.Runtime.Crypto;
using NeoFrameworkStorage = Neo.SmartContract.Framework.Services.Storage;
namespace Neo.Sol.Runtime.Storage;
public sealed class StorageManager : IDisposable
{
private readonly StorageContext _context;
private readonly ConcurrentDictionary<BigInteger, CachedSlot> _cache = new();
private readonly ConcurrentHashSet<BigInteger> _modifiedSlots = new();
private readonly ReaderWriterLockSlim _lock = new();
private readonly Timer _cacheCleanupTimer;
private readonly Stopwatch _accessTimer = new();
private bool _disposed = false;
private ulong _cacheHits = 0;
private ulong _cacheMisses = 0;
private ulong _storageReads = 0;
private ulong _storageWrites = 0;
private ulong _cacheEvictions = 0;
private const int SLOT_SIZE = 32; private const int MAX_PACKED_SLOTS = 8; private const int MAX_CACHE_SIZE = 10000; private const int CACHE_CLEANUP_INTERVAL_MS = 60000; private const ulong CACHE_TTL_MS = 300000;
public StorageManager(StorageContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_accessTimer.Start();
_cacheCleanupTimer = new Timer(CleanupCache, null, CACHE_CLEANUP_INTERVAL_MS, CACHE_CLEANUP_INTERVAL_MS);
}
private byte[] GenerateStorageKey(BigInteger slot)
{
var prefix = "evm_storage_"u8.ToArray();
var slotBytes = slot.ToByteArray(isUnsigned: true, isBigEndian: true);
var paddedSlot = new byte[SLOT_SIZE];
if (slotBytes.Length <= SLOT_SIZE)
{
Array.Copy(slotBytes, 0, paddedSlot, SLOT_SIZE - slotBytes.Length, slotBytes.Length);
}
else
{
Array.Copy(slotBytes, slotBytes.Length - SLOT_SIZE, paddedSlot, 0, SLOT_SIZE);
}
var combined = new byte[prefix.Length + paddedSlot.Length];
Array.Copy(prefix, 0, combined, 0, prefix.Length);
Array.Copy(paddedSlot, 0, combined, prefix.Length, paddedSlot.Length);
return CryptoLib.Keccak256(combined);
}
public byte[] Load(BigInteger slot)
{
if (_disposed)
throw new ObjectDisposedException(nameof(StorageManager));
var currentTime = (ulong)_accessTimer.ElapsedMilliseconds;
if (_cache.TryGetValue(slot, out var cachedSlot))
{
cachedSlot.LastAccessed = currentTime;
cachedSlot.AccessCount++;
Interlocked.Increment(ref _cacheHits);
return cachedSlot.Value;
}
Interlocked.Increment(ref _cacheMisses);
Interlocked.Increment(ref _storageReads);
var key = GenerateStorageKey(slot);
var value = NeoFrameworkStorage.Get(_context, key);
var result = new byte[SLOT_SIZE];
if (value != null && value.Length > 0)
{
var valueBytes = (byte[])value;
if (value.Length <= SLOT_SIZE)
{
Array.Copy(valueBytes, 0, result, SLOT_SIZE - value.Length, value.Length);
}
else
{
Array.Copy(valueBytes, value.Length - SLOT_SIZE, result, 0, SLOT_SIZE);
}
}
var cached = new CachedSlot
{
Value = result,
LastAccessed = currentTime,
AccessCount = 1,
IsModified = false
};
if (_cache.Count >= MAX_CACHE_SIZE)
{
EvictOldestCacheEntry();
}
_cache.TryAdd(slot, cached);
return result;
}
public BigInteger LoadBigInteger(BigInteger slot)
{
var bytes = Load(slot);
return new BigInteger(bytes, isUnsigned: true, isBigEndian: true);
}
public void Store(BigInteger slot, byte[] value)
{
if (_disposed)
throw new ObjectDisposedException(nameof(StorageManager));
if (value.Length != SLOT_SIZE)
throw new ArgumentException($"Value must be exactly {SLOT_SIZE} bytes");
var currentTime = (ulong)_accessTimer.ElapsedMilliseconds;
Interlocked.Increment(ref _storageWrites);
var cachedSlot = new CachedSlot
{
Value = (byte[])value.Clone(),
LastAccessed = currentTime,
AccessCount = _cache.TryGetValue(slot, out var existing) ? existing.AccessCount + 1 : 1,
IsModified = true
};
_cache.AddOrUpdate(slot, cachedSlot, (_, _) => cachedSlot);
_modifiedSlots.Add(slot);
var key = GenerateStorageKey(slot);
if (IsZero(value))
{
NeoFrameworkStorage.Delete(_context, key);
}
else
{
var compressedValue = CompressValueIfBeneficial(value);
NeoFrameworkStorage.Put(_context, key, compressedValue);
}
}
public void Store(BigInteger slot, BigInteger value)
{
var bytes = value.ToByteArray(isUnsigned: true, isBigEndian: true);
var paddedBytes = new byte[SLOT_SIZE];
if (bytes.Length <= SLOT_SIZE)
{
Array.Copy(bytes, 0, paddedBytes, SLOT_SIZE - bytes.Length, bytes.Length);
}
else
{
Array.Copy(bytes, bytes.Length - SLOT_SIZE, paddedBytes, 0, SLOT_SIZE);
}
Store(slot, paddedBytes);
}
public static BigInteger CalculateArrayElementSlot(BigInteger arraySlot, BigInteger index)
{
var arraySlotBytes = arraySlot.ToByteArray(isUnsigned: true, isBigEndian: true);
var paddedSlot = new byte[SLOT_SIZE];
if (arraySlotBytes.Length <= SLOT_SIZE)
{
Array.Copy(arraySlotBytes, 0, paddedSlot, SLOT_SIZE - arraySlotBytes.Length, arraySlotBytes.Length);
}
var baseSlotHash = CryptoLib.Keccak256(paddedSlot);
var baseSlot = new BigInteger(baseSlotHash, isUnsigned: true, isBigEndian: true);
return baseSlot + index;
}
public static BigInteger CalculateMappingElementSlot(BigInteger mappingSlot, byte[] key)
{
var mappingSlotBytes = mappingSlot.ToByteArray(isUnsigned: true, isBigEndian: true);
var paddedSlot = new byte[SLOT_SIZE];
if (mappingSlotBytes.Length <= SLOT_SIZE)
{
Array.Copy(mappingSlotBytes, 0, paddedSlot, SLOT_SIZE - mappingSlotBytes.Length, mappingSlotBytes.Length);
}
var paddedKey = new byte[SLOT_SIZE];
if (key.Length <= SLOT_SIZE)
{
Array.Copy(key, 0, paddedKey, SLOT_SIZE - key.Length, key.Length);
}
else
{
Array.Copy(key, key.Length - SLOT_SIZE, paddedKey, 0, SLOT_SIZE);
}
var combined = new byte[SLOT_SIZE * 2];
Array.Copy(paddedKey, 0, combined, 0, SLOT_SIZE);
Array.Copy(paddedSlot, 0, combined, SLOT_SIZE, SLOT_SIZE);
var hash = CryptoLib.Keccak256(combined);
return new BigInteger(hash, isUnsigned: true, isBigEndian: true);
}
public static BigInteger CalculateMappingElementSlot(BigInteger mappingSlot, BigInteger key)
{
var keyBytes = key.ToByteArray(isUnsigned: true, isBigEndian: true);
var paddedKey = new byte[SLOT_SIZE];
if (keyBytes.Length <= SLOT_SIZE)
{
Array.Copy(keyBytes, 0, paddedKey, SLOT_SIZE - keyBytes.Length, keyBytes.Length);
}
else
{
Array.Copy(keyBytes, keyBytes.Length - SLOT_SIZE, paddedKey, 0, SLOT_SIZE);
}
return CalculateMappingElementSlot(mappingSlot, paddedKey);
}
private static bool IsZero(byte[] value)
{
return value.All(b => b == 0);
}
public IReadOnlySet<BigInteger> GetModifiedSlots()
{
return new HashSet<BigInteger>(_modifiedSlots.Snapshot());
}
private void CleanupCache(object? state)
{
if (_disposed) return;
try
{
var currentTime = (ulong)_accessTimer.ElapsedMilliseconds;
var expiredSlots = new System.Collections.Generic.List<BigInteger>();
foreach (var kvp in _cache)
{
if (currentTime - kvp.Value.LastAccessed > CACHE_TTL_MS &&
!kvp.Value.IsModified)
{
expiredSlots.Add(kvp.Key);
}
}
foreach (var slot in expiredSlots)
{
if (_cache.TryRemove(slot, out _))
{
Interlocked.Increment(ref _cacheEvictions);
}
}
}
catch
{
}
}
private void EvictOldestCacheEntry()
{
BigInteger? oldestSlot = null;
ulong oldestTime = ulong.MaxValue;
foreach (var kvp in _cache)
{
if (kvp.Value.LastAccessed < oldestTime && !kvp.Value.IsModified)
{
oldestTime = kvp.Value.LastAccessed;
oldestSlot = kvp.Key;
}
}
if (oldestSlot.HasValue && _cache.TryRemove(oldestSlot.Value, out _))
{
Interlocked.Increment(ref _cacheEvictions);
}
}
private byte[] CompressValueIfBeneficial(byte[] value)
{
var consecutiveZeros = 0;
for (int i = value.Length - 1; i >= 0; i--)
{
if (value[i] == 0)
consecutiveZeros++;
else
break;
}
if (consecutiveZeros > SLOT_SIZE / 2)
{
var nonZeroLength = SLOT_SIZE - consecutiveZeros;
var compressed = new byte[nonZeroLength + 1];
compressed[0] = (byte)consecutiveZeros; Array.Copy(value, 0, compressed, 1, nonZeroLength);
return compressed;
}
return value;
}
public void ClearCache()
{
if (_disposed)
throw new ObjectDisposedException(nameof(StorageManager));
_cache.Clear();
_modifiedSlots.Clear();
_cacheHits = 0;
_cacheMisses = 0;
_storageReads = 0;
_storageWrites = 0;
_cacheEvictions = 0;
}
public Dictionary<BigInteger, byte[]> BatchLoad(IEnumerable<BigInteger> slots)
{
if (_disposed)
throw new ObjectDisposedException(nameof(StorageManager));
var result = new Dictionary<BigInteger, byte[]>();
var slotsToFetch = new System.Collections.Generic.List<BigInteger>();
foreach (var slot in slots)
{
if (_cache.TryGetValue(slot, out var cached))
{
result[slot] = cached.Value;
Interlocked.Increment(ref _cacheHits);
}
else
{
slotsToFetch.Add(slot);
Interlocked.Increment(ref _cacheMisses);
}
}
foreach (var slot in slotsToFetch)
{
result[slot] = Load(slot);
}
return result;
}
public void BatchStore(Dictionary<BigInteger, byte[]> updates)
{
if (_disposed)
throw new ObjectDisposedException(nameof(StorageManager));
foreach (var kvp in updates)
{
Store(kvp.Key, kvp.Value);
}
}
public StorageStats GetStats()
{
var totalOperations = _cacheHits + _cacheMisses;
var modifiedCount = _cache.Values.Count(c => c.IsModified);
return new StorageStats
{
CachedSlots = (uint)_cache.Count,
ModifiedSlots = (uint)_modifiedSlots.Count,
ModifiedCachedSlots = (uint)modifiedCount,
CacheHitRatio = totalOperations > 0 ? (double)_cacheHits / totalOperations : 0.0,
StorageReads = _storageReads,
StorageWrites = _storageWrites,
CacheEvictions = _cacheEvictions,
CacheUtilization = MAX_CACHE_SIZE > 0 ? (double)_cache.Count / MAX_CACHE_SIZE : 0.0
};
}
public void Dispose()
{
if (!_disposed)
{
_cacheCleanupTimer?.Dispose();
_lock?.Dispose();
_accessTimer?.Stop();
ClearCache();
_disposed = true;
}
}
}
public record StorageStats
{
public uint CachedSlots { get; init; }
public uint ModifiedSlots { get; init; }
public uint ModifiedCachedSlots { get; init; }
public double CacheHitRatio { get; init; }
public ulong StorageReads { get; init; }
public ulong StorageWrites { get; init; }
public ulong CacheEvictions { get; init; }
public double CacheUtilization { get; init; }
}
internal sealed class CachedSlot
{
public byte[] Value { get; set; } = Array.Empty<byte>();
public ulong LastAccessed { get; set; }
public ulong AccessCount { get; set; }
public bool IsModified { get; set; }
}
internal sealed class ConcurrentHashSet<T> : IDisposable where T : notnull
{
private readonly HashSet<T> _set = new();
private readonly ReaderWriterLockSlim _lock = new();
private bool _disposed = false;
public void Add(T item)
{
if (_disposed) return;
_lock.EnterWriteLock();
try
{
_set.Add(item);
}
finally
{
_lock.ExitWriteLock();
}
}
public bool Contains(T item)
{
if (_disposed) return false;
_lock.EnterReadLock();
try
{
return _set.Contains(item);
}
finally
{
_lock.ExitReadLock();
}
}
public void Clear()
{
if (_disposed) return;
_lock.EnterWriteLock();
try
{
_set.Clear();
}
finally
{
_lock.ExitWriteLock();
}
}
public int Count
{
get
{
if (_disposed) return 0;
_lock.EnterReadLock();
try
{
return _set.Count;
}
finally
{
_lock.ExitReadLock();
}
}
}
public T[] Snapshot()
{
if (_disposed) return Array.Empty<T>();
_lock.EnterReadLock();
try
{
return _set.ToArray();
}
finally
{
_lock.ExitReadLock();
}
}
public void Dispose()
{
if (!_disposed)
{
_lock.Dispose();
_disposed = true;
}
}
}