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..c72b0e2c4ef --- /dev/null +++ b/test/Libplanet.Explorer.Tests/Queries/EvidenceQueryTest.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Bencodex.Types; +using GraphQL; +using GraphQL.Execution; +using GraphQL.Types; +using Libplanet.Action; +using Libplanet.Action.Sys; +using Libplanet.Blockchain; +using Libplanet.Blockchain.Policies; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Libplanet.Types.Tx; +using Libplanet.Explorer.Queries; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Xunit; +using static Libplanet.Explorer.Tests.GraphQLTestUtils; +using Libplanet.Action.Loader; +using Libplanet.Types.Evidence; +using Libplanet.Types.Blocks; +using System.Globalization; + +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 ExecuteAsync() + { + var blocks = GetBlocks().ToArray(); + var block = blocks[System.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 evidenceData = (object[])resultDict["committedEvidence"]; + Assert.Equal(block.Evidence.Count, evidenceData.Length); + + for (var i = 0; i < block.Evidence.Count; i++) + { + var evidence = block.Evidence[i]; + var data = (IDictionary)evidenceData[i]; + Assert.Equal(evidence.Id.ToString(), data["id"]); + Assert.Equal(evidence.GetType().FullName, data["type"]); + Assert.Equal(evidence.Height, data["height"]); + Assert.Equal(evidence.TargetAddress.ToString(), data["targetAddress"]); + Assert.Equal( + new DateTimeOffsetGraphType().Serialize(evidence.Timestamp), + data["timestamp"]); + // Assert.Equal(block.Evidence[i].Id.ToString(), ((IList>)resultDict["committedEvidence"])[i]["id"]); + } + } + + 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/test/Libplanet.Explorer.Tests/Queries/TransactionQueryTest.cs b/test/Libplanet.Explorer.Tests/Queries/TransactionQueryTest.cs index a44d928054e..e609f8523e0 100644 --- a/test/Libplanet.Explorer.Tests/Queries/TransactionQueryTest.cs +++ b/test/Libplanet.Explorer.Tests/Queries/TransactionQueryTest.cs @@ -21,6 +21,7 @@ using Xunit; using static Libplanet.Explorer.Tests.GraphQLTestUtils; using Libplanet.Action.Loader; +using Libplanet.Types.Evidence; namespace Libplanet.Explorer.Tests.Queries; diff --git a/tools/Libplanet.Explorer/Queries/EvidenceQuery.cs b/tools/Libplanet.Explorer/Queries/EvidenceQuery.cs index 74e4f4a7679..3f1977d51d4 100644 --- a/tools/Libplanet.Explorer/Queries/EvidenceQuery.cs +++ b/tools/Libplanet.Explorer/Queries/EvidenceQuery.cs @@ -20,35 +20,32 @@ public EvidenceQuery() Field>>>( "committedEvidence", arguments: new QueryArguments( - new QueryArgument - { - Name = "blockHash", - DefaultValue = null, - }, - new QueryArgument + new QueryArgument { Name = "hash" }, + new QueryArgument { Name = "index" } + ), + resolve: context => + { + string hash = context.GetArgument("hash"); + long? index = context.GetArgument("index", null); + + 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 => - { - var blockHash = context.GetArgument("blockHash"); - bool desc = context.GetArgument("desc"); - int offset = context.GetArgument("offset"); - int? limit = context.GetArgument("limit"); - return ExplorerQuery.ListCommitEvidence(blockHash, desc, offset, limit); + throw new ExecutionError("Unexpected block query"); } ); @@ -87,8 +84,7 @@ public EvidenceQuery() 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..d83245e9c6b 100644 --- a/tools/Libplanet.Explorer/Queries/ExplorerQuery.cs +++ b/tools/Libplanet.Explorer/Queries/ExplorerQuery.cs @@ -131,18 +131,18 @@ internal static IEnumerable ListPendingEvidence( return evidence; } - internal static IEnumerable ListCommitEvidence( - BlockHash? blockHash, bool desc, int offset, int? limit) + internal static IEnumerable ListCommitEvidence(BlockHash blockHash) { 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); + var block = blockChain[blockHash]; + return block.Evidence; + } - return evidence; + internal static IEnumerable ListCommitEvidence(long index) + { + var blockChain = Chain; + var block = blockChain[index]; + return block.Evidence; } internal static Block? GetBlockByHash(BlockHash hash) => Store.GetBlock(hash);