diff --git a/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs b/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs index 57cca506565..650a82b5dd6 100644 --- a/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs +++ b/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs @@ -144,7 +144,8 @@ private IAuRaValidator CreateAuRaValidator(IBlockProcessor processor, IReadOnlyT _api.EngineSigner, _api.SpecProvider, _api.GasPriceOracle, - _api.ReportingContractValidatorCache, chainSpecAuRa.PosdaoTransition, false) + _api.ReportingContractValidatorCache, + chainSpecAuRa.PosdaoTransition) .CreateValidatorProcessor(chainSpecAuRa.Validators, _api.BlockTree.Head?.Header); if (validator is IDisposable disposableValidator) diff --git a/src/Nethermind/Nethermind.Consensus/Processing/BlockchainProcessor.cs b/src/Nethermind/Nethermind.Consensus/Processing/BlockchainProcessor.cs index 9b2f420a505..7b64a048f07 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/BlockchainProcessor.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/BlockchainProcessor.cs @@ -28,6 +28,11 @@ public class BlockchainProcessor : IBlockchainProcessor, IBlockProcessingQueue public int SoftMaxRecoveryQueueSizeInTx = 10000; // adjust based on tx or gas public const int MaxProcessingQueueSize = 2000; // adjust based on tx or gas + [ThreadStatic] + private static bool _isMainProcessingThread; + public static bool IsMainProcessingThread => _isMainProcessingThread; + public bool IsMainProcessor { get; init; } + public ITracerBag Tracers => _compositeBlockTracer; private readonly IBlockProcessor _blockProcessor; @@ -56,7 +61,7 @@ public class BlockchainProcessor : IBlockchainProcessor, IBlockProcessingQueue private readonly CompositeBlockTracer _compositeBlockTracer = new(); private readonly Stopwatch _stopwatch = new(); - public event EventHandler InvalidBlock; + public event EventHandler? InvalidBlock; /// /// @@ -256,6 +261,8 @@ private Task RunProcessing() Thread thread = new(() => { + _isMainProcessingThread = IsMainProcessor; + try { RunProcessingLoop(); diff --git a/src/Nethermind/Nethermind.Core/Extensions/TypeExtensions.cs b/src/Nethermind/Nethermind.Core/Extensions/TypeExtensions.cs index 515c3148892..9097f8e580f 100644 --- a/src/Nethermind/Nethermind.Core/Extensions/TypeExtensions.cs +++ b/src/Nethermind/Nethermind.Core/Extensions/TypeExtensions.cs @@ -55,5 +55,17 @@ public static bool CanBeAssignedNull(this Type type) => public static bool CannotBeAssignedNull(this Type type) => type.IsValueType && Nullable.GetUnderlyingType(type) is null; + + /// + /// Returns the type name. If this is a generic type, appends + /// the list of generic type arguments between angle brackets. + /// (Does not account for embedded / inner generic arguments.) + /// + /// The type. + /// System.String. + public static string NameWithGenerics(this Type type) => + type.IsGenericType + ? $"{type.Name.Substring(0, type.Name.IndexOf("`", StringComparison.InvariantCultureIgnoreCase))}<{string.Join(",", type.GetGenericArguments().Select(NameWithGenerics))}>" + : type.Name; } } diff --git a/src/Nethermind/Nethermind.Core/IKeyValueStore.cs b/src/Nethermind/Nethermind.Core/IKeyValueStore.cs index da22dbe0501..27b7ded8b3f 100644 --- a/src/Nethermind/Nethermind.Core/IKeyValueStore.cs +++ b/src/Nethermind/Nethermind.Core/IKeyValueStore.cs @@ -9,8 +9,8 @@ public interface IKeyValueStore : IReadOnlyKeyValueStore { new byte[]? this[ReadOnlySpan key] { - get => Get(key, ReadFlags.None); - set => Set(key, value, WriteFlags.None); + get => Get(key); + set => Set(key, value); } void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None); @@ -18,7 +18,7 @@ public interface IKeyValueStore : IReadOnlyKeyValueStore public interface IReadOnlyKeyValueStore { - byte[]? this[ReadOnlySpan key] => Get(key, ReadFlags.None); + byte[]? this[ReadOnlySpan key] => Get(key); byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None); } diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeBlockchain.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeBlockchain.cs index 943d1358b20..d9dbc157ffd 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeBlockchain.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeBlockchain.cs @@ -36,6 +36,7 @@ using Nethermind.State; using Nethermind.State.Witnesses; using Nethermind.Synchronization.ParallelSync; +using Nethermind.Synchronization.Trie; using Nethermind.Synchronization.Witness; using Nethermind.Trie; using Nethermind.Trie.Pruning; @@ -105,11 +106,12 @@ private Task InitBlockchain() IKeyValueStore codeDb = getApi.DbProvider.CodeDb .WitnessedBy(witnessCollector); - TrieStore trieStore; IKeyValueStoreWithBatching stateWitnessedBy = setApi.MainStateDbWithCache.WitnessedBy(witnessCollector); + IPersistenceStrategy persistenceStrategy; + IPruningStrategy pruningStrategy; if (pruningConfig.Mode.IsMemory()) { - IPersistenceStrategy persistenceStrategy = Persist.IfBlockOlderThan(pruningConfig.PersistenceInterval); // TODO: this should be based on time + persistenceStrategy = Persist.IfBlockOlderThan(pruningConfig.PersistenceInterval); // TODO: this should be based on time if (pruningConfig.Mode.IsFull()) { PruningTriggerPersistenceStrategy triggerPersistenceStrategy = new((IFullPruningDb)getApi.DbProvider!.StateDb, getApi.BlockTree!, getApi.LogManager); @@ -117,29 +119,34 @@ private Task InitBlockchain() persistenceStrategy = persistenceStrategy.Or(triggerPersistenceStrategy); } - setApi.TrieStore = trieStore = new TrieStore( - stateWitnessedBy, - Prune.WhenCacheReaches(pruningConfig.CacheMb.MB()), // TODO: memory hint should define this - persistenceStrategy, - getApi.LogManager); - - if (pruningConfig.Mode.IsFull()) - { - IFullPruningDb fullPruningDb = (IFullPruningDb)getApi.DbProvider!.StateDb; - fullPruningDb.PruningStarted += (_, args) => - { - cachedStateDb.PersistCache(args.Context); - trieStore.PersistCache(args.Context, args.Context.CancellationTokenSource.Token); - }; - } + pruningStrategy = Prune.WhenCacheReaches(pruningConfig.CacheMb.MB()); // TODO: memory hint should define this } else { - setApi.TrieStore = trieStore = new TrieStore( - stateWitnessedBy, - No.Pruning, - Persist.EveryBlock, - getApi.LogManager); + pruningStrategy = No.Pruning; + persistenceStrategy = Persist.EveryBlock; + } + + TrieStore trieStore = new HealingTrieStore( + stateWitnessedBy, + pruningStrategy, + persistenceStrategy, + getApi.LogManager); + setApi.TrieStore = trieStore; + + IWorldState worldState = setApi.WorldState = new HealingWorldState( + trieStore, + codeDb, + getApi.LogManager); + + if (pruningConfig.Mode.IsFull()) + { + IFullPruningDb fullPruningDb = (IFullPruningDb)getApi.DbProvider!.StateDb; + fullPruningDb.PruningStarted += (_, args) => + { + cachedStateDb.PersistCache(args.Context); + trieStore.PersistCache(args.Context, args.Context.CancellationTokenSource.Token); + }; } TrieStoreBoundaryWatcher trieStoreBoundaryWatcher = new(trieStore, _api.BlockTree!, _api.LogManager); @@ -148,11 +155,6 @@ private Task InitBlockchain() ITrieStore readOnlyTrieStore = setApi.ReadOnlyTrieStore = trieStore.AsReadOnly(cachedStateDb); - IWorldState worldState = setApi.WorldState = new WorldState( - trieStore, - codeDb, - getApi.LogManager); - ReadOnlyDbProvider readOnly = new(getApi.DbProvider, false); IStateReader stateReader = setApi.StateReader = new StateReader(readOnlyTrieStore, readOnly.GetDb(DbNames.Code), getApi.LogManager); @@ -259,7 +261,10 @@ private Task InitBlockchain() { StoreReceiptsByDefault = initConfig.StoreReceipts, DumpOptions = initConfig.AutoDump - }); + }) + { + IsMainProcessor = true + }; setApi.BlockProcessingQueue = blockchainProcessor; setApi.BlockchainProcessor = blockchainProcessor; diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs index 2799191f6fc..0d232677403 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs @@ -39,6 +39,8 @@ using Nethermind.Synchronization.Peers; using Nethermind.Synchronization.Reporting; using Nethermind.Synchronization.SnapSync; +using Nethermind.Synchronization.StateSync; +using Nethermind.Synchronization.Trie; namespace Nethermind.Init.Steps; @@ -87,6 +89,7 @@ public async Task Execute(CancellationToken cancellationToken) private async Task Initialize(CancellationToken cancellationToken) { if (_api.DbProvider is null) throw new StepDependencyException(nameof(_api.DbProvider)); + if (_api.BlockTree is null) throw new StepDependencyException(nameof(_api.BlockTree)); if (_networkConfig.DiagTracerEnabled) { @@ -100,11 +103,11 @@ private async Task Initialize(CancellationToken cancellationToken) CanonicalHashTrie cht = new CanonicalHashTrie(_api.DbProvider!.ChtDb); - ProgressTracker progressTracker = new(_api.BlockTree!, _api.DbProvider.StateDb, _api.LogManager, _syncConfig.SnapSyncAccountRangePartitionCount); + ProgressTracker progressTracker = new(_api.BlockTree, _api.DbProvider.StateDb, _api.LogManager, _syncConfig.SnapSyncAccountRangePartitionCount); _api.SnapProvider = new SnapProvider(progressTracker, _api.DbProvider, _api.LogManager); SyncProgressResolver syncProgressResolver = new( - _api.BlockTree!, + _api.BlockTree, _api.ReceiptStorage!, _api.DbProvider.StateDb, _api.ReadOnlyTrieStore!, @@ -118,12 +121,22 @@ private async Task Initialize(CancellationToken cancellationToken) int maxPeersCount = _networkConfig.ActivePeersMaxCount; int maxPriorityPeersCount = _networkConfig.PriorityPeersMaxCount; Network.Metrics.PeerLimit = maxPeersCount; - SyncPeerPool apiSyncPeerPool = new(_api.BlockTree!, _api.NodeStatsManager!, _api.BetterPeerStrategy, _api.LogManager, maxPeersCount, maxPriorityPeersCount); + SyncPeerPool apiSyncPeerPool = new(_api.BlockTree, _api.NodeStatsManager!, _api.BetterPeerStrategy, _api.LogManager, maxPeersCount, maxPriorityPeersCount); _api.SyncPeerPool = apiSyncPeerPool; _api.PeerDifficultyRefreshPool = apiSyncPeerPool; _api.DisposeStack.Push(_api.SyncPeerPool); + if (_api.TrieStore is HealingTrieStore healingTrieStore) + { + healingTrieStore.InitializeNetwork(new GetNodeDataTrieNodeRecovery(apiSyncPeerPool, _api.LogManager)); + } + + if (_api.WorldState is HealingWorldState healingWorldState) + { + healingWorldState.InitializeNetwork(new SnapTrieNodeRecovery(apiSyncPeerPool, _api.LogManager)); + } + IEnumerable synchronizationPlugins = _api.GetSynchronizationPlugins(); foreach (ISynchronizationPlugin plugin in synchronizationPlugins) { @@ -133,7 +146,7 @@ private async Task Initialize(CancellationToken cancellationToken) _api.SyncModeSelector ??= CreateMultiSyncModeSelector(syncProgressResolver); _api.TxGossipPolicy.Policies.Add(new SyncedTxGossipPolicy(_api.SyncModeSelector)); - _api.EthSyncingInfo = new EthSyncingInfo(_api.BlockTree!, _api.ReceiptStorage!, _syncConfig, _api.SyncModeSelector, _api.LogManager); + _api.EthSyncingInfo = new EthSyncingInfo(_api.BlockTree, _api.ReceiptStorage!, _syncConfig, _api.SyncModeSelector, _api.LogManager); _api.DisposeStack.Push(_api.SyncModeSelector); _api.Pivot ??= new Pivot(_syncConfig); @@ -144,7 +157,7 @@ private async Task Initialize(CancellationToken cancellationToken) _api.BlockDownloaderFactory ??= new BlockDownloaderFactory( _api.SpecProvider!, - _api.BlockTree!, + _api.BlockTree, _api.ReceiptStorage!, _api.BlockValidator!, _api.SealValidator!, @@ -155,7 +168,7 @@ private async Task Initialize(CancellationToken cancellationToken) _api.Synchronizer ??= new Synchronizer( _api.DbProvider, _api.SpecProvider!, - _api.BlockTree!, + _api.BlockTree, _api.ReceiptStorage!, _api.SyncPeerPool, _api.NodeStatsManager!, @@ -171,9 +184,9 @@ private async Task Initialize(CancellationToken cancellationToken) _api.DisposeStack.Push(_api.Synchronizer); ISyncServer syncServer = _api.SyncServer = new SyncServer( - _api.TrieStore!, + _api.TrieStore!.AsKeyValueStore(), _api.DbProvider.CodeDb, - _api.BlockTree!, + _api.BlockTree, _api.ReceiptStorage!, _api.BlockValidator!, _api.SealValidator!, @@ -214,8 +227,9 @@ await InitPeer().ContinueWith(initPeerTask => } else if (_logger.IsDebug) _logger.Debug("Skipped enabling eth67 & eth68 capabilities"); - if (_syncConfig.SnapSync && !stateSyncFinished) + if (_syncConfig.SnapSync) { + // TODO: Should we keep snap capability even after finishing sync? SnapCapabilitySwitcher snapCapabilitySwitcher = new(_api.ProtocolsManager, _api.SyncModeSelector, _api.LogManager); snapCapabilitySwitcher.EnableSnapCapabilityUntilSynced(); } @@ -513,7 +527,7 @@ private async Task InitPeer() ISyncServer syncServer = _api.SyncServer!; ForkInfo forkInfo = new(_api.SpecProvider!, syncServer.Genesis.Hash!); - ProtocolValidator protocolValidator = new(_api.NodeStatsManager!, _api.BlockTree!, forkInfo, _api.LogManager); + ProtocolValidator protocolValidator = new(_api.NodeStatsManager!, _api.BlockTree, forkInfo, _api.LogManager); PooledTxsRequestor pooledTxsRequestor = new(_api.TxPool!); _api.ProtocolsManager = new ProtocolsManager( _api.SyncPeerPool!, diff --git a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Snap/SnapProtocolHandler.cs b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Snap/SnapProtocolHandler.cs index 8abf8e3d0b1..5fb01d69015 100644 --- a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Snap/SnapProtocolHandler.cs +++ b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Snap/SnapProtocolHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Nethermind.Blockchain.Synchronization; using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; using Nethermind.Logging; using Nethermind.Network.Contract.P2P; using Nethermind.Network.P2P.EventArg; @@ -17,6 +18,7 @@ using Nethermind.State.Snap; using Nethermind.Stats; using Nethermind.Stats.Model; +using Nethermind.Trie; namespace Nethermind.Network.P2P.Subprotocols.Snap { diff --git a/src/Nethermind/Nethermind.Network/SnapCapabilitySwitcher.cs b/src/Nethermind/Nethermind.Network/SnapCapabilitySwitcher.cs index f4053b45c75..579ec10b578 100644 --- a/src/Nethermind/Nethermind.Network/SnapCapabilitySwitcher.cs +++ b/src/Nethermind/Nethermind.Network/SnapCapabilitySwitcher.cs @@ -23,7 +23,7 @@ public SnapCapabilitySwitcher(IProtocolsManager? protocolsManager, ISyncModeSele { _protocolsManager = protocolsManager ?? throw new ArgumentNullException(nameof(protocolsManager)); _syncModeSelector = syncModeSelector ?? throw new ArgumentNullException(nameof(syncModeSelector)); - _logger = logManager.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); + _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); } /// diff --git a/src/Nethermind/Nethermind.State/IStorageTreeFactory.cs b/src/Nethermind/Nethermind.State/IStorageTreeFactory.cs new file mode 100644 index 00000000000..7a78cc6e4a6 --- /dev/null +++ b/src/Nethermind/Nethermind.State/IStorageTreeFactory.cs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Trie.Pruning; + +namespace Nethermind.State; + +public interface IStorageTreeFactory +{ + StorageTree Create(Address address, ITrieStore trieStore, Keccak storageRoot, Keccak stateRoot, ILogManager? logManager); +} diff --git a/src/Nethermind/Nethermind.State/PersistentStorageProvider.cs b/src/Nethermind/Nethermind.State/PersistentStorageProvider.cs index 0f496cf0a13..31801e41096 100644 --- a/src/Nethermind/Nethermind.State/PersistentStorageProvider.cs +++ b/src/Nethermind/Nethermind.State/PersistentStorageProvider.cs @@ -23,21 +23,27 @@ internal class PersistentStorageProvider : PartialStorageProviderBase private readonly ITrieStore _trieStore; private readonly StateProvider _stateProvider; private readonly ILogManager? _logManager; + internal readonly IStorageTreeFactory _storageTreeFactory; private readonly ResettableDictionary _storages = new(); + /// /// EIP-1283 /// private readonly ResettableDictionary _originalValues = new(); + private readonly ResettableHashSet _committedThisRound = new(); - public PersistentStorageProvider(ITrieStore? trieStore, StateProvider? stateProvider, ILogManager? logManager) + public PersistentStorageProvider(ITrieStore? trieStore, StateProvider? stateProvider, ILogManager? logManager, IStorageTreeFactory? storageTreeFactory = null) : base(logManager) { _trieStore = trieStore ?? throw new ArgumentNullException(nameof(trieStore)); _stateProvider = stateProvider ?? throw new ArgumentNullException(nameof(stateProvider)); _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); + _storageTreeFactory = storageTreeFactory ?? new StorageTreeFactory(); } + public Keccak StateRoot { get; set; } = null!; + /// /// Reset the storage state /// @@ -220,7 +226,7 @@ private StorageTree GetOrCreateStorage(Address address) { if (!_storages.ContainsKey(address)) { - StorageTree storageTree = new(_trieStore, _stateProvider.GetStorageRoot(address), _logManager); + StorageTree storageTree = _storageTreeFactory.Create(address, _trieStore, _stateProvider.GetStorageRoot(address), StateRoot, _logManager); return _storages[address] = storageTree; } @@ -281,5 +287,11 @@ public override void ClearStorage(Address address) // TODO: how does it work with pruning? _storages[address] = new StorageTree(_trieStore, Keccak.EmptyTreeHash, _logManager); } + + private class StorageTreeFactory : IStorageTreeFactory + { + public StorageTree Create(Address address, ITrieStore trieStore, Keccak storageRoot, Keccak stateRoot, ILogManager? logManager) + => new(trieStore, storageRoot, logManager); + } } } diff --git a/src/Nethermind/Nethermind.State/StateProvider.cs b/src/Nethermind/Nethermind.State/StateProvider.cs index b4e20f219ea..e5aaea60b06 100644 --- a/src/Nethermind/Nethermind.State/StateProvider.cs +++ b/src/Nethermind/Nethermind.State/StateProvider.cs @@ -39,11 +39,11 @@ internal class StateProvider private Change?[] _changes = new Change?[StartCapacity]; private int _currentPosition = Resettable.EmptyPosition; - public StateProvider(ITrieStore? trieStore, IKeyValueStore? codeDb, ILogManager? logManager) + public StateProvider(ITrieStore? trieStore, IKeyValueStore? codeDb, ILogManager? logManager, StateTree? stateTree = null) { _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); _codeDb = codeDb ?? throw new ArgumentNullException(nameof(codeDb)); - _tree = new StateTree(trieStore, logManager); + _tree = stateTree ?? new StateTree(trieStore, logManager); } public void Accept(ITreeVisitor? visitor, Keccak? stateRoot, VisitingOptions? visitingOptions = null) @@ -76,7 +76,7 @@ public Keccak StateRoot set => _tree.RootHash = value; } - private readonly StateTree _tree; + internal readonly StateTree _tree; public bool IsContract(Address address) { diff --git a/src/Nethermind/Nethermind.State/StateTree.cs b/src/Nethermind/Nethermind.State/StateTree.cs index 225a97d1b59..5a666b68946 100644 --- a/src/Nethermind/Nethermind.State/StateTree.cs +++ b/src/Nethermind/Nethermind.State/StateTree.cs @@ -36,24 +36,14 @@ public StateTree(ITrieStore? store, ILogManager? logManager) public Account? Get(Address address, Keccak? rootHash = null) { byte[]? bytes = Get(ValueKeccak.Compute(address.Bytes).BytesAsSpan, rootHash); - if (bytes is null) - { - return null; - } - - return _decoder.Decode(bytes.AsRlpStream()); + return bytes is null ? null : _decoder.Decode(bytes.AsRlpStream()); } [DebuggerStepThrough] internal Account? Get(Keccak keccak) // for testing { byte[]? bytes = Get(keccak.Bytes); - if (bytes is null) - { - return null; - } - - return _decoder.Decode(bytes.AsRlpStream()); + return bytes is null ? null : _decoder.Decode(bytes.AsRlpStream()); } public void Set(Address address, Account? account) diff --git a/src/Nethermind/Nethermind.State/WorldState.cs b/src/Nethermind/Nethermind.State/WorldState.cs index da0db388b5d..f1590558cc0 100644 --- a/src/Nethermind/Nethermind.State/WorldState.cs +++ b/src/Nethermind/Nethermind.State/WorldState.cs @@ -18,19 +18,24 @@ [assembly: InternalsVisibleTo("Nethermind.Benchmark")] [assembly: InternalsVisibleTo("Nethermind.Blockchain.Test")] [assembly: InternalsVisibleTo("Nethermind.Synchronization.Test")] +[assembly: InternalsVisibleTo("Nethermind.Synchronization")] namespace Nethermind.State { public class WorldState : IWorldState { - private readonly StateProvider _stateProvider; - private readonly PersistentStorageProvider _persistentStorageProvider; + internal readonly StateProvider _stateProvider; + internal readonly PersistentStorageProvider _persistentStorageProvider; private readonly TransientStorageProvider _transientStorageProvider; public Keccak StateRoot { get => _stateProvider.StateRoot; - set => _stateProvider.StateRoot = value; + set + { + _stateProvider.StateRoot = value; + _persistentStorageProvider.StateRoot = value; + } } public WorldState(ITrieStore? trieStore, IKeyValueStore? codeDb, ILogManager? logManager) @@ -39,6 +44,14 @@ public WorldState(ITrieStore? trieStore, IKeyValueStore? codeDb, ILogManager? lo _persistentStorageProvider = new PersistentStorageProvider(trieStore, _stateProvider, logManager); _transientStorageProvider = new TransientStorageProvider(logManager); } + + internal WorldState(ITrieStore? trieStore, IKeyValueStore? codeDb, ILogManager? logManager, StateTree stateTree, IStorageTreeFactory storageTreeFactory) + { + _stateProvider = new StateProvider(trieStore, codeDb, logManager, stateTree); + _persistentStorageProvider = new PersistentStorageProvider(trieStore, _stateProvider, logManager, storageTreeFactory); + _transientStorageProvider = new TransientStorageProvider(logManager); + } + public Account GetAccount(Address address) { return _stateProvider.GetAccount(address); @@ -130,6 +143,7 @@ public void CommitTree(long blockNumber) { _persistentStorageProvider.CommitTrees(blockNumber); _stateProvider.CommitTree(blockNumber); + _persistentStorageProvider.StateRoot = _stateProvider.StateRoot; } public void TouchCode(Keccak codeHash) diff --git a/src/Nethermind/Nethermind.Synchronization.Test/OldStyleFullSynchronizerTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/OldStyleFullSynchronizerTests.cs index 06a84baba15..e89755acb24 100644 --- a/src/Nethermind/Nethermind.Synchronization.Test/OldStyleFullSynchronizerTests.cs +++ b/src/Nethermind/Nethermind.Synchronization.Test/OldStyleFullSynchronizerTests.cs @@ -94,7 +94,7 @@ public async Task Setup() syncReport, LimboLogs.Instance); _syncServer = new SyncServer( - trieStore, + trieStore.AsKeyValueStore(), _codeDb, _blockTree, _receiptStorage, diff --git a/src/Nethermind/Nethermind.Synchronization.Test/SyncServerTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/SyncServerTests.cs index 4b6f51798cd..bfabaefef7f 100644 --- a/src/Nethermind/Nethermind.Synchronization.Test/SyncServerTests.cs +++ b/src/Nethermind/Nethermind.Synchronization.Test/SyncServerTests.cs @@ -583,7 +583,7 @@ public void GetNodeData_returns_cached_trie_nodes() MemDb stateDb = new(); TrieStore trieStore = new(stateDb, Prune.WhenCacheReaches(10.MB()), NoPersistence.Instance, LimboLogs.Instance); ctx.SyncServer = new SyncServer( - trieStore, + trieStore.AsKeyValueStore(), new MemDb(), localBlockTree, NullReceiptStorage.Instance, diff --git a/src/Nethermind/Nethermind.Synchronization.Test/SyncThreadTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/SyncThreadTests.cs index a3ca85cfec9..97563a2745c 100644 --- a/src/Nethermind/Nethermind.Synchronization.Test/SyncThreadTests.cs +++ b/src/Nethermind/Nethermind.Synchronization.Test/SyncThreadTests.cs @@ -376,7 +376,7 @@ private SyncTestContext CreateSyncManager(int index) syncReport, logManager); SyncServer syncServer = new( - trieStore, + trieStore.AsKeyValueStore(), codeDb, tree, receiptStorage, diff --git a/src/Nethermind/Nethermind.Synchronization.Test/SynchronizerTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/SynchronizerTests.cs index b39eee5d4d9..973b0109bdd 100644 --- a/src/Nethermind/Nethermind.Synchronization.Test/SynchronizerTests.cs +++ b/src/Nethermind/Nethermind.Synchronization.Test/SynchronizerTests.cs @@ -413,7 +413,7 @@ ISyncConfig GetSyncConfig() => } SyncServer = new SyncServer( - trieStore, + trieStore.AsKeyValueStore(), codeDb, BlockTree, NullReceiptStorage.Instance, diff --git a/src/Nethermind/Nethermind.Synchronization.Test/Trie/HealingTreeTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/Trie/HealingTreeTests.cs new file mode 100644 index 00000000000..2f8ec918721 --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization.Test/Trie/HealingTreeTests.cs @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test; +using Nethermind.Core.Test.Builders; +using Nethermind.Logging; +using Nethermind.State.Snap; +using Nethermind.Synchronization.Trie; +using Nethermind.Trie; +using Nethermind.Trie.Pruning; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Synchronization.Test.Trie; + +[Parallelizable(ParallelScope.Fixtures)] +public class HealingTreeTests +{ + private static readonly byte[] _rlp = { 3, 4 }; + private static readonly Keccak _key = Keccak.Compute(_rlp); + + [Test] + public void get_state_tree_works() + { + HealingStateTree stateTree = new(Substitute.For(), LimboLogs.Instance); + stateTree.Get(stackalloc byte[] { 1, 2, 3 }); + } + + [Test] + public void get_storage_tree_works() + { + HealingStorageTree stateTree = new(Substitute.For(), Keccak.EmptyTreeHash, LimboLogs.Instance, TestItem.AddressA, TestItem.KeccakA, null); + stateTree.Get(stackalloc byte[] { 1, 2, 3 }); + } + + [Test] + public void recovery_works_state_trie([Values(true, false)] bool isMainThread, [Values(true, false)] bool successfullyRecovered) + { + HealingStateTree CreateHealingStateTree(ITrieStore trieStore, ITrieNodeRecovery recovery) + { + HealingStateTree stateTree = new(trieStore, LimboLogs.Instance); + stateTree.InitializeNetwork(recovery); + return stateTree; + } + + byte[] path = { 1, 2 }; + recovery_works(isMainThread, successfullyRecovered, path, CreateHealingStateTree, r => PathMatch(r, path, 0)); + } + + private static bool PathMatch(GetTrieNodesRequest r, byte[] path, int lastPathIndex) => + r.RootHash == _key + && r.AccountAndStoragePaths.Length == 1 + && r.AccountAndStoragePaths[0].Group.Length == lastPathIndex + 1 + && Bytes.AreEqual(r.AccountAndStoragePaths[0].Group[lastPathIndex], Nibbles.EncodePath(path)); + + [Test] + public void recovery_works_storage_trie([Values(true, false)] bool isMainThread, [Values(true, false)] bool successfullyRecovered) + { + HealingStorageTree CreateHealingStorageTree(ITrieStore trieStore, ITrieNodeRecovery recovery) => + new(trieStore, Keccak.EmptyTreeHash, LimboLogs.Instance, TestItem.AddressA, _key, recovery); + byte[] path = { 1, 2 }; + byte[] addressPath = ValueKeccak.Compute(TestItem.AddressA.Bytes).Bytes.ToArray(); + recovery_works(isMainThread, successfullyRecovered, path, CreateHealingStorageTree, + r => PathMatch(r, path, 1) && Bytes.AreEqual(r.AccountAndStoragePaths[0].Group[0], addressPath)); + } + + private void recovery_works( + bool isMainThread, + bool successfullyRecovered, + byte[] path, + Func, T> createTrie, + Expression> requestMatch) + where T : PatriciaTree + { + ITrieStore trieStore = Substitute.For(); + trieStore.FindCachedOrUnknown(_key).Returns( + k => throw new MissingTrieNodeException("", new TrieNodeException("", _key), path, 1), + k => new TrieNode(NodeType.Leaf) { Key = path }); + TestMemDb db = new(); + trieStore.AsKeyValueStore().Returns(db); + + ITrieNodeRecovery recovery = Substitute.For>(); + recovery.CanRecover.Returns(isMainThread); + recovery.Recover(_key, Arg.Is(requestMatch)).Returns(successfullyRecovered ? Task.FromResult(_rlp) : Task.FromResult(null)); + + T trie = createTrie(trieStore, recovery); + + Action action = () => trie.Get(stackalloc byte[] { 1, 2, 3 }, _key); + if (isMainThread && successfullyRecovered) + { + action.Should().NotThrow(); + db.KeyWasWritten(kvp => Bytes.AreEqual(kvp.Item1, ValueKeccak.Compute(_rlp).Bytes) && Bytes.AreEqual(kvp.Item2, _rlp)); + } + else + { + action.Should().Throw(); + } + } +} diff --git a/src/Nethermind/Nethermind.Synchronization.Test/Trie/HealingTrieStoreTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/Trie/HealingTrieStoreTests.cs new file mode 100644 index 00000000000..da0c7fb693b --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization.Test/Trie/HealingTrieStoreTests.cs @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test; +using Nethermind.Core.Test.Builders; +using Nethermind.Logging; +using Nethermind.Synchronization.Trie; +using Nethermind.Trie; +using Nethermind.Trie.Pruning; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Synchronization.Test.Trie; + +public class HealingTrieStoreTests +{ + [Test] + public void get_works() + { + TestMemDb db = new(); + db[TestItem.KeccakA.Bytes] = new byte[] { 1, 2 }; + HealingTrieStore healingTrieStore = new(db, Nethermind.Trie.Pruning.No.Pruning, Persist.EveryBlock, LimboLogs.Instance); + healingTrieStore.LoadRlp(TestItem.KeccakA, ReadFlags.None); + } + + [Test] + public void recovery_works([Values(true, false)] bool isMainThread, [Values(true, false)] bool successfullyRecovered) + { + byte[] rlp = { 1, 2 }; + Keccak key = TestItem.KeccakA; + TestMemDb db = new(); + HealingTrieStore healingTrieStore = new(db, Nethermind.Trie.Pruning.No.Pruning, Persist.EveryBlock, LimboLogs.Instance); + ITrieNodeRecovery> recovery = Substitute.For>>(); + recovery.CanRecover.Returns(isMainThread); + recovery.Recover(key, Arg.Is>(l => l.SequenceEqual(new[] { key }))) + .Returns(successfullyRecovered ? Task.FromResult(rlp) : Task.FromResult(null)); + + healingTrieStore.InitializeNetwork(recovery); + Action action = () => healingTrieStore.LoadRlp(key, ReadFlags.None); + if (isMainThread && successfullyRecovered) + { + action.Should().NotThrow(); + db.KeyWasWritten(kvp => Bytes.AreEqual(kvp.Item1, key.Bytes) && Bytes.AreEqual(kvp.Item2, rlp)); + } + else + { + action.Should().Throw(); + } + } +} diff --git a/src/Nethermind/Nethermind.Synchronization.Test/Trie/RecoveryTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/Trie/RecoveryTests.cs new file mode 100644 index 00000000000..d5fb2393feb --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization.Test/Trie/RecoveryTests.cs @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Blockchain.Synchronization; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Logging; +using Nethermind.Network.Contract.P2P; +using Nethermind.State.Snap; +using Nethermind.Synchronization.Peers; +using Nethermind.Synchronization.Trie; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Synchronization.Test.Trie; + +public class RecoveryTests +{ + private byte[] _keyRlp = null!; + private byte[] _returnedRlp = null!; + private Keccak _key = null!; + private ISyncPeer _syncPeerEth66 = null!; + private PeerInfo _peerEth66 = null!; + private ISyncPeer _syncPeerEth67 = null!; + private PeerInfo _peerEth67 = null!; + private ISnapSyncPeer _snapSyncPeer = null!; + private GetTrieNodesRequest _snapRequest = null!; + private ISyncPeerPool _syncPeerPool = null!; + private SnapTrieNodeRecovery _snapRecovery = null!; + private GetNodeDataTrieNodeRecovery _nodeDataRecovery = null!; + + [SetUp] + public void SetUp() + { + _returnedRlp = _keyRlp = new byte[] { 1, 2, 3 }; + _key = Keccak.Compute(_keyRlp); + _syncPeerEth66 = Substitute.For(); + _syncPeerEth66.ProtocolVersion.Returns(EthVersions.Eth66); + _syncPeerEth66.GetNodeData(Arg.Is>(l => l.Contains(_key)), Arg.Any()) + .Returns(_ => Task.FromResult(new[] { _returnedRlp })); + _peerEth66 = new(_syncPeerEth66); + + _snapSyncPeer = Substitute.For(); + _snapSyncPeer.GetTrieNodes(Arg.Any(), Arg.Any()) + .Returns(c => Task.FromResult(new[] { _returnedRlp })); + _syncPeerEth67 = Substitute.For(); + _syncPeerEth67.ProtocolVersion.Returns(EthVersions.Eth67); + _syncPeerEth67.TryGetSatelliteProtocol(Protocol.Snap, out Arg.Any()) + .Returns(c => + { + c[1] = _snapSyncPeer; + return true; + }); + _peerEth67 = new(_syncPeerEth67); + + _snapRequest = new GetTrieNodesRequest { AccountAndStoragePaths = Array.Empty() }; + _syncPeerPool = Substitute.For(); + _snapRecovery = new SnapTrieNodeRecovery(_syncPeerPool, LimboLogs.Instance); + _nodeDataRecovery = new GetNodeDataTrieNodeRecovery(_syncPeerPool, LimboLogs.Instance); + } + + [Test] + public async Task can_recover_eth66() + { + byte[]? rlp = await Recover(_nodeDataRecovery, new List { _key }, _peerEth66); + rlp.Should().BeEquivalentTo(_keyRlp); + } + + [Test] + public async Task cannot_recover_eth66_no_peers() + { + byte[]? rlp = await Recover(_nodeDataRecovery, new List { _key }, _peerEth67); + rlp.Should().BeNull(); + } + + [Test] + public async Task cannot_recover_eth66_empty_response() + { + _syncPeerEth66.GetNodeData(Arg.Is>(l => l.Contains(_key)), Arg.Any()) + .Returns(Task.FromResult(Array.Empty())); + byte[]? rlp = await Recover(_nodeDataRecovery, new List { _key }, _peerEth66); + rlp.Should().BeNull(); + } + + [Test] + public async Task cannot_recover_eth66_invalid_rlp() + { + _returnedRlp = new byte[] { 5, 6, 7 }; + byte[]? rlp = await Recover(_nodeDataRecovery, new List { _key }, _peerEth66); + rlp.Should().BeNull(); + } + + [Test] + public async Task can_recover_eth67() + { + byte[]? rlp = await Recover(_snapRecovery, _snapRequest, _peerEth67); + rlp.Should().BeEquivalentTo(_keyRlp); + } + + [Test] + public async Task cannot_recover_eth67_no_peers() + { + byte[]? rlp = await Recover(_snapRecovery, _snapRequest, _peerEth66); + rlp.Should().BeNull(); + } + + [Test] + public async Task cannot_recover_eth67_empty_response() + { + _snapSyncPeer.GetTrieNodes(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(Array.Empty())); + byte[]? rlp = await Recover(_snapRecovery, _snapRequest, _peerEth67); + rlp.Should().BeNull(); + } + + private Task Recover(T recovery, TRequest request, params PeerInfo[] peers) where T : ITrieNodeRecovery + { + _syncPeerPool.InitializedPeers.Returns(peers); + return recovery.Recover(_key, request); + } +} diff --git a/src/Nethermind/Nethermind.Synchronization/ParallelSync/SyncDispatcher.cs b/src/Nethermind/Nethermind.Synchronization/ParallelSync/SyncDispatcher.cs index d20edf05dca..40cca61b6ce 100644 --- a/src/Nethermind/Nethermind.Synchronization/ParallelSync/SyncDispatcher.cs +++ b/src/Nethermind/Nethermind.Synchronization/ParallelSync/SyncDispatcher.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Nethermind.Core.Exceptions; +using Nethermind.Core.Extensions; using Nethermind.Logging; using Nethermind.Synchronization.Peers; @@ -69,14 +70,14 @@ public async Task Start(CancellationToken cancellationToken) if (currentStateLocal == SyncFeedState.Dormant) { - if (Logger.IsDebug) Logger.Debug($"{GetType().Name} is going to sleep."); + if (Logger.IsDebug) Logger.Debug($"{GetType().NameWithGenerics()} is going to sleep."); if (dormantTaskLocal is null) { if (Logger.IsWarn) Logger.Warn("Dormant task is NULL when trying to await it"); } await (dormantTaskLocal?.Task ?? Task.CompletedTask).WaitAsync(cancellationToken); - if (Logger.IsDebug) Logger.Debug($"{GetType().Name} got activated."); + if (Logger.IsDebug) Logger.Debug($"{GetType().NameWithGenerics()} got activated."); } else if (currentStateLocal == SyncFeedState.Active) { @@ -86,7 +87,7 @@ public async Task Start(CancellationToken cancellationToken) { if (!Feed.IsMultiFeed) { - if (Logger.IsTrace) Logger.Trace($"{Feed.GetType().Name} enqueued a null request."); + if (Logger.IsTrace) Logger.Trace($"{Feed.GetType().NameWithGenerics()} enqueued a null request."); } await Task.Delay(10, cancellationToken); @@ -106,20 +107,20 @@ public async Task Start(CancellationToken cancellationToken) if (!Feed.IsMultiFeed) { - if (Logger.IsDebug) Logger.Debug($"Awaiting single dispatch from {Feed.GetType().Name} with allocated {allocatedPeer}"); + if (Logger.IsDebug) Logger.Debug($"Awaiting single dispatch from {Feed.GetType().NameWithGenerics()} with allocated {allocatedPeer}"); await task; - if (Logger.IsDebug) Logger.Debug($"Single dispatch from {Feed.GetType().Name} with allocated {allocatedPeer} has been processed"); + if (Logger.IsDebug) Logger.Debug($"Single dispatch from {Feed.GetType().NameWithGenerics()} with allocated {allocatedPeer} has been processed"); } } else { - Logger.Debug($"DISPATCHER - {this.GetType().Name}: peer NOT allocated"); + Logger.Debug($"DISPATCHER - {GetType().NameWithGenerics()}: peer NOT allocated"); DoHandleResponse(request); } } else if (currentStateLocal == SyncFeedState.Finished) { - if (Logger.IsInfo) Logger.Info($"{GetType().Name} has finished work."); + if (Logger.IsInfo) Logger.Info($"{GetType().NameWithGenerics()} has finished work."); break; } } diff --git a/src/Nethermind/Nethermind.Synchronization/Peers/SyncPeerAllocation.cs b/src/Nethermind/Nethermind.Synchronization/Peers/SyncPeerAllocation.cs index 3b26c14b6e0..4cf37a1b0e9 100644 --- a/src/Nethermind/Nethermind.Synchronization/Peers/SyncPeerAllocation.cs +++ b/src/Nethermind/Nethermind.Synchronization/Peers/SyncPeerAllocation.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Nethermind.Blockchain; using Nethermind.Stats; using Nethermind.Synchronization.Peers.AllocationStrategies; @@ -16,10 +17,12 @@ public class SyncPeerAllocation /// /// this should be used whenever we change IsAllocated property on PeerInfo- /// - private static object _allocationLock = new(); + private static readonly object _allocationLock = new(); - private IPeerAllocationStrategy _peerAllocationStrategy; + private readonly IPeerAllocationStrategy _peerAllocationStrategy; public AllocationContexts Contexts { get; } + + [MemberNotNullWhen(true, nameof(HasPeer))] public PeerInfo? Current { get; private set; } public bool HasPeer => Current is not null; diff --git a/src/Nethermind/Nethermind.Synchronization/Peers/SyncPeerPool.cs b/src/Nethermind/Nethermind.Synchronization/Peers/SyncPeerPool.cs index 672d38778e7..e9565c660a1 100644 --- a/src/Nethermind/Nethermind.Synchronization/Peers/SyncPeerPool.cs +++ b/src/Nethermind/Nethermind.Synchronization/Peers/SyncPeerPool.cs @@ -325,18 +325,9 @@ public async Task Allocate(IPeerAllocationStrategy peerAlloc SyncPeerAllocation allocation = new(peerAllocationStrategy, allocationContexts); while (true) { - lock (_isAllocatedChecks) + if (TryAllocateOnce(peerAllocationStrategy, allocationContexts, allocation)) { - allocation.AllocateBestPeer(InitializedPeers.Where(p => p.CanBeAllocated(allocationContexts)), _stats, _blockTree); - if (allocation.HasPeer) - { - if (peerAllocationStrategy.CanBeReplaced) - { - _replaceableAllocations.TryAdd(allocation, null); - } - - return allocation; - } + return allocation; } bool timeoutReached = timeoutMilliseconds == 0 @@ -356,6 +347,25 @@ public async Task Allocate(IPeerAllocationStrategy peerAlloc } } + private bool TryAllocateOnce(IPeerAllocationStrategy peerAllocationStrategy, AllocationContexts allocationContexts, SyncPeerAllocation allocation) + { + lock (_isAllocatedChecks) + { + allocation.AllocateBestPeer(InitializedPeers.Where(p => p.CanBeAllocated(allocationContexts)), _stats, _blockTree); + if (allocation.HasPeer) + { + if (peerAllocationStrategy.CanBeReplaced) + { + _replaceableAllocations.TryAdd(allocation, null); + } + + return true; + } + } + + return false; + } + /// /// Frees the allocation space borrowed earlier for some sync consumer. /// diff --git a/src/Nethermind/Nethermind.Synchronization/SnapSync/SnapProvider.cs b/src/Nethermind/Nethermind.Synchronization/SnapSync/SnapProvider.cs index 0eb7021a69b..aa3e4020339 100644 --- a/src/Nethermind/Nethermind.Synchronization/SnapSync/SnapProvider.cs +++ b/src/Nethermind/Nethermind.Synchronization/SnapSync/SnapProvider.cs @@ -336,7 +336,7 @@ public ITrieStore Create() { return new TrieStore( _stateDb, - Trie.Pruning.No.Pruning, + Nethermind.Trie.Pruning.No.Pruning, Persist.EveryBlock, _logManager); } diff --git a/src/Nethermind/Nethermind.Synchronization/StateSync/PeerInfoExtensions.cs b/src/Nethermind/Nethermind.Synchronization/StateSync/PeerInfoExtensions.cs index 88f16d24532..e9ab2b1bb25 100644 --- a/src/Nethermind/Nethermind.Synchronization/StateSync/PeerInfoExtensions.cs +++ b/src/Nethermind/Nethermind.Synchronization/StateSync/PeerInfoExtensions.cs @@ -1,20 +1,21 @@ // SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Blockchain.Synchronization; using Nethermind.Network.Contract.P2P; +using Nethermind.Stats.Model; using Nethermind.Synchronization.Peers; namespace Nethermind.Synchronization.StateSync; public static class PeerInfoExtensions { - public static bool CanGetNodeData(this PeerInfo peerInfo) - { - return peerInfo.SyncPeer.ProtocolVersion < EthVersions.Eth67; - } + public static bool CanGetNodeData(this PeerInfo peerInfo) => peerInfo.SyncPeer.CanGetNodeData(); - public static bool CanGetSnapData(this PeerInfo peerInfo) - { - return peerInfo.SyncPeer.TryGetSatelliteProtocol(Protocol.Snap, out _); - } + public static bool CanGetNodeData(this ISyncPeer peer) => peer.ProtocolVersion < EthVersions.Eth67; + + public static bool CanGetSnapData(this PeerInfo peerInfo) => peerInfo.SyncPeer.CanGetSnapData(); + + public static bool CanGetSnapData(this ISyncPeer peer) => + peer.ClientType != NodeClientType.Nethermind && peer.TryGetSatelliteProtocol(Protocol.Snap, out _); } diff --git a/src/Nethermind/Nethermind.Synchronization/StateSync/StateSyncDownloader.cs b/src/Nethermind/Nethermind.Synchronization/StateSync/StateSyncDownloader.cs index 8c5f1970058..5e391d93a7a 100644 --- a/src/Nethermind/Nethermind.Synchronization/StateSync/StateSyncDownloader.cs +++ b/src/Nethermind/Nethermind.Synchronization/StateSync/StateSyncDownloader.cs @@ -111,7 +111,7 @@ private GetTrieNodesRequest GetGroupedRequest(StateSyncBatch batch) for (; accountPathIndex < accountTreePaths.Count; accountPathIndex++) { (byte[] path, StateSyncItem syncItem) accountPath = accountTreePaths[accountPathIndex]; - request.AccountAndStoragePaths[accountPathIndex] = new PathGroup() { Group = new[] { EncodePath(accountPath.path) } }; + request.AccountAndStoragePaths[accountPathIndex] = new PathGroup() { Group = new[] { Nibbles.EncodePath(accountPath.path) } }; // We validate the order of the response later and it has to be the same as RequestedNodes batch.RequestedNodes[requestedNodeIndex] = accountPath.syncItem; @@ -122,12 +122,12 @@ private GetTrieNodesRequest GetGroupedRequest(StateSyncBatch batch) foreach (var kvp in itemsGroupedByAccount) { byte[][] group = new byte[kvp.Value.Count + 1][]; - group[0] = EncodePath(kvp.Key); + group[0] = Nibbles.EncodePath(kvp.Key); for (int groupIndex = 1; groupIndex < group.Length; groupIndex++) { (byte[] path, StateSyncItem syncItem) storagePath = kvp.Value[groupIndex - 1]; - group[groupIndex] = EncodePath(storagePath.path); + group[groupIndex] = Nibbles.EncodePath(storagePath.path); // We validate the order of the response later and it has to be the same as RequestedNodes batch.RequestedNodes[requestedNodeIndex] = storagePath.syncItem; @@ -148,8 +148,6 @@ private GetTrieNodesRequest GetGroupedRequest(StateSyncBatch batch) return request; } - private static byte[] EncodePath(byte[] input) => input.Length == 64 ? Nibbles.ToBytes(input) : Nibbles.ToCompactHexEncoding(input); - /// /// Present an array of StateSyncItem[] as IReadOnlyList to avoid allocating secondary array /// Also Rent and Return cache for single item to try and avoid allocating the HashList in common case diff --git a/src/Nethermind/Nethermind.Synchronization/Trie/GetNodeDataTrieNodeRecovery.cs b/src/Nethermind/Nethermind.Synchronization/Trie/GetNodeDataTrieNodeRecovery.cs new file mode 100644 index 00000000000..8f061870c0c --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization/Trie/GetNodeDataTrieNodeRecovery.cs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Nethermind.Blockchain.Synchronization; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Synchronization.Peers; +using Nethermind.Synchronization.StateSync; + +namespace Nethermind.Synchronization.Trie; + +public class GetNodeDataTrieNodeRecovery : TrieNodeRecovery> +{ + public GetNodeDataTrieNodeRecovery(ISyncPeerPool syncPeerPool, ILogManager? logManager) : base(syncPeerPool, logManager) + { + } + + protected override string GetMissingNodes(IReadOnlyList request) => string.Join(", ", request); + + protected override bool CanAllocatePeer(ISyncPeer peer) => peer.CanGetNodeData(); + + protected override async Task RecoverRlpFromPeerBase(ValueKeccak rlpHash, ISyncPeer peer, IReadOnlyList request, CancellationTokenSource cts) + { + byte[][] rlp = await peer.GetNodeData(request, cts.Token); + if (rlp.Length == 1) + { + byte[] recoveredRlp = rlp[0]; + if (ValueKeccak.Compute(recoveredRlp) == rlpHash) + { + return recoveredRlp; + } + + if (_logger.IsDebug) _logger.Debug($"Recovered RLP from peer {peer} but the hash does not match"); + } + + return null; + } +} diff --git a/src/Nethermind/Nethermind.Synchronization/Trie/HealingStateTree.cs b/src/Nethermind/Nethermind.Synchronization/Trie/HealingStateTree.cs new file mode 100644 index 00000000000..25280848f2c --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization/Trie/HealingStateTree.cs @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Diagnostics; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.State; +using Nethermind.State.Snap; +using Nethermind.Trie; +using Nethermind.Trie.Pruning; + +namespace Nethermind.Synchronization.Trie; + +public class HealingStateTree : StateTree +{ + private ITrieNodeRecovery? _recovery; + + [DebuggerStepThrough] + public HealingStateTree(ITrieStore? store, ILogManager? logManager) + : base(store, logManager) + { + } + + public void InitializeNetwork(ITrieNodeRecovery recovery) + { + _recovery = recovery; + } + + public override byte[]? Get(ReadOnlySpan rawKey, Keccak? rootHash = null) + { + try + { + return base.Get(rawKey, rootHash); + } + catch (MissingTrieNodeException e) + { + if (Recover(e.TrieNodeException.NodeHash, e.GetPathPart(), rootHash ?? RootHash)) + { + return base.Get(rawKey, rootHash); + } + + throw; + } + } + + public override void Set(ReadOnlySpan rawKey, byte[] value) + { + try + { + base.Set(rawKey, value); + } + catch (MissingTrieNodeException e) + { + if (Recover(e.TrieNodeException.NodeHash, e.GetPathPart(), RootHash)) + { + base.Set(rawKey, value); + } + else + { + throw; + } + } + } + + private bool Recover(in ValueKeccak rlpHash, ReadOnlySpan pathPart, Keccak rootHash) + { + if (_recovery?.CanRecover == true) + { + GetTrieNodesRequest request = new() + { + RootHash = rootHash, + AccountAndStoragePaths = new[] + { + new PathGroup + { + Group = new[] { Nibbles.EncodePath(pathPart) } + } + } + }; + + byte[]? rlp = _recovery.Recover(rlpHash, request).GetAwaiter().GetResult(); + if (rlp is not null) + { + TrieStore.AsKeyValueStore().Set(rlpHash.Bytes, rlp); + return true; + } + } + + return false; + } +} diff --git a/src/Nethermind/Nethermind.Synchronization/Trie/HealingStorageTree.cs b/src/Nethermind/Nethermind.Synchronization/Trie/HealingStorageTree.cs new file mode 100644 index 00000000000..906062d8370 --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization/Trie/HealingStorageTree.cs @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Consensus.Processing; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.State; +using Nethermind.State.Snap; +using Nethermind.Trie; +using Nethermind.Trie.Pruning; + +namespace Nethermind.Synchronization.Trie; + +public class HealingStorageTree : StorageTree +{ + private readonly Address _address; + private readonly Keccak _stateRoot; + private readonly ITrieNodeRecovery? _recovery; + + public HealingStorageTree(ITrieStore? trieStore, Keccak rootHash, ILogManager? logManager, Address address, Keccak stateRoot, ITrieNodeRecovery? recovery) + : base(trieStore, rootHash, logManager) + { + _address = address; + _stateRoot = stateRoot; + _recovery = recovery; + } + + public override byte[]? Get(ReadOnlySpan rawKey, Keccak? rootHash = null) + { + try + { + return base.Get(rawKey, rootHash); + } + catch (MissingTrieNodeException e) + { + if (Recover(e.TrieNodeException.NodeHash, e.GetPathPart())) + { + return base.Get(rawKey, rootHash); + } + + throw; + } + } + + public override void Set(ReadOnlySpan rawKey, byte[] value) + { + try + { + base.Set(rawKey, value); + } + catch (MissingTrieNodeException e) + { + if (Recover(e.TrieNodeException.NodeHash, e.GetPathPart())) + { + base.Set(rawKey, value); + } + else + { + throw; + } + } + } + + private bool Recover(in ValueKeccak rlpHash, ReadOnlySpan pathPart) + { + if (_recovery?.CanRecover == true) + { + GetTrieNodesRequest request = new() + { + RootHash = _stateRoot, + AccountAndStoragePaths = new[] + { + new PathGroup + { + Group = new[] { ValueKeccak.Compute(_address.Bytes).ToByteArray(), Nibbles.EncodePath(pathPart) } + } + } + }; + + byte[]? rlp = _recovery.Recover(rlpHash, request).GetAwaiter().GetResult(); + if (rlp is not null) + { + TrieStore.AsKeyValueStore().Set(rlpHash.Bytes, rlp); + return true; + } + } + + return false; + } +} diff --git a/src/Nethermind/Nethermind.Synchronization/Trie/HealingStorageTreeFactory.cs b/src/Nethermind/Nethermind.Synchronization/Trie/HealingStorageTreeFactory.cs new file mode 100644 index 00000000000..1e6784919a6 --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization/Trie/HealingStorageTreeFactory.cs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.State; +using Nethermind.State.Snap; +using Nethermind.Synchronization.Peers; +using Nethermind.Trie.Pruning; + +namespace Nethermind.Synchronization.Trie; + +public class HealingStorageTreeFactory : IStorageTreeFactory +{ + private ITrieNodeRecovery? _recovery; + + public void InitializeNetwork(ITrieNodeRecovery recovery) + { + _recovery = recovery; + } + + public StorageTree Create(Address address, ITrieStore trieStore, Keccak storageRoot, Keccak stateRoot, ILogManager? logManager) => + new HealingStorageTree(trieStore, storageRoot, logManager, address, stateRoot, _recovery); +} diff --git a/src/Nethermind/Nethermind.Synchronization/Trie/HealingTrieStore.cs b/src/Nethermind/Nethermind.Synchronization/Trie/HealingTrieStore.cs new file mode 100644 index 00000000000..116cdff2051 --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization/Trie/HealingTrieStore.cs @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Nethermind.Core; +using Nethermind.Core.Collections; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Trie; +using Nethermind.Trie.Pruning; + +namespace Nethermind.Synchronization.Trie; + +/// +/// Trie store that can recover from network using eth63-eth66 protocol and GetNodeData. +/// +public class HealingTrieStore : TrieStore +{ + private ITrieNodeRecovery>? _recovery; + + public HealingTrieStore( + IKeyValueStoreWithBatching? keyValueStore, + IPruningStrategy? pruningStrategy, + IPersistenceStrategy? persistenceStrategy, + ILogManager? logManager) + : base(keyValueStore, pruningStrategy, persistenceStrategy, logManager) + { + } + + public void InitializeNetwork(ITrieNodeRecovery> recovery) + { + _recovery = recovery; + } + + public override byte[] LoadRlp(Keccak keccak, ReadFlags readFlags = ReadFlags.None) + { + try + { + return base.LoadRlp(keccak, readFlags); + } + catch (TrieNodeException) + { + if (TryRecover(keccak, out byte[] rlp)) + { + return rlp; + } + + throw; + } + } + + private bool TryRecover(Keccak rlpHash, [NotNullWhen(true)] out byte[]? rlp) + { + if (_recovery?.CanRecover == true) + { + using ArrayPoolList request = new(1) { rlpHash }; + rlp = _recovery.Recover(rlpHash, request).GetAwaiter().GetResult(); + if (rlp is not null) + { + _keyValueStore.Set(rlpHash.Bytes, rlp); + return true; + } + } + + rlp = null; + return false; + } +} diff --git a/src/Nethermind/Nethermind.Synchronization/Trie/HealingWorldState.cs b/src/Nethermind/Nethermind.Synchronization/Trie/HealingWorldState.cs new file mode 100644 index 00000000000..6ee9035a88f --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization/Trie/HealingWorldState.cs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Logging; +using Nethermind.State; +using Nethermind.State.Snap; +using Nethermind.Trie.Pruning; + +namespace Nethermind.Synchronization.Trie; + +public class HealingWorldState : WorldState +{ + public HealingWorldState(ITrieStore? trieStore, IKeyValueStore? codeDb, ILogManager? logManager) + : base(trieStore, codeDb, logManager, new HealingStateTree(trieStore, logManager), new HealingStorageTreeFactory()) + { + } + + public void InitializeNetwork(ITrieNodeRecovery recovery) + { + StateProviderTree.InitializeNetwork(recovery); + StorageTreeFactory.InitializeNetwork(recovery); + } + + private HealingStorageTreeFactory StorageTreeFactory => ((HealingStorageTreeFactory)_persistentStorageProvider._storageTreeFactory); + + private HealingStateTree StateProviderTree => ((HealingStateTree)_stateProvider._tree); +} diff --git a/src/Nethermind/Nethermind.Synchronization/Trie/SnapTrieNodeRecovery.cs b/src/Nethermind/Nethermind.Synchronization/Trie/SnapTrieNodeRecovery.cs new file mode 100644 index 00000000000..ad02f249666 --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization/Trie/SnapTrieNodeRecovery.cs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Nethermind.Blockchain.Synchronization; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Logging; +using Nethermind.Network.Contract.P2P; +using Nethermind.State.Snap; +using Nethermind.Synchronization.Peers; +using Nethermind.Synchronization.StateSync; + +namespace Nethermind.Synchronization.Trie; + +public class SnapTrieNodeRecovery : TrieNodeRecovery +{ + public SnapTrieNodeRecovery(ISyncPeerPool syncPeerPool, ILogManager? logManager) : base(syncPeerPool, logManager) + { + } + + protected override string GetMissingNodes(GetTrieNodesRequest request) => + string.Join("; ", request.AccountAndStoragePaths.Select(GetMissingNodes)); + + private string GetMissingNodes(PathGroup requestAccountAndStoragePaths) => + requestAccountAndStoragePaths.Group.Length switch + { + 1 => $"Account: {requestAccountAndStoragePaths.Group[0].ToHexString()}", + > 1 => $"Account: {requestAccountAndStoragePaths.Group[0].ToHexString()}, Storage: {string.Join(", ", requestAccountAndStoragePaths.Group.Skip(1).Select(g => g.ToHexString()))}", + _ => "", + }; + + protected override bool CanAllocatePeer(ISyncPeer peer) => peer.CanGetSnapData(); + + protected override async Task RecoverRlpFromPeerBase(ValueKeccak rlpHash, ISyncPeer peer, GetTrieNodesRequest request, CancellationTokenSource cts) + { + if (peer.TryGetSatelliteProtocol(Protocol.Snap, out ISnapSyncPeer? snapPeer)) + { + byte[][] rlp = await snapPeer.GetTrieNodes(request, cts.Token); + if (rlp.Length == 1 && rlp[0]?.Length > 0 && ValueKeccak.Compute(rlp[0]) == rlpHash) + { + return rlp[0]; + } + } + + return null; + } +} diff --git a/src/Nethermind/Nethermind.Synchronization/Trie/TrieNodeRecovery.cs b/src/Nethermind/Nethermind.Synchronization/Trie/TrieNodeRecovery.cs new file mode 100644 index 00000000000..4f2b6e7aa41 --- /dev/null +++ b/src/Nethermind/Nethermind.Synchronization/Trie/TrieNodeRecovery.cs @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Nethermind.Blockchain.Synchronization; +using Nethermind.Consensus.Processing; +using Nethermind.Core.Collections; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Synchronization.Peers; + +namespace Nethermind.Synchronization.Trie; + +public interface ITrieNodeRecovery +{ + bool CanRecover => BlockchainProcessor.IsMainProcessingThread; + Task Recover(ValueKeccak rlpHash, TRequest request); +} + +public abstract class TrieNodeRecovery : ITrieNodeRecovery +{ + private readonly ISyncPeerPool _syncPeerPool; + protected readonly ILogger _logger; + private const int MaxPeersForRecovery = 30; + + protected TrieNodeRecovery(ISyncPeerPool syncPeerPool, ILogManager? logManager) + { + _syncPeerPool = syncPeerPool; + _logger = logManager?.GetClassLogger>() ?? NullLogger.Instance; + } + + public async Task Recover(ValueKeccak rlpHash, TRequest request) + { + if (_logger.IsWarn) _logger.Warn($"Missing trie node {GetMissingNodes(request)}, trying to recover from network"); + using CancellationTokenSource cts = new(Timeouts.Eth); + using ArrayPoolList keyRecoveries = GenerateKeyRecoveries(rlpHash, request, cts); + return await CheckKeyRecoveriesResults(keyRecoveries, cts); + } + + protected abstract string GetMissingNodes(TRequest request); + + protected async Task CheckKeyRecoveriesResults(ArrayPoolList keyRecoveries, CancellationTokenSource cts) + { + while (keyRecoveries.Count > 0) + { + Task<(Recovery, byte[]?)> task = await Task.WhenAny(keyRecoveries.Select(kr => kr.Task!)); + (Recovery Recovery, byte[]? Data) result = await task; + if (result.Data is null) + { + if (_logger.IsDebug) _logger.Debug($"Got empty response from peer {result.Recovery.Peer}"); + keyRecoveries.Remove(result.Recovery); + } + else + { + if (_logger.IsWarn) _logger.Warn($"Successfully recovered from peer {result.Recovery.Peer} with {result.Data.Length} bytes!"); + cts.Cancel(); + return result.Data; + } + } + + if (_logger.IsWarn) _logger.Warn("Failed to recover missing trie node"); + + return null; + } + + protected ArrayPoolList GenerateKeyRecoveries(in ValueKeccak rlpHash, TRequest request, CancellationTokenSource cts) + { + ArrayPoolList keyRecoveries = AllocatePeers(); + if (_logger.IsDebug) _logger.Debug($"Allocated {keyRecoveries.Count} peers (out of {_syncPeerPool!.InitializedPeers.Count()} initialized peers)"); + foreach (Recovery keyRecovery in keyRecoveries) + { + keyRecovery.Task = RecoverRlpFromPeer(rlpHash, keyRecovery, request, cts); + } + + return keyRecoveries; + } + + private ArrayPoolList AllocatePeers() => + new(MaxPeersForRecovery, + _syncPeerPool!.InitializedPeers + .Select(p => p.SyncPeer) + .Where(CanAllocatePeer) + .OrderByDescending(p => p.HeadNumber) + .Take(MaxPeersForRecovery) + .Select(peer => new Recovery { Peer = peer }) + ); + + protected abstract bool CanAllocatePeer(ISyncPeer peer); + + private async Task<(Recovery, byte[]?)> RecoverRlpFromPeer(ValueKeccak rlpHash, Recovery recovery, TRequest request, CancellationTokenSource cts) + { + ISyncPeer peer = recovery.Peer; + + try + { + return (recovery, await RecoverRlpFromPeerBase(rlpHash, peer, request, cts)); + } + catch (OperationCanceledException) + { + if (_logger.IsTrace) _logger.Trace($"Cancelled recovering RLP from peer {peer}"); + } + catch (Exception e) + { + if (_logger.IsError) _logger.Error($"Could not recover from {peer}", e); + } + + return (recovery, null); + } + + protected abstract Task RecoverRlpFromPeerBase(ValueKeccak rlpHash, ISyncPeer peer, TRequest request, CancellationTokenSource cts); + + protected class Recovery + { + public ISyncPeer Peer { get; init; } = null!; + public Task<(Recovery, byte[]?)>? Task { get; set; } + } +} diff --git a/src/Nethermind/Nethermind.Trie.Test/TrieExceptionTests.cs b/src/Nethermind/Nethermind.Trie.Test/TrieExceptionTests.cs deleted file mode 100644 index 535c896195d..00000000000 --- a/src/Nethermind/Nethermind.Trie.Test/TrieExceptionTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using FluentAssertions; -using Nethermind.Core.Extensions; -using Nethermind.Core.Test.Builders; -using NUnit.Framework; - -namespace Nethermind.Trie.Test; - -public class TrieExceptionTests -{ - [Test] - public void When_MissingBaseExceptionIsRootHash_Then_MentionItClearly() - { - MissingNodeException? baseException = new(TestItem.KeccakA); - try - { - TrieException.ThrowOnLoadFailure(TestItem.KeccakB.Bytes, TestItem.KeccakA, baseException); - Assert.Fail("Should throw"); - } - catch (TrieException exception) - { - exception.Message.Should().Be($"Failed to load root hash {TestItem.KeccakA} while loading key {TestItem.KeccakB.Bytes.ToHexString()}."); - } - } - - [Test] - public void When_CreateOnLoadFailure_WrapMessage() - { - MissingNodeException? baseException = new(TestItem.KeccakA); - try - { - TrieException.ThrowOnLoadFailure(TestItem.KeccakB.Bytes, TestItem.KeccakC, baseException); - Assert.Fail("Should throw"); - } - catch (TrieException exception) - { - exception.Message.Should().Be($"Failed to load key {TestItem.KeccakB.Bytes.ToHexString()} from root hash {TestItem.KeccakC}."); - } - } -} diff --git a/src/Nethermind/Nethermind.Trie.Test/TrieNodeTests.cs b/src/Nethermind/Nethermind.Trie.Test/TrieNodeTests.cs index ae03126a259..cd17fd5825e 100644 --- a/src/Nethermind/Nethermind.Trie.Test/TrieNodeTests.cs +++ b/src/Nethermind/Nethermind.Trie.Test/TrieNodeTests.cs @@ -79,7 +79,7 @@ public void Forward_read_flags_on_resolve() public void Throws_trie_exception_on_unexpected_format() { TrieNode trieNode = new(NodeType.Unknown, new byte[42]); - Assert.Throws(() => trieNode.ResolveNode(NullTrieNodeResolver.Instance)); + Assert.Throws(() => trieNode.ResolveNode(NullTrieNodeResolver.Instance)); } [Test] diff --git a/src/Nethermind/Nethermind.Trie/MissingNodeException.cs b/src/Nethermind/Nethermind.Trie/MissingNodeException.cs deleted file mode 100644 index 048a1446d34..00000000000 --- a/src/Nethermind/Nethermind.Trie/MissingNodeException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; - -namespace Nethermind.Trie; - -public class MissingNodeException : TrieException -{ - public ValueKeccak NodeHash { get; private set; } - - public MissingNodeException(Keccak keccak) : base($"Node {keccak} is missing from the DB") - { - NodeHash = keccak; - } -} diff --git a/src/Nethermind/Nethermind.Trie/MissingTrieNodeException.cs b/src/Nethermind/Nethermind.Trie/MissingTrieNodeException.cs new file mode 100644 index 00000000000..f9431ed5a8c --- /dev/null +++ b/src/Nethermind/Nethermind.Trie/MissingTrieNodeException.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; + +namespace Nethermind.Trie; + +public class MissingTrieNodeException : TrieException +{ + public MissingTrieNodeException(string message, TrieNodeException inner, byte[] updatePath, int currentIndex) : base(message, inner) + { + UpdatePath = updatePath; + CurrentIndex = currentIndex; + TrieNodeException = inner; + } + + public TrieNodeException TrieNodeException { get; } + public byte[] UpdatePath { get; } + public int CurrentIndex { get; } + public ReadOnlySpan GetPathPart() => UpdatePath.AsSpan(0, CurrentIndex + 1); +} diff --git a/src/Nethermind/Nethermind.Trie/NibbleExtensions.cs b/src/Nethermind/Nethermind.Trie/NibbleExtensions.cs index c849d018e19..a760a021440 100644 --- a/src/Nethermind/Nethermind.Trie/NibbleExtensions.cs +++ b/src/Nethermind/Nethermind.Trie/NibbleExtensions.cs @@ -89,7 +89,7 @@ public static byte ToByte(Nibble highNibble, Nibble lowNibble) return (byte)(((byte)highNibble << 4) | (byte)lowNibble); } - public static byte[] ToBytes(byte[] nibbles) + public static byte[] ToBytes(ReadOnlySpan nibbles) { byte[] bytes = new byte[nibbles.Length / 2]; for (int i = 0; i < bytes.Length; i++) @@ -100,7 +100,7 @@ public static byte[] ToBytes(byte[] nibbles) return bytes; } - public static byte[] ToCompactHexEncoding(byte[] nibbles) + public static byte[] ToCompactHexEncoding(ReadOnlySpan nibbles) { int oddity = nibbles.Length % 2; byte[] bytes = new byte[nibbles.Length / 2 + 1]; @@ -116,5 +116,7 @@ public static byte[] ToCompactHexEncoding(byte[] nibbles) return bytes; } + + public static byte[] EncodePath(ReadOnlySpan input) => input.Length == 64 ? ToBytes(input) : ToCompactHexEncoding(input); } } diff --git a/src/Nethermind/Nethermind.Trie/PatriciaTree.cs b/src/Nethermind/Nethermind.Trie/PatriciaTree.cs index 38510db150b..2362a5dbd97 100644 --- a/src/Nethermind/Nethermind.Trie/PatriciaTree.cs +++ b/src/Nethermind/Nethermind.Trie/PatriciaTree.cs @@ -33,7 +33,7 @@ public class PatriciaTree /// public static readonly Keccak EmptyTreeHash = Keccak.EmptyTreeHash; - public TrieType TrieType { get; protected set; } + public TrieType TrieType { get; init; } /// /// To save allocations this used to be static but this caused one of the hardest to reproduce issues @@ -45,7 +45,7 @@ public class PatriciaTree private readonly ConcurrentQueue? _currentCommit; - public readonly ITrieStore TrieStore; + public ITrieStore TrieStore { get; } private readonly bool _parallelBranches; @@ -53,7 +53,7 @@ public class PatriciaTree private Keccak _rootHash = Keccak.EmptyTreeHash; - public TrieNode? RootRef; + public TrieNode? RootRef { get; set; } /// /// Only used in EthereumTests @@ -302,31 +302,56 @@ private void SetRootHash(Keccak? value, bool resetObjects) [SkipLocalsInit] [DebuggerStepThrough] - public byte[]? Get(Span rawKey, Keccak? rootHash = null) + public virtual byte[]? Get(ReadOnlySpan rawKey, Keccak? rootHash = null) { try { int nibblesCount = 2 * rawKey.Length; byte[] array = null; Span nibbles = (rawKey.Length <= MaxKeyStackAlloc - ? stackalloc byte[MaxKeyStackAlloc] - : array = ArrayPool.Shared.Rent(nibblesCount)) - [..nibblesCount]; // Slice to exact size; - Nibbles.BytesToNibbleBytes(rawKey, nibbles); - var result = Run(nibbles, nibblesCount, Array.Empty(), false, startRootHash: rootHash); - if (array is not null) ArrayPool.Shared.Return(array); - return result; + ? stackalloc byte[MaxKeyStackAlloc] + : array = ArrayPool.Shared.Rent(nibblesCount)) + [..nibblesCount]; // Slice to exact size; + + try + { + Nibbles.BytesToNibbleBytes(rawKey, nibbles); + return Run(nibbles, nibblesCount, Array.Empty(), false, startRootHash: rootHash); + } + finally + { + if (array is not null) ArrayPool.Shared.Return(array); + } } catch (TrieException e) { - TrieException.ThrowOnLoadFailure(rawKey, rootHash ?? RootHash, e); - return null; + EnhanceException(rawKey, rootHash ?? RootHash, e); + throw; + } + } + + private static void EnhanceException(ReadOnlySpan rawKey, ValueKeccak rootHash, TrieException baseException) + { + TrieNodeException? GetTrieNodeException(TrieException? exception) => + exception switch + { + null => null, + TrieNodeException ex => ex, + _ => GetTrieNodeException(exception.InnerException as TrieException) + }; + + TrieNodeException? trieNodeException = GetTrieNodeException(baseException); + if (trieNodeException is not null) + { + trieNodeException.EnhancedMessage = trieNodeException.NodeHash == rootHash + ? $"Failed to load root hash {rootHash} while loading key {rawKey.ToHexString()}." + : $"Failed to load key {rawKey.ToHexString()} from root hash {rootHash}."; } } [SkipLocalsInit] [DebuggerStepThrough] - public void Set(ReadOnlySpan rawKey, byte[] value) + public virtual void Set(ReadOnlySpan rawKey, byte[] value) { if (_logger.IsTrace) _logger.Trace($"{(value.Length == 0 ? $"Deleting {rawKey.ToHexString()}" : $"Setting {rawKey.ToHexString()} = {value.ToHexString()}")}"); @@ -334,12 +359,19 @@ public void Set(ReadOnlySpan rawKey, byte[] value) int nibblesCount = 2 * rawKey.Length; byte[] array = null; Span nibbles = (rawKey.Length <= MaxKeyStackAlloc - ? stackalloc byte[MaxKeyStackAlloc] // Fixed size stack allocation - : array = ArrayPool.Shared.Rent(nibblesCount)) + ? stackalloc byte[MaxKeyStackAlloc] // Fixed size stack allocation + : array = ArrayPool.Shared.Rent(nibblesCount)) [..nibblesCount]; // Slice to exact size - Nibbles.BytesToNibbleBytes(rawKey, nibbles); - Run(nibbles, nibblesCount, value, true); - if (array is not null) ArrayPool.Shared.Return(array); + + try + { + Nibbles.BytesToNibbleBytes(rawKey, nibbles); + Run(nibbles, nibblesCount, value, true); + } + finally + { + if (array is not null) ArrayPool.Shared.Return(array); + } } [DebuggerStepThrough] @@ -382,7 +414,7 @@ public void Set(ReadOnlySpan rawKey, Rlp? value) { if (_logger.IsTrace) _logger.Trace($"Starting from {startRootHash} - {traverseContext.ToString()}"); TrieNode startNode = TrieStore.FindCachedOrUnknown(startRootHash); - startNode.ResolveNode(TrieStore); + ResolveNode(startNode, in traverseContext); result = TraverseNode(startNode, in traverseContext); } else @@ -402,7 +434,7 @@ public void Set(ReadOnlySpan rawKey, Rlp? value) } else { - RootRef.ResolveNode(TrieStore); + ResolveNode(RootRef, in traverseContext); if (_logger.IsTrace) _logger.Trace($"{traverseContext.ToString()}"); result = TraverseNode(RootRef, in traverseContext); } @@ -411,6 +443,18 @@ public void Set(ReadOnlySpan rawKey, Rlp? value) return result; } + private void ResolveNode(TrieNode node, in TraverseContext traverseContext) + { + try + { + node.ResolveNode(TrieStore); + } + catch (TrieNodeException e) + { + ThrowMissingTrieNodeException(in traverseContext, e); + } + } + private byte[]? TraverseNode(TrieNode node, in TraverseContext traverseContext) { if (_logger.IsTrace) @@ -429,7 +473,7 @@ public void Set(ReadOnlySpan rawKey, Rlp? value) }; } - private void ConnectNodes(TrieNode? node) + private void ConnectNodes(TrieNode? node, in TraverseContext traverseContext) { bool isRoot = _nodeStack.Count == 0; TrieNode nextNode = node; @@ -504,7 +548,7 @@ L X - - - - - - - - - - - - - - */ "Before updating branch should have had at least two non-empty children"); } - childNode.ResolveNode(TrieStore); + ResolveNode(childNode, in traverseContext); if (childNode.IsBranch) { TrieNode extensionFromBranch = @@ -669,7 +713,7 @@ which is not possible within the Ethereum protocol which has keys of the same le return null; } - ConnectNodes(null); + ConnectNodes(null, in traverseContext); } else if (Bytes.AreEqual(traverseContext.UpdateValue, node.Value)) { @@ -678,7 +722,7 @@ which is not possible within the Ethereum protocol which has keys of the same le else { TrieNode withUpdatedValue = node.CloneWithChangedValue(traverseContext.UpdateValue); - ConnectNodes(withUpdatedValue); + ConnectNodes(withUpdatedValue, in traverseContext); } return traverseContext.UpdateValue; @@ -711,12 +755,12 @@ which is not possible within the Ethereum protocol which has keys of the same le byte[] leafPath = traverseContext.UpdatePath[ currentIndex..].ToArray(); TrieNode leaf = TrieNodeFactory.CreateLeaf(leafPath, traverseContext.UpdateValue); - ConnectNodes(leaf); + ConnectNodes(leaf, in traverseContext); return traverseContext.UpdateValue; } - childNode.ResolveNode(TrieStore); + ResolveNode(childNode, in traverseContext); TrieNode nextNode = childNode; return TraverseNext(in traverseContext, 1, nextNode); @@ -767,14 +811,14 @@ which is not possible within the Ethereum protocol which has keys of the same le if (traverseContext.IsDelete) { - ConnectNodes(null); + ConnectNodes(null, in traverseContext); return traverseContext.UpdateValue; } if (!Bytes.AreEqual(node.Value, traverseContext.UpdateValue)) { TrieNode withUpdatedValue = node.CloneWithChangedValue(traverseContext.UpdateValue); - ConnectNodes(withUpdatedValue); + ConnectNodes(withUpdatedValue, in traverseContext); return traverseContext.UpdateValue; } @@ -820,7 +864,7 @@ which is not possible within the Ethereum protocol which has keys of the same le leafPath.ToArray(), longerPathValue); _nodeStack.Push(new StackedNode(branch, longerPath[extensionLength])); - ConnectNodes(withUpdatedKeyAndValue); + ConnectNodes(withUpdatedKeyAndValue, in traverseContext); return traverseContext.UpdateValue; } @@ -849,7 +893,7 @@ which is not possible within the Ethereum protocol which has keys of the same le ThrowMissingChildException(node); } - next.ResolveNode(TrieStore); + ResolveNode(next, in traverseContext); return TraverseNext(in traverseContext, extensionLength, next); } @@ -908,7 +952,7 @@ TrieNode secondExtension branch.SetChild(pathBeforeUpdate[extensionLength], childNode); } - ConnectNodes(branch); + ConnectNodes(branch, in traverseContext); return traverseContext.UpdateValue; } @@ -1078,5 +1122,12 @@ private static void ThrowMissingPrefixException() { throw new InvalidDataException("An attempt to visit a node without a prefix path."); } + + [DoesNotReturn] + [StackTraceHidden] + private static void ThrowMissingTrieNodeException(in TraverseContext traverseContext, TrieNodeException e) + { + throw new MissingTrieNodeException(e.Message, e, traverseContext.UpdatePath.ToArray(), traverseContext.CurrentIndex); + } } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/ITrieStore.cs b/src/Nethermind/Nethermind.Trie/Pruning/ITrieStore.cs index 9f7a010b4ef..282a49ea0f4 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/ITrieStore.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/ITrieStore.cs @@ -7,17 +7,18 @@ namespace Nethermind.Trie.Pruning { - public interface ITrieStore : ITrieNodeResolver, IReadOnlyKeyValueStore, IDisposable + public interface ITrieStore : ITrieNodeResolver, IDisposable { void CommitNode(long blockNumber, NodeCommitInfo nodeCommitInfo, WriteFlags writeFlags = WriteFlags.None); void FinishBlockCommit(TrieType trieType, long blockNumber, TrieNode? root, WriteFlags writeFlags = WriteFlags.None); - bool IsPersisted(Keccak keccak); bool IsPersisted(in ValueKeccak keccak); IReadOnlyTrieStore AsReadOnly(IKeyValueStore? keyValueStore); event EventHandler? ReorgBoundaryReached; + + IKeyValueStore AsKeyValueStore(); } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/NullTrieStore.cs b/src/Nethermind/Nethermind.Trie/Pruning/NullTrieStore.cs index bafc5893e81..06a0d6f0552 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/NullTrieStore.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/NullTrieStore.cs @@ -19,10 +19,7 @@ public void FinishBlockCommit(TrieType trieType, long blockNumber, TrieNode? roo public void HackPersistOnShutdown() { } - public IReadOnlyTrieStore AsReadOnly(IKeyValueStore keyValueStore) - { - return this; - } + public IReadOnlyTrieStore AsReadOnly(IKeyValueStore keyValueStore) => this; public event EventHandler ReorgBoundaryReached { @@ -30,24 +27,16 @@ public event EventHandler ReorgBoundaryReached remove { } } - public TrieNode FindCachedOrUnknown(Keccak hash) - { - return new(NodeType.Unknown, hash); - } + public IKeyValueStore AsKeyValueStore() => null!; - public byte[] LoadRlp(Keccak hash, ReadFlags flags = ReadFlags.None) - { - return Array.Empty(); - } + public TrieNode FindCachedOrUnknown(Keccak hash) => new(NodeType.Unknown, hash); + + public byte[] LoadRlp(Keccak hash, ReadFlags flags = ReadFlags.None) => Array.Empty(); - public bool IsPersisted(Keccak keccak) => true; public bool IsPersisted(in ValueKeccak keccak) => true; public void Dispose() { } - public byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None) - { - return null; - } + public byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None) => null; } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/ReadOnlyTrieStore.cs b/src/Nethermind/Nethermind.Trie/Pruning/ReadOnlyTrieStore.cs index 0b87cf7f1e5..2898cc4caa1 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/ReadOnlyTrieStore.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/ReadOnlyTrieStore.cs @@ -15,11 +15,13 @@ public class ReadOnlyTrieStore : IReadOnlyTrieStore { private readonly TrieStore _trieStore; private readonly IKeyValueStore? _readOnlyStore; + private readonly ReadOnlyValueStore _publicStore; public ReadOnlyTrieStore(TrieStore trieStore, IKeyValueStore? readOnlyStore) { _trieStore = trieStore ?? throw new ArgumentNullException(nameof(trieStore)); _readOnlyStore = readOnlyStore; + _publicStore = new ReadOnlyValueStore(_trieStore.AsKeyValueStore()); } public TrieNode FindCachedOrUnknown(Keccak hash) => @@ -27,7 +29,6 @@ public TrieNode FindCachedOrUnknown(Keccak hash) => public byte[] LoadRlp(Keccak hash, ReadFlags flags) => _trieStore.LoadRlp(hash, _readOnlyStore, flags); - public bool IsPersisted(Keccak keccak) => _trieStore.IsPersisted(keccak); public bool IsPersisted(in ValueKeccak keccak) => _trieStore.IsPersisted(keccak); public IReadOnlyTrieStore AsReadOnly(IKeyValueStore keyValueStore) @@ -44,10 +45,25 @@ public event EventHandler ReorgBoundaryReached add { } remove { } } + + public IKeyValueStore AsKeyValueStore() => _publicStore; + public void Dispose() { } - public byte[]? this[ReadOnlySpan key] => _trieStore[key]; + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) { } + + private class ReadOnlyValueStore : IKeyValueStore + { + private readonly IKeyValueStore _keyValueStore; - public byte[]? Get(ReadOnlySpan key, ReadFlags flags) => _trieStore.Get(key, flags); + public ReadOnlyValueStore(IKeyValueStore keyValueStore) + { + _keyValueStore = keyValueStore; + } + + public byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None) => _keyValueStore.Get(key, flags); + + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) { } + } } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs b/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs index bb4d7053388..1b248d17cad 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs @@ -149,6 +149,7 @@ public TrieStore( _pruningStrategy = pruningStrategy ?? throw new ArgumentNullException(nameof(pruningStrategy)); _persistenceStrategy = persistenceStrategy ?? throw new ArgumentNullException(nameof(persistenceStrategy)); _dirtyNodes = new DirtyNodesCache(this); + _publicStore = new TrieKeyValueStore(this); } public long LastPersistedBlockNumber @@ -328,7 +329,7 @@ public byte[] LoadRlp(Keccak keccak, IKeyValueStore? keyValueStore, ReadFlags re if (rlp is null) { - throw new MissingNodeException(keccak); + throw new TrieNodeException($"Node {keccak} is missing from the DB", keccak); } Metrics.LoadedFromDbNodesCount++; @@ -336,21 +337,7 @@ public byte[] LoadRlp(Keccak keccak, IKeyValueStore? keyValueStore, ReadFlags re return rlp; } - public byte[] LoadRlp(Keccak keccak, ReadFlags readFlags = ReadFlags.None) => LoadRlp(keccak, null, readFlags); - - public bool IsPersisted(Keccak keccak) - { - byte[]? rlp = _currentBatch?[keccak.Bytes] ?? _keyValueStore[keccak.Bytes]; - - if (rlp is null) - { - return false; - } - - Metrics.LoadedFromDbNodesCount++; - - return true; - } + public virtual byte[] LoadRlp(Keccak keccak, ReadFlags readFlags = ReadFlags.None) => LoadRlp(keccak, null, readFlags); public bool IsPersisted(in ValueKeccak keccak) { @@ -569,7 +556,9 @@ public void Dispose() #region Private - private readonly IKeyValueStoreWithBatching _keyValueStore; + protected readonly IKeyValueStoreWithBatching _keyValueStore; + + private readonly TrieKeyValueStore _publicStore; private readonly IPruningStrategy _pruningStrategy; @@ -829,12 +818,7 @@ void PersistNode(TrieNode n) }); } - public byte[]? this[ReadOnlySpan key] - { - get => Get(key); - } - - public byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None) + private byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None) { return _pruningStrategy.PruningEnabled && _dirtyNodes.AllNodes.TryGetValue(new ValueKeccak(key), out TrieNode? trieNode) @@ -844,5 +828,22 @@ public byte[]? this[ReadOnlySpan key] ? trieNode.FullRlp : _currentBatch?.Get(key, flags) ?? _keyValueStore.Get(key, flags); } + + public IKeyValueStore AsKeyValueStore() => _publicStore; + + private class TrieKeyValueStore : IKeyValueStore + { + private readonly TrieStore _trieStore; + + public TrieKeyValueStore(TrieStore trieStore) + { + _trieStore = trieStore; + } + + public byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None) => _trieStore.Get(key, flags); + + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) + => _trieStore._keyValueStore.Set(key, value, flags); + } } } diff --git a/src/Nethermind/Nethermind.Trie/TrieException.cs b/src/Nethermind/Nethermind.Trie/TrieException.cs index 8a259dd47ba..bbee010f750 100644 --- a/src/Nethermind/Nethermind.Trie/TrieException.cs +++ b/src/Nethermind/Nethermind.Trie/TrieException.cs @@ -16,23 +16,8 @@ public TrieException() { } - public TrieException(string message) : base(message) + public TrieException(string message, Exception? inner = null) : base(message, inner) { } - - public TrieException(string message, Exception inner) : base(message, inner) - { - } - - [DoesNotReturn] - [StackTraceHidden] - public static void ThrowOnLoadFailure(Span rawKey, ValueKeccak rootHash, Exception baseException) - { - if (baseException is MissingNodeException nodeException && nodeException.NodeHash == rootHash) - { - throw new TrieException($"Failed to load root hash {rootHash} while loading key {rawKey.ToHexString()}.", baseException); - } - throw new TrieException($"Failed to load key {rawKey.ToHexString()} from root hash {rootHash}.", baseException); - } } } diff --git a/src/Nethermind/Nethermind.Trie/TrieNode.cs b/src/Nethermind/Nethermind.Trie/TrieNode.cs index d21dbcb1522..48a179fd082 100644 --- a/src/Nethermind/Nethermind.Trie/TrieNode.cs +++ b/src/Nethermind/Nethermind.Trie/TrieNode.cs @@ -300,12 +300,12 @@ public void ResolveNode(ITrieNodeResolver tree, ReadFlags readFlags = ReadFlags. } else { - throw new TrieException($"Unexpected number of items = {numberOfItems} when decoding a node from RLP ({FullRlp?.ToHexString()})"); + throw new TrieNodeException($"Unexpected number of items = {numberOfItems} when decoding a node from RLP ({FullRlp?.ToHexString()})", Keccak ?? Keccak.Zero); } } catch (RlpException rlpException) { - throw new TrieException($"Error when decoding node {Keccak}", rlpException); + throw new TrieNodeException($"Error when decoding node {Keccak}", Keccak ?? Keccak.Zero, rlpException); } } diff --git a/src/Nethermind/Nethermind.Trie/TrieNodeException.cs b/src/Nethermind/Nethermind.Trie/TrieNodeException.cs new file mode 100644 index 00000000000..4fe6e870880 --- /dev/null +++ b/src/Nethermind/Nethermind.Trie/TrieNodeException.cs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Core.Crypto; + +namespace Nethermind.Trie; + +public class TrieNodeException : TrieException +{ + public ValueKeccak NodeHash { get; private set; } + public string? EnhancedMessage { get; set; } + public override string Message => EnhancedMessage is null ? base.Message : EnhancedMessage + Environment.NewLine + base.Message; + + public TrieNodeException(string message, Keccak keccak, Exception? inner = null) : base(message, inner) + { + NodeHash = keccak; + } +}