diff --git a/CHANGES.md b/CHANGES.md index 8b2da7def2a..3b60b535b95 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,27 +4,30 @@ Libplanet changelog Version 0.14.0 -------------- -To be released. - -### Backward-incompatible API changes - -### Backward-incompatible network protocol changes - -### Backward-incompatible storage format changes +Released on Aug 5, 2021. ### Added APIs - Added `NonblockRenderer` class. [[#1402], [#1422]] - Added `NonblockActionRenderer` class. [[#1402], [#1422]] -### Behavioral changes +[#1402]: https://github.com/planetarium/libplanet/issues/1402 +[#1422]: https://github.com/planetarium/libplanet/pull/1422 -### Bug fixes -### CLI tools +Version 0.13.2 +-------------- -[#1402]: https://github.com/planetarium/libplanet/issues/1402 -[#1422]: https://github.com/planetarium/libplanet/pull/1422 +Released on Aug 5, 2021. + + - When a reorg happens, `Swarm` now broadcasts a reorged chain tip first + before rendering. [[#1385], [#1415]] + - Fixed a bug where `TurnClient` hadn't been recovered when TURN connection + had been disconnected. [[#1424]] + +[#1385]: https://github.com/planetarium/libplanet/issues/1385 +[#1415]: https://github.com/planetarium/libplanet/pull/1415 +[#1424]: https://github.com/planetarium/libplanet/pull/1424 Version 0.13.1 diff --git a/Libplanet.Stun/Stun/TurnClient.cs b/Libplanet.Stun/Stun/TurnClient.cs index eea256afe58..f4bf115d619 100644 --- a/Libplanet.Stun/Stun/TurnClient.cs +++ b/Libplanet.Stun/Stun/TurnClient.cs @@ -74,9 +74,7 @@ public TurnClient( public async Task InitializeTurnAsync(CancellationToken cancellationToken) { _control = new TcpClient(); -#pragma warning disable PC001 // API not supported on all platforms - _control.Connect(_host, _port); -#pragma warning restore PC001 // API not supported on all platforms + await _control.ConnectAsync(_host, _port); _processMessage = ProcessMessage(_turnTaskCts.Token); BehindNAT = await IsBehindNAT(cancellationToken); @@ -119,7 +117,19 @@ public async Task ReconnectTurn(int listenPort, CancellationToken cancellationTo ClearSession(); _turnTaskCts = new CancellationTokenSource(); - await InitializeTurnAsync(cancellationToken); + try + { + await InitializeTurnAsync(cancellationToken); + } + catch (Exception e) + { + _logger.Error( + e, + "Failed to initialize due to error ({Exception}); retry...", + e + ); + await Task.Delay(1000, cancellationToken); + } } } } @@ -384,24 +394,14 @@ await stream.WriteAsync( private async Task ProcessMessage(CancellationToken cancellationToken) { - while (!cancellationToken.IsCancellationRequested || _control.Connected) + while (!cancellationToken.IsCancellationRequested && _control.Connected) { - NetworkStream stream = _control.GetStream(); - try { + NetworkStream stream = _control.GetStream(); StunMessage message; - try - { - message = await StunMessage.ParseAsync(stream, cancellationToken); - _logger.Debug("Stun Message is: {message}", message); - } - catch (TurnClientException e) - { - _logger.Error(e, "Failed to parse StunMessage. {e}", e); - ClearResponses(); - break; - } + message = await StunMessage.ParseAsync(stream, cancellationToken); + _logger.Debug("Parsed " + nameof(StunMessage) + ": {Message}", message); if (message is ConnectionAttempt attempt) { @@ -415,6 +415,12 @@ private async Task ProcessMessage(CancellationToken cancellationToken) tcs.TrySetResult(message); } } + catch (TurnClientException e) + { + _logger.Error(e, "Failed to parse " + nameof(StunMessage) + ": {Exception}", e); + ClearResponses(); + break; + } catch (Exception e) { _logger.Error( diff --git a/Libplanet.Tests/Blockchain/BlockChainTest.cs b/Libplanet.Tests/Blockchain/BlockChainTest.cs index ea3597ffc8f..b118027a601 100644 --- a/Libplanet.Tests/Blockchain/BlockChainTest.cs +++ b/Libplanet.Tests/Blockchain/BlockChainTest.cs @@ -329,7 +329,7 @@ IEnumerable NonRehearsalExecutions() => fx.GenesisBlock, renderers: new[] { renderer } ); - chain.Swap(newChain, true); + chain.Swap(newChain, true)(); Assert.Empty(renderer.ActionRecords); Assert.Empty(NonRehearsalExecutions()); } @@ -760,7 +760,7 @@ public async void GetBlockLocator() [InlineData(false)] public void Swap(bool render) { - Assert.Throws(() => _blockChain.Swap(null, render)); + Assert.Throws(() => _blockChain.Swap(null, render)()); (var addresses, Transaction[] txs1) = MakeFixturesForAppendTests(); @@ -882,7 +882,7 @@ public void Swap(bool render) Guid previousChainId = _blockChain.Id; _renderer.ResetRecords(); - _blockChain.Swap(fork, render); + _blockChain.Swap(fork, render)(); Assert.Empty(_blockChain.Store.IterateIndexes(previousChainId)); Assert.Empty(_blockChain.Store.ListTxNonces(previousChainId)); @@ -960,7 +960,7 @@ public void SwapForSameTip(bool render) { BlockChain fork = _blockChain.Fork(_blockChain.Tip.Hash); IReadOnlyList> prevRecords = _renderer.Records; - _blockChain.Swap(fork, render: render); + _blockChain.Swap(fork, render: render)(); // Render methods should be invoked if and only if the tip changes Assert.Equal(prevRecords, _renderer.Records); @@ -976,7 +976,7 @@ public async Task SwapWithoutReorg(bool render) // The lower chain goes to the higher chain [#N -> #N+1] await fork.MineBlock(default); IReadOnlyList.Reorg> prevRecords = _renderer.ReorgRecords; - _blockChain.Swap(fork, render: render); + _blockChain.Swap(fork, render: render)(); // RenderReorg() should be invoked if and only if the actual reorg happens Assert.Equal(prevRecords, _renderer.ReorgRecords); @@ -990,7 +990,7 @@ public async Task TreatGoingBackwardAsReorg() // The higher chain goes to the lower chain [#N -> #N-1] await _blockChain.MineBlock(default); IReadOnlyList.Reorg> prevRecords = _renderer.ReorgRecords; - _blockChain.Swap(fork, render: true); + _blockChain.Swap(fork, render: true)(); // RenderReorg() should be invoked if and only if the actual reorg happens Assert.Equal(prevRecords.Count + 2, _renderer.ReorgRecords.Count); @@ -1035,7 +1035,7 @@ public async Task ReorgIsUnableToHeterogenousChain(bool render) ); Assert.Throws(() => - _blockChain.Swap(chain2, render) + _blockChain.Swap(chain2, render)() ); } } @@ -1740,7 +1740,7 @@ private async void Reorged() Assert.Empty(_renderer.ReorgRecords); - _blockChain.Swap(fork, true); + _blockChain.Swap(fork, true)(); IReadOnlyList.Reorg> reorgRecords = _renderer.ReorgRecords; Assert.Equal(2, reorgRecords.Count); diff --git a/Libplanet.Tests/Blockchain/Renderers/AtomicActionRendererTest.cs b/Libplanet.Tests/Blockchain/Renderers/AtomicActionRendererTest.cs index d3334ba48b2..4ee0c75613e 100644 --- a/Libplanet.Tests/Blockchain/Renderers/AtomicActionRendererTest.cs +++ b/Libplanet.Tests/Blockchain/Renderers/AtomicActionRendererTest.cs @@ -71,7 +71,7 @@ public async Task Reorg() BlockChain @base = _fx.Chain.Fork(_fx.Genesis.Hash); await _fx.Mine(); _record.ResetRecords(); - _fx.Chain.Swap(@base, true); + _fx.Chain.Swap(@base, true)(); IReadOnlyList> records = _record.Records; Assert.Equal(7, records.Count); AssertTypeAnd.Reorg>(records[0], r => Assert.True(r.Begin)); diff --git a/Libplanet.Tests/Blockchain/Renderers/DelayedActionRendererTest.cs b/Libplanet.Tests/Blockchain/Renderers/DelayedActionRendererTest.cs index 9c045879c09..519fbffcecb 100644 --- a/Libplanet.Tests/Blockchain/Renderers/DelayedActionRendererTest.cs +++ b/Libplanet.Tests/Blockchain/Renderers/DelayedActionRendererTest.cs @@ -489,9 +489,9 @@ Block Branchpoint Assert.Equal(17, delayedRenderer.GetBufferedActionRendererCount()); Assert.Equal(0, delayedRenderer.GetBufferedActionUnRendererCount()); - chain.Swap(fork1, true); - chain.Swap(fork2, true); - chain.Swap(fork3, true); + chain.Swap(fork1, true)(); + chain.Swap(fork2, true)(); + chain.Swap(fork3, true)(); Assert.Equal(17, delayedRenderer.GetBufferedActionRendererCount()); Assert.Equal(15, delayedRenderer.GetBufferedActionUnRendererCount()); @@ -619,7 +619,7 @@ Block Branchpoint renderActions: false ); - chain.Swap(forked, true); + chain.Swap(forked, true)(); Assert.Equal(chain[2], delayedRenderer.Tip); Assert.Empty(reorgLogs); @@ -723,7 +723,7 @@ Block Branchpoint renderActions: false ); - chain.Swap(forked, true); + chain.Swap(forked, true)(); Assert.Equal(chain[1], delayedRenderer.Tip); Assert.Empty(reorgLogs); diff --git a/Libplanet.Tests/Libplanet.Tests.csproj b/Libplanet.Tests/Libplanet.Tests.csproj index 17e831d6305..dc182556c32 100644 --- a/Libplanet.Tests/Libplanet.Tests.csproj +++ b/Libplanet.Tests/Libplanet.Tests.csproj @@ -55,6 +55,10 @@ + + + + diff --git a/Libplanet/Blockchain/BlockChain.Swap.cs b/Libplanet/Blockchain/BlockChain.Swap.cs new file mode 100644 index 00000000000..e2382ee8a98 --- /dev/null +++ b/Libplanet/Blockchain/BlockChain.Swap.cs @@ -0,0 +1,461 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Libplanet.Action; +using Libplanet.Blockchain.Renderers; +using Libplanet.Blocks; +using Libplanet.Store; +using Libplanet.Tx; + +namespace Libplanet.Blockchain +{ + public partial class BlockChain + { + // FIXME it's very dangerous because replacing Id means + // ALL blocks (referenced by MineBlock(), etc.) will be changed. + internal System.Action Swap( + BlockChain other, + bool render, + StateCompleterSet? stateCompleters = null) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + + // As render/unrender processing requires every step's states from the branchpoint + // to the new/stale tip, incomplete states need to be complemented anyway... + StateCompleterSet completers = stateCompleters ?? StateCompleterSet.Recalculate; + + if (Tip.Equals(other.Tip)) + { + // If it's swapped for a chain with the same tip, it means there is no state change. + // Hence render is unnecessary. + render = false; + } + else + { + _logger.Debug( + "The blockchain was reorged from " + + "{OldChainId} (#{OldTipIndex} {OldTipHash}) " + + "to {NewChainId} (#{NewTipIndex} {NewTipHash}).", + Id, + Tip.Index, + Tip.Hash, + other.Id, + other.Tip.Index, + other.Tip.Hash); + } + + System.Action renderSwap = () => { }; + + _rwlock.EnterUpgradeableReadLock(); + try + { + // Finds the branch point. + Block branchpoint = FindTopCommon(this, other); + + if (branchpoint is null) + { + const string msg = + "A chain cannot be reorged into a heterogeneous chain with a " + + "different genesis."; + throw new InvalidGenesisBlockException(Genesis.Hash, other.Genesis.Hash, msg); + } + + _logger.Debug( + "The branchpoint is #{BranchpointIndex} {BranchpointHash}.", + branchpoint.Index, + branchpoint + ); + + Block oldTip = Tip ?? Genesis, newTip = other.Tip ?? other.Genesis; + + ImmutableList> rewindPath = + GetRewindPath(this, branchpoint.Hash); + ImmutableList> fastForwardPath = + GetFastForwardPath(other, branchpoint.Hash); + + // If there is no rewind, it is not considered as a reorg. + bool reorg = rewindPath.Count > 0; + + _rwlock.EnterWriteLock(); + try + { + IEnumerable> + GetTxsWithRange(BlockChain chain, Block start, Block end) + => Enumerable + .Range((int)start.Index + 1, (int)(end.Index - start.Index)) + .SelectMany(x => chain[x].Transactions); + + // It assumes reorg is small size. If it was big, this may be heavy task. + ImmutableHashSet> unstagedTxs = + GetTxsWithRange(this, branchpoint, Tip).ToImmutableHashSet(); + ImmutableHashSet> stageTxs = + GetTxsWithRange(other, branchpoint, other.Tip).ToImmutableHashSet(); + ImmutableHashSet> restageTxs = unstagedTxs.Except(stageTxs); + foreach (Transaction restageTx in restageTxs) + { + StagePolicy.Stage(this, restageTx); + } + + Guid obsoleteId = Id; + Id = other.Id; + Store.SetCanonicalChainId(Id); + _blocks = new BlockSet(Policy.GetHashAlgorithm, Store); + TipChanged?.Invoke(this, (oldTip, newTip)); + + Store.DeleteChainId(obsoleteId); + } + finally + { + _rwlock.ExitWriteLock(); + } + + renderSwap = () => RenderSwap( + render: render, + oldTip: oldTip, + newTip: newTip, + branchpoint: branchpoint, + rewindPath: rewindPath, + fastForwardPath: fastForwardPath, + stateCompleters: completers); + } + finally + { + _rwlock.ExitUpgradeableReadLock(); + } + + return renderSwap; + } + + internal void RenderSwap( + bool render, + Block oldTip, + Block newTip, + Block branchpoint, + IReadOnlyList> rewindPath, + IReadOnlyList> fastForwardPath, + StateCompleterSet stateCompleters) + { + // If there is no rewind, it is not considered as a reorg. + bool reorg = rewindPath.Count > 0; + + if (render && reorg) + { + foreach (IRenderer renderer in Renderers) + { + renderer.RenderReorg( + oldTip: oldTip, + newTip: newTip, + branchpoint: branchpoint); + } + } + + RenderRewind( + render: render, + oldTip: oldTip, + newTip: newTip, + branchpoint: branchpoint, + rewindPath: rewindPath, + stateCompleters: stateCompleters); + + if (render) + { + foreach (IRenderer renderer in Renderers) + { + renderer.RenderBlock( + oldTip: oldTip, + newTip: newTip); + } + } + + RenderFastForward( + render: render, + oldTip: oldTip, + newTip: newTip, + branchpoint: branchpoint, + fastForwardPath: fastForwardPath, + stateCompleters: stateCompleters); + + if (render && reorg) + { + foreach (IRenderer renderer in Renderers) + { + renderer.RenderReorgEnd( + oldTip: oldTip, + newTip: newTip, + branchpoint: branchpoint); + } + } + } + + internal void RenderRewind( + bool render, + Block oldTip, + Block newTip, + Block branchpoint, + IReadOnlyList> rewindPath, + StateCompleterSet stateCompleters) + { + if (render && ActionRenderers.Any()) + { + // Unrender stale actions. + _logger.Debug("Unrendering abandoned actions..."); + + long count = 0; + + foreach (Block block in rewindPath) + { + ImmutableList evaluations = + ActionEvaluator.Evaluate(block, stateCompleters) + .ToImmutableList().Reverse(); + + count += UnrenderActions( + evaluations: evaluations, + block: block, + stateCompleters: stateCompleters); + } + + _logger.Debug( + $"{nameof(Swap)}() completed unrendering {{Actions}} actions.", + count); + } + } + + internal void RenderFastForward( + bool render, + Block oldTip, + Block newTip, + Block branchpoint, + IReadOnlyList> fastForwardPath, + StateCompleterSet stateCompleters) + { + if (render && ActionRenderers.Any()) + { + _logger.Debug("Rendering actions in new chain."); + + long count = 0; + foreach (Block block in fastForwardPath) + { + ImmutableList evaluations = + ActionEvaluator.Evaluate(block, stateCompleters).ToImmutableList(); + + count += RenderActions( + evaluations: evaluations, + block: block, + stateCompleters: stateCompleters); + } + + _logger.Debug( + $"{nameof(Swap)}() completed rendering {{Count}} actions.", + count); + + foreach (IActionRenderer renderer in ActionRenderers) + { + renderer.RenderBlockEnd(oldTip, newTip); + } + } + } + + /// + /// Render actions of the given . + /// + /// s of the block. If it is + /// null, evaluate actions of the again. + /// to render actions. + /// The strategy to complement incomplete block states. + /// by default. + /// The number of actions rendered. + internal long RenderActions( + IReadOnlyList evaluations, + Block block, + StateCompleterSet stateCompleters) + { + _logger.Debug("Render actions in block {blockIndex}: {block}", block?.Index, block); + + if (evaluations is null) + { + evaluations = ActionEvaluator.Evaluate(block, stateCompleters); + } + + long count = 0; + foreach (var evaluation in evaluations) + { + foreach (IActionRenderer renderer in ActionRenderers) + { + if (evaluation.Exception is null) + { + renderer.RenderAction( + evaluation.Action, + evaluation.InputContext.GetUnconsumedContext(), + evaluation.OutputStates); + } + else + { + renderer.RenderActionError( + evaluation.Action, + evaluation.InputContext.GetUnconsumedContext(), + evaluation.Exception); + } + + count++; + } + } + + return count; + } + + internal long UnrenderActions( + IReadOnlyList evaluations, + Block block, + StateCompleterSet stateCompleters) + { + _logger.Debug("Unender actions in block {blockIndex}: {block}", block?.Index, block); + + if (evaluations is null) + { + evaluations = + ActionEvaluator.Evaluate(block, stateCompleters) + .Reverse().ToImmutableList(); + } + + long count = 0; + foreach (ActionEvaluation evaluation in evaluations) + { + foreach (IActionRenderer renderer in ActionRenderers) + { + if (evaluation.Exception is null) + { + renderer.UnrenderAction( + evaluation.Action, + evaluation.InputContext.GetUnconsumedContext(), + evaluation.OutputStates); + } + else + { + renderer.UnrenderActionError( + evaluation.Action, + evaluation.InputContext.GetUnconsumedContext(), + evaluation.Exception); + } + } + + count++; + } + + return count; + } + + /// + /// Generates a list of s to traverse starting from + /// the tip of to reach . + /// + /// The to traverse. + /// The target to reach. + /// + /// An of s to traverse from + /// the tip of to reach excluding + /// the with as its hash. + /// + /// + /// + /// This is a reverse of . + /// + /// + /// As the genesis is always fixed, returned results never include the genesis. + /// + /// + private static ImmutableList> GetRewindPath( + BlockChain chain, + BlockHash targetHash) + { + if (!chain.ContainsBlock(targetHash)) + { + throw new KeyNotFoundException( + $"Given chain {chain.Id} must contain target hash {targetHash}"); + } + + Block target = chain[targetHash]; + List> path = new List>(); + + for ( + Block current = chain.Tip; + current.Index > target.Index && current.PreviousHash is { } ph; + current = chain[ph]) + { + path.Add(current); + } + + return path.ToImmutableList(); + } + + /// + /// Generates a list of s to traverse starting from + /// to reach the tip of . + /// + /// The to traverse. + /// The to start from. + /// + /// An of s to traverse + /// to reach the tip of from + /// excluding the with as its hash. + /// + /// + /// This is a reverse of . + /// + private static ImmutableList> GetFastForwardPath( + BlockChain chain, + BlockHash originHash) + { + if (!chain.ContainsBlock(originHash)) + { + throw new KeyNotFoundException( + $"Given chain {chain.Id} must contain origin hash {originHash}"); + } + + return GetRewindPath(chain, originHash).Reverse(); + } + + /// + /// Finds the top most common between chains + /// and . + /// + /// The first to compare. + /// The second to compare. + /// + /// The top most common between chains + /// and . If there is no such , + /// returns null instead. + /// + private static Block FindTopCommon(BlockChain c1, BlockChain c2) + { + if (!(c1.Tip is null)) + { + long shorterHeight = Math.Min(c1.Count, c2.Count) - 1; + Block b1 = c1[shorterHeight], b2 = c2[shorterHeight]; + + while (true) + { + if (b1.Equals(b2)) + { + return b1; + } + + if (b1.PreviousHash is { } b1ph && b2.PreviousHash is { } b2ph) + { + b1 = c1[b1ph]; + b2 = c2[b2ph]; + } + else + { + break; + } + } + } + + return null; + } + } +} diff --git a/Libplanet/Blockchain/BlockChain.cs b/Libplanet/Blockchain/BlockChain.cs index de57ba66601..140dfc0cb1a 100644 --- a/Libplanet/Blockchain/BlockChain.cs +++ b/Libplanet/Blockchain/BlockChain.cs @@ -1415,13 +1415,16 @@ internal void Append( if (ActionRenderers.Any()) { - foreach (IActionRenderer renderer in ActionRenderers) + if (renderActions) { - if (renderActions) - { - RenderActions(actionEvaluations, block, renderer, stateCompleters); - } + RenderActions( + evaluations: actionEvaluations, + block: block, + stateCompleters: (StateCompleterSet)stateCompleters); + } + foreach (IActionRenderer renderer in ActionRenderers) + { renderer.RenderBlockEnd(oldTip: prevTip ?? Genesis, newTip: block); } } @@ -1445,87 +1448,6 @@ internal void Append( } #pragma warning restore MEN003 - /// - /// Render actions from block index of . - /// - /// Index of the block to start rendering from. - /// The renderer to render actions. - /// The strategy to complement incomplete block states. - /// by default. - /// The number of actions rendered. - internal int RenderActionsInBlocks( - long offset, - IActionRenderer renderer, - StateCompleterSet? stateCompleters = null) - { - // Since rendering process requires every step's states, if required block states - // are incomplete they are complemented anyway: - stateCompleters ??= StateCompleterSet.Recalculate; - - // FIXME: We should consider the case where block count is larger than int.MaxSize. - int cnt = 0; - foreach (var block in IterateBlocks((int)offset)) - { - cnt += RenderActions(null, block, renderer, stateCompleters); - } - - return cnt; - } - - /// - /// Render actions of the given . - /// - /// s of the block. If it is - /// null, evaluate actions of the again. - /// to render actions. - /// The renderer to render actions. - /// The strategy to complement incomplete block states. - /// by default. - /// The number of actions rendered. - internal int RenderActions( - IReadOnlyList evaluations, - Block block, - IActionRenderer renderer, - StateCompleterSet? stateCompleters = null - ) - { - _logger.Debug("Render actions in block {blockIndex}: {block}", block?.Index, block); - - // Since rendering process requires every step's states, if required block states - // are incomplete they are complemented anyway: - stateCompleters ??= StateCompleterSet.Recalculate; - - if (evaluations is null) - { - evaluations = ActionEvaluator.Evaluate(block, stateCompleters.Value); - } - - int cnt = 0; - foreach (var evaluation in evaluations) - { - if (evaluation.Exception is null) - { - renderer.RenderAction( - evaluation.Action, - evaluation.InputContext.GetUnconsumedContext(), - evaluation.OutputStates - ); - } - else - { - renderer.RenderActionError( - evaluation.Action, - evaluation.InputContext.GetUnconsumedContext(), - evaluation.Exception - ); - } - - cnt++; - } - - return cnt; - } - /// /// Find an approximate to the topmost common ancestor between this /// and a given . @@ -1699,229 +1621,6 @@ internal BlockLocator GetBlockLocator(int threshold = 10) } } - // FIXME it's very dangerous because replacing Id means - // ALL blocks (referenced by MineBlock(), etc.) will be changed. - // we need to add a synchronization mechanism to handle this correctly. -#pragma warning disable MEN003 - internal void Swap( - BlockChain other, - bool render, - StateCompleterSet? stateCompleters = null - ) - { - if (other is null) - { - throw new ArgumentNullException(nameof(other)); - } - - // As render/unrender processing requires every step's states from the branchpoint - // to the new/stale tip, incomplete states need to be complemented anyway... - StateCompleterSet completers = stateCompleters ?? StateCompleterSet.Recalculate; - - if (Tip.Equals(other.Tip)) - { - // If it's swapped for a chain with the same tip, it means there is no state change. - // Hence render is unnecessary. - render = false; - } - else - { - _logger.Debug( - "The blockchain was reorged from " + - "{OldChainId} (#{OldTipIndex} {OldTipHash}) " + - "to {NewChainId} (#{NewTipIndex} {NewTipHash}).", - Id, - Tip.Index, - Tip.Hash, - other.Id, - other.Tip.Index, - other.Tip.Hash); - } - - // Finds the branch point. - Block topmostCommon = null; - if (!(Tip is null)) - { - long shorterHeight = - Math.Min(Count, other.Count) - 1; - Block t = this[shorterHeight], o = other[shorterHeight]; - - while (true) - { - if (t.Equals(o)) - { - topmostCommon = t; - break; - } - - if (t.PreviousHash is { } tp && o.PreviousHash is { } op) - { - t = this[tp]; - o = other[op]; - } - else - { - break; - } - } - } - - if (topmostCommon is null) - { - const string msg = - "A chain cannot be reorged into a heterogeneous chain which has " + - "no common genesis at all."; - throw new InvalidGenesisBlockException(Genesis.Hash, other.Genesis.Hash, msg); - } - - _logger.Debug( - "The branchpoint is #{BranchpointIndex} {BranchpointHash}.", - topmostCommon.Index, - topmostCommon - ); - - _rwlock.EnterUpgradeableReadLock(); - try - { - bool reorged = !Tip.Equals(topmostCommon); - if (render && reorged) - { - foreach (IRenderer renderer in Renderers) - { - renderer.RenderReorg(Tip, other.Tip, branchpoint: topmostCommon); - } - } - - if (render && ActionRenderers.Any()) - { - // Unrender stale actions. - _logger.Debug("Unrendering abandoned actions..."); - int cnt = 0; - - for ( - Block b = Tip; - !(b is null) && b.Index > (topmostCommon?.Index ?? -1) && - b.PreviousHash is { } ph; - b = this[ph] - ) - { - List evaluations = - ActionEvaluator.Evaluate(b, completers).ToList(); - evaluations.Reverse(); - - foreach (var evaluation in evaluations) - { - _logger.Debug("Unrender an action: {Action}.", evaluation.Action); - if (evaluation.Exception is null) - { - foreach (IActionRenderer renderer in ActionRenderers) - { - renderer.UnrenderAction( - evaluation.Action, - evaluation.InputContext.GetUnconsumedContext(), - evaluation.OutputStates - ); - } - } - else - { - foreach (IActionRenderer renderer in ActionRenderers) - { - renderer.UnrenderActionError( - evaluation.Action, - evaluation.InputContext.GetUnconsumedContext(), - evaluation.Exception - ); - } - } - - cnt++; - } - } - - _logger.Debug( - $"{nameof(Swap)}() completed unrendering {{Actions}} actions.", - cnt); - } - - Block oldTip = Tip ?? Genesis, newTip = other.Tip ?? other.Genesis; - - _rwlock.EnterWriteLock(); - try - { - IEnumerable> - GetTxsWithRange(BlockChain chain, Block start, Block end) - => Enumerable - .Range((int)start.Index + 1, (int)(end.Index - start.Index)) - .SelectMany(x => chain[x].Transactions); - - // It assumes reorg is small size. If it was big, this may be heavy task. - ImmutableHashSet> unstagedTxs = - GetTxsWithRange(this, topmostCommon, Tip).ToImmutableHashSet(); - ImmutableHashSet> stageTxs = - GetTxsWithRange(other, topmostCommon, other.Tip).ToImmutableHashSet(); - ImmutableHashSet> restageTxs = unstagedTxs.Except(stageTxs); - foreach (Transaction restageTx in restageTxs) - { - StagePolicy.Stage(this, restageTx); - } - - Guid obsoleteId = Id; - Id = other.Id; - Store.SetCanonicalChainId(Id); - _blocks = new BlockSet(Policy.GetHashAlgorithm, Store); - TipChanged?.Invoke(this, (oldTip, newTip)); - - if (render) - { - foreach (IRenderer renderer in Renderers) - { - renderer.RenderBlock(oldTip: oldTip, newTip: newTip); - } - } - - Store.DeleteChainId(obsoleteId); - } - finally - { - _rwlock.ExitWriteLock(); - } - - if (render && ActionRenderers.Any()) - { - _logger.Debug("Rendering actions in new chain."); - - // Render actions that had been behind. - long startToRenderIndex = topmostCommon is Block branchpoint - ? branchpoint.Index + 1 - : 0; - - foreach (IActionRenderer renderer in ActionRenderers) - { - int cnt = RenderActionsInBlocks(startToRenderIndex, renderer, completers); - _logger.Debug( - $"{nameof(Swap)}() completed rendering {{Count}} actions.", - cnt); - - renderer.RenderBlockEnd(oldTip, newTip); - } - } - - if (render && reorged) - { - foreach (IRenderer renderer in Renderers) - { - renderer.RenderReorgEnd(oldTip, newTip, topmostCommon); - } - } - } - finally - { - _rwlock.ExitUpgradeableReadLock(); - } - } -#pragma warning restore MEN003 - internal ImmutableArray> ListStagedTransactions() { Transaction[] txs = StagePolicy.Iterate(this).ToArray(); diff --git a/Libplanet/Net/Swarm.BlockSync.cs b/Libplanet/Net/Swarm.BlockSync.cs index d76a14c90ca..9b18987f9e6 100644 --- a/Libplanet/Net/Swarm.BlockSync.cs +++ b/Libplanet/Net/Swarm.BlockSync.cs @@ -62,7 +62,7 @@ CancellationToken cancellationToken "{SessionId}: Got a new " + nameof(BlockDemand) + " from {Peer}; started " + "to fetch the block #{BlockIndex} {BlockHash}..."; _logger.Debug(startLogMsg, sessionId, peer, blockDemand.Header.Index, hash); - await SyncPreviousBlocksAsync( + System.Action renderSwap = await SyncPreviousBlocksAsync( blockChain: BlockChain, peer: peer, stop: hash, @@ -78,10 +78,12 @@ await SyncPreviousBlocksAsync( peer ); + BroadcastBlock(peer.Address, BlockChain.Tip); + renderSwap(); + // FIXME: Clean up events BlockReceived.Set(); BlockAppended.Set(); - BroadcastBlock(peer.Address, BlockChain.Tip); ProcessFillBlocksFinished.Set(); } @@ -125,7 +127,7 @@ await SyncPreviousBlocksAsync( } } - private async Task SyncPreviousBlocksAsync( + private async Task SyncPreviousBlocksAsync( BlockChain blockChain, BoundPeer peer, BlockHash? stop, @@ -138,6 +140,7 @@ CancellationToken cancellationToken { long previousTipIndex = blockChain.Tip?.Index ?? -1; BlockChain synced = null; + System.Action renderSwap = () => { }; try { @@ -184,11 +187,10 @@ CancellationToken cancellationToken blockChain.Id, synced.Id ); - blockChain.Swap( + renderSwap = blockChain.Swap( synced, render: true, - stateCompleters: null - ); + stateCompleters: null); _logger.Debug( "{SessionId}: The chain {ChainIdB} replaced {ChainIdA}", logSessionId, @@ -197,6 +199,8 @@ CancellationToken cancellationToken ); } } + + return renderSwap; } #pragma warning disable MEN003 diff --git a/Libplanet/Net/Swarm.cs b/Libplanet/Net/Swarm.cs index 6f8e94dd4c4..8170d7444c5 100644 --- a/Libplanet/Net/Swarm.cs +++ b/Libplanet/Net/Swarm.cs @@ -837,7 +837,8 @@ var pair in completedBlocks.WithCancellation(blockDownloadCts.Token)) wId, workspace.Tip ); - BlockChain.Swap(workspace, render: render); + System.Action renderSwap = BlockChain.Swap(workspace, render: render); + renderSwap(); } foreach (Guid chainId in chainIds)