diff --git a/CHANGES.md b/CHANGES.md index 469b35369d1..7a821052d79 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -34,6 +34,39 @@ To be released. [#3999]: https://github.com/planetarium/libplanet/pull/3999 +Version 5.4.2 +------------- + +Released on December 13, 2024. + +### Backward-incompatible API changes + + - Removed `ContextTimeoutOption` class. Instead, added `ContextOption` class. + The unit of the time-related options in `ContextOption` is millisecond, + whereas `ContextTimeoutOption` was second. [[#4007]] + - Removed `ConsensusReactorOption.ContextTimeoutOptions` property. + Instead, added `ConsensusReactorOption.ContextOption` property. [[#4007]] + - `ConsensusReactor` constructor requires `ContextOption` parameter + instead of the `ContextTimeoutOption` parameter. [[#4007]] + +### Behavioral changes + + - `Gossip.RebuildTableAsync()` now bootstrap peers from the seed peers. + [[#4007]] + +[#4007]: https://github.com/planetarium/libplanet/pull/4007 + + +Version 5.4.1 +------------- + +Released on November 22, 2024. + + - Ported changes from [Libplanet 5.3.2] release. [[#3973]] + +[Libplanet 5.4.1]: https://www.nuget.org/packages/Libplanet/5.4.1 + + Version 5.4.0 ------------- diff --git a/src/Libplanet.Net/Consensus/ConsensusContext.cs b/src/Libplanet.Net/Consensus/ConsensusContext.cs index 38339996e65..b72323d0b5c 100644 --- a/src/Libplanet.Net/Consensus/ConsensusContext.cs +++ b/src/Libplanet.Net/Consensus/ConsensusContext.cs @@ -32,7 +32,7 @@ namespace Libplanet.Net.Consensus public partial class ConsensusContext : IDisposable { private readonly object _contextLock; - private readonly ContextTimeoutOption _contextTimeoutOption; + private readonly ContextOption _contextOption; private readonly IConsensusMessageCommunicator _consensusMessageCommunicator; private readonly BlockChain _blockChain; private readonly PrivateKey _privateKey; @@ -58,14 +58,14 @@ private readonly EvidenceExceptionCollector _evidenceCollector /// A time delay in starting the consensus for the next height /// block. /// - /// A for - /// configuring a timeout for each . + /// A for + /// configuring a timeout or delay for each . public ConsensusContext( IConsensusMessageCommunicator consensusMessageCommunicator, BlockChain blockChain, PrivateKey privateKey, TimeSpan newHeightDelay, - ContextTimeoutOption contextTimeoutOption) + ContextOption contextOption) { _consensusMessageCommunicator = consensusMessageCommunicator; _blockChain = blockChain; @@ -73,7 +73,7 @@ public ConsensusContext( Running = false; _newHeightDelay = newHeightDelay; - _contextTimeoutOption = contextTimeoutOption; + _contextOption = contextOption; _currentContext = CreateContext( _blockChain.Tip.Index + 1, _blockChain.GetBlockCommit(_blockChain.Tip.Index)); @@ -498,7 +498,7 @@ private Context CreateContext(long height, BlockCommit? lastCommit) lastCommit, _privateKey, validatorSet, - contextTimeoutOptions: _contextTimeoutOption); + contextOption: _contextOption); return context; } diff --git a/src/Libplanet.Net/Consensus/ConsensusReactor.cs b/src/Libplanet.Net/Consensus/ConsensusReactor.cs index 8cad857799a..4fd97d8f370 100644 --- a/src/Libplanet.Net/Consensus/ConsensusReactor.cs +++ b/src/Libplanet.Net/Consensus/ConsensusReactor.cs @@ -43,7 +43,7 @@ public class ConsensusReactor : IReactor /// A time delay in starting the consensus for the next height /// block. /// - /// A for + /// A for /// configuring a timeout for each . public ConsensusReactor( ITransport consensusTransport, @@ -52,7 +52,7 @@ public ConsensusReactor( ImmutableList validatorPeers, ImmutableList seedPeers, TimeSpan newHeightDelay, - ContextTimeoutOption contextTimeoutOption) + ContextOption contextOption) { validatorPeers ??= ImmutableList.Empty; seedPeers ??= ImmutableList.Empty; @@ -71,7 +71,7 @@ public ConsensusReactor( blockChain, privateKey, newHeightDelay, - contextTimeoutOption); + contextOption); _logger = Log .ForContext("Tag", "Consensus") diff --git a/src/Libplanet.Net/Consensus/ConsensusReactorOption.cs b/src/Libplanet.Net/Consensus/ConsensusReactorOption.cs index f39aa792e34..1b4440463e8 100644 --- a/src/Libplanet.Net/Consensus/ConsensusReactorOption.cs +++ b/src/Libplanet.Net/Consensus/ConsensusReactorOption.cs @@ -7,7 +7,7 @@ namespace Libplanet.Net.Consensus { /// - /// A option struct for initializing . + /// An option struct for initializing . /// public struct ConsensusReactorOption { @@ -42,8 +42,8 @@ public struct ConsensusReactorOption public TimeSpan TargetBlockInterval { get; set; } /// - /// A timeout second and multiplier value for used in . + /// A timeout and delay value for used in in milliseconds. /// - public ContextTimeoutOption ContextTimeoutOptions { get; set; } + public ContextOption ContextOption { get; set; } } } diff --git a/src/Libplanet.Net/Consensus/Context.Async.cs b/src/Libplanet.Net/Consensus/Context.Async.cs index 790339c3355..a3d73b2974b 100644 --- a/src/Libplanet.Net/Consensus/Context.Async.cs +++ b/src/Libplanet.Net/Consensus/Context.Async.cs @@ -189,6 +189,40 @@ private void AppendBlock(Block block) _ = Task.Run(() => _blockChain.Append(block, GetBlockCommit())); } + private async Task EnterPreCommitWait(int round, BlockHash hash) + { + if (!_preCommitWaitFlags.Add(round)) + { + return; + } + + if (_contextOption.EnterPreCommitDelay > 0) + { + await Task.Delay( + _contextOption.EnterPreCommitDelay, + _cancellationTokenSource.Token); + } + + ProduceMutation(() => EnterPreCommit(round, hash)); + } + + private async Task EnterEndCommitWait(int round) + { + if (!_endCommitWaitFlags.Add(round)) + { + return; + } + + if (_contextOption.EnterEndCommitDelay > 0) + { + await Task.Delay( + _contextOption.EnterEndCommitDelay, + _cancellationTokenSource.Token); + } + + ProduceMutation(() => EnterEndCommit(round)); + } + /// /// Schedules to be queued after /// amount of time. @@ -212,7 +246,18 @@ private async Task OnTimeoutPropose(int round) /// A round that the timeout task is scheduled for. private async Task OnTimeoutPreVote(int round) { + if (_preCommitTimeoutFlags.Contains(round) || !_preVoteTimeoutFlags.Add(round)) + { + return; + } + TimeSpan timeout = TimeoutPreVote(round); + _logger.Debug( + "PreVote step in round {Round} is scheduled to be timed out after {Timeout} " + + "because 2/3+ PreVotes are collected for the round. (context: {Context})", + round, + timeout, + ToString()); await Task.Delay(timeout, _cancellationTokenSource.Token); _logger.Information( "TimeoutPreVote has occurred in {Timeout}. {Info}", @@ -228,7 +273,18 @@ private async Task OnTimeoutPreVote(int round) /// The round that the timeout task is scheduled for. private async Task OnTimeoutPreCommit(int round) { + if (!_preCommitTimeoutFlags.Add(round)) + { + return; + } + TimeSpan timeout = TimeoutPreCommit(round); + _logger.Debug( + "PreCommit step in round {Round} is scheduled to be timed out in {Timeout} " + + "because 2/3+ PreCommits are collected for the round. (context: {Context})", + round, + timeout, + ToString()); await Task.Delay(timeout, _cancellationTokenSource.Token); _logger.Information( "TimeoutPreCommit has occurred in {Timeout}. {Info}", diff --git a/src/Libplanet.Net/Consensus/Context.Mutate.cs b/src/Libplanet.Net/Consensus/Context.Mutate.cs index 8b71cfee34b..6417befbe8f 100644 --- a/src/Libplanet.Net/Consensus/Context.Mutate.cs +++ b/src/Libplanet.Net/Consensus/Context.Mutate.cs @@ -245,17 +245,14 @@ private void ProcessGenericUponRules() "Entering PreVote step due to proposal message with " + "valid round -1. (context: {Context})", ToString()); - Step = ConsensusStep.PreVote; if (IsValid(p1.Block) && (_lockedRound == -1 || _lockedValue == p1.Block)) { - PublishMessage( - new ConsensusPreVoteMsg(MakeVote(Round, p1.Block.Hash, VoteFlag.PreVote))); + EnterPreVote(Round, p1.Block.Hash); } else { - PublishMessage( - new ConsensusPreVoteMsg(MakeVote(Round, default, VoteFlag.PreVote))); + EnterPreVote(Round, default); } } @@ -271,31 +268,20 @@ private void ProcessGenericUponRules() "2/3+ PreVote for valid round {ValidRound}. (context: {Context})", p2.ValidRound, ToString()); - Step = ConsensusStep.PreVote; if (IsValid(p2.Block) && (_lockedRound <= p2.ValidRound || _lockedValue == p2.Block)) { - PublishMessage( - new ConsensusPreVoteMsg(MakeVote(Round, p2.Block.Hash, VoteFlag.PreVote))); + EnterPreVote(Round, p2.Block.Hash); } else { - PublishMessage( - new ConsensusPreVoteMsg(MakeVote(Round, default, VoteFlag.PreVote))); + EnterPreVote(Round, default); } } - if (_heightVoteSet.PreVotes(Round).HasTwoThirdsAny() && - Step == ConsensusStep.PreVote && - !_preVoteTimeoutFlags.Contains(Round)) + if (_heightVoteSet.PreVotes(Round).HasTwoThirdsAny() && Step == ConsensusStep.PreVote) { - _logger.Debug( - "PreVote step in round {Round} is scheduled to be timed out because " + - "2/3+ PreVotes are collected for the round. (context: {Context})", - Round, - ToString()); - _preVoteTimeoutFlags.Add(Round); _ = OnTimeoutPreVote(Round); } @@ -315,16 +301,13 @@ private void ProcessGenericUponRules() if (Step == ConsensusStep.PreVote) { _logger.Debug( - "Entering PreCommit step due to proposal message and have collected " + - "2/3+ PreVote for current round {Round}. (context: {Context})", + "Schedule to enter PreCommit step due to proposal message and have " + + "collected 2/3+ PreVote for current round {Round}. (context: {Context})", Round, ToString()); - Step = ConsensusStep.PreCommit; _lockedValue = p3.Block; _lockedRound = Round; - PublishMessage( - new ConsensusPreCommitMsg( - MakeVote(Round, p3.Block.Hash, VoteFlag.PreCommit))); + _ = EnterPreCommitWait(Round, p3.Block.Hash); // Maybe need to broadcast periodically? PublishMessage( @@ -346,9 +329,7 @@ private void ProcessGenericUponRules() "were collected. (context: {Context})", Round, ToString()); - Step = ConsensusStep.PreCommit; - PublishMessage( - new ConsensusPreCommitMsg(MakeVote(Round, default, VoteFlag.PreCommit))); + _ = EnterPreCommitWait(Round, default); } else if (Proposal is { } proposal && !proposal.BlockHash.Equals(hash3)) { @@ -372,15 +353,8 @@ private void ProcessGenericUponRules() } } - if (_heightVoteSet.PreCommits(Round).HasTwoThirdsAny() && - !_preCommitTimeoutFlags.Contains(Round)) + if (_heightVoteSet.PreCommits(Round).HasTwoThirdsAny()) { - _logger.Debug( - "PreCommit step in round {Round} is scheduled to be timed out because " + - "2/3+ PreCommits are collected for the round. (context: {Context})", - Round, - ToString()); - _preCommitTimeoutFlags.Add(Round); _ = OnTimeoutPreCommit(Round); } } @@ -406,7 +380,6 @@ private void ProcessHeightOrRoundUponRules(ConsensusMsg message) block4.Hash.Equals(hash) && IsValid(block4)) { - Step = ConsensusStep.EndCommit; _decision = block4; _committedRound = round; @@ -414,39 +387,12 @@ private void ProcessHeightOrRoundUponRules(ConsensusMsg message) PublishMessage( new ConsensusMaj23Msg( MakeMaj23(round, block4.Hash, VoteFlag.PreCommit))); - - try - { - _logger.Information( - "Committing block #{Index} {Hash} (context: {Context})", - block4.Index, - block4.Hash, - ToString()); - - IsValid(block4); - - AppendBlock(block4); - } - catch (Exception e) - { - _logger.Error( - e, - "Failed to commit block #{Index} {Hash}", - block4.Index, - block4.Hash); - ExceptionOccurred?.Invoke(this, e); - return; - } - - _logger.Information( - "Committed block #{Index} {Hash}", - block4.Index, - block4.Hash); + _ = EnterEndCommitWait(Round); return; } // NOTE: +1/3 prevote received, skip round - // FIXME: Tendermint uses +2/3, should fixed? + // FIXME: Tendermint uses +2/3, should be fixed? if (round > Round && _heightVoteSet.PreVotes(round).HasOneThirdsAny()) { @@ -461,6 +407,81 @@ private void ProcessHeightOrRoundUponRules(ConsensusMsg message) } } + private void EnterPreVote(int round, BlockHash hash) + { + if (Round != round || Step >= ConsensusStep.PreVote) + { + // Round and step mismatch + return; + } + + Step = ConsensusStep.PreVote; + PublishMessage( + new ConsensusPreVoteMsg(MakeVote(round, hash, VoteFlag.PreVote))); + } + + private void EnterPreCommit(int round, BlockHash hash) + { + if (Round != round || Step >= ConsensusStep.PreCommit) + { + // Round and step mismatch + return; + } + + _logger.Debug( + "Entering PreCommit step current round {Round}. (context: {Context})", + Round, + ToString()); + Step = ConsensusStep.PreCommit; + PublishMessage( + new ConsensusPreCommitMsg(MakeVote(round, hash, VoteFlag.PreCommit))); + } + + private void EnterEndCommit(int round) + { + if (Round != round || + Step == ConsensusStep.Default || + Step == ConsensusStep.EndCommit) + { + // Round and step mismatch + return; + } + + Step = ConsensusStep.EndCommit; + if (_decision is not { } block) + { + StartRound(Round + 1); + return; + } + + try + { + _logger.Information( + "Committing block #{Index} {Hash} (context: {Context})", + block.Index, + block.Hash, + ToString()); + + IsValid(block); + AppendBlock(block); + } + catch (Exception e) + { + _logger.Error( + e, + "Failed to commit block #{Index} {Hash}", + block.Index, + block.Hash); + ExceptionOccurred?.Invoke(this, e); + return; + } + + _logger.Information( + "Committed block #{Index} {Hash}", + block.Index, + block.Hash); + } + /// /// A timeout mutation to run if no is received in /// and is still in step. @@ -470,9 +491,7 @@ private void ProcessTimeoutPropose(int round) { if (round == Round && Step == ConsensusStep.Propose) { - PublishMessage( - new ConsensusPreVoteMsg(MakeVote(Round, default, VoteFlag.PreVote))); - Step = ConsensusStep.PreVote; + EnterPreVote(round, default); TimeoutProcessed?.Invoke(this, (round, ConsensusStep.Propose)); } } @@ -487,9 +506,7 @@ private void ProcessTimeoutPreVote(int round) { if (round == Round && Step == ConsensusStep.PreVote) { - PublishMessage( - new ConsensusPreCommitMsg(MakeVote(Round, default, VoteFlag.PreCommit))); - Step = ConsensusStep.PreCommit; + EnterPreCommit(round, default); TimeoutProcessed?.Invoke(this, (round, ConsensusStep.PreVote)); } } @@ -510,7 +527,7 @@ private void ProcessTimeoutPreCommit(int round) if (round == Round) { - StartRound(Round + 1); + EnterEndCommit(round); TimeoutProcessed?.Invoke(this, (round, ConsensusStep.PreCommit)); } } diff --git a/src/Libplanet.Net/Consensus/Context.cs b/src/Libplanet.Net/Consensus/Context.cs index 69453b615fb..daa7f0063eb 100644 --- a/src/Libplanet.Net/Consensus/Context.cs +++ b/src/Libplanet.Net/Consensus/Context.cs @@ -78,7 +78,7 @@ namespace Libplanet.Net.Consensus /// public partial class Context : IDisposable { - private readonly ContextTimeoutOption _contextTimeoutOption; + private readonly ContextOption _contextOption; private readonly BlockChain _blockChain; private readonly Codec _codec; @@ -87,9 +87,11 @@ public partial class Context : IDisposable private readonly Channel _mutationRequests; private readonly HeightVoteSet _heightVoteSet; private readonly PrivateKey _privateKey; - private readonly HashSet _preVoteTimeoutFlags; private readonly HashSet _hasTwoThirdsPreVoteFlags; + private readonly HashSet _preVoteTimeoutFlags; private readonly HashSet _preCommitTimeoutFlags; + private readonly HashSet _preCommitWaitFlags; + private readonly HashSet _endCommitWaitFlags; private readonly EvidenceExceptionCollector _evidenceCollector = new EvidenceExceptionCollector(); @@ -98,6 +100,8 @@ private readonly EvidenceExceptionCollector _evidenceCollector private readonly ILogger _logger; private readonly LRUCache _blockValidationCache; + private Proposal? _proposal; + private Block? _proposalBlock; private Block? _lockedValue; private int _lockedRound; private Block? _validValue; @@ -122,15 +126,15 @@ private readonly EvidenceExceptionCollector _evidenceCollector /// /// The for /// given . - /// A for - /// configuring a timeout for each . + /// A for + /// configuring a timeout and delay for each . public Context( BlockChain blockChain, long height, BlockCommit? lastCommit, PrivateKey privateKey, ValidatorSet validators, - ContextTimeoutOption contextTimeoutOptions) + ContextOption contextOption) : this( blockChain, height, @@ -140,7 +144,7 @@ public Context( ConsensusStep.Default, -1, 128, - contextTimeoutOptions) + contextOption) { } @@ -153,7 +157,7 @@ private Context( ConsensusStep consensusStep, int round = -1, int cacheSize = 128, - ContextTimeoutOption? contextTimeoutOptions = null) + ContextOption? contextOption = null) { if (height < 1) { @@ -183,15 +187,17 @@ private Context( _messageRequests = Channel.CreateUnbounded(); _mutationRequests = Channel.CreateUnbounded(); _heightVoteSet = new HeightVoteSet(height, validators); - _preVoteTimeoutFlags = new HashSet(); _hasTwoThirdsPreVoteFlags = new HashSet(); + _preVoteTimeoutFlags = new HashSet(); _preCommitTimeoutFlags = new HashSet(); + _preCommitWaitFlags = new HashSet(); + _endCommitWaitFlags = new HashSet(); _validatorSet = validators; _cancellationTokenSource = new CancellationTokenSource(); _blockValidationCache = new LRUCache(cacheSize, Math.Max(cacheSize / 64, 8)); - _contextTimeoutOption = contextTimeoutOptions ?? new ContextTimeoutOption(); + _contextOption = contextOption ?? new ContextOption(); _logger.Information( "Created Context for height #{Height}, round #{Round}", @@ -214,7 +220,24 @@ private Context( /// public ConsensusStep Step { get; private set; } - public Proposal? Proposal { get; private set; } + public Proposal? Proposal + { + get => _proposal; + private set + { + if (value is { } p) + { + _proposal = p; + _proposalBlock = + BlockMarshaler.UnmarshalBlock((Dictionary)_codec.Decode(p.MarshaledBlock)); + } + else + { + _proposal = null; + _proposalBlock = null; + } + } + } /// public void Dispose() @@ -379,9 +402,9 @@ public override string ToString() /// A duration in . private TimeSpan TimeoutPreVote(long round) { - return TimeSpan.FromSeconds( - _contextTimeoutOption.PreVoteSecondBase + - round * _contextTimeoutOption.PreVoteMultiplier); + return TimeSpan.FromMilliseconds( + _contextOption.PreVoteTimeoutBase + + round * _contextOption.PreVoteTimeoutDelta); } /// @@ -392,9 +415,9 @@ private TimeSpan TimeoutPreVote(long round) /// A duration in . private TimeSpan TimeoutPreCommit(long round) { - return TimeSpan.FromSeconds( - _contextTimeoutOption.PreCommitSecondBase + - round * _contextTimeoutOption.PreCommitMultiplier); + return TimeSpan.FromMilliseconds( + _contextOption.PreCommitTimeoutBase + + round * _contextOption.PreCommitTimeoutDelta); } /// @@ -405,9 +428,9 @@ private TimeSpan TimeoutPreCommit(long round) /// A duration in . private TimeSpan TimeoutPropose(long round) { - return TimeSpan.FromSeconds( - _contextTimeoutOption.ProposeSecondBase + - round * _contextTimeoutOption.ProposeMultiplier); + return TimeSpan.FromMilliseconds( + _contextOption.ProposeTimeoutBase + + round * _contextOption.ProposeTimeoutDelta); } /// @@ -590,16 +613,7 @@ private Maj23 MakeMaj23(int round, BlockHash hash, VoteFlag flag) /// Returns a tuple of proposer and valid round. If proposal for the round /// does not exist, returns instead. /// - private (Block, int)? GetProposal() - { - if (Proposal is { } p) - { - var block = BlockMarshaler.UnmarshalBlock( - (Dictionary)_codec.Decode(p.MarshaledBlock)); - return (block, p.ValidRound); - } - - return null; - } + private (Block, int)? GetProposal() => + Proposal is { } p && _proposalBlock is { } b ? (b, p.ValidRound) : null; } } diff --git a/src/Libplanet.Net/Consensus/ContextOption.cs b/src/Libplanet.Net/Consensus/ContextOption.cs new file mode 100644 index 00000000000..95ef6022cfb --- /dev/null +++ b/src/Libplanet.Net/Consensus/ContextOption.cs @@ -0,0 +1,122 @@ +using System; + +namespace Libplanet.Net.Consensus +{ + /// + /// An options class to configure timeout and delay + /// for each in milliseconds. + /// + public class ContextOption + { + public ContextOption( + int proposeTimeoutBase = 8_000, + int preVoteTimeoutBase = 1_000, + int preCommitTimeoutBase = 1_000, + int proposeTimeoutDelta = 4_000, + int preVoteTimeoutDelta = 500, + int preCommitTimeoutDelta = 500, + int enterPreVoteDelay = 0, + int enterPreCommitDelay = 0, + int enterEndCommitDelay = 0) + { + if (proposeTimeoutBase <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(proposeTimeoutBase), + "ProposeTimeoutBase must be greater than 0."); + } + + ProposeTimeoutBase = proposeTimeoutBase; + + if (preVoteTimeoutBase <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(preVoteTimeoutBase), + "PreVoteTimeoutBase must be greater than 0."); + } + + PreVoteTimeoutBase = preVoteTimeoutBase; + + if (preCommitTimeoutBase <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(preCommitTimeoutBase), + "PreCommitTimeoutBase must be greater than 0."); + } + + PreCommitTimeoutBase = preCommitTimeoutBase; + + if (proposeTimeoutDelta <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(proposeTimeoutDelta), + "ProposeTimeoutDelta must be greater than 0."); + } + + ProposeTimeoutDelta = proposeTimeoutDelta; + + if (preVoteTimeoutDelta <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(preVoteTimeoutDelta), + "PreVoteTimeoutDelta must be greater than 0."); + } + + PreVoteTimeoutDelta = preVoteTimeoutDelta; + + if (preCommitTimeoutDelta <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(preCommitTimeoutDelta), + "PreCommitTimeoutDelta must be greater than 0."); + } + + PreCommitTimeoutDelta = preCommitTimeoutDelta; + + if (enterPreVoteDelay < 0) + { + throw new ArgumentOutOfRangeException( + nameof(enterPreVoteDelay), + "EnterPreVoteDelay must be greater than or equal to 0."); + } + + EnterPreVoteDelay = enterPreVoteDelay; + + if (enterPreCommitDelay < 0) + { + throw new ArgumentOutOfRangeException( + nameof(enterPreCommitDelay), + "EnterPreCommitDelay must be greater than or equal to 0."); + } + + EnterPreCommitDelay = enterPreCommitDelay; + + if (enterEndCommitDelay < 0) + { + throw new ArgumentOutOfRangeException( + nameof(enterEndCommitDelay), + "EnterEndCommitDelay must be greater than or equal to 0."); + } + + EnterEndCommitDelay = enterEndCommitDelay; + } + + public int ProposeTimeoutBase { get; } + + public int PreVoteTimeoutBase { get; } + + public int PreCommitTimeoutBase { get; } + + public int ProposeTimeoutDelta { get; } + + public int PreVoteTimeoutDelta { get; } + + public int PreCommitTimeoutDelta { get; } + + public int EnterPreVoteDelay { get; } + + public int EnterPreCommitDelay { get; } + + public int EnterEndCommitDelay { get; } + } +} diff --git a/src/Libplanet.Net/Consensus/ContextTimeoutOption.cs b/src/Libplanet.Net/Consensus/ContextTimeoutOption.cs deleted file mode 100644 index 78a5515ffd2..00000000000 --- a/src/Libplanet.Net/Consensus/ContextTimeoutOption.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; - -namespace Libplanet.Net.Consensus -{ - /// - /// A options class to configure timeout - /// for each . - /// - public class ContextTimeoutOption - { - public ContextTimeoutOption( - int proposeSecondBase = 8, - int preVoteSecondBase = 4, - int preCommitSecondBase = 4, - int proposeMultiplier = 4, - int preVoteMultiplier = 2, - int preCommitMultiplier = 2) - { - if (proposeSecondBase <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(proposeSecondBase), - "ProposeSecondBase must be greater than 0."); - } - - ProposeSecondBase = proposeSecondBase; - - if (preVoteSecondBase <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(preVoteSecondBase), - "PreVoteSecondBase must be greater than 0."); - } - - PreVoteSecondBase = preVoteSecondBase; - - if (preCommitSecondBase <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(preCommitSecondBase), - "PreCommitSecondBase must be greater than 0."); - } - - PreCommitSecondBase = preCommitSecondBase; - - if (proposeMultiplier <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(proposeMultiplier), - "ProposeMultiplier must be greater than 0."); - } - - ProposeMultiplier = proposeMultiplier; - - if (preVoteMultiplier <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(preVoteMultiplier), - "PreVoteMultiplier must be greater than 0."); - } - - PreVoteMultiplier = preVoteMultiplier; - - if (preCommitMultiplier <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(preCommitMultiplier), - "PreCommitMultiplier must be greater than 0."); - } - - PreCommitMultiplier = preCommitMultiplier; - } - - public int ProposeSecondBase { get; } - - public int PreVoteSecondBase { get; } - - public int PreCommitSecondBase { get; } - - public int ProposeMultiplier { get; } - - public int PreVoteMultiplier { get; } - - public int PreCommitMultiplier { get; } - } -} diff --git a/src/Libplanet.Net/Consensus/Gossip.cs b/src/Libplanet.Net/Consensus/Gossip.cs index 3e0a0031eb0..9ec93da2d74 100644 --- a/src/Libplanet.Net/Consensus/Gossip.cs +++ b/src/Libplanet.Net/Consensus/Gossip.cs @@ -542,7 +542,21 @@ private async Task RebuildTableAsync(CancellationToken ctx) "{FName}: Updating peer table from seed(s) {Seeds}...", nameof(RebuildTableAsync), _seeds.Select(s => s.Address.ToHex())); - await _protocol.RebuildConnectionAsync(Kademlia.MaxDepth, ctx); + try + { + await _protocol.BootstrapAsync( + _seeds, + TimeSpan.FromSeconds(1), + Kademlia.MaxDepth, + ctx); + } + catch (Exception e) + { + _logger.Error( + e, + "Peer discovery exception occurred during {FName}.", + nameof(RebuildTableAsync)); + } } } diff --git a/src/Libplanet.Net/Swarm.cs b/src/Libplanet.Net/Swarm.cs index ca4949609ec..aab456222fb 100644 --- a/src/Libplanet.Net/Swarm.cs +++ b/src/Libplanet.Net/Swarm.cs @@ -124,7 +124,7 @@ public Swarm( consensusReactorOption.ConsensusPeers, consensusReactorOption.SeedPeers, consensusReactorOption.TargetBlockInterval, - consensusReactorOption.ContextTimeoutOptions); + consensusReactorOption.ContextOption); } } diff --git a/test/Libplanet.Explorer.Tests/GraphTypes/BlockTypeTest.cs b/test/Libplanet.Explorer.Tests/GraphTypes/BlockTypeTest.cs index 1c47d06289f..772c4cfc066 100644 --- a/test/Libplanet.Explorer.Tests/GraphTypes/BlockTypeTest.cs +++ b/test/Libplanet.Explorer.Tests/GraphTypes/BlockTypeTest.cs @@ -122,7 +122,7 @@ public async void Query() { "blockHash", lastVotes[0].BlockHash.ToString() }, { "timestamp", new DateTimeOffsetGraphType().Serialize(lastVotes[0].Timestamp) }, { "validatorPublicKey", lastVotes[0].ValidatorPublicKey.ToString() }, - { "validatorPower", lastVotes[0].ValidatorPower }, + { "validatorPower", lastVotes[0].ValidatorPower?.ToString() }, { "flag", lastVotes[0].Flag.ToString() }, { "signature", ByteUtil.Hex(lastVotes[0].Signature) }, } diff --git a/test/Libplanet.Explorer.Tests/GraphTypes/VoteTypeTest.cs b/test/Libplanet.Explorer.Tests/GraphTypes/VoteTypeTest.cs index 475928c556f..16968d0f8ca 100644 --- a/test/Libplanet.Explorer.Tests/GraphTypes/VoteTypeTest.cs +++ b/test/Libplanet.Explorer.Tests/GraphTypes/VoteTypeTest.cs @@ -57,7 +57,7 @@ public async void Query() Assert.Equal(vote.BlockHash.ToString(), resultData["blockHash"]); Assert.Equal(new DateTimeOffsetGraphType().Serialize(vote.Timestamp), resultData["timestamp"]); Assert.Equal(vote.ValidatorPublicKey.ToString(), resultData["validatorPublicKey"]); - Assert.Equal(vote.ValidatorPower, resultData["validatorPower"]); + Assert.Equal(vote.ValidatorPower?.ToString(), resultData["validatorPower"]); Assert.Equal(vote.Flag.ToString(), resultData["flag"]); Assert.Equal(ByteUtil.Hex(vote.Signature), resultData["signature"]); } diff --git a/test/Libplanet.Net.Tests/Consensus/ContextNonProposerTest.cs b/test/Libplanet.Net.Tests/Consensus/ContextNonProposerTest.cs index 9c4e0824ad2..9de309e2db6 100644 --- a/test/Libplanet.Net.Tests/Consensus/ContextNonProposerTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ContextNonProposerTest.cs @@ -531,7 +531,7 @@ public async void TimeoutPropose() var (_, context) = TestUtils.CreateDummyContext( privateKey: TestUtils.PrivateKeys[0], - contextTimeoutOptions: new ContextTimeoutOption(proposeSecondBase: 1)); + contextOption: new ContextOption(proposeTimeoutBase: 1_000)); context.StateChanged += (_, eventArgs) => { @@ -560,9 +560,9 @@ public async Task UponRulesCheckAfterTimeout() { var (blockChain, context) = TestUtils.CreateDummyContext( privateKey: TestUtils.PrivateKeys[0], - contextTimeoutOptions: new ContextTimeoutOption( - preVoteSecondBase: 1, - preCommitSecondBase: 1)); + contextOption: new ContextOption( + preVoteTimeoutBase: 1_000, + preCommitTimeoutBase: 1_000)); var block1 = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); var block2 = blockChain.ProposeBlock(TestUtils.PrivateKeys[2]); @@ -642,7 +642,7 @@ public async Task TimeoutPreVote() { var (blockChain, context) = TestUtils.CreateDummyContext( privateKey: TestUtils.PrivateKeys[0], - contextTimeoutOptions: new ContextTimeoutOption(preVoteSecondBase: 1)); + contextOption: new ContextOption(preVoteTimeoutBase: 1_000)); var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); var timeoutProcessed = new AsyncAutoResetEvent(); @@ -693,7 +693,7 @@ public async Task TimeoutPreCommit() { var (blockChain, context) = TestUtils.CreateDummyContext( privateKey: TestUtils.PrivateKeys[0], - contextTimeoutOptions: new ContextTimeoutOption(preCommitSecondBase: 1)); + contextOption: new ContextOption(preCommitTimeoutBase: 1_000)); var block = blockChain.ProposeBlock(TestUtils.PrivateKeys[1]); var timeoutProcessed = new AsyncAutoResetEvent(); diff --git a/test/Libplanet.Net.Tests/Consensus/ContextTest.cs b/test/Libplanet.Net.Tests/Consensus/ContextTest.cs index 926d0622ebd..703d5e2b60d 100644 --- a/test/Libplanet.Net.Tests/Consensus/ContextTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ContextTest.cs @@ -28,6 +28,7 @@ using Serilog; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; namespace Libplanet.Net.Tests.Consensus { @@ -340,7 +341,7 @@ public async Task CanPreCommitOnEndCommit() blockChain .GetNextWorldState(0L) .GetValidatorSet(), - contextTimeoutOptions: new ContextTimeoutOption()); + contextOption: new ContextOption()); context.MessageToPublish += (sender, message) => context.ProduceMessage(message); context.StateChanged += (_, eventArgs) => @@ -639,7 +640,7 @@ public async Task CanCreateContextWithLastingEvaluation() blockChain, TestUtils.PrivateKeys[0], newHeightDelay, - new ContextTimeoutOption()); + new ContextOption()); Context context = consensusContext.CurrentContext; context.MessageToPublish += (sender, message) => context.ProduceMessage(message); @@ -715,6 +716,91 @@ public async Task CanCreateContextWithLastingEvaluation() Assert.Equal(2, consensusContext.Height); } + [Theory(Timeout = Timeout)] + [InlineData(0)] + [InlineData(100)] + [InlineData(500)] + public async Task CanCollectPreVoteAfterMajority(int delay) + { + var stepChangedToPreVote = new AsyncAutoResetEvent(); + var stepChangedToPreCommit = new AsyncAutoResetEvent(); + Block? proposedBlock = null; + int numPreVotes = 0; + var (_, context) = TestUtils.CreateDummyContext( + contextOption: new ContextOption( + enterPreCommitDelay: delay)); + context.StateChanged += (_, eventArgs) => + { + if (eventArgs.Step == ConsensusStep.PreVote) + { + stepChangedToPreVote.Set(); + } + else if (eventArgs.Step == ConsensusStep.PreCommit) + { + stepChangedToPreCommit.Set(); + } + }; + context.MessageToPublish += (_, message) => + { + if (message is ConsensusProposalMsg proposalMsg) + { + proposedBlock = BlockMarshaler.UnmarshalBlock( + (Dictionary)new Codec().Decode(proposalMsg!.Proposal.MarshaledBlock)); + } + }; + context.VoteSetModified += (_, tuple) => + { + if (tuple.Flag == VoteFlag.PreVote) + { + numPreVotes = tuple.Votes.Count(); + } + }; + context.Start(); + await stepChangedToPreVote.WaitAsync(); + Assert.Equal(ConsensusStep.PreVote, context.Step); + if (proposedBlock is not { } block) + { + throw new XunitException("No proposal is made"); + } + + for (int i = 0; i < 3; i++) + { + context.ProduceMessage( + new ConsensusPreVoteMsg( + new VoteMetadata( + block.Index, + 0, + block.Hash, + DateTimeOffset.UtcNow, + TestUtils.PrivateKeys[i].PublicKey, + TestUtils.ValidatorSet[i].Power, + VoteFlag.PreVote).Sign(TestUtils.PrivateKeys[i]))); + } + + // Send delayed PreVote message after sending preCommit message + var cts = new CancellationTokenSource(); + const int preVoteDelay = 300; + _ = Task.Run( + async () => + { + await Task.Delay(preVoteDelay, cts.Token); + context.ProduceMessage( + new ConsensusPreVoteMsg( + new VoteMetadata( + block.Index, + 0, + block.Hash, + DateTimeOffset.UtcNow, + TestUtils.PrivateKeys[3].PublicKey, + TestUtils.ValidatorSet[3].Power, + VoteFlag.PreVote).Sign(TestUtils.PrivateKeys[3]))); + }, cts.Token); + + await stepChangedToPreCommit.WaitAsync(); + cts.Cancel(); + Assert.Equal(delay < preVoteDelay ? 3 : 4, numPreVotes); + } + public struct ContextJson { #pragma warning disable SA1300 diff --git a/test/Libplanet.Net.Tests/SwarmTest.Evidence.cs b/test/Libplanet.Net.Tests/SwarmTest.Evidence.cs index 828e44f54ec..9914e9fc432 100644 --- a/test/Libplanet.Net.Tests/SwarmTest.Evidence.cs +++ b/test/Libplanet.Net.Tests/SwarmTest.Evidence.cs @@ -41,7 +41,7 @@ public async Task DuplicateVote_Test() ConsensusPrivateKey = privateKeys[i], ConsensusWorkers = 100, TargetBlockInterval = TimeSpan.FromSeconds(4), - ContextTimeoutOptions = new ContextTimeoutOption(), + ContextOption = new ContextOption(), }).ToList(); var swarmTasks = privateKeys.Select( diff --git a/test/Libplanet.Net.Tests/SwarmTest.cs b/test/Libplanet.Net.Tests/SwarmTest.cs index bd41a0e8fa9..83bbf8ce0ec 100644 --- a/test/Libplanet.Net.Tests/SwarmTest.cs +++ b/test/Libplanet.Net.Tests/SwarmTest.cs @@ -411,7 +411,7 @@ public async Task BootstrapContext() ConsensusPrivateKey = TestUtils.PrivateKeys[i], ConsensusWorkers = 100, TargetBlockInterval = TimeSpan.FromSeconds(10), - ContextTimeoutOptions = new ContextTimeoutOption(), + ContextOption = new ContextOption(), }).ToList(); var swarms = new List(); for (int i = 0; i < 4; i++) diff --git a/test/Libplanet.Net.Tests/TestUtils.cs b/test/Libplanet.Net.Tests/TestUtils.cs index c73486a3b1e..1942d0280c0 100644 --- a/test/Libplanet.Net.Tests/TestUtils.cs +++ b/test/Libplanet.Net.Tests/TestUtils.cs @@ -252,7 +252,7 @@ public static (BlockChain BlockChain, ConsensusContext ConsensusContext) IBlockPolicy? policy = null, IActionLoader? actionLoader = null, PrivateKey? privateKey = null, - ContextTimeoutOption? contextTimeoutOptions = null) + ContextOption? contextOption = null) { policy ??= Policy; var blockChain = CreateDummyBlockChain(policy, actionLoader); @@ -272,7 +272,7 @@ void BroadcastMessage(ConsensusMsg message) => blockChain, privateKey, newHeightDelay, - contextTimeoutOptions ?? new ContextTimeoutOption()); + contextOption ?? new ContextOption()); return (blockChain, consensusContext); } @@ -282,7 +282,7 @@ public static Context CreateDummyContext( long height = 1, BlockCommit? lastCommit = null, PrivateKey? privateKey = null, - ContextTimeoutOption? contextTimeoutOptions = null, + ContextOption? contextOption = null, ValidatorSet? validatorSet = null) { Context? context = null; @@ -295,7 +295,7 @@ public static Context CreateDummyContext( validatorSet ?? blockChain .GetNextWorldState(height - 1) .GetValidatorSet(), - contextTimeoutOptions: contextTimeoutOptions ?? new ContextTimeoutOption()); + contextOption: contextOption ?? new ContextOption()); context.MessageToPublish += (sender, message) => context.ProduceMessage(message); return context; } @@ -307,7 +307,7 @@ public static (BlockChain BlockChain, Context Context) IBlockPolicy? policy = null, IActionLoader? actionLoader = null, PrivateKey? privateKey = null, - ContextTimeoutOption? contextTimeoutOptions = null, + ContextOption? contextOption = null, ValidatorSet? validatorSet = null) { Context? context = null; @@ -323,7 +323,7 @@ public static (BlockChain BlockChain, Context Context) validatorSet ?? blockChain .GetNextWorldState(height - 1) .GetValidatorSet(), - contextTimeoutOptions: contextTimeoutOptions ?? new ContextTimeoutOption()); + contextOption: contextOption ?? new ContextOption()); context.MessageToPublish += (sender, message) => context.ProduceMessage(message); return (blockChain, context); @@ -336,7 +336,7 @@ public static ConsensusReactor CreateDummyConsensusReactor( int consensusPort = 5101, List? validatorPeers = null, int newHeightDelayMilliseconds = 10_000, - ContextTimeoutOption? contextTimeoutOptions = null) + ContextOption? contextOption = null) { key ??= PrivateKeys[1]; validatorPeers ??= Peers; @@ -356,7 +356,7 @@ public static ConsensusReactor CreateDummyConsensusReactor( validatorPeers.ToImmutableList(), new List().ToImmutableList(), TimeSpan.FromMilliseconds(newHeightDelayMilliseconds), - contextTimeoutOption: contextTimeoutOptions ?? new ContextTimeoutOption()); + contextOption: contextOption ?? new ContextOption()); } public static byte[] GetRandomBytes(int size) diff --git a/tools/Libplanet.Explorer/GraphTypes/VoteType.cs b/tools/Libplanet.Explorer/GraphTypes/VoteType.cs index 2171c4717c6..04975126b9c 100644 --- a/tools/Libplanet.Explorer/GraphTypes/VoteType.cs +++ b/tools/Libplanet.Explorer/GraphTypes/VoteType.cs @@ -30,10 +30,10 @@ public VoteType() "ValidatorPublicKey", description: "Public key of the validator which is subject of the vote.", resolve: ctx => ctx.Source.ValidatorPublicKey); - Field( + Field( "ValidatorPower", description: "Power of the validator which is subject of the vote.", - resolve: ctx => ctx.Source.ValidatorPower); + resolve: ctx => ctx.Source.ValidatorPower?.ToString()); Field>( "Flag", description: "Flag of the vote",