diff --git a/Libplanet.Net/Consensus/ConsensusContext.cs b/Libplanet.Net/Consensus/ConsensusContext.cs
index e9bc272560f..2f061829b9b 100644
--- a/Libplanet.Net/Consensus/ConsensusContext.cs
+++ b/Libplanet.Net/Consensus/ConsensusContext.cs
@@ -130,6 +130,11 @@ public ConsensusStep Step
}
}
+ ///
+ /// Represents if is bootstrapping or not.
+ ///
+ internal bool Bootstrapping => _bootstrapping;
+
///
/// A dictionary of for each heights. Each key represents the
/// height of value, and value is the .
@@ -304,6 +309,78 @@ public IEnumerable HandleVoteSetBits(VoteSetBits voteSetBits)
return Array.Empty();
}
+ ///
+ /// Handles a received
+ /// and return to send as a reply.
+ ///
+ /// The
+ /// received from bootstrapping validator.
+ ///
+ ///
+ /// A nullable to reply back.
+ ///
+ public VotesRecall? HandleBootstrap(ConsensusBootstrapMsg bootstrapMsg)
+ {
+ long height = bootstrapMsg.Bootstrap.Height;
+ int round = bootstrapMsg.Bootstrap.Round;
+ if (height < Height)
+ {
+ _logger.Debug(
+ "Ignore a received Bootstrap as its height " +
+ "#{Height} is lower than the current context's height #{ContextHeight}",
+ height,
+ Height);
+ }
+ else
+ {
+ lock (_contextLock)
+ {
+ if (_contexts.ContainsKey(height))
+ {
+ return _contexts[height]
+ .GetVotesRecall(round);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Handles a received .
+ ///
+ /// The
+ /// received from running validator.
+ ///
+ ///
+ /// if is dispatched to
+ /// a , otherwise.
+ ///
+ public bool HandleVotesRecall(ConsensusVotesRecallMsg votesRecallMsg)
+ {
+ long height = votesRecallMsg.Height;
+ if (height < Height)
+ {
+ _logger.Debug(
+ "Discarding a received message as its height #{MessageHeight} " +
+ "is lower than the current context's height #{ContextHeight}",
+ height,
+ Height);
+ return false;
+ }
+
+ lock (_contextLock)
+ {
+ if (!_contexts.ContainsKey(height))
+ {
+ _contexts[height] = CreateContext(height);
+ }
+
+ _contexts[height].ProduceMessage(votesRecallMsg);
+ return true;
+ }
+ }
+
///
/// Returns the summary for .
///
diff --git a/Libplanet.Net/Consensus/ConsensusReactor.cs b/Libplanet.Net/Consensus/ConsensusReactor.cs
index 157f1beb3fa..040f78f9b93 100644
--- a/Libplanet.Net/Consensus/ConsensusReactor.cs
+++ b/Libplanet.Net/Consensus/ConsensusReactor.cs
@@ -190,7 +190,7 @@ private void ProcessMessage(MessageContent content)
VoteSetBits voteSetBits = _consensusContext.Contexts[maj23Msg.Height]
.GetVoteSetBits(
maj23Msg.Round,
- maj23Msg.BlockHash,
+ maj23Msg.Maj23.BlockHash,
maj23Msg.Maj23.Flag);
var sender = _gossip.Peers.First(
peer => peer.PublicKey.Equals(maj23Msg.ValidatorPublicKey));
@@ -210,6 +210,38 @@ private void ProcessMessage(MessageContent content)
break;
+ case ConsensusBootstrapMsg bootstrapMsg:
+ try
+ {
+ _consensusContext.HandleMessage(bootstrapMsg);
+ var sender = _gossip.Peers.First(
+ peer => peer.PublicKey.Equals(bootstrapMsg.ValidatorPublicKey));
+ var reply = _consensusContext.HandleBootstrap(bootstrapMsg);
+ if (reply is null)
+ {
+ // Reply is not needed. Ignore the message.
+ break;
+ }
+
+ _gossip.PublishMessage(
+ new ConsensusVotesRecallMsg(reply),
+ new[] { sender });
+ }
+ catch (InvalidOperationException)
+ {
+ _logger.Debug(
+ "Cannot respond received ConsensusBootstrapMsg message " +
+ "{Message} since there is no corresponding peer in the table",
+ bootstrapMsg);
+ }
+
+ break;
+
+ case ConsensusVotesRecallMsg votesRecallMsg:
+ // Note: ConsensusVoteSetBitsMsg will not be stored to context's message log.
+ _consensusContext.HandleVotesRecall(votesRecallMsg);
+ break;
+
case ConsensusMsg consensusMsg:
_consensusContext.HandleMessage(consensusMsg);
break;
diff --git a/Libplanet.Net/Consensus/Context.Async.cs b/Libplanet.Net/Consensus/Context.Async.cs
index 0198fa90503..d3d8035ee2a 100644
--- a/Libplanet.Net/Consensus/Context.Async.cs
+++ b/Libplanet.Net/Consensus/Context.Async.cs
@@ -23,16 +23,12 @@ public void Start(BlockCommit? lastCommit = null, bool bootstrapping = false)
Height,
lastCommit);
_lastCommit = lastCommit;
+ _bootstrapping = bootstrapping;
ProduceMutation(() => StartRound(0));
// FIXME: Exceptions inside tasks should be handled properly.
_ = MessageConsumerTask(_cancellationTokenSource.Token);
_ = MutationConsumerTask(_cancellationTokenSource.Token);
-
- if (bootstrapping)
- {
- _ = BootstrappingTask(_cancellationTokenSource.Token);
- }
}
///
@@ -99,30 +95,6 @@ internal async Task MutationConsumerTask(CancellationToken cancellationToken)
}
}
- internal async Task BootstrappingTask(CancellationToken cancellationToken)
- {
- while (true)
- {
- try
- {
- cancellationToken.ThrowIfCancellationRequested();
- await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
- }
- catch (OperationCanceledException oce)
- {
- _logger.Debug(oce, "Cancellation was requested");
- ExceptionOccurred?.Invoke(this, oce);
- throw;
- }
-#pragma warning disable S125
- /*if (_heightVoteSet.GetRandomMessage() is { } message)
- {
- BroadcastMessage(message)
- }*/
-#pragma warning restore S125
- }
- }
-
///
/// Adds to the message queue.
///
diff --git a/Libplanet.Net/Consensus/Context.Mutate.cs b/Libplanet.Net/Consensus/Context.Mutate.cs
index 880507dd1a9..d3f0d06a7f2 100644
--- a/Libplanet.Net/Consensus/Context.Mutate.cs
+++ b/Libplanet.Net/Consensus/Context.Mutate.cs
@@ -25,6 +25,14 @@ private void StartRound(int round)
ToString());
Round = round;
_heightVoteSet.SetRound(round);
+ if (_bootstrapping)
+ {
+ Bootstrap bootstrap = new BootstrapMetadata(
+ Height, round, DateTimeOffset.UtcNow, _privateKey.PublicKey).Sign(_privateKey);
+ BroadcastMessage(
+ new ConsensusBootstrapMsg(bootstrap));
+ }
+
Proposal = null;
Step = ConsensusStep.Propose;
if (_validatorSet.GetProposer(Height, Round).PublicKey == _privateKey.PublicKey)
@@ -101,33 +109,55 @@ private bool AddMessage(ConsensusMsg message)
message);
}
- switch (message)
+ if (message is ConsensusVoteMsg voteMsg)
{
- case ConsensusProposalMsg proposal:
- AddProposal(proposal.Proposal);
- break;
- case ConsensusPreVoteMsg preVote:
- _heightVoteSet.AddVote(preVote.PreVote);
- break;
- case ConsensusPreCommitMsg preCommit:
- _heightVoteSet.AddVote(preCommit.PreCommit);
- break;
- case ConsensusMaj23Msg maj23:
- _heightVoteSet.SetPeerMaj23(maj23.Maj23);
- break;
+ switch (voteMsg)
+ {
+ case ConsensusProposalMsg proposal:
+ AddProposal(proposal.Proposal);
+ break;
+ case ConsensusPreVoteMsg preVote:
+ _heightVoteSet.AddVote(preVote.PreVote);
+ break;
+ case ConsensusPreCommitMsg preCommit:
+ _heightVoteSet.AddVote(preCommit.PreCommit);
+ break;
+ }
+
+ _logger.Debug(
+ "{FName}: Message: {Message} => Height: {Height}, Round: {Round}, " +
+ "Validator Address: {VAddress}, " +
+ "Hash: {BlockHash}. (context: {Context})",
+ nameof(AddMessage),
+ voteMsg,
+ voteMsg.Height,
+ voteMsg.Round,
+ voteMsg.ValidatorPublicKey.ToAddress(),
+ voteMsg.BlockHash,
+ ToString());
}
+ else
+ {
+ switch (message)
+ {
+ case ConsensusMaj23Msg maj23:
+ _heightVoteSet.SetPeerMaj23(maj23.Maj23);
+ break;
+ case ConsensusVotesRecallMsg votesRecall:
+ CatchupWithVotesRecall(votesRecall.VotesRecall);
+ break;
+ }
- _logger.Debug(
- "{FName}: Message: {Message} => Height: {Height}, Round: {Round}, " +
- "Validator Address: {VAddress}, " +
- "Hash: {BlockHash}. (context: {Context})",
- nameof(AddMessage),
- message,
- message.Height,
- message.Round,
- message.ValidatorPublicKey.ToAddress(),
- message.BlockHash,
- ToString());
+ _logger.Debug(
+ "{FName}: Message: {Message} => Height: {Height}, Round: {Round}, " +
+ "Validator Address: {VAddress}. (context: {Context})",
+ nameof(AddMessage),
+ message,
+ message.Height,
+ message.Round,
+ message.ValidatorPublicKey.ToAddress(),
+ ToString());
+ }
return true;
}
diff --git a/Libplanet.Net/Consensus/Context.cs b/Libplanet.Net/Consensus/Context.cs
index aeabe6568c4..dfb90dd6554 100644
--- a/Libplanet.Net/Consensus/Context.cs
+++ b/Libplanet.Net/Consensus/Context.cs
@@ -105,6 +105,7 @@ private readonly
private Block? _decision;
private int _committedRound;
private BlockCommit? _lastCommit;
+ private bool _bootstrapping;
///
/// Initializes a new instance of the class.
@@ -178,6 +179,7 @@ private Context(
_validRound = -1;
_decision = null;
_committedRound = -1;
+ _bootstrapping = false;
_blockChain = blockChain;
_codec = new Codec();
_messageRequests = Channel.CreateUnbounded();
@@ -299,6 +301,48 @@ public IEnumerable GetVoteSetBitsResponse(VoteSetBits voteSetBits)
});
}
+ public VotesRecall GetVotesRecall(int round)
+ {
+ ImmutableHashSet votes;
+ if (round < Round)
+ {
+ round++;
+ }
+
+ votes = _heightVoteSet.PreVotes(round).List().Concat(
+ _heightVoteSet.PreCommits(round).List()).ToImmutableHashSet();
+
+ return new VotesRecallMetadata(
+ Height,
+ round,
+ DateTimeOffset.UtcNow,
+ _privateKey.PublicKey,
+ votes).Sign(_privateKey);
+ }
+
+ public void CatchupWithVotesRecall(VotesRecall votesRecall)
+ {
+ foreach (Vote vote in votesRecall.Votes)
+ {
+ if (vote.Height != Height)
+ {
+ continue;
+ }
+
+ if (vote.Round > Round + 1)
+ {
+ continue;
+ }
+
+ if (!vote.Verify())
+ {
+ continue;
+ }
+
+ _heightVoteSet.AddVote(vote);
+ }
+ }
+
///
/// Returns the summary of context in JSON-formatted string.
///
diff --git a/Libplanet.Net/Consensus/HeightVoteSet.cs b/Libplanet.Net/Consensus/HeightVoteSet.cs
index b13aa7a5c38..d02f73f0580 100644
--- a/Libplanet.Net/Consensus/HeightVoteSet.cs
+++ b/Libplanet.Net/Consensus/HeightVoteSet.cs
@@ -74,7 +74,6 @@ public void SetRound(int round)
{
lock (_lock)
{
- // FIXME: This shouldn't be _round + 1?
var newRound = _round + 1;
if (_round != 0 && (round < newRound))
{
diff --git a/Libplanet.Net/Messages/ConsensusBootstrapMsg.cs b/Libplanet.Net/Messages/ConsensusBootstrapMsg.cs
new file mode 100644
index 00000000000..767e5693a5a
--- /dev/null
+++ b/Libplanet.Net/Messages/ConsensusBootstrapMsg.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using Libplanet.Consensus;
+
+namespace Libplanet.Net.Messages
+{
+ ///
+ /// A message class for informing that peer is on bootstrapping.
+ ///
+ public class ConsensusBootstrapMsg : ConsensusMsg
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A information
+ /// of .
+ public ConsensusBootstrapMsg(Bootstrap bootstrap)
+ : base(bootstrap.ValidatorPublicKey, bootstrap.Height, bootstrap.Round)
+ {
+ Bootstrap = bootstrap;
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with marshalled message.
+ ///
+ /// A marshalled message.
+ public ConsensusBootstrapMsg(byte[][] dataframes)
+ : this(bootstrap: new Bootstrap(dataframes[0]))
+ {
+ }
+
+ ///
+ /// A of the message.
+ ///
+ public Bootstrap Bootstrap { get; }
+
+ ///
+ public override IEnumerable DataFrames =>
+ new List { Bootstrap.ToByteArray() };
+
+ ///
+ public override MessageType Type => MessageType.BootstrapMsg;
+
+ ///
+ public override bool Equals(ConsensusMsg? other)
+ {
+ return other is ConsensusBootstrapMsg message &&
+ message.Bootstrap.Equals(Bootstrap);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ return obj is ConsensusMsg other && Equals(other);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Type, Bootstrap);
+ }
+ }
+}
diff --git a/Libplanet.Net/Messages/ConsensusMaj23Msg.cs b/Libplanet.Net/Messages/ConsensusMaj23Msg.cs
index 40551d1709f..f1d34e932b3 100644
--- a/Libplanet.Net/Messages/ConsensusMaj23Msg.cs
+++ b/Libplanet.Net/Messages/ConsensusMaj23Msg.cs
@@ -14,7 +14,7 @@ public class ConsensusMaj23Msg : ConsensusMsg
///
/// A of given height and round.
public ConsensusMaj23Msg(Maj23 maj23)
- : base(maj23.ValidatorPublicKey, maj23.Height, maj23.Round, maj23.BlockHash)
+ : base(maj23.ValidatorPublicKey, maj23.Height, maj23.Round)
{
Maj23 = maj23;
}
@@ -41,17 +41,20 @@ public ConsensusMaj23Msg(byte[][] dataframes)
///
public override MessageType Type => MessageType.ConsensusMaj23Msg;
+ ///
public override bool Equals(ConsensusMsg? other)
{
return other is ConsensusMaj23Msg message &&
message.Maj23.Equals(Maj23);
}
+ ///
public override bool Equals(object? obj)
{
return obj is ConsensusMaj23Msg other && Equals(other);
}
+ ///
public override int GetHashCode()
{
return HashCode.Combine(Type, Maj23);
diff --git a/Libplanet.Net/Messages/ConsensusMsg.cs b/Libplanet.Net/Messages/ConsensusMsg.cs
index 3f152502f6d..808404679f3 100644
--- a/Libplanet.Net/Messages/ConsensusMsg.cs
+++ b/Libplanet.Net/Messages/ConsensusMsg.cs
@@ -1,5 +1,4 @@
using System;
-using Libplanet.Blocks;
using Libplanet.Crypto;
using Libplanet.Net.Consensus;
@@ -17,17 +16,14 @@ public abstract class ConsensusMsg : MessageContent, IEquatable
/// A of the validator who made this message.
/// A the message is for.
/// A the message is written for.
- /// A the message is written for.
protected ConsensusMsg(
PublicKey validatorPublicKey,
long height,
- int round,
- BlockHash blockHash)
+ int round)
{
ValidatorPublicKey = validatorPublicKey;
Round = round;
Height = height;
- BlockHash = blockHash;
}
///
@@ -46,14 +42,21 @@ protected ConsensusMsg(
public int Round { get; }
///
- /// A the message is written for.
+ /// Indicates whether the current
+ /// is equal to another .
///
- public BlockHash BlockHash { get; }
-
+ /// An to compare with this
+ /// .
+ ///
+ /// true if the current is equal to the other parameter;
+ /// otherwise, false.
+ ///
public abstract bool Equals(ConsensusMsg? other);
+ ///
public abstract override bool Equals(object? obj);
+ ///
public abstract override int GetHashCode();
}
}
diff --git a/Libplanet.Net/Messages/ConsensusPreCommitMsg.cs b/Libplanet.Net/Messages/ConsensusPreCommitMsg.cs
index 117cb12c326..10b31787871 100644
--- a/Libplanet.Net/Messages/ConsensusPreCommitMsg.cs
+++ b/Libplanet.Net/Messages/ConsensusPreCommitMsg.cs
@@ -8,7 +8,7 @@ namespace Libplanet.Net.Messages
///
/// A message class for .
///
- public class ConsensusPreCommitMsg : ConsensusMsg
+ public class ConsensusPreCommitMsg : ConsensusVoteMsg
{
private static Bencodex.Codec _codec = new Bencodex.Codec();
@@ -55,17 +55,20 @@ public ConsensusPreCommitMsg(byte[][] dataframes)
///
public override MessageType Type => MessageType.ConsensusCommit;
+ ///
public override bool Equals(ConsensusMsg? other)
{
return other is ConsensusPreCommitMsg message &&
PreCommit.Equals(message.PreCommit);
}
+ ///
public override bool Equals(object? obj)
{
return obj is ConsensusMsg other && Equals(other);
}
+ ///
public override int GetHashCode()
{
return HashCode.Combine(Type, PreCommit);
diff --git a/Libplanet.Net/Messages/ConsensusPreVoteMsg.cs b/Libplanet.Net/Messages/ConsensusPreVoteMsg.cs
index 107b896beff..1bae7271bb9 100644
--- a/Libplanet.Net/Messages/ConsensusPreVoteMsg.cs
+++ b/Libplanet.Net/Messages/ConsensusPreVoteMsg.cs
@@ -8,7 +8,7 @@ namespace Libplanet.Net.Messages
///
/// A message class for .
///
- public class ConsensusPreVoteMsg : ConsensusMsg
+ public class ConsensusPreVoteMsg : ConsensusVoteMsg
{
private static Bencodex.Codec _codec = new Bencodex.Codec();
@@ -55,17 +55,20 @@ public ConsensusPreVoteMsg(byte[][] dataframes)
///
public override MessageType Type => MessageType.ConsensusVote;
+ ///
public override bool Equals(ConsensusMsg? other)
{
return other is ConsensusPreVoteMsg message &&
PreVote.Equals(message.PreVote);
}
+ ///
public override bool Equals(object? obj)
{
return obj is ConsensusMsg other && Equals(other);
}
+ ///
public override int GetHashCode()
{
return HashCode.Combine(Type, PreVote);
diff --git a/Libplanet.Net/Messages/ConsensusProposalMsg.cs b/Libplanet.Net/Messages/ConsensusProposalMsg.cs
index d55b599d7a5..de45ab198b6 100644
--- a/Libplanet.Net/Messages/ConsensusProposalMsg.cs
+++ b/Libplanet.Net/Messages/ConsensusProposalMsg.cs
@@ -8,7 +8,7 @@ namespace Libplanet.Net.Messages
///
/// A message class for .
///
- public class ConsensusProposalMsg : ConsensusMsg
+ public class ConsensusProposalMsg : ConsensusVoteMsg
{
///
/// Initializes a new instance of the class.
@@ -43,17 +43,20 @@ public ConsensusProposalMsg(byte[][] dataframes)
///
public override MessageType Type => MessageType.ConsensusProposal;
+ ///
public override bool Equals(ConsensusMsg? other)
{
return other is ConsensusProposalMsg message &&
message.Proposal.Equals(Proposal);
}
+ ///
public override bool Equals(object? obj)
{
return obj is ConsensusMsg other && Equals(other);
}
+ ///
public override int GetHashCode()
{
return HashCode.Combine(Type, Proposal);
diff --git a/Libplanet.Net/Messages/ConsensusVoteMsg.cs b/Libplanet.Net/Messages/ConsensusVoteMsg.cs
new file mode 100644
index 00000000000..a0b5d2b647c
--- /dev/null
+++ b/Libplanet.Net/Messages/ConsensusVoteMsg.cs
@@ -0,0 +1,35 @@
+using Libplanet.Blocks;
+using Libplanet.Crypto;
+using Libplanet.Net.Consensus;
+
+namespace Libplanet.Net.Messages
+{
+ ///
+ /// A abstract base class message for consensus.
+ ///
+ public abstract class ConsensusVoteMsg : ConsensusMsg
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A of the validator who made this message.
+ /// A the message is for.
+ /// A the message is written for.
+ /// A the message is written for.
+ protected ConsensusVoteMsg(
+ PublicKey validatorPublicKey,
+ long height,
+ int round,
+ BlockHash blockHash)
+ : base(validatorPublicKey, height, round)
+ {
+ BlockHash = blockHash;
+ }
+
+ ///
+ /// A the message is written for.
+ ///
+ public BlockHash BlockHash { get; }
+ }
+}
diff --git a/Libplanet.Net/Messages/ConsensusVoteSetBitsMsg.cs b/Libplanet.Net/Messages/ConsensusVoteSetBitsMsg.cs
index 8ae1bddee47..6d03c8a92d1 100644
--- a/Libplanet.Net/Messages/ConsensusVoteSetBitsMsg.cs
+++ b/Libplanet.Net/Messages/ConsensusVoteSetBitsMsg.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Libplanet.Blocks;
using Libplanet.Consensus;
namespace Libplanet.Net.Messages
@@ -18,10 +19,10 @@ public ConsensusVoteSetBitsMsg(VoteSetBits voteSetBits)
: base(
voteSetBits.ValidatorPublicKey,
voteSetBits.Height,
- voteSetBits.Round,
- voteSetBits.BlockHash)
+ voteSetBits.Round)
{
VoteSetBits = voteSetBits;
+ BlockHash = voteSetBits.BlockHash;
}
///
@@ -39,6 +40,11 @@ public ConsensusVoteSetBitsMsg(byte[][] dataframes)
///
public VoteSetBits VoteSetBits { get; }
+ ///
+ /// A of the message.
+ ///
+ public BlockHash BlockHash { get; }
+
///
public override IEnumerable DataFrames =>
new List { VoteSetBits.ToByteArray() };
diff --git a/Libplanet.Net/Messages/ConsensusVotesRecallMsg.cs b/Libplanet.Net/Messages/ConsensusVotesRecallMsg.cs
new file mode 100644
index 00000000000..2696b48cf3b
--- /dev/null
+++ b/Libplanet.Net/Messages/ConsensusVotesRecallMsg.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using Libplanet.Consensus;
+
+namespace Libplanet.Net.Messages
+{
+ ///
+ /// A message class for informing that peer is on bootstrapping.
+ ///
+ public class ConsensusVotesRecallMsg : ConsensusMsg
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A information
+ /// of .
+ public ConsensusVotesRecallMsg(VotesRecall votesRecall)
+ : base(votesRecall.ValidatorPublicKey, votesRecall.Height, votesRecall.Round)
+ {
+ VotesRecall = votesRecall;
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// with marshalled message.
+ ///
+ /// A marshalled message.
+ public ConsensusVotesRecallMsg(byte[][] dataframes)
+ : this(votesRecall: new VotesRecall(dataframes[0]))
+ {
+ }
+
+ ///
+ /// A of the message.
+ ///
+ public VotesRecall VotesRecall { get; }
+
+ ///
+ public override IEnumerable DataFrames =>
+ new List { VotesRecall.ToByteArray() };
+
+ ///
+ public override MessageType Type => MessageType.VotesRecallMsg;
+
+ ///
+ public override bool Equals(ConsensusMsg? other)
+ {
+ return other is ConsensusVotesRecallMsg message &&
+ message.VotesRecall.Equals(VotesRecall);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ return obj is ConsensusMsg other && Equals(other);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Type, VotesRecall);
+ }
+ }
+}
diff --git a/Libplanet.Net/Messages/MessageContent.cs b/Libplanet.Net/Messages/MessageContent.cs
index 1c2c910f399..bf5a5f528aa 100644
--- a/Libplanet.Net/Messages/MessageContent.cs
+++ b/Libplanet.Net/Messages/MessageContent.cs
@@ -121,6 +121,16 @@ public enum MessageType : byte
///
ConsensusVoteSetBitsMsg = 0x46,
+ ///
+ /// Message that informs peer is on bootstrapping to other peer.
+ ///
+ BootstrapMsg = 0x47,
+
+ ///
+ /// Message that helps peer by recalling past votes.
+ ///
+ VotesRecallMsg = 0x48,
+
///
/// List of message IDs that the peer seen recently.
///
diff --git a/Libplanet/Consensus/Bootstrap.cs b/Libplanet/Consensus/Bootstrap.cs
new file mode 100644
index 00000000000..e6e04ee9dd4
--- /dev/null
+++ b/Libplanet/Consensus/Bootstrap.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Immutable;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Bencodex;
+using Bencodex.Types;
+using Libplanet.Crypto;
+
+namespace Libplanet.Consensus
+{
+ ///
+ /// A class used to bootstrap .
+ ///
+ public class Bootstrap : IEquatable
+ {
+ private static readonly byte[] SignatureKey = { 0x53 }; // 'S'
+ private static Codec _codec = new Codec();
+
+ private readonly BootstrapMetadata _bootstrapMetadata;
+
+ ///
+ /// Instantiates a with given
+ /// and its .
+ ///
+ /// A for bootstrapping.
+ ///
+ /// A signature signed with .
+ ///
+ /// Thrown if given is
+ /// empty.
+ /// Thrown if given is
+ /// invalid and cannot be verified with .
+ public Bootstrap(BootstrapMetadata bootstrapMetadata, ImmutableArray signature)
+ {
+ _bootstrapMetadata = bootstrapMetadata;
+ Signature = signature;
+
+ if (signature.IsDefaultOrEmpty)
+ {
+ throw new ArgumentNullException(
+ nameof(signature),
+ "Signature cannot be null or empty.");
+ }
+ else if (!Verify())
+ {
+ throw new ArgumentException("Signature is invalid.", nameof(signature));
+ }
+ }
+
+ public Bootstrap(byte[] marshaled)
+ : this((Dictionary)_codec.Decode(marshaled))
+ {
+ }
+
+#pragma warning disable SA1118 // The parameter spans multiple lines
+ public Bootstrap(Dictionary encoded)
+ : this(
+ new BootstrapMetadata(encoded),
+ encoded.ContainsKey(SignatureKey)
+ ? encoded.GetValue(SignatureKey).ToImmutableArray()
+ : ImmutableArray.Empty)
+ {
+ }
+#pragma warning restore SA1118
+
+ ///
+ public long Height => _bootstrapMetadata.Height;
+
+ ///
+ public int Round => _bootstrapMetadata.Round;
+
+ ///
+ public PublicKey ValidatorPublicKey => _bootstrapMetadata.ValidatorPublicKey;
+
+ ///
+ public DateTimeOffset Timestamp => _bootstrapMetadata.Timestamp;
+
+ ///
+ /// A signature that signed with .
+ ///
+ public ImmutableArray Signature { get; }
+
+ ///
+ /// A Bencodex-encoded value of .
+ ///
+ [JsonIgnore]
+ public Dictionary Encoded =>
+ !Signature.IsEmpty
+ ? _bootstrapMetadata.Encoded.Add(SignatureKey, Signature)
+ : _bootstrapMetadata.Encoded;
+
+ ///
+ /// encoded data.
+ ///
+ public ImmutableArray ByteArray => ToByteArray().ToImmutableArray();
+
+ public byte[] ToByteArray() => _codec.Encode(Encoded);
+
+ ///
+ /// Verifies whether the is properly signed by
+ /// .
+ ///
+ /// if the is not empty
+ /// and is a valid signature signed by .
+ [Pure]
+ public bool Verify() =>
+ !Signature.IsDefaultOrEmpty &&
+ ValidatorPublicKey.Verify(
+ _bootstrapMetadata.ByteArray.ToImmutableArray(),
+ Signature);
+
+ ///
+ [Pure]
+ public bool Equals(Bootstrap? other)
+ {
+ return other is { } bootstrap &&
+ _bootstrapMetadata.Equals(bootstrap._bootstrapMetadata) &&
+ Signature.SequenceEqual(bootstrap.Signature);
+ }
+
+ ///
+ [Pure]
+ public override bool Equals(object? obj)
+ {
+ return obj is Bootstrap other && Equals(other);
+ }
+
+ ///
+ [Pure]
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(
+ _bootstrapMetadata.GetHashCode(),
+ ByteUtil.CalculateHashCode(Signature.ToArray()));
+ }
+ }
+}
diff --git a/Libplanet/Consensus/BootstrapMetadata.cs b/Libplanet/Consensus/BootstrapMetadata.cs
new file mode 100644
index 00000000000..fcb245f3112
--- /dev/null
+++ b/Libplanet/Consensus/BootstrapMetadata.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Immutable;
+using System.Globalization;
+using System.Text.Json.Serialization;
+using Bencodex;
+using Bencodex.Types;
+using Libplanet.Crypto;
+
+namespace Libplanet.Consensus
+{
+ public class BootstrapMetadata : IEquatable
+ {
+ private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
+ private static readonly byte[] HeightKey = { 0x48 }; // 'H'
+ private static readonly byte[] RoundKey = { 0x52 }; // 'R'
+ private static readonly byte[] TimestampKey = { 0x74 }; // 't'
+ private static readonly byte[] ValidatorPublicKeyKey = { 0x50 }; // 'P'
+
+ private static Codec _codec = new Codec();
+
+ public BootstrapMetadata(
+ long height,
+ int round,
+ DateTimeOffset timestamp,
+ PublicKey validatorPublicKey)
+ {
+ if (height < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(height),
+ "Height must be greater than or equal to 0.");
+ }
+
+ if (round < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(round),
+ "Round must be greater than or equal to 0.");
+ }
+
+ Height = height;
+ Round = round;
+ Timestamp = timestamp;
+ ValidatorPublicKey = validatorPublicKey;
+ }
+
+#pragma warning disable SA1118 // The parameter spans multiple lines
+ public BootstrapMetadata(Dictionary encoded)
+ : this(
+ height: encoded.GetValue(HeightKey),
+ round: encoded.GetValue(RoundKey),
+ timestamp: DateTimeOffset.ParseExact(
+ encoded.GetValue(TimestampKey),
+ TimestampFormat,
+ CultureInfo.InvariantCulture),
+ validatorPublicKey: new PublicKey(
+ encoded.GetValue(ValidatorPublicKeyKey).ByteArray))
+ {
+ }
+#pragma warning restore SA1118
+ ///
+ /// Current height of bootstrapping validator.
+ ///
+ public long Height { get; }
+
+ ///
+ /// Current round of bootstrapping validator.
+ ///
+ public int Round { get; }
+
+ ///
+ /// A of bootstrapping validator.
+ ///
+ public PublicKey ValidatorPublicKey { get; }
+
+ ///
+ /// The time at which the bootstrapping requested.
+ ///
+ public DateTimeOffset Timestamp { get; }
+
+ ///
+ /// A Bencodex-encoded value of .
+ ///
+ [JsonIgnore]
+ public Dictionary Encoded
+ {
+ get
+ {
+ Dictionary encoded = Bencodex.Types.Dictionary.Empty
+ .Add(HeightKey, Height)
+ .Add(RoundKey, Round)
+ .Add(ValidatorPublicKeyKey, ValidatorPublicKey.Format(compress: true))
+ .Add(
+ TimestampKey,
+ Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture));
+
+ return encoded;
+ }
+ }
+
+ public ImmutableArray ByteArray => ToByteArray().ToImmutableArray();
+
+ public byte[] ToByteArray() => _codec.Encode(Encoded);
+
+ ///
+ /// Signs given with given .
+ ///
+ /// A to sign.
+ /// Returns a signed .
+ public Bootstrap Sign(PrivateKey signer) =>
+ new Bootstrap(this, signer.Sign(ByteArray).ToImmutableArray());
+
+ ///
+ public bool Equals(BootstrapMetadata? other)
+ {
+ return other is { } metadata &&
+ Height == metadata.Height &&
+ Round == metadata.Round &&
+ ValidatorPublicKey.Equals(metadata.ValidatorPublicKey) &&
+ Timestamp
+ .ToString(TimestampFormat, CultureInfo.InvariantCulture).Equals(
+ metadata.Timestamp.ToString(
+ TimestampFormat,
+ CultureInfo.InvariantCulture));
+ }
+
+ ///
+ public override bool Equals(object? obj) =>
+ obj is BootstrapMetadata other && Equals(other);
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(
+ Height,
+ Round,
+ ValidatorPublicKey,
+ Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture));
+ }
+ }
+}
diff --git a/Libplanet/Consensus/VotesRecall.cs b/Libplanet/Consensus/VotesRecall.cs
new file mode 100644
index 00000000000..724ddfe0668
--- /dev/null
+++ b/Libplanet/Consensus/VotesRecall.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Immutable;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Bencodex;
+using Bencodex.Types;
+using Libplanet.Crypto;
+
+namespace Libplanet.Consensus
+{
+ ///
+ /// A class used to help peer to bootstrap by sending
+ /// s that the peer has.
+ ///
+ public class VotesRecall : IEquatable
+ {
+ private static readonly byte[] SignatureKey = { 0x53 }; // 'S'
+ private static Codec _codec = new Codec();
+
+ private readonly VotesRecallMetadata _votesRecallMetadata;
+
+ ///
+ /// Instantiates a with given
+ /// and its .
+ ///
+ ///
+ /// A to help bootstrapping.
+ /// A signature signed with .
+ ///
+ /// Thrown if given is
+ /// empty.
+ /// Thrown if given is
+ /// invalid and cannot be verified with .
+ public VotesRecall(VotesRecallMetadata votesRecallMetadata, ImmutableArray signature)
+ {
+ _votesRecallMetadata = votesRecallMetadata;
+ Signature = signature;
+
+ if (signature.IsDefaultOrEmpty)
+ {
+ throw new ArgumentNullException(
+ nameof(signature),
+ "Signature cannot be null or empty.");
+ }
+ else if (!Verify())
+ {
+ throw new ArgumentException("Signature is invalid.", nameof(signature));
+ }
+ }
+
+ public VotesRecall(byte[] marshaled)
+ : this((Dictionary)_codec.Decode(marshaled))
+ {
+ }
+
+#pragma warning disable SA1118 // The parameter spans multiple lines
+ public VotesRecall(Dictionary encoded)
+ : this(
+ new VotesRecallMetadata(encoded),
+ encoded.ContainsKey(SignatureKey)
+ ? encoded.GetValue(SignatureKey).ToImmutableArray()
+ : ImmutableArray.Empty)
+ {
+ }
+#pragma warning restore SA1118
+
+ ///
+ public long Height => _votesRecallMetadata.Height;
+
+ ///
+ public int Round => _votesRecallMetadata.Round;
+
+ ///
+ public DateTimeOffset Timestamp => _votesRecallMetadata.Timestamp;
+
+ ///
+ public PublicKey ValidatorPublicKey => _votesRecallMetadata.ValidatorPublicKey;
+
+ ///
+ public ImmutableHashSet Votes => _votesRecallMetadata.Votes;
+
+ ///
+ /// A signature that signed with .
+ ///
+ public ImmutableArray Signature { get; }
+
+ ///
+ /// A Bencodex-encoded value of .
+ ///
+ [JsonIgnore]
+ public Dictionary Encoded =>
+ !Signature.IsEmpty
+ ? _votesRecallMetadata.Encoded.Add(SignatureKey, Signature)
+ : _votesRecallMetadata.Encoded;
+
+ ///
+ /// encoded data.
+ ///
+ public ImmutableArray ByteArray => ToByteArray().ToImmutableArray();
+
+ public byte[] ToByteArray() => _codec.Encode(Encoded);
+
+ ///
+ /// Verifies whether the is properly signed by
+ /// .
+ ///
+ /// if the is not empty
+ /// and is a valid signature signed by .
+ [Pure]
+ public bool Verify() =>
+ !Signature.IsDefaultOrEmpty &&
+ ValidatorPublicKey.Verify(
+ _votesRecallMetadata.ByteArray.ToImmutableArray(),
+ Signature);
+
+ ///
+ [Pure]
+ public bool Equals(VotesRecall? other)
+ {
+ return other is { } votesRecall &&
+ _votesRecallMetadata.Equals(votesRecall._votesRecallMetadata) &&
+ Signature.SequenceEqual(votesRecall.Signature);
+ }
+
+ ///
+ [Pure]
+ public override bool Equals(object? obj)
+ {
+ return obj is VotesRecall other && Equals(other);
+ }
+
+ ///
+ [Pure]
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(
+ _votesRecallMetadata.GetHashCode(),
+ ByteUtil.CalculateHashCode(Signature.ToArray()));
+ }
+ }
+}
diff --git a/Libplanet/Consensus/VotesRecallMetadata.cs b/Libplanet/Consensus/VotesRecallMetadata.cs
new file mode 100644
index 00000000000..851967793ea
--- /dev/null
+++ b/Libplanet/Consensus/VotesRecallMetadata.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Bencodex;
+using Bencodex.Types;
+using Libplanet.Crypto;
+
+namespace Libplanet.Consensus
+{
+ public class VotesRecallMetadata : IEquatable
+ {
+ private const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
+ private static readonly byte[] HeightKey = { 0x48 }; // 'H'
+ private static readonly byte[] RoundKey = { 0x52 }; // 'R'
+ private static readonly byte[] TimestampKey = { 0x74 }; // 't'
+ private static readonly byte[] ValidatorPublicKeyKey = { 0x50 }; // 'P'
+ private static readonly byte[] VotesKey = { 0x76 }; // 'v'
+
+ private static Codec _codec = new Codec();
+
+ public VotesRecallMetadata(
+ long height,
+ int round,
+ DateTimeOffset timestamp,
+ PublicKey validatorPublicKey,
+ ImmutableHashSet votes)
+ {
+ if (height < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(height),
+ "Height must be greater than or equal to 0.");
+ }
+
+ if (round < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(round),
+ "Round must be greater than or equal to 0.");
+ }
+
+ Height = height;
+ Round = round;
+ Timestamp = timestamp;
+ ValidatorPublicKey = validatorPublicKey;
+ Votes = votes;
+ }
+
+#pragma warning disable SA1118 // The parameter spans multiple lines
+ public VotesRecallMetadata(Dictionary encoded)
+ : this(
+ height: encoded.GetValue(HeightKey),
+ round: encoded.GetValue(RoundKey),
+ timestamp: DateTimeOffset.ParseExact(
+ encoded.GetValue(TimestampKey),
+ TimestampFormat,
+ CultureInfo.InvariantCulture),
+ validatorPublicKey: new PublicKey(
+ encoded.GetValue(ValidatorPublicKeyKey).ByteArray),
+ votes: encoded.GetValue(VotesKey).Select(
+ v => new Vote(v)).ToImmutableHashSet())
+ {
+ }
+#pragma warning restore SA1118
+
+ ///
+ /// The height of given votes recall.
+ ///
+ public long Height { get; }
+
+ ///
+ /// The round of given votes recall.
+ ///
+ public int Round { get; }
+
+ ///
+ /// The time at which the votes recall took place.
+ ///
+ public DateTimeOffset Timestamp { get; }
+
+ ///
+ /// A of
+ /// that helps bootstrapping with votes recall.
+ ///
+ public PublicKey ValidatorPublicKey { get; }
+
+ ///
+ /// Set of recalled s.
+ ///
+ public ImmutableHashSet Votes { get; }
+
+ ///
+ /// A Bencodex-encoded value of .
+ ///
+ [JsonIgnore]
+ public Dictionary Encoded
+ {
+ get
+ {
+ Dictionary encoded = Bencodex.Types.Dictionary.Empty
+ .Add(HeightKey, Height)
+ .Add(RoundKey, Round)
+ .Add(
+ TimestampKey,
+ Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture))
+ .Add(ValidatorPublicKeyKey, ValidatorPublicKey.Format(compress: true))
+ .Add(
+ VotesKey,
+ new List(Votes.Select(v => v.Bencoded)));
+
+ return encoded;
+ }
+ }
+
+ public ImmutableArray ByteArray => ToByteArray().ToImmutableArray();
+
+ public byte[] ToByteArray() => _codec.Encode(Encoded);
+
+ ///
+ /// Signs given with given .
+ ///
+ /// A to sign.
+ /// Returns a signed .
+ public VotesRecall Sign(PrivateKey signer) =>
+ new VotesRecall(this, signer.Sign(ByteArray).ToImmutableArray());
+
+ ///
+ public bool Equals(VotesRecallMetadata? other)
+ {
+ return other is { } metadata &&
+ Height == metadata.Height &&
+ Round == metadata.Round &&
+ Timestamp
+ .ToString(TimestampFormat, CultureInfo.InvariantCulture).Equals(
+ metadata.Timestamp.ToString(
+ TimestampFormat,
+ CultureInfo.InvariantCulture)) &&
+ ValidatorPublicKey.Equals(metadata.ValidatorPublicKey) &&
+ Votes.SetEquals(metadata.Votes);
+ }
+
+ ///
+ public override bool Equals(object? obj) =>
+ obj is VotesRecallMetadata other && Equals(other);
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(
+ Height,
+ Round,
+ Timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture),
+ ValidatorPublicKey);
+ }
+ }
+}