Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@ namespace Nethermind.Consensus.Stateless;
/// Delegates all logic to base store except for writing trie nodes (readonly!)
/// Adds logic for capturing trie nodes accessed during execution and state root recomputation.
/// </remarks>
public class WitnessCapturingTrieStore(IReadOnlyTrieStore baseStore) : ITrieStore
public class WitnessCapturingTrieStore(IReadOnlyTrieStore baseStore) : ITrieStore, IScopedReadOnlyTraversalProvider
{
private readonly IReadOnlyTrieStore _baseStore = baseStore;
private readonly ConcurrentDictionary<Hash256AsKey, byte[]> _rlpCollector = new();

public IEnumerable<byte[]> TouchedNodesRlp => _rlpCollector.Select(static kvp => kvp.Value);

public void Dispose() => baseStore.Dispose();
public void Dispose() => _baseStore.Dispose();

public TrieNode FindCachedOrUnknown(Hash256? address, in TreePath path, Hash256 hash)
{
TrieNode node = baseStore.FindCachedOrUnknown(address, in path, hash);
if (node.NodeType != NodeType.Unknown) _rlpCollector.TryAdd(node.Keccak, node.FullRlp.ToArray());
TrieNode node = _baseStore.FindCachedOrUnknown(address, in path, hash);
CaptureNode(node);
return node;
}

Expand All @@ -37,21 +38,59 @@ public TrieNode FindCachedOrUnknown(Hash256? address, in TreePath path, Hash256

public byte[]? TryLoadRlp(Hash256? address, in TreePath path, Hash256 hash, ReadFlags flags = ReadFlags.None)
{
byte[]? rlp = baseStore.TryLoadRlp(address, in path, hash, flags);
if (rlp is not null) _rlpCollector.TryAdd(hash, rlp);
byte[]? rlp = _baseStore.TryLoadRlp(address, in path, hash, flags);
CaptureRlp(hash, rlp);
return rlp;
}

public bool HasRoot(Hash256 stateRoot) => baseStore.HasRoot(stateRoot);
public bool HasRoot(Hash256 stateRoot) => _baseStore.HasRoot(stateRoot);

public IDisposable BeginScope(BlockHeader? baseBlock) => baseStore.BeginScope(baseBlock);
public IDisposable BeginScope(BlockHeader? baseBlock) => _baseStore.BeginScope(baseBlock);

public IScopedTrieStore GetTrieStore(Hash256? address) => new ScopedTrieStore(this, address);

public INodeStorage.KeyScheme Scheme => baseStore.Scheme;
public INodeStorage.KeyScheme Scheme => _baseStore.Scheme;

public IBlockCommitter BeginBlockCommit(long blockNumber) => NullCommitter.Instance;

// WitnessCapturingTrieStore is read-only, so we return a no-op committer that doesn't persist any trie nodes
public ICommitter BeginCommit(Hash256? address, TrieNode? root, WriteFlags writeFlags) => NullCommitter.Instance;

private void CaptureNode(TrieNode node)
{
if (node.NodeType != NodeType.Unknown)
{
_rlpCollector.TryAdd(node.Keccak, node.FullRlp.ToArray());
}
}

private void CaptureRlp(Hash256 hash, byte[]? rlp)
{
if (rlp is not null)
{
_rlpCollector.TryAdd(hash, rlp);
}
}

public ITrieNodeResolver? GetReadOnlyTraversalResolver(Hash256? address) =>
_baseStore.GetTrieStore(address) is ITrieNodeResolverSource source
&& source.GetReadOnlyTraversalResolver() is { } readOnlyResolver
? new WitnessCapturingReadOnlyTraversalResolver(this, address, readOnlyResolver)
: null;

private sealed class WitnessCapturingReadOnlyTraversalResolver(
WitnessCapturingTrieStore fullTrieStore,
Hash256? address,
ITrieNodeResolver inner) : ReadOnlyTraversalResolverBase(fullTrieStore, address)
{
public override TrieNode FindCachedOrUnknown(in TreePath path, Hash256 hash)
{
TrieNode node = inner.FindCachedOrUnknown(path, hash);
fullTrieStore.CaptureNode(node);
return node;
}

protected override ITrieNodeResolver WithAddress(Hash256? address1) =>
new WitnessCapturingReadOnlyTraversalResolver(fullTrieStore, address1, inner.GetStorageTrieNodeResolver(address1));
}
}
75 changes: 62 additions & 13 deletions src/Nethermind/Nethermind.Core/Buffers/CappedArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
namespace Nethermind.Core.Buffers;

/// <summary>
/// Basically like ArraySegment, but only contain length, which reduces it size from 16byte to 12byte. Useful for
/// polling memory where memory pool usually can't return exactly the same size of data. To conserve space, The
/// underlying array can be null and this struct is meant to be non nullable, checking the `IsNull` property to check
/// if it represent null.
/// Like ArraySegment but with explicit offset+length, supporting zero-copy logical slices over a shared
/// backing array. Useful for pooled memory where the pool may return a larger buffer than requested. The
/// underlying array can be null; check the <see cref="IsNull"/> property to detect that case.
/// </summary>
public readonly struct CappedArray<T> where T : struct
{
Expand All @@ -25,11 +24,18 @@ namespace Nethermind.Core.Buffers;
public static object EmptyBoxed { get; } = _empty;

private readonly T[]? _array;
private readonly int _offset;
private readonly int _length;

public CappedArray(T[]? array, int length)
: this(array, 0, length)
{
}

public CappedArray(T[]? array, int offset, int length)
{
_array = array;
_offset = offset;
_length = length;
}

Expand All @@ -38,8 +44,15 @@ public CappedArray(T[]? array)
if (array is not null)
{
_array = array;
_offset = 0;
_length = array.Length;
}
else
{
_array = null;
_offset = 0;
_length = 0;
}
}

public static implicit operator ReadOnlySpan<T>(in CappedArray<T> array) => array.AsSpan();
Expand All @@ -50,47 +63,74 @@ public T this[int index]
{
get
{
// Validate against the logical slice first - a negative `index` would otherwise wrap into
// `_offset + index` and silently read bytes from before the slice (e.g. parent RLP for an
// inline trie child). The underlying-array check defends against an oversized _length passed
// to the (array, offset, length) constructor.
if ((uint)index >= (uint)_length)
{
ThrowArgumentOutOfRangeException();
}

T[] array = _array!;
if (index >= _length || (uint)index >= (uint)array.Length)
int arrayIndex = _offset + index;
if ((uint)arrayIndex >= (uint)array.Length)
{
ThrowArgumentOutOfRangeException();
}

return array[index];
return array[arrayIndex];
}
set
{
if ((uint)index >= (uint)_length)
{
ThrowArgumentOutOfRangeException();
}

T[] array = _array!;
if (index >= _length || (uint)index >= (uint)array.Length)
int arrayIndex = _offset + index;
if ((uint)arrayIndex >= (uint)array.Length)
{
ThrowArgumentOutOfRangeException();
}

array[index] = value;
array[arrayIndex] = value;
}
}

[DoesNotReturn, StackTraceHidden]
private static void ThrowArgumentOutOfRangeException() => throw new ArgumentOutOfRangeException();

public int Length => _length;
public int Offset => _offset;
public int UnderlyingLength => _array?.Length ?? 0;
public T[]? UnderlyingArray => _array;
public bool IsUncapped => _length == _array?.Length;
public bool IsUncapped => _offset == 0 && _length == _array?.Length;
public bool IsNull => _array is null;
public bool IsNotNull => _array is not null;
public bool IsNullOrEmpty => _length == 0;
public bool IsNotNullOrEmpty => _length > 0;
public Span<T> AsSpan() => _array.AsSpan(0, _length);
public Span<T> AsSpan(int start, int length) => _array.AsSpan(start, length);
public Span<T> AsSpan() => _array.AsSpan(_offset, _length);
public Span<T> AsSpan(int start, int length)
{
// Validate against the logical slice first; otherwise a negative `start` would wrap into
// `_offset + start` and yield a span over bytes outside the slice (e.g. parent RLP).
if ((uint)start > (uint)_length || (uint)length > (uint)(_length - start))
{
ThrowArgumentOutOfRangeException();
}

return _array.AsSpan(_offset + start, length);
}

public T[]? ToArray()
{
T[]? array = _array;

if (array is null) return null;
if (array.Length == 0) return [];
if (_length == array.Length) return array;
if (_offset == 0 && _length == array.Length) return array;
return AsSpan().ToArray();
}

Expand All @@ -99,5 +139,14 @@ public T this[int index]
: base.ToString();

public ArraySegment<T> AsArraySegment() => AsArraySegment(0, _length);
public ArraySegment<T> AsArraySegment(int start, int length) => new(_array!, start, length);
public ArraySegment<T> AsArraySegment(int start, int length)
{
// See AsSpan(start, length) - validate against the logical slice before adding _offset.
if ((uint)start > (uint)_length || (uint)length > (uint)(_length - start))
{
ThrowArgumentOutOfRangeException();
}

return new(_array!, _offset + start, length);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Nethermind.Synchronization.SnapSync;
/// UpperBound. This is to prevent double writes on partitioned snap ranges.
/// </summary>
/// <param name="baseTrieStore"></param>
public class SnapUpperBoundAdapter(IScopedTrieStore baseTrieStore) : IScopedTrieStore
public class SnapUpperBoundAdapter(IScopedTrieStore baseTrieStore) : IScopedTrieStore, ITrieNodeResolverSource
{
public ValueHash256 UpperBound = ValueKeccak.MaxValue;

Expand All @@ -30,6 +30,16 @@ public class SnapUpperBoundAdapter(IScopedTrieStore baseTrieStore) : IScopedTrie

public ICommitter BeginCommit(TrieNode? root, WriteFlags writeFlags = WriteFlags.None) => new BoundedSnapCommitter(baseTrieStore.BeginCommit(root, writeFlags), UpperBound);

public ITrieNodeResolver? GetReadOnlyTraversalResolver()
{
if (baseTrieStore is ITrieNodeResolverSource source)
{
return source.GetReadOnlyTraversalResolver();
}

return null;
}

Comment thread
benaadams marked this conversation as resolved.
Outdated
private sealed class BoundedSnapCommitter(ICommitter baseCommitter, ValueHash256 subtreeLimit) : ICommitter
{
public void Dispose() => baseCommitter.Dispose();
Expand Down
31 changes: 31 additions & 0 deletions src/Nethermind/Nethermind.Trie.Test/OverlayTrieStoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,35 @@ public void TrieStore_OverlayExistingStore()
// After all this, the original should not change.
dbProvider.StateDb.GetAllKeys().Count().Should().Be(originalKeyCount);
}

[Test]
[NonParallelizable]
public void OverlayStore_read_only_lookup_uses_shared_base_nodes()
{
using TrieStore existingStore = TestTrieStoreFactory.Build(new MemDb(), No.Pruning, No.Persistence, LimboLogs.Instance);
PatriciaTree patriciaTree = new(existingStore, LimboLogs.Instance);
{
using IBlockCommitter _ = existingStore.BeginBlockCommit(0);
patriciaTree.Set(TestItem.Keccaks[0].Bytes, TestItem.Keccaks[0].BytesToArray());
patriciaTree.Set(TestItem.Keccaks[1].Bytes, TestItem.Keccaks[1].BytesToArray());
patriciaTree.Commit();
}

IDbProvider dbProvider = TestMemDbProvider.Init();
ReadOnlyDbProvider readOnlyDbProvider = dbProvider.AsReadOnly(true);
ITrieStore overlayStore = new OverlayTrieStore(readOnlyDbProvider.GetDb<IDb>(DbNames.State), existingStore.AsReadOnly());

long sharedHitsBefore = existingStore.SharedNodeHitCount;
long clonesBefore = existingStore.CloneForReadOnlyCount;

PatriciaTree overlaidTree = new(overlayStore, LimboLogs.Instance)
{
RootHash = patriciaTree.RootHash
};

overlaidTree.Get(TestItem.Keccaks[0].Bytes).ToArray().Should().BeEquivalentTo(TestItem.Keccaks[0].BytesToArray());

existingStore.SharedNodeHitCount.Should().BeGreaterThan(sharedHitsBefore);
existingStore.CloneForReadOnlyCount.Should().Be(clonesBefore);
}
}
Loading
Loading