diff --git a/neo-modules.sln b/neo-modules.sln
index 9c00ef862..8a1931586 100644
--- a/neo-modules.sln
+++ b/neo-modules.sln
@@ -39,7 +39,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MPTTrie", "src\MPTTrie\MPTT
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Cryptography.MPTTrie.Tests", "tests\Neo.Cryptography.MPTTrie.Tests\Neo.Cryptography.MPTTrie.Tests.csproj", "{8D2EE375-2E2D-45FE-A4E9-0254D12C7554}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLiteWallet", "src\SQLiteWallet\SQLiteWallet.csproj", "{D121D57A-512E-4F74-ADA1-24482BF5C42B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SQLiteWallet", "src\SQLiteWallet\SQLiteWallet.csproj", "{D121D57A-512E-4F74-ADA1-24482BF5C42B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StorageDumper", "src\StorageDumper\StorageDumper.csproj", "{938D86EA-0F48-436B-9255-4AD9A8E6B9AC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -115,6 +117,10 @@ Global
{D121D57A-512E-4F74-ADA1-24482BF5C42B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D121D57A-512E-4F74-ADA1-24482BF5C42B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D121D57A-512E-4F74-ADA1-24482BF5C42B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {938D86EA-0F48-436B-9255-4AD9A8E6B9AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {938D86EA-0F48-436B-9255-4AD9A8E6B9AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {938D86EA-0F48-436B-9255-4AD9A8E6B9AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {938D86EA-0F48-436B-9255-4AD9A8E6B9AC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -137,6 +143,7 @@ Global
{D167FA6B-D2A3-4D8A-A65D-686DD06650F6} = {97E81C78-1637-481F-9485-DA1225E94C23}
{8D2EE375-2E2D-45FE-A4E9-0254D12C7554} = {59D802AB-C552-422A-B9C3-64D329FBCDCC}
{D121D57A-512E-4F74-ADA1-24482BF5C42B} = {97E81C78-1637-481F-9485-DA1225E94C23}
+ {938D86EA-0F48-436B-9255-4AD9A8E6B9AC} = {97E81C78-1637-481F-9485-DA1225E94C23}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {61D3ADE6-BBFC-402D-AB42-1C71C9F9EDE3}
diff --git a/src/StorageDumper/Settings.cs b/src/StorageDumper/Settings.cs
new file mode 100644
index 000000000..841b2a3c4
--- /dev/null
+++ b/src/StorageDumper/Settings.cs
@@ -0,0 +1,48 @@
+// Copyright (C) 2015-2023 The Neo Project.
+//
+// The Neo.Plugins.StatesDumper is free software distributed under the MIT software license,
+// see the accompanying file LICENSE in the main directory of the
+// project or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using Microsoft.Extensions.Configuration;
+using Neo.SmartContract.Native;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Neo.Plugins
+{
+ internal class Settings
+ {
+ ///
+ /// Amount of storages states (heights) to be dump in a given json file
+ ///
+ public uint BlockCacheSize { get; }
+ ///
+ /// Height to begin storage dump
+ ///
+ public uint HeightToBegin { get; }
+
+ public IReadOnlyList Exclude { get; }
+
+ public static Settings Default { get; private set; }
+
+ private Settings(IConfigurationSection section)
+ {
+ /// Geting settings for storage changes state dumper
+ this.BlockCacheSize = section.GetValue("BlockCacheSize", 1000u);
+ this.HeightToBegin = section.GetValue("HeightToBegin", 0u);
+ this.Exclude = section.GetSection("Exclude").Exists()
+ ? section.GetSection("Exclude").GetChildren().Select(p => int.Parse(p.Value)).ToArray()
+ : new[] { NativeContract.Ledger.Id };
+ }
+
+ public static void Load(IConfigurationSection section)
+ {
+ Default = new Settings(section);
+ }
+ }
+}
diff --git a/src/StorageDumper/StorageDumper.cs b/src/StorageDumper/StorageDumper.cs
new file mode 100644
index 000000000..4eb3b434f
--- /dev/null
+++ b/src/StorageDumper/StorageDumper.cs
@@ -0,0 +1,185 @@
+// Copyright (C) 2015-2023 The Neo Project.
+//
+// The Neo.Plugins.StatesDumper is free software distributed under the MIT software license,
+// see the accompanying file LICENSE in the main directory of the
+// project or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using Neo.ConsoleService;
+using Neo.IO;
+using Neo.Json;
+using Neo.Ledger;
+using Neo.Network.P2P.Payloads;
+using Neo.Persistence;
+using Neo.SmartContract.Native;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Neo.Plugins
+{
+ public class StorageDumper : Plugin
+ {
+ private readonly Dictionary systems = new Dictionary();
+
+ private StreamWriter _writer;
+ private JObject _currentBlock;
+ private string _lastCreateDirectory;
+
+
+ public override string Description => "Exports Neo-CLI status data";
+
+ public StorageDumper()
+ {
+ Blockchain.Committing += OnCommitting;
+ Blockchain.Committed += OnCommitted;
+ }
+
+ public override void Dispose()
+ {
+ Blockchain.Committing -= OnCommitting;
+ Blockchain.Committed -= OnCommitted;
+ }
+
+ protected override void Configure()
+ {
+ Settings.Load(GetConfiguration());
+ }
+
+ protected override void OnSystemLoaded(NeoSystem system)
+ {
+ systems.Add(system.Settings.Network, system);
+ }
+
+ ///
+ /// Process "dump contract-storage" command
+ ///
+ [ConsoleCommand("dump contract-storage", Category = "Storage", Description = "You can specify the contract script hash or use null to get the corresponding information from the storage")]
+ private void OnDumpStorage(uint network, UInt160 contractHash = null)
+ {
+ if (!systems.ContainsKey(network)) throw new InvalidOperationException("invalid network");
+ string path = $"dump_{network}.json";
+ byte[] prefix = null;
+ if (contractHash is not null)
+ {
+ var contract = NativeContract.ContractManagement.GetContract(systems[network].StoreView, contractHash);
+ if (contract is null) throw new InvalidOperationException("contract not found");
+ prefix = BitConverter.GetBytes(contract.Id);
+ }
+ var states = systems[network].StoreView.Find(prefix);
+ JArray array = new JArray(states.Where(p => !Settings.Default.Exclude.Contains(p.Key.Id)).Select(p => new JObject
+ {
+ ["key"] = Convert.ToBase64String(p.Key.ToArray()),
+ ["value"] = Convert.ToBase64String(p.Value.ToArray())
+ }));
+ File.WriteAllText(path, array.ToString());
+ ConsoleHelper.Info("States",
+ $"({array.Count})",
+ " have been dumped into file ",
+ $"{path}");
+ }
+
+ private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList applicationExecutedList)
+ {
+ InitFileWriter(system.Settings.Network, snapshot);
+ OnPersistStorage(system.Settings.Network, snapshot);
+ }
+
+ private void OnPersistStorage(uint network, DataCache snapshot)
+ {
+ uint blockIndex = NativeContract.Ledger.CurrentIndex(snapshot);
+ if (blockIndex >= Settings.Default.HeightToBegin)
+ {
+ JArray array = new JArray();
+
+ foreach (var trackable in snapshot.GetChangeSet())
+ {
+ if (Settings.Default.Exclude.Contains(trackable.Key.Id))
+ continue;
+ JObject state = new JObject();
+ switch (trackable.State)
+ {
+ case TrackState.Added:
+ state["state"] = "Added";
+ state["key"] = Convert.ToBase64String(trackable.Key.ToArray());
+ state["value"] = Convert.ToBase64String(trackable.Item.ToArray());
+ // Here we have a new trackable.Key and trackable.Item
+ break;
+ case TrackState.Changed:
+ state["state"] = "Changed";
+ state["key"] = Convert.ToBase64String(trackable.Key.ToArray());
+ state["value"] = Convert.ToBase64String(trackable.Item.ToArray());
+ break;
+ case TrackState.Deleted:
+ state["state"] = "Deleted";
+ state["key"] = Convert.ToBase64String(trackable.Key.ToArray());
+ break;
+ }
+ array.Add(state);
+ }
+
+ JObject bs_item = new JObject();
+ bs_item["block"] = blockIndex;
+ bs_item["size"] = array.Count;
+ bs_item["storage"] = array;
+ _currentBlock = bs_item;
+ }
+ }
+
+
+ private void OnCommitted(NeoSystem system, Block block)
+ {
+ OnCommitStorage(system.Settings.Network, system.StoreView);
+ }
+
+ void OnCommitStorage(uint network, DataCache snapshot)
+ {
+ if (_currentBlock != null)
+ {
+ _writer.WriteLine(_currentBlock.ToString());
+ _writer.Flush();
+ }
+ }
+
+ private void InitFileWriter(uint network, DataCache snapshot)
+ {
+ uint blockIndex = NativeContract.Ledger.CurrentIndex(snapshot);
+ if (_writer == null
+ || blockIndex % Settings.Default.BlockCacheSize == 0)
+ {
+ string path = GetOrCreateDirectory(network, blockIndex);
+ var filepart = (blockIndex / Settings.Default.BlockCacheSize) * Settings.Default.BlockCacheSize;
+ path = $"{path}/dump-block-{filepart}.dump";
+ if (_writer != null)
+ {
+ _writer.Dispose();
+ }
+ _writer = new StreamWriter(new FileStream(path, FileMode.Append));
+ }
+ }
+
+ private string GetOrCreateDirectory(uint network, uint blockIndex)
+ {
+ string dirPathWithBlock = GetDirectoryPath(network, blockIndex);
+ if (_lastCreateDirectory != dirPathWithBlock)
+ {
+ Directory.CreateDirectory(dirPathWithBlock);
+ _lastCreateDirectory = dirPathWithBlock;
+ }
+ return dirPathWithBlock;
+ }
+
+ private string GetDirectoryPath(uint network, uint blockIndex)
+ {
+ //Default Parameter
+ uint storagePerFolder = 100000;
+ uint folder = (blockIndex / storagePerFolder) * storagePerFolder;
+ return $"./StorageDumper_{network}/BlockStorage_{folder}";
+ }
+
+ }
+}
diff --git a/src/StorageDumper/StorageDumper.csproj b/src/StorageDumper/StorageDumper.csproj
new file mode 100644
index 000000000..6ba3c63ea
--- /dev/null
+++ b/src/StorageDumper/StorageDumper.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Neo.Plugins.StorageDumper
+ enable
+ enable
+
+
+
+
+
+
diff --git a/src/StorageDumper/config.json b/src/StorageDumper/config.json
new file mode 100644
index 000000000..3f5c0537f
--- /dev/null
+++ b/src/StorageDumper/config.json
@@ -0,0 +1,7 @@
+{
+ "PluginConfiguration": {
+ "BlockCacheSize": 1000,
+ "HeightToBegin": 0,
+ "Exclude": [ -4 ]
+ }
+}