Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Neo Plugin New Feature] Rollback ledger #3313

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions neo.sln
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokensTracker", "src\Plugin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RpcClient", "src\Plugins\RpcClient\RpcClient.csproj", "{185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RollbackServer", "src\Plugins\RollbackService\RollbackServer.csproj", "{651CD479-1AF3-4E1A-8994-19FF2CB766F4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -216,6 +218,10 @@ Global
{185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0}.Release|Any CPU.Build.0 = Release|Any CPU
{651CD479-1AF3-4E1A-8994-19FF2CB766F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{651CD479-1AF3-4E1A-8994-19FF2CB766F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{651CD479-1AF3-4E1A-8994-19FF2CB766F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{651CD479-1AF3-4E1A-8994-19FF2CB766F4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -255,6 +261,7 @@ Global
{FF76D8A4-356B-461A-8471-BC1B83E57BBC} = {C2DC830A-327A-42A7-807D-295216D30DBB}
{5E4947F3-05D3-4806-B0F3-30DAC71B5986} = {C2DC830A-327A-42A7-807D-295216D30DBB}
{185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0} = {C2DC830A-327A-42A7-807D-295216D30DBB}
{651CD479-1AF3-4E1A-8994-19FF2CB766F4} = {C2DC830A-327A-42A7-807D-295216D30DBB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC}
Expand Down
8 changes: 7 additions & 1 deletion src/Neo/Persistence/DataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -477,11 +477,17 @@ public IEnumerable<(StorageKey Key, StorageItem Value)> Seek(byte[] keyOrPrefix
/// Reads a specified entry from the cache. If the entry is not in the cache, it will be automatically loaded from the underlying storage.
/// </summary>
/// <param name="key">The key of the entry.</param>
/// <param name="useInternal">Get from the cache or directly from storage.</param>
/// <returns>The cached data. Or <see langword="null"/> if it is neither in the cache nor in the underlying storage.</returns>
public StorageItem TryGet(StorageKey key)
public StorageItem TryGet(StorageKey key, bool useInternal = false)
{
lock (dictionary)
{
if (useInternal)
{
return TryGetInternal(key);
}

if (dictionary.TryGetValue(key, out Trackable trackable))
{
if (trackable.State == TrackState.Deleted || trackable.State == TrackState.NotFound)
Expand Down
2 changes: 1 addition & 1 deletion src/Neo/SmartContract/StorageKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public sealed record StorageKey

public StorageKey() { }

internal StorageKey(byte[] cache)
public StorageKey(byte[] cache)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be reverted by setting neo internal accessable to plugin

{
this.cache = cache;
Id = BinaryPrimitives.ReadInt32LittleEndian(cache);
Expand Down
246 changes: 246 additions & 0 deletions src/Plugins/RollbackService/RollbackPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Copyright (C) 2015-2024 The Neo Project.

Check failure on line 1 in src/Plugins/RollbackService/RollbackPlugin.cs

View workflow job for this annotation

GitHub Actions / Format

A source file contains a header that does not match the required text
//
// RpcServerPlugin.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.ConsoleService;
using Neo.Ledger;
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.SmartContract;
using Neo.SmartContract.Native;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using JsonSerializer = System.Text.Json.JsonSerializer;

namespace Neo.Plugins.RollbackService
{

/// <summary>
/// Plugin to allow Neo node to rollback to any specified block height.
/// </summary>
public class RollbackPlugin : Plugin
{
public const string RollbackPayloadCategory = "RollbackService";
public override string Name => "RollbackService";
public override string Description => "Allows the node to rollback to any specified height";
public override string ConfigFile => System.IO.Path.Combine(RootPath, "config.json");

internal static NeoSystem _system;

public RollbackPlugin()
{
Blockchain.Committing += OnCommitting;
}

protected override void Configure()
{
try
{
Settings.Load(GetConfiguration());
}
catch (Exception ex)
{
ConsoleHelper.Error($"Failed to load configuration: {ex.Message}");
}
}

protected override void OnSystemLoaded(NeoSystem system)
{
_system = system;
}

public override void Dispose()
{
base.Dispose();
Blockchain.Committing -= OnCommitting;
}

/// <summary>
/// Handles the committing event to save state changes for rollback.
/// </summary>
/// <param name="system">The NeoSystem instance.</param>
/// <param name="block">The current block being committed.</param>
/// <param name="snapshot">The data cache snapshot.</param>
/// <param name="applicationExecutedList">The list of executed applications.</param>
private void OnCommitting(NeoSystem system, Block block, DataCache snapshot, IReadOnlyList<Blockchain.ApplicationExecuted> applicationExecutedList)
{
if (system.Settings.Network != Settings.Default!.Network) return;
SaveFallback(snapshot, block.Index);
}

/// <summary>
/// Command to rollback the ledger to a specified block height.
/// </summary>
/// <param name="target">The target block height to rollback to.</param>
[ConsoleCommand("fallback ledger", Category = "Blockchain Commands")]
private void OnBlockFallbackCommand(uint targetHeight)
{
var height = NativeContract.Ledger.CurrentIndex(_system.StoreView);
if (height < targetHeight)
{
ConsoleHelper.Error("Invalid fallback target height.");
return;
}

OnBlockFallback(_system, targetHeight);
}

/// <summary>
/// Saves the state changes to a persistent store for fallback.
/// </summary>
/// <param name="snapshot">The data cache snapshot.</param>
/// <param name="blockId">The block index for which changes are being saved.</param>
private static void SaveFallback(DataCache snapshot, uint blockId)
{
var changeSet = snapshot.GetChangeSet();
using var memoryStream = new MemoryStream();
foreach (var item in changeSet)
{
FallbackOperation operation = item.State switch
{
TrackState.Deleted => FallbackOperation.Deleted,
TrackState.Added => FallbackOperation.Added,
TrackState.Changed => FallbackOperation.Changed,
_ => throw new InvalidOperationException("Invalid fallback operation")
};

var value = snapshot.TryGet(item.Key, true)?.Value.ToArray() ?? Array.Empty<byte>();

var encoded = ChangeEncode(
blockId,
operation,
item.Key.Key.ToArray(),
operation != FallbackOperation.Added ? value : Array.Empty<byte>()
);
memoryStream.Write(BitConverter.GetBytes(encoded.Length), 0, 4);
memoryStream.Write(encoded, 0, encoded.Length);
}
snapshot.Add(new StorageKey(Encoding.UTF8.GetBytes($"fallback{blockId}")), new StorageItem(memoryStream.ToArray()));
}

/// <summary>
/// Rolls back the blockchain state to a specified height.
/// </summary>
/// <param name="system">The NeoSystem instance.</param>
/// <param name="height">The target block height to rollback to.</param>
private static void OnBlockFallback(NeoSystem system, uint height)
{
var snapshot = system.GetSnapshot();
var currentIndex = NativeContract.Ledger.CurrentIndex(snapshot);
if (currentIndex <= height) return;

for (var i = currentIndex; i > height; i--)
{
using var snapshotFallback = system.GetSnapshot();
var fallbackKey = new StorageKey(Encoding.UTF8.GetBytes($"fallback{i}"));
var fallbackItem = snapshotFallback.TryGet(fallbackKey)?.Value.ToArray();
if (fallbackItem == null) continue;

using (var memoryStream = new MemoryStream(fallbackItem))
using (var reader = new BinaryReader(memoryStream))
{
while (memoryStream.Position < memoryStream.Length)
{
var length = reader.ReadInt32();
if (length < 0 || length > memoryStream.Length - memoryStream.Position)
{
throw new InvalidDataException("Invalid length value.");
}
byte[] encodedItem = reader.ReadBytes(length);
var decoded = ChangeDecode(encodedItem);
var key = new StorageKey(decoded.key);
var value = new StorageItem(decoded.value);
try
{
switch (decoded.operation)
{
case FallbackOperation.Deleted:
snapshot.Add(key, value);
break;
case FallbackOperation.Added:
snapshot.Delete(key);
break;
case FallbackOperation.Changed:
snapshot.GetAndChange(key).FromReplica(value);
break;
default:
throw new InvalidOperationException("Invalid fallback operation");
}
}
catch (Exception e)
{
ConsoleHelper.Warning($"Exception during fallback: {e.Message}");
}
}
snapshotFallback.Delete(fallbackKey);
snapshotFallback.Commit();
ConsoleHelper.Info($"Fallback to block {i}");
}
}
}

/// <summary>
/// Encodes the change data for persistent storage.
/// </summary>
/// <param name="blockId">The block index.</param>
/// <param name="operation">The operation type.</param>
/// <param name="key">The storage key.</param>
/// <param name="value">The storage value.</param>
/// <returns>The encoded byte array.</returns>
private static byte[] ChangeEncode(uint blockId, FallbackOperation operation, byte[] key, byte[] value)
{
var change = new
{
BlockId = blockId,
Operation = operation,
Key = key,
Value = value
};
return JsonSerializer.SerializeToUtf8Bytes(change);
}

/// <summary>
/// Decodes the change data from persistent storage.
/// </summary>
/// <param name="data">The encoded byte array.</param>
/// <returns>The decoded change data.</returns>
private static (uint blockId, FallbackOperation operation, byte[] key, byte[] value) ChangeDecode(byte[] data)
{
var change = JsonSerializer.Deserialize<Dictionary<string, object>>(data);
var blockId = Convert.ToUInt32(change["BlockId"]);
var operation = (FallbackOperation)Convert.ToByte(change["Operation"]);
var key = JsonSerializer.Deserialize<byte[]>((string)change["Key"]);
var value = JsonSerializer.Deserialize<byte[]>((string)change["Value"]);
return (blockId, operation, key, value);
}

/// <summary>
/// Defines the operations for fallback actions.
/// </summary>
private enum FallbackOperation : byte
{
/// <summary>
/// Indicates the state was deleted.
/// </summary>
Deleted = 0,
/// <summary>
/// Indicates the state was added.
/// </summary>
Added = 1,
/// <summary>
/// Indicates the state was changed.
/// </summary>
Changed = 2
}
}
}

23 changes: 23 additions & 0 deletions src/Plugins/RollbackService/RollbackServer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>Neo.Plugins.RollbackService</PackageId>
<BaseOutputPath>$(SolutionDir)/bin/$(PackageId)</BaseOutputPath>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<None Update="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Neo.ConsoleService\Neo.ConsoleService.csproj" />
</ItemGroup>

</Project>
32 changes: 32 additions & 0 deletions src/Plugins/RollbackService/Settings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (C) 2015-2024 The Neo Project.
//
// Settings.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Microsoft.Extensions.Configuration;

namespace Neo.Plugins.RollbackService
{
public class Settings
{
public uint Network { get; }

public static Settings? Default { get; private set; }

internal Settings(IConfigurationSection section)
{
Network = section.GetValue("Network", 5195086u);
}

public static void Load(IConfigurationSection section)
{
Default = new Settings(section);
}
}
}
5 changes: 5 additions & 0 deletions src/Plugins/RollbackService/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"PluginConfiguration": {
"Network": 860833102
}
}
Loading