From 9b7e35393dc846cc9a0a6b51286b2d295146f98f Mon Sep 17 00:00:00 2001 From: s2quake Date: Thu, 4 Jul 2024 10:43:48 +0900 Subject: [PATCH] feat: Add evidence implementations. --- .vscode/launch.json | 10 +- libplanet-console.sln | 14 +++ .../LibplanetConsole.Common/AppPrivateKey.cs | 8 ++ .../BlockChainUtility.cs | 2 +- .../Commands/EvidenceCommand.cs | 53 +++++++++ .../EvidenceNodeContent.cs | 42 +++++++ .../IEvidenceContent.cs | 16 +++ .../LibplanetConsole.Consoles.Evidence.csproj | 11 ++ ...ibplanetConsole.Consoles.Executable.csproj | 1 + .../Commands/EvidenceCommand.cs | 84 +++++++++++++ .../DuplicateVoteViolator.cs | 103 ++++++++++++++++ .../EvidenceNode.cs | 110 ++++++++++++++++++ .../Extensions/ConsensusContextExtensions.cs | 33 ++++++ .../Extensions/ConsensusReactorExtensions.cs | 24 ++++ .../Extensions/ContextExtensions.cs | 38 ++++++ .../Extensions/SwarmContextExtensions.cs | 24 ++++ .../IEvidenceNode.cs | 23 ++++ .../LibplanetConsole.Nodes.Evidence.csproj | 11 ++ .../Services/EvidenceNodeService.cs | 33 ++++++ .../LibplanetConsole.Nodes.Executable.csproj | 1 + src/node/LibplanetConsole.Nodes/INode.cs | 2 +- src/node/LibplanetConsole.Nodes/Node.cs | 2 + .../Serializations/EvidenceInfo.cs | 29 +++++ .../Services/IEvidenceService.cs | 16 +++ .../LibplanetConsole.Evidence/TestEvidence.cs | 42 +++++++ 25 files changed, 727 insertions(+), 5 deletions(-) create mode 100644 src/console/LibplanetConsole.Consoles.Evidence/Commands/EvidenceCommand.cs create mode 100644 src/console/LibplanetConsole.Consoles.Evidence/EvidenceNodeContent.cs create mode 100644 src/console/LibplanetConsole.Consoles.Evidence/IEvidenceContent.cs create mode 100644 src/console/LibplanetConsole.Consoles.Evidence/LibplanetConsole.Consoles.Evidence.csproj create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/Commands/EvidenceCommand.cs create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/DuplicateVoteViolator.cs create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/EvidenceNode.cs create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/Extensions/ConsensusContextExtensions.cs create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/Extensions/ConsensusReactorExtensions.cs create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/Extensions/ContextExtensions.cs create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/Extensions/SwarmContextExtensions.cs create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/IEvidenceNode.cs create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/LibplanetConsole.Nodes.Evidence.csproj create mode 100644 src/node/LibplanetConsole.Nodes.Evidence/Services/EvidenceNodeService.cs create mode 100644 src/shared/LibplanetConsole.Evidence/Serializations/EvidenceInfo.cs create mode 100644 src/shared/LibplanetConsole.Evidence/Services/IEvidenceService.cs create mode 100644 src/shared/LibplanetConsole.Evidence/TestEvidence.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 780149c4..d1d01731 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "console": "integratedTerminal", "args": [ "--nodes", - "b05c8b2bc981219c2afc32725f1dd7bdfce356ac7382699cb74647ab20895e32", + "b52e619962057e397f47efcb009ce45341f84cb86f425cd081cb64f1f1c1b220,cc1f459ea12f97cc8e996cf1d6ea74c4991f38cf4b555fa26a84d1db22c4481c,c7ffc6717833a51396ec992c0804946422a4ab85ab9fd64dfde99ba25a58ffe0,07cd0cf1242dd6ddef68a865658a57bdbe6b21c98d74c80365a274297e98667d", "--clients", "698e1dc854bfba5c359710ded3770010aa58625c2fe6ee1765ed9d0cf99c6b0d", "--end-point", @@ -31,7 +31,11 @@ "args": [ "--end-point", "127.0.0.1:5353", - "--explorer-end-point" + "--explorer-end-point", + "--private-key", + "b52e619962057e397f47efcb009ce45341f84cb86f425cd081cb64f1f1c1b220", + "--log-path", + ".log/node.log" ] }, { @@ -83,4 +87,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/libplanet-console.sln b/libplanet-console.sln index e2a8d0e4..31f9a78f 100644 --- a/libplanet-console.sln +++ b/libplanet-console.sln @@ -47,6 +47,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibplanetConsole.Consoles.E EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibplanetConsole.Common.Tests", "test\LibplanetConsole.Common.Tests\LibplanetConsole.Common.Tests.csproj", "{65341396-A058-4577-9B70-C1DD3D146501}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibplanetConsole.Nodes.Evidence", "src\node\LibplanetConsole.Nodes.Evidence\LibplanetConsole.Nodes.Evidence.csproj", "{4C151EAE-2105-4DA1-B645-C09513EA8532}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibplanetConsole.Consoles.Evidence", "src\console\LibplanetConsole.Consoles.Evidence\LibplanetConsole.Consoles.Evidence.csproj", "{965DF40E-F3BA-45F2-A177-A3DE550C824E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -117,6 +121,14 @@ Global {65341396-A058-4577-9B70-C1DD3D146501}.Debug|Any CPU.Build.0 = Debug|Any CPU {65341396-A058-4577-9B70-C1DD3D146501}.Release|Any CPU.ActiveCfg = Release|Any CPU {65341396-A058-4577-9B70-C1DD3D146501}.Release|Any CPU.Build.0 = Release|Any CPU + {4C151EAE-2105-4DA1-B645-C09513EA8532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C151EAE-2105-4DA1-B645-C09513EA8532}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C151EAE-2105-4DA1-B645-C09513EA8532}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C151EAE-2105-4DA1-B645-C09513EA8532}.Release|Any CPU.Build.0 = Release|Any CPU + {965DF40E-F3BA-45F2-A177-A3DE550C824E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {965DF40E-F3BA-45F2-A177-A3DE550C824E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {965DF40E-F3BA-45F2-A177-A3DE550C824E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {965DF40E-F3BA-45F2-A177-A3DE550C824E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -142,6 +154,8 @@ Global {FEF8E9D4-CBB7-4EFC-A5C2-2C9E91498D79} = {4A8F8EE9-769C-4C97-89BC-19D038E69998} {AF1A0011-2795-42FD-B67B-7F1956268577} = {CAB76DA9-6E57-4422-98C6-DD2D6299F675} {65341396-A058-4577-9B70-C1DD3D146501} = {56942891-CFBD-41E4-8881-47F455D7BEFD} + {4C151EAE-2105-4DA1-B645-C09513EA8532} = {4A8F8EE9-769C-4C97-89BC-19D038E69998} + {965DF40E-F3BA-45F2-A177-A3DE550C824E} = {CAB76DA9-6E57-4422-98C6-DD2D6299F675} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution {1778FDCB-AC26-43E4-97FE-FC4F0C427672} = {23C68389-8F13-48E7-9878-440917F70DAF} diff --git a/src/common/LibplanetConsole.Common/AppPrivateKey.cs b/src/common/LibplanetConsole.Common/AppPrivateKey.cs index 8f4c6541..8806ee07 100644 --- a/src/common/LibplanetConsole.Common/AppPrivateKey.cs +++ b/src/common/LibplanetConsole.Common/AppPrivateKey.cs @@ -3,6 +3,8 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using Bencodex; +using Bencodex.Types; using Libplanet.Common; using Libplanet.Crypto; using LibplanetConsole.Common.Converters; @@ -13,6 +15,7 @@ namespace LibplanetConsole.Common; [JsonConverter(typeof(AppPrivateKeyJsonConverter))] public sealed record class AppPrivateKey { + private static readonly Codec _codec = new(); private readonly PrivateKey _privateKey; public AppPrivateKey(PrivateKey privateKey) => _privateKey = privateKey; @@ -106,6 +109,11 @@ public T Decrypt(string text) public byte[] Sign(object obj) { + if (obj is IValue value) + { + return _privateKey.Sign(_codec.Encode(value)); + } + var json = JsonSerializer.Serialize(obj); var bytes = Encoding.UTF8.GetBytes(json); return _privateKey.Sign(bytes); diff --git a/src/common/LibplanetConsole.Common/BlockChainUtility.cs b/src/common/LibplanetConsole.Common/BlockChainUtility.cs index fe700b4c..d46fb8c4 100644 --- a/src/common/LibplanetConsole.Common/BlockChainUtility.cs +++ b/src/common/LibplanetConsole.Common/BlockChainUtility.cs @@ -65,7 +65,7 @@ public static BlockChain CreateBlockChain( transactions: transactions, timestamp: DateTimeOffset.MinValue); var policy = new BlockPolicy( - blockInterval: TimeSpan.FromMilliseconds(8), + blockInterval: TimeSpan.FromSeconds(10), getMaxTransactionsPerBlock: _ => int.MaxValue, getMaxTransactionsBytes: _ => long.MaxValue); var stagePolicy = new VolatileStagePolicy(); diff --git a/src/console/LibplanetConsole.Consoles.Evidence/Commands/EvidenceCommand.cs b/src/console/LibplanetConsole.Consoles.Evidence/Commands/EvidenceCommand.cs new file mode 100644 index 00000000..b06dca24 --- /dev/null +++ b/src/console/LibplanetConsole.Consoles.Evidence/Commands/EvidenceCommand.cs @@ -0,0 +1,53 @@ +using System.ComponentModel; +using System.ComponentModel.Composition; +using JSSoft.Commands; +using LibplanetConsole.Common.Extensions; + +namespace LibplanetConsole.Consoles.Evidence.Commands; + +[Export(typeof(ICommand))] +[CommandSummary("Provides evidence-related commands.")] +[Category("Evidence")] +[method: ImportingConstructor] +internal sealed class EvidenceCommand(INodeCollection nodes) + : CommandMethodBase +{ + [CommandMethod] + public async Task NewAsync( + string nodeAddress = "", CancellationToken cancellationToken = default) + { + var node = nodes.Current ?? throw new InvalidOperationException("No node is selected."); + var evidenceContent = node.GetService(); + var evidenceInfo = await evidenceContent.AddEvidenceAsync(cancellationToken); + await Out.WriteLineAsJsonAsync(evidenceInfo); + } + + [CommandMethod] + public async Task RaiseAsync( + CancellationToken cancellationToken = default) + { + var node = nodes.Current ?? throw new InvalidOperationException("No node is selected."); + var evidenceContent = node.GetService(); + await evidenceContent.ViolateAsync(cancellationToken); + } + + [CommandMethod] + public async Task ListAsync(long height = -1, CancellationToken cancellationToken = default) + { + var node = nodes.Current ?? throw new InvalidOperationException("No node is selected."); + var evidenceContent = node.GetService(); + var evidenceInfos = await evidenceContent.GetEvidenceAsync(height, cancellationToken); + await Out.WriteLineAsJsonAsync(evidenceInfos); + } + +#if LIBPLANET_DPOS + [CommandMethod] + public async Task UnjailAsync( + CancellationToken cancellationToken = default) + { + var node = nodes.Current ?? throw new InvalidOperationException("No node is selected."); + var evidenceContent = node.GetService(); + await evidenceContent.UnjailAsync(cancellationToken); + } +#endif // LIBPLANET_DPOS +} diff --git a/src/console/LibplanetConsole.Consoles.Evidence/EvidenceNodeContent.cs b/src/console/LibplanetConsole.Consoles.Evidence/EvidenceNodeContent.cs new file mode 100644 index 00000000..9eed72a5 --- /dev/null +++ b/src/console/LibplanetConsole.Consoles.Evidence/EvidenceNodeContent.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.Composition; +using LibplanetConsole.Common.Services; +using LibplanetConsole.Consoles.Services; +using LibplanetConsole.Evidence.Serializations; +using LibplanetConsole.Evidence.Services; + +namespace LibplanetConsole.Consoles.Evidence; + +[Export(typeof(INodeContent))] +[Export(typeof(IEvidenceContent))] +[Export(typeof(INodeContentService))] +[method: ImportingConstructor] +internal sealed class EvidenceNodeContent(INode node) + : INodeContent, IEvidenceContent, INodeContentService +{ + private readonly RemoteService _evidenceService = new(); + + INode INodeContent.Node => node; + + string INodeContent.Name => "evidence"; + + IRemoteService INodeContentService.RemoteService => _evidenceService; + + private IEvidenceService Service => _evidenceService.Service; + + public Task AddEvidenceAsync(CancellationToken cancellationToken) + => Service.AddEvidenceAsync(cancellationToken); + + public Task GetEvidenceAsync(long height, CancellationToken cancellationToken) + => Service.GetEvidenceAsync(height, cancellationToken); + + public Task ViolateAsync(CancellationToken cancellationToken) + => Service.ViolateAsync(cancellationToken); + +#if LIBPLANET_DPOS + public Task UnjailAsync(CancellationToken cancellationToken) + { + var signature = node.Sign(true); + return Service.UnjailAsync(signature, cancellationToken); + } +#endif // LIBPLANET_DPOS +} diff --git a/src/console/LibplanetConsole.Consoles.Evidence/IEvidenceContent.cs b/src/console/LibplanetConsole.Consoles.Evidence/IEvidenceContent.cs new file mode 100644 index 00000000..b9826335 --- /dev/null +++ b/src/console/LibplanetConsole.Consoles.Evidence/IEvidenceContent.cs @@ -0,0 +1,16 @@ +using LibplanetConsole.Evidence.Serializations; + +namespace LibplanetConsole.Consoles.Evidence; + +internal interface IEvidenceContent +{ + Task AddEvidenceAsync(CancellationToken cancellationToken); + + Task GetEvidenceAsync(long height, CancellationToken cancellationToken); + + Task ViolateAsync(CancellationToken cancellationToken); + +#if LIBPLANET_DPOS + Task UnjailAsync(CancellationToken cancellationToken); +#endif // LIBPLANET_DPOS +} diff --git a/src/console/LibplanetConsole.Consoles.Evidence/LibplanetConsole.Consoles.Evidence.csproj b/src/console/LibplanetConsole.Consoles.Evidence/LibplanetConsole.Consoles.Evidence.csproj new file mode 100644 index 00000000..93d83910 --- /dev/null +++ b/src/console/LibplanetConsole.Consoles.Evidence/LibplanetConsole.Consoles.Evidence.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/console/LibplanetConsole.Consoles.Executable/LibplanetConsole.Consoles.Executable.csproj b/src/console/LibplanetConsole.Consoles.Executable/LibplanetConsole.Consoles.Executable.csproj index 3f34655f..1084687c 100644 --- a/src/console/LibplanetConsole.Consoles.Executable/LibplanetConsole.Consoles.Executable.csproj +++ b/src/console/LibplanetConsole.Consoles.Executable/LibplanetConsole.Consoles.Executable.csproj @@ -6,6 +6,7 @@ + diff --git a/src/node/LibplanetConsole.Nodes.Evidence/Commands/EvidenceCommand.cs b/src/node/LibplanetConsole.Nodes.Evidence/Commands/EvidenceCommand.cs new file mode 100644 index 00000000..b46c3bcd --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/Commands/EvidenceCommand.cs @@ -0,0 +1,84 @@ +using System.ComponentModel; +using System.ComponentModel.Composition; +using JSSoft.Commands; +using LibplanetConsole.Common.Extensions; + +namespace LibplanetConsole.Nodes.Evidence.Commands; + +[Export(typeof(ICommand))] +[CommandSummary("Provides evidence-related commands.")] +[Category("Evidence")] +[method: ImportingConstructor] +internal sealed class EvidenceCommand(INode node, IEvidenceNode evidenceNode) + : CommandMethodBase +{ + public override bool IsEnabled => node.IsRunning is true; + + [CommandMethod] + [CommandSummary("Adds a new evidence.")] + public async Task NewAsync(CancellationToken cancellationToken) + { + var evidenceInfo = await evidenceNode.AddEvidenceAsync(cancellationToken); + await Out.WriteLineAsJsonAsync(evidenceInfo); + } + + [CommandMethod] + [CommandSummary("Raises a infraction.")] + public async Task RaiseAsync(CancellationToken cancellationToken) + { + await evidenceNode.ViolateAsync(cancellationToken); + } + + [CommandMethod] + [CommandMethodStaticProperty(typeof(ListProperties))] + [CommandSummary("Gets the evidence list.")] + public async Task ListAsync(CancellationToken cancellationToken = default) + { + var height = ListProperties.Height; + var isPending = ListProperties.IsPending; + var evidenceInfos = isPending == true ? + await evidenceNode.GetPendingEvidenceAsync(cancellationToken) : + await evidenceNode.GetEvidenceAsync(height, cancellationToken); + await Out.WriteLineAsJsonAsync(evidenceInfos); + } + + [CommandMethod] + [CommandMethodStaticProperty(typeof(GetProperties))] + [CommandSummary("Gets the evidence.")] + public async Task GetAsync(string evidenceId, CancellationToken cancellationToken) + { + var isPending = GetProperties.IsPending; + var evidenceInfo = isPending == true ? + await evidenceNode.GetPendingEvidenceAsync(evidenceId, cancellationToken) : + await evidenceNode.GetEvidenceAsync(evidenceId, cancellationToken); + await Out.WriteLineAsJsonAsync(evidenceInfo); + } + +#if LIBPLANET_DPOS + [CommandMethod] + public async Task UnjailAsync(CancellationToken cancellationToken) + { + await evidenceNode.UnjailAsync(cancellationToken); + } +#endif // LIBPLANET_DPOS + + public static class ListProperties + { + [CommandPropertyRequired(DefaultValue = -1)] + [CommandSummary("The height of the block to get the evidence. default is the tip.")] + public static long Height { get; set; } + + [CommandPropertySwitch("pending", 'p')] + [CommandPropertyCondition(nameof(Height), -1, IsNot = true)] + [CommandSummary("Indicates whether to get pending evidence. " + + "if true, the height is ignored.")] + public static bool IsPending { get; set; } + } + + public static class GetProperties + { + [CommandPropertySwitch("pending", 'p')] + [CommandSummary("Indicates whether to get pending evidence.")] + public static bool IsPending { get; set; } + } +} diff --git a/src/node/LibplanetConsole.Nodes.Evidence/DuplicateVoteViolator.cs b/src/node/LibplanetConsole.Nodes.Evidence/DuplicateVoteViolator.cs new file mode 100644 index 00000000..7c0b664c --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/DuplicateVoteViolator.cs @@ -0,0 +1,103 @@ +using System.Numerics; +using Libplanet.Crypto; +using Libplanet.Net; +using Libplanet.Net.Consensus; +using Libplanet.Net.Messages; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using LibplanetConsole.Common.Extensions; +using LibplanetConsole.Nodes.Evidence.Extensions; +using Serilog; + +namespace LibplanetConsole.Nodes.Evidence; + +internal sealed class DuplicateVoteViolator(INode node) : IAsyncDisposable +{ + private const int Timeout = 5000; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly ILogger _logger = node.GetService(); + + public bool IsRunning { get; private set; } + + public async Task ViolateAsync(CancellationToken cancellationToken) + { + if (IsRunning == true) + { + throw new InvalidOperationException("The perpetrator is already running."); + } + + using var scope = new RunningScope(this); + var swarm = node.GetService(); + var consensusReactor = swarm.GetConsensusReactor(); + var consensusContext = consensusReactor.GetConsensusContext(); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + _cancellationTokenSource.Token, cancellationToken); + cancellationTokenSource.CancelAfter(Timeout); + await consensusContext.WaitUntilPreVoteAsync(cancellationTokenSource.Token); + InvokeViolation(consensusContext); + } + + public async ValueTask DisposeAsync() + { + await _cancellationTokenSource.CancelAsync(); + _cancellationTokenSource.Dispose(); + } + + private static Vote MakeRandomVote( + INode node, long height, int round, BigInteger power) + { + var hash = new BlockHash(GetRandomBytes(BlockHash.Size)); + var publicKey = (PublicKey)node.PublicKey; + var voteMetadata = new VoteMetadata( + height, + round, + hash, + DateTimeOffset.UtcNow, + publicKey, + power, + VoteFlag.PreVote); + var signature = node.Sign(voteMetadata.Bencoded); + + return new Vote(voteMetadata, [.. signature]); + + static byte[] GetRandomBytes(int size) + { + var bytes = new byte[size]; + var random = new Random(); + random.NextBytes(bytes); + + return bytes; + } + } + + private void InvokeViolation(ConsensusContext consensusContext) + { + var height = consensusContext.Height; + var round = (int)consensusContext.Round; + var context = consensusContext.GetContext(); + var validatorSet = context.GetValidatorSet(); + var publicKey = (PublicKey)node.PublicKey; + var validator = validatorSet.Validators.First(item => item.PublicKey == publicKey); + var power = validator.Power; + var vote = MakeRandomVote(node, height, round, power); + var message = new ConsensusPreVoteMsg(vote); + context.PublishMessage(message); + _logger.Debug("Violation invoked: {Height}, {Round}", height, round); + } + + private sealed class RunningScope : IDisposable + { + private readonly DuplicateVoteViolator _obj; + + public RunningScope(DuplicateVoteViolator obj) + { + _obj = obj; + _obj.IsRunning = true; + } + + public void Dispose() + { + _obj.IsRunning = false; + } + } +} diff --git a/src/node/LibplanetConsole.Nodes.Evidence/EvidenceNode.cs b/src/node/LibplanetConsole.Nodes.Evidence/EvidenceNode.cs new file mode 100644 index 00000000..dabad17c --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/EvidenceNode.cs @@ -0,0 +1,110 @@ +using System.ComponentModel.Composition; +using Libplanet.Blockchain; +using Libplanet.Crypto; +using Libplanet.Types.Evidence; +using LibplanetConsole.Common; +using LibplanetConsole.Common.Extensions; +using LibplanetConsole.Evidence; +using LibplanetConsole.Evidence.Serializations; + +namespace LibplanetConsole.Nodes.Evidence; + +[Export(typeof(IEvidenceNode))] +[Export] +[method: ImportingConstructor] +internal sealed class EvidenceNode(INode node) : IEvidenceNode, IAsyncDisposable +{ + private readonly DuplicateVoteViolator _duplicateVotePerpetrator = new(node); + + public async Task AddEvidenceAsync(CancellationToken cancellationToken) + { + var blockChain = node.GetService(); + var height = blockChain.Tip.Index; + var validatorAddress = node.Address; + var evidence = new TestEvidence(height, (Address)validatorAddress, DateTimeOffset.UtcNow); + blockChain.AddEvidence(evidence); + await Task.CompletedTask; + return (EvidenceInfo)evidence; + } + + public async Task GetEvidenceAsync( + long height, CancellationToken cancellationToken) + { + var blockChain = node.GetService(); + var block = height == -1 ? blockChain.Tip : blockChain[height]; + var evidences = block.Evidence.Select(item => new EvidenceInfo() + { + Type = item.GetType().Name, + Id = item.Id.ToString(), + Height = item.Height, + TargetAddress = (AppAddress)item.TargetAddress, + Timestamp = item.Timestamp, + }); + await Task.CompletedTask; + return [.. evidences]; + } + + public async Task GetEvidenceAsync( + string evidenceId, CancellationToken cancellationToken) + { + var blockChain = node.GetService(); + if (blockChain.GetCommittedEvidence(EvidenceId.Parse(evidenceId)) is { } evidence) + { + await Task.CompletedTask; + return (EvidenceInfo)evidence; + } + + throw new ArgumentException( + message: $"The evidence {evidenceId} does not exist.", + paramName: nameof(evidenceId)); + } + + public async Task GetPendingEvidenceAsync(CancellationToken cancellationToken) + { + var blockChain = node.GetService(); + var evidences = blockChain.GetPendingEvidence().Select(item => (EvidenceInfo)item); + await Task.CompletedTask; + return [.. evidences]; + } + + public async Task GetPendingEvidenceAsync( + string evidenceId, CancellationToken cancellationToken) + { + var blockChain = node.GetService(); + if (blockChain.GetPendingEvidence(EvidenceId.Parse(evidenceId)) is { } evidence) + { + await Task.CompletedTask; + return (EvidenceInfo)evidence; + } + + throw new ArgumentException( + message: $"The evidence {evidenceId} does not exist.", + paramName: nameof(evidenceId)); + } + + public async Task ViolateAsync(CancellationToken cancellationToken) + { + await _duplicateVotePerpetrator.ViolateAsync(cancellationToken); + } + + public async ValueTask DisposeAsync() + { + await _duplicateVotePerpetrator.DisposeAsync(); + } + +#if LIBPLANET_DPOS + public async Task UnjailAsync(CancellationToken cancellationToken) + { + var nodeAddress = node.Address; + var validatorAddress = Nekoyume.Action.DPoS.Model.Validator.DeriveAddress(nodeAddress); + var actions = new IAction[] + { + new Nekoyume.Action.DPoS.Unjail + { + Validator = validatorAddress, + }, + }; + await node.AddTransactionAsync(actions, cancellationToken); + } +#endif //LIBPLANET_DPOS +} diff --git a/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ConsensusContextExtensions.cs b/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ConsensusContextExtensions.cs new file mode 100644 index 00000000..4470966c --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ConsensusContextExtensions.cs @@ -0,0 +1,33 @@ +// Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable S3011 +using System.Reflection; +using Libplanet.Net.Consensus; + +namespace LibplanetConsole.Nodes.Evidence.Extensions; + +internal static class ConsensusContextExtensions +{ + public static Context GetContext(this ConsensusContext @this) + { + var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; + var type = typeof(ConsensusContext); + var propertyName = "CurrentContext"; + var propertyInfo = type.GetProperty(propertyName, bindingFlags) ?? + throw new InvalidOperationException($"{propertyName} property not found."); + if (propertyInfo.GetValue(@this) is Context context) + { + return context; + } + + throw new InvalidOperationException($"{propertyName} value cannot be null."); + } + + public static async Task WaitUntilPreVoteAsync( + this ConsensusContext @this, CancellationToken cancellationToken) + { + while (@this.Step != ConsensusStep.PreVote) + { + await Task.Delay(100, cancellationToken); + } + } +} diff --git a/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ConsensusReactorExtensions.cs b/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ConsensusReactorExtensions.cs new file mode 100644 index 00000000..3d845e7d --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ConsensusReactorExtensions.cs @@ -0,0 +1,24 @@ +// Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable S3011 +using System.Reflection; +using Libplanet.Net.Consensus; + +namespace LibplanetConsole.Nodes.Evidence.Extensions; + +internal static class ConsensusReactorExtensions +{ + public static ConsensusContext GetConsensusContext(this ConsensusReactor @this) + { + var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; + var type = typeof(ConsensusReactor); + var propertyName = "ConsensusContext"; + var propertyInfo = type.GetProperty(propertyName, bindingFlags) ?? + throw new InvalidOperationException($"{propertyName} property not found."); + if (propertyInfo.GetValue(@this) is ConsensusContext consensusContext) + { + return consensusContext; + } + + throw new InvalidOperationException($"{propertyName} value cannot be null."); + } +} diff --git a/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ContextExtensions.cs b/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ContextExtensions.cs new file mode 100644 index 00000000..560198fc --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/Extensions/ContextExtensions.cs @@ -0,0 +1,38 @@ +// Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable S3011 +using System.Reflection; +using Libplanet.Net.Consensus; +using Libplanet.Net.Messages; +using Libplanet.Types.Consensus; + +namespace LibplanetConsole.Nodes.Evidence.Extensions; + +internal static class ContextExtensions +{ + public static ValidatorSet GetValidatorSet(this Context @this) + { + var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; + var type = typeof(Context); + var fieldName = "_validatorSet"; + var fieldInfo = type.GetField(fieldName, bindingFlags) ?? + throw new InvalidOperationException($"{fieldName} field not found."); + if (fieldInfo.GetValue(@this) is ValidatorSet validatorSet) + { + return validatorSet; + } + + throw new InvalidOperationException($"{fieldName} value cannot be null."); + } + + public static void PublishMessage(this Context @this, ConsensusMsg message) + { + var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; + var type = typeof(Context); + var methodName = "PublishMessage"; + var methodInfo = type.GetMethod(methodName, bindingFlags) ?? + throw new InvalidOperationException($"{methodName} method not found."); + + var args = new object[] { message }; + methodInfo.Invoke(@this, args); + } +} diff --git a/src/node/LibplanetConsole.Nodes.Evidence/Extensions/SwarmContextExtensions.cs b/src/node/LibplanetConsole.Nodes.Evidence/Extensions/SwarmContextExtensions.cs new file mode 100644 index 00000000..49cb8aca --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/Extensions/SwarmContextExtensions.cs @@ -0,0 +1,24 @@ +// Reflection should not be used to increase accessibility of classes, methods, or fields +#pragma warning disable S3011 +using System.Reflection; +using Libplanet.Net; +using Libplanet.Net.Consensus; + +namespace LibplanetConsole.Nodes.Evidence.Extensions; + +internal static class SwarmContextExtensions +{ + public static ConsensusReactor GetConsensusReactor(this Swarm @this) + { + var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; + var propertyName = "ConsensusReactor"; + var propertyInfo = typeof(Swarm).GetProperty(propertyName, bindingFlags) ?? + throw new InvalidOperationException($"{propertyName} property not found."); + if (propertyInfo.GetValue(@this) is ConsensusReactor consensusReactor) + { + return consensusReactor; + } + + throw new InvalidOperationException($"{propertyName} value cannot be null."); + } +} diff --git a/src/node/LibplanetConsole.Nodes.Evidence/IEvidenceNode.cs b/src/node/LibplanetConsole.Nodes.Evidence/IEvidenceNode.cs new file mode 100644 index 00000000..766bb775 --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/IEvidenceNode.cs @@ -0,0 +1,23 @@ +using LibplanetConsole.Evidence.Serializations; + +namespace LibplanetConsole.Nodes.Evidence; + +public interface IEvidenceNode +{ + Task AddEvidenceAsync(CancellationToken cancellationToken); + + Task GetEvidenceAsync(long height, CancellationToken cancellationToken); + + Task GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken); + + Task GetPendingEvidenceAsync(CancellationToken cancellationToken); + + Task GetPendingEvidenceAsync( + string evidenceId, CancellationToken cancellationToken); + + Task ViolateAsync(CancellationToken cancellationToken); + +#if LIBPLANET_DPOS + Task UnjailAsync(CancellationToken cancellationToken); +#endif // LIBPLANET_DPOS +} diff --git a/src/node/LibplanetConsole.Nodes.Evidence/LibplanetConsole.Nodes.Evidence.csproj b/src/node/LibplanetConsole.Nodes.Evidence/LibplanetConsole.Nodes.Evidence.csproj new file mode 100644 index 00000000..1b10617e --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/LibplanetConsole.Nodes.Evidence.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/node/LibplanetConsole.Nodes.Evidence/Services/EvidenceNodeService.cs b/src/node/LibplanetConsole.Nodes.Evidence/Services/EvidenceNodeService.cs new file mode 100644 index 00000000..19dc386e --- /dev/null +++ b/src/node/LibplanetConsole.Nodes.Evidence/Services/EvidenceNodeService.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.Composition; +using LibplanetConsole.Common.Services; +using LibplanetConsole.Evidence.Serializations; +using LibplanetConsole.Evidence.Services; + +namespace LibplanetConsole.Nodes.Evidence.Services; + +[Export(typeof(ILocalService))] +[method: ImportingConstructor] +internal sealed class EvidenceNodeService(EvidenceNode evidenceNode) + : LocalService, IEvidenceService +{ + public Task AddEvidenceAsync(CancellationToken cancellationToken) + => evidenceNode.AddEvidenceAsync(cancellationToken); + + public Task GetEvidenceAsync(long height, CancellationToken cancellationToken) + => evidenceNode.GetEvidenceAsync(height, cancellationToken); + + public Task ViolateAsync(CancellationToken cancellationToken) + => evidenceNode.ViolateAsync(cancellationToken); + +#if LIBPLANET_DPOS + public async Task UnjailAsync(byte[] signarue, CancellationToken cancellationToken) + { + if (node.Verify(true, signarue) == true) + { + await evidenceNode.UnjailAsync(cancellationToken); + } + + throw new ArgumentException("Invalid signature.", nameof(signarue)); + } +#endif // LIBPLANET_DPOS +} diff --git a/src/node/LibplanetConsole.Nodes.Executable/LibplanetConsole.Nodes.Executable.csproj b/src/node/LibplanetConsole.Nodes.Executable/LibplanetConsole.Nodes.Executable.csproj index 6984e7c8..076408e2 100644 --- a/src/node/LibplanetConsole.Nodes.Executable/LibplanetConsole.Nodes.Executable.csproj +++ b/src/node/LibplanetConsole.Nodes.Executable/LibplanetConsole.Nodes.Executable.csproj @@ -5,6 +5,7 @@ + diff --git a/src/node/LibplanetConsole.Nodes/INode.cs b/src/node/LibplanetConsole.Nodes/INode.cs index 674a54f4..1c3a6882 100644 --- a/src/node/LibplanetConsole.Nodes/INode.cs +++ b/src/node/LibplanetConsole.Nodes/INode.cs @@ -5,7 +5,7 @@ namespace LibplanetConsole.Nodes; -public interface INode : IVerifier, IServiceProvider +public interface INode : IVerifier, ISigner, IServiceProvider { event EventHandler? BlockAppended; diff --git a/src/node/LibplanetConsole.Nodes/Node.cs b/src/node/LibplanetConsole.Nodes/Node.cs index b35f5056..aadac990 100644 --- a/src/node/LibplanetConsole.Nodes/Node.cs +++ b/src/node/LibplanetConsole.Nodes/Node.cs @@ -147,6 +147,8 @@ public AppPeer ConsensusSeedPeer public bool Verify(object obj, byte[] signature) => PublicKey.Verify(obj, signature); + public byte[] Sign(object obj) => AppPrivateKey.FromSecureString(_privateKey).Sign(obj); + public Task AddTransactionAsync(IAction[] values, CancellationToken cancellationToken) => AddTransactionAsync( AppPrivateKey.FromSecureString(_privateKey), values, cancellationToken); diff --git a/src/shared/LibplanetConsole.Evidence/Serializations/EvidenceInfo.cs b/src/shared/LibplanetConsole.Evidence/Serializations/EvidenceInfo.cs new file mode 100644 index 00000000..82bfead6 --- /dev/null +++ b/src/shared/LibplanetConsole.Evidence/Serializations/EvidenceInfo.cs @@ -0,0 +1,29 @@ +using Libplanet.Types.Evidence; +using LibplanetConsole.Common; + +namespace LibplanetConsole.Evidence.Serializations; + +public readonly record struct EvidenceInfo +{ + public string Type { get; init; } + + public string Id { get; init; } + + public AppAddress TargetAddress { get; init; } + + public long Height { get; init; } + + public DateTimeOffset Timestamp { get; init; } + + public static explicit operator EvidenceInfo(EvidenceBase evidence) + { + return new EvidenceInfo + { + Type = evidence.GetType().Name, + Id = evidence.Id.ToString(), + Height = evidence.Height, + TargetAddress = (AppAddress)evidence.TargetAddress, + Timestamp = evidence.Timestamp, + }; + } +} diff --git a/src/shared/LibplanetConsole.Evidence/Services/IEvidenceService.cs b/src/shared/LibplanetConsole.Evidence/Services/IEvidenceService.cs new file mode 100644 index 00000000..2e3502f7 --- /dev/null +++ b/src/shared/LibplanetConsole.Evidence/Services/IEvidenceService.cs @@ -0,0 +1,16 @@ +using LibplanetConsole.Evidence.Serializations; + +namespace LibplanetConsole.Evidence.Services; + +public interface IEvidenceService +{ + Task AddEvidenceAsync(CancellationToken cancellationToken); + + Task GetEvidenceAsync(long height, CancellationToken cancellationToken); + + Task ViolateAsync(CancellationToken cancellationToken); + +#if LIBPLANET_DPOS + Task UnjailAsync(byte[] signarue, CancellationToken cancellationToken); +#endif // LIBPLANET_DPOS +} diff --git a/src/shared/LibplanetConsole.Evidence/TestEvidence.cs b/src/shared/LibplanetConsole.Evidence/TestEvidence.cs new file mode 100644 index 00000000..d7359d9a --- /dev/null +++ b/src/shared/LibplanetConsole.Evidence/TestEvidence.cs @@ -0,0 +1,42 @@ +using System.Globalization; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Evidence; + +namespace LibplanetConsole.Evidence; + +public sealed class TestEvidence : EvidenceBase, IEquatable +{ + public TestEvidence( + long height, + Address validatorAddress, + DateTimeOffset timestamp) + : base(height, validatorAddress, timestamp) + { + } + + public TestEvidence(IValue bencoded) + : base(bencoded) + { + } + + public Address ValidatorAddress => TargetAddress; + + public bool Equals(TestEvidence? other) => base.Equals(other); + + public override bool Equals(object? obj) + => obj is TestEvidence other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine( + Height, + ValidatorAddress.ToString(), + Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture)); + + protected override Dictionary OnBencoded(Dictionary dictionary) + => dictionary; + + protected override void OnVerify(IEvidenceContext evidenceContext) + { + } +}