-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Closed
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
// Copyright (C) 2015-2024 The Neo Project. | ||
// | ||
// 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 | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"PluginConfiguration": { | ||
"Network": 860833102 | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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