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); + } + } +}