diff --git a/CHANGES.md b/CHANGES.md index 78db4731cb4..c3d027b88b7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,16 @@ Libplanet changelog Version 5.4.3 ------------- -To be released. +### Backward-incompatible API changes + + - (Libplanet.Explorer) The parameter required by the `committedEvidence` query + was changed to hash or index. [[#4014]] + - (Libplanet.Explorer) The `desc`, `offset`, and `limit` parameters were + removed from the `committedEvidence` query. [[#4014]] + - (Libplanet.Explorer) Fixed an issue where an exception was thrown when + running evidence queries with id. [[#4014]] + +[#4014]: https://github.com/planetarium/libplanet/pull/4014 Version 5.4.2 diff --git a/src/Libplanet/Blockchain/Policies/BlockPolicy.cs b/src/Libplanet/Blockchain/Policies/BlockPolicy.cs index 63b1f24196a..5700b504eb3 100644 --- a/src/Libplanet/Blockchain/Policies/BlockPolicy.cs +++ b/src/Libplanet/Blockchain/Policies/BlockPolicy.cs @@ -160,7 +160,7 @@ public BlockPolicy( if (block.Evidence.Any(evidence => evidence.Height < evidenceExpirationHeight)) { return new InvalidBlockEvidencePendingDurationException( - $"Block #{block.Index} {block.Hash} includes evidence" + + $"Block #{block.Index} {block.Hash} includes evidence " + $"that is older than expiration height {evidenceExpirationHeight}"); } diff --git a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs index 79487a95b6d..319a73be956 100644 --- a/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs +++ b/test/Libplanet.Explorer.Tests/GeneratedBlockChainFixture.cs @@ -16,6 +16,7 @@ using Libplanet.Types.Tx; using Libplanet.Store; using Libplanet.Store.Trie; +using Libplanet.Tests.Blockchain.Evidence; namespace Libplanet.Explorer.Tests; @@ -29,6 +30,8 @@ public class GeneratedBlockChainFixture public int MaxTxCount { get; } + public int MaxEvidenceCount { get; } + public ImmutableDictionary> MinedBlocks { get; private set; } @@ -43,7 +46,8 @@ public GeneratedBlockChainFixture( int maxTxCount = 20, int privateKeyCount = 10, ImmutableArray>>? - txActionsForSuffixBlocks = null) + txActionsForSuffixBlocks = null, + int maxEvidenceCount = 2) { txActionsForSuffixBlocks ??= ImmutableArray>>.Empty; @@ -65,6 +69,7 @@ public GeneratedBlockChainFixture( .ToImmutableDictionary( key => key.Address, key => ImmutableArray.Empty); + MaxEvidenceCount = maxEvidenceCount; var privateKey = new PrivateKey(); var policy = new BlockPolicy( @@ -105,7 +110,7 @@ public GeneratedBlockChainFixture( while (Chain.Count < blockCount) { - AddBlock(GetRandomTransactions()); + AddBlock(GetRandomTransactions(), GetRandomEvidence(height: Chain.Count - 1)); } if (txActionsForSuffixBlocks is { } txActionsForSuffixBlocksVal) @@ -113,14 +118,16 @@ public GeneratedBlockChainFixture( foreach (var actionsForTransactions in txActionsForSuffixBlocksVal) { var pk = PrivateKeys[Random.Next(PrivateKeys.Length)]; - AddBlock(actionsForTransactions + var txs = actionsForTransactions .Select(actions => Transaction.Create( nonce: Chain.GetNextTxNonce(pk.Address), privateKey: pk, genesisHash: Chain.Genesis.Hash, actions: actions.ToPlainValues())) - .ToImmutableArray()); + .ToImmutableArray(); + var evs = ImmutableArray.Empty; + AddBlock(txs, evs); } } } @@ -159,6 +166,21 @@ private Transaction GetRandomTransaction(PrivateKey pk, long nonce) gasLimit: null); } + private ImmutableArray GetRandomEvidence(long height) + { + return Enumerable + .Range(0, Random.Next(MaxEvidenceCount)) + .Select(_ => + { + return new TestEvidence( + height: height, + validatorAddress: new PrivateKey().Address, + timestamp: DateTimeOffset.UtcNow); + }) + .OrderBy(ev => ev.Id) + .ToImmutableArray(); + } + private ImmutableArray GetRandomActions() { return Enumerable @@ -167,7 +189,8 @@ private ImmutableArray GetRandomActions() .ToImmutableArray(); } - private void AddBlock(ImmutableArray transactions) + private void AddBlock( + ImmutableArray transactions, ImmutableArray evidence) { var proposer = PrivateKeys[Random.Next(PrivateKeys.Length)]; var block = Chain.EvaluateAndSign( @@ -179,9 +202,9 @@ private void AddBlock(ImmutableArray transactions) Chain.Tip.Hash, BlockContent.DeriveTxHash(transactions), Chain.Store.GetChainBlockCommit(Chain.Store.GetCanonicalChainId()!.Value), - evidenceHash: null), + evidenceHash: BlockContent.DeriveEvidenceHash(evidence)), transactions, - evidence: Array.Empty()).Propose(), + evidence: evidence).Propose(), proposer); Chain.Append( block, diff --git a/test/Libplanet.Explorer.Tests/GraphTypes/EvidenceIdTypeTest.cs b/test/Libplanet.Explorer.Tests/GraphTypes/EvidenceIdTypeTest.cs new file mode 100644 index 00000000000..5b6353bfc82 --- /dev/null +++ b/test/Libplanet.Explorer.Tests/GraphTypes/EvidenceIdTypeTest.cs @@ -0,0 +1,58 @@ +using System; +using GraphQL.Language.AST; +using Libplanet.Common; +using Libplanet.Explorer.GraphTypes; +using Libplanet.Types.Evidence; +using Xunit; + +namespace Libplanet.Explorer.Tests.GraphTypes +{ + public class EvidenceIdTypeTest : ScalarGraphTypeTestBase + { + [Fact] + public void ParseLiteral() + { + Assert.Null(_type.ParseLiteral(new NullValue())); + + var bytes = TestUtils.GetRandomBytes(EvidenceId.Size); + var evidenceId = new EvidenceId(bytes); + var hex = ByteUtil.Hex(bytes); + Assert.Equal( + evidenceId, + Assert.IsType(_type.ParseLiteral(new StringValue(hex)))); + + Assert.Throws( + () => _type.ParseLiteral(new LongValue(1234))); + Assert.Throws( + () => _type.ParseValue(new StringValue("evidenceId"))); + } + + [Fact] + public void ParseValue() + { + Assert.Null(_type.ParseValue(null)); + + var bytes = TestUtils.GetRandomBytes(EvidenceId.Size); + var evidenceId = new EvidenceId(bytes); + var hex = ByteUtil.Hex(bytes); + Assert.Equal(evidenceId, _type.ParseValue(hex)); + + Assert.Throws(() => _type.ParseValue(0)); + Assert.Throws(() => _type.ParseValue(new EvidenceId())); + Assert.Throws(() => _type.ParseValue(new object())); + } + + [Fact] + public void Serialize() + { + var bytes = TestUtils.GetRandomBytes(EvidenceId.Size); + var evidenceId = new EvidenceId(bytes); + var hex = ByteUtil.Hex(bytes); + Assert.Equal(hex, _type.Serialize(evidenceId)); + + Assert.Throws(() => _type.Serialize(0)); + Assert.Throws(() => _type.Serialize("")); + Assert.Throws(() => _type.Serialize(new object())); + } + } +} diff --git a/test/Libplanet.Explorer.Tests/Queries/EvidenceQueryTest.cs b/test/Libplanet.Explorer.Tests/Queries/EvidenceQueryTest.cs new file mode 100644 index 00000000000..a39628084cf --- /dev/null +++ b/test/Libplanet.Explorer.Tests/Queries/EvidenceQueryTest.cs @@ -0,0 +1,189 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GraphQL.Execution; +using Libplanet.Explorer.Queries; +using Xunit; +using static Libplanet.Explorer.Tests.GraphQLTestUtils; +using Libplanet.Types.Blocks; +using System; +using Libplanet.Tests.Blockchain.Evidence; +using Libplanet.Crypto; +using Libplanet.Types.Evidence; + +namespace Libplanet.Explorer.Tests.Queries; + +public class EvidenceQueryTest +{ + private readonly GeneratedBlockChainFixture _fixture; + private readonly MockBlockChainContext _source; + private readonly EvidenceQuery _queryGraph; + + public EvidenceQueryTest() + { + _fixture = new GeneratedBlockChainFixture(seed: 0); + _source = new MockBlockChainContext(_fixture.Chain); + _queryGraph = new EvidenceQuery(); + var _ = new ExplorerQuery(_source); + } + + [Fact] + public async Task ExecuteCommittedEvidenceByHeightAsync() + { + var blocks = GetBlocks().ToArray(); + var block = blocks[Random.Shared.Next(blocks.Length)]; + + var result = await ExecuteQueryAsync(@$"{{ + committedEvidence( + index: {block.Index}) {{ + id + type + height + targetAddress + timestamp + }} + }}", _queryGraph, source: _source); + Assert.Null(result.Errors); + var resultData = Assert.IsAssignableFrom(result.Data); + var resultDict = + Assert.IsAssignableFrom>(resultData.ToValue()); + var resultEvidence = Assert.IsAssignableFrom(resultDict["committedEvidence"]); + + for (var i = 0; i < block.Evidence.Count; i++) + { + var evidence = block.Evidence[i]; + var resultEvidenceDict + = Assert.IsAssignableFrom>(resultEvidence[i]); + Assert.Equal(evidence.Id.ToString(), resultEvidenceDict["id"]); + Assert.Equal(evidence.GetType().FullName, resultEvidenceDict["type"]); + Assert.Equal(evidence.Height, resultEvidenceDict["height"]); + Assert.Equal(evidence.TargetAddress.ToString(), resultEvidenceDict["targetAddress"]); + Assert.Equal( + evidence.Timestamp, DateTimeOffset.Parse($"{resultEvidenceDict["timestamp"]}")); + } + } + + [Fact] + public async Task ExecuteCommittedEvidenceByHashAsync() + { + var blocks = GetBlocks().ToArray(); + var block = blocks[Random.Shared.Next(blocks.Length)]; + + var result = await ExecuteQueryAsync(@$"{{ + committedEvidence( + hash: ""{block.Hash}"") {{ + id + type + height + targetAddress + timestamp + }} + }}", _queryGraph, source: _source); + Assert.Null(result.Errors); + var resultData = Assert.IsAssignableFrom(result.Data); + var resultDict = + Assert.IsAssignableFrom>(resultData.ToValue()); + var resultEvidence = Assert.IsAssignableFrom(resultDict["committedEvidence"]); + + for (var i = 0; i < block.Evidence.Count; i++) + { + var evidence = block.Evidence[i]; + var resultEvidenceDict + = Assert.IsAssignableFrom>(resultEvidence[i]); + Assert.Equal(evidence.Id.ToString(), resultEvidenceDict["id"]); + Assert.Equal(evidence.GetType().FullName, resultEvidenceDict["type"]); + Assert.Equal(evidence.Height, resultEvidenceDict["height"]); + Assert.Equal(evidence.TargetAddress.ToString(), resultEvidenceDict["targetAddress"]); + Assert.Equal( + evidence.Timestamp, DateTimeOffset.Parse($"{resultEvidenceDict["timestamp"]}")); + } + } + + [Fact] + public async Task ExecutePendingEvidenceAsync() + { + var evidenceList = new List + { + new TestEvidence( + height: _fixture.Chain.Count - 3, + validatorAddress: new PrivateKey().Address, + timestamp: DateTimeOffset.UtcNow) + }; + + foreach (var evidence in evidenceList) + { + _fixture.Chain.AddEvidence(evidence); + } + + var result = await ExecuteQueryAsync(@$"{{ + pendingEvidence {{ + id + type + height + targetAddress + timestamp + }} + }}", _queryGraph, source: _source); + Assert.Null(result.Errors); + var resultData = Assert.IsAssignableFrom(result.Data); + var resultDict = + Assert.IsAssignableFrom>(resultData.ToValue()); + var resultEvidence = Assert.IsAssignableFrom(resultDict["pendingEvidence"]); + + for (var i = 0; i < evidenceList.Count; i++) + { + var evidence = evidenceList[i]; + var resultEvidenceDict + = Assert.IsAssignableFrom>(resultEvidence[i]); + Assert.Equal(evidence.Id.ToString(), resultEvidenceDict["id"]); + Assert.Equal(evidence.GetType().FullName, resultEvidenceDict["type"]); + Assert.Equal(evidence.Height, resultEvidenceDict["height"]); + Assert.Equal(evidence.TargetAddress.ToString(), resultEvidenceDict["targetAddress"]); + Assert.Equal( + evidence.Timestamp, DateTimeOffset.Parse($"{resultEvidenceDict["timestamp"]}")); + } + } + + [Fact] + public async Task ExecuteEvidenceByIdAsync() + { + var blocks = GetBlocks().ToArray(); + var block = blocks[Random.Shared.Next(blocks.Length)]; + var evidence = block.Evidence[Random.Shared.Next(block.Evidence.Count)]; + + var result = await ExecuteQueryAsync(@$"{{ + evidence(id: ""{evidence.Id}"") {{ + id + type + height + targetAddress + timestamp + }} + }}", _queryGraph, source: _source); + Assert.Null(result.Errors); + var resultData = Assert.IsAssignableFrom(result.Data); + var resultDict = + Assert.IsAssignableFrom>(resultData.ToValue()); + + var resultEvidenceDict + = Assert.IsAssignableFrom>(resultDict["evidence"]); + Assert.Equal(evidence.Id.ToString(), resultEvidenceDict["id"]); + Assert.Equal(evidence.GetType().FullName, resultEvidenceDict["type"]); + Assert.Equal(evidence.Height, resultEvidenceDict["height"]); + Assert.Equal(evidence.TargetAddress.ToString(), resultEvidenceDict["targetAddress"]); + Assert.Equal( + evidence.Timestamp, DateTimeOffset.Parse($"{resultEvidenceDict["timestamp"]}")); + } + + private IEnumerable GetBlocks() + { + for (var i = 0; i < _fixture.Chain.Count; i++) + { + var block = _fixture.Chain[i]; + if (block.Evidence.Count > 0) + { + yield return block; + } + } + } +} diff --git a/tools/Libplanet.Explorer/Queries/EvidenceQuery.cs b/tools/Libplanet.Explorer/Queries/EvidenceQuery.cs index 74e4f4a7679..71d33094aae 100644 --- a/tools/Libplanet.Explorer/Queries/EvidenceQuery.cs +++ b/tools/Libplanet.Explorer/Queries/EvidenceQuery.cs @@ -1,7 +1,5 @@ -using System; using GraphQL; using GraphQL.Types; -using Libplanet.Common; using Libplanet.Explorer.GraphTypes; using Libplanet.Types.Blocks; using Libplanet.Types.Evidence; @@ -20,75 +18,47 @@ public EvidenceQuery() Field>>>( "committedEvidence", arguments: new QueryArguments( - new QueryArgument - { - Name = "blockHash", - DefaultValue = null, - }, - new QueryArgument - { - Name = "desc", - DefaultValue = false, - }, - new QueryArgument - { - Name = "offset", - DefaultValue = 0, - }, - new QueryArgument - { - Name = "limit", - DefaultValue = MaxLimit, - } + new QueryArgument { Name = "hash" }, + new QueryArgument { Name = "index" } ), resolve: context => { - var blockHash = context.GetArgument("blockHash"); - bool desc = context.GetArgument("desc"); - int offset = context.GetArgument("offset"); - int? limit = context.GetArgument("limit"); + string hash = context.GetArgument("hash"); + long? index = context.GetArgument("index", null); - return ExplorerQuery.ListCommitEvidence(blockHash, desc, offset, limit); - } - ); - - Field>>>( - "pendingEvidence", - arguments: new QueryArguments( - new QueryArgument + if (!(hash is null ^ index is null)) { - Name = "desc", - DefaultValue = false, - }, - new QueryArgument + throw new ExecutionError( + "The parameters hash and index are mutually exclusive; " + + "give only one at a time."); + } + + if (hash is { } nonNullHash) { - Name = "offset", - DefaultValue = 0, - }, - new QueryArgument + return ExplorerQuery.ListCommitEvidence(BlockHash.FromString(nonNullHash)); + } + + if (index is { } nonNullIndex) { - Name = "limit", - DefaultValue = MaxLimit, + return ExplorerQuery.ListCommitEvidence(nonNullIndex); } - ), - resolve: context => - { - bool desc = context.GetArgument("desc"); - int offset = context.GetArgument("offset"); - int? limit = context.GetArgument("limit", null); - return ExplorerQuery.ListPendingEvidence(desc, offset, limit); + throw new ExecutionError("Unexpected block query"); } ); + Field>>>( + "pendingEvidence", + resolve: context => ExplorerQuery.ListPendingEvidence() + ); + Field( "Evidence", arguments: new QueryArguments( new QueryArgument { Name = "id" } ), resolve: context => ExplorerQuery.GetEvidence( - new EvidenceId(ByteUtil.ParseHex(context.GetArgument("id") - ?? throw new ExecutionError("Given id cannot be null.")))) + context.GetArgument("id")) ); } } diff --git a/tools/Libplanet.Explorer/Queries/ExplorerQuery.cs b/tools/Libplanet.Explorer/Queries/ExplorerQuery.cs index c992ed71df3..2c20b651554 100644 --- a/tools/Libplanet.Explorer/Queries/ExplorerQuery.cs +++ b/tools/Libplanet.Explorer/Queries/ExplorerQuery.cs @@ -111,38 +111,24 @@ internal static IEnumerable ListStagedTransactions( return stagedTxs; } - internal static IEnumerable ListPendingEvidence( - bool desc, int offset, int? limit) + internal static IEnumerable ListPendingEvidence() { - if (offset < 0) - { - throw new ArgumentOutOfRangeException( - nameof(offset), - $"{nameof(ListPendingEvidence)} doesn't support negative offset."); - } - var blockChain = Chain; - var comparer = desc ? EvidenceIdComparer.Descending : EvidenceIdComparer.Ascending; - var evidence = blockChain.GetPendingEvidence() - .Skip(offset) - .Take(limit ?? int.MaxValue) - .OrderBy(ev => ev.Id, comparer); + return blockChain.GetPendingEvidence(); + } - return evidence; + internal static IEnumerable ListCommitEvidence(BlockHash blockHash) + { + var blockChain = Chain; + var block = blockChain[blockHash]; + return block.Evidence; } - internal static IEnumerable ListCommitEvidence( - BlockHash? blockHash, bool desc, int offset, int? limit) + internal static IEnumerable ListCommitEvidence(long index) { var blockChain = Chain; - var block = blockHash != null ? blockChain[blockHash.Value] : blockChain.Tip; - var comparer = desc ? EvidenceIdComparer.Descending : EvidenceIdComparer.Ascending; - var evidence = block.Evidence - .Skip(offset) - .Take(limit ?? int.MaxValue) - .OrderBy(ev => ev.Id, comparer); - - return evidence; + var block = blockChain[index]; + return block.Evidence; } internal static Block? GetBlockByHash(BlockHash hash) => Store.GetBlock(hash);