Skip to content

Commit

Permalink
feat: add ProposalClaim and its related features
Browse files Browse the repository at this point in the history
  • Loading branch information
limebell committed Jun 26, 2023
1 parent c07d824 commit 3b417c1
Show file tree
Hide file tree
Showing 7 changed files with 504 additions and 1 deletion.
47 changes: 46 additions & 1 deletion Libplanet.Net.Tests/Consensus/ConsensusContextTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ public async void HandleVoteSetBits()
}
while (step != ConsensusStep.PreCommit);

// VoteSetBits expects missing votes and corresponding proposal
// VoteSetBits expects missing votes
var voteSetBits =
new VoteSetBitsMetadata(
1,
Expand All @@ -395,5 +395,50 @@ public async void HandleVoteSetBits()
Assert.Equal(TestUtils.PrivateKeys[1].PublicKey, votes[1].ValidatorPublicKey);
Assert.Equal(TestUtils.PrivateKeys[3].PublicKey, votes[2].ValidatorPublicKey);
}

[Fact]
public async void HandleProposalClaim()
{
PrivateKey proposer = TestUtils.PrivateKeys[1];
ConsensusStep step = ConsensusStep.Default;
var stepChanged = new AsyncAutoResetEvent();
var (blockChain, consensusContext) = TestUtils.CreateDummyConsensusContext(
TimeSpan.FromSeconds(1),
TestUtils.Policy,
TestUtils.PrivateKeys[0]);
consensusContext.NewHeight(1);
consensusContext.StateChanged += (_, eventArgs) =>
{
if (eventArgs.Step != step)
{
step = eventArgs.Step;
stepChanged.Set();
}
};
var block = blockChain.ProposeBlock(proposer);
var proposal = new ProposalMetadata(
1,
0,
DateTimeOffset.UtcNow,
proposer.PublicKey,
new Codec().Encode(block.MarshalBlock()),
-1).Sign(proposer);
consensusContext.HandleMessage(new ConsensusProposalMsg(proposal));
await stepChanged.WaitAsync();

// ProposalClaim expects corresponding proposal if exists
var proposalClaim =
new ProposalClaimMetadata(
1,
0,
block.Hash,
DateTimeOffset.UtcNow,
TestUtils.PrivateKeys[1].PublicKey)
.Sign(TestUtils.PrivateKeys[1]);
Proposal? reply =
consensusContext.HandleProposalClaimMessage(proposalClaim);
Assert.NotNull(reply);
Assert.Equal(proposal, reply);
}
}
}
36 changes: 36 additions & 0 deletions Libplanet.Net/Consensus/ConsensusContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,42 @@ public IEnumerable<ConsensusMsg> HandleVoteSetBits(VoteSetBits voteSetBits)
return Array.Empty<ConsensusMsg>();
}

public Proposal? HandleProposalClaimMessage(ProposalClaim proposalClaim)
{
long height = proposalClaim.Height;
int round = proposalClaim.Round;
if (height != Height)
{
_logger.Debug(
"Ignore a received ProposalClaim as its height " +
"#{Height} does not match with the current context's height #{ContextHeight}",
height,
Height);
}
else if (round != Round)
{
_logger.Debug(
"Ignore a received ProposalClaim as its round " +
"#{Round} does not match with the current context's round #{ContextRound}",
round,
Round);
}
else
{
lock (_contextLock)
{
if (_contexts.ContainsKey(height))
{
// NOTE: Should check if collected messages have same BlockHash with
// VoteSetBit's BlockHash?
return _contexts[height].Proposal;
}
}
}

return null;
}

/// <summary>
/// Handles a received <see cref="ConsensusBootstrapMsg"/>
/// and return <see cref="VotesRecall"/> to send as a reply.
Expand Down
25 changes: 25 additions & 0 deletions Libplanet.Net/Consensus/ConsensusReactor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ private void ProcessMessage(MessageContent content)
}

break;

case ConsensusMaj23Msg maj23Msg:
try
{
Expand Down Expand Up @@ -210,6 +211,30 @@ private void ProcessMessage(MessageContent content)

break;

case ConsensusProposalClaimMsg proposalClaimMsg:
try
{
Proposal? proposal = _consensusContext.HandleProposalClaimMessage(
proposalClaimMsg.ProposalClaim);
if (proposal is { } proposalNotNull)
{
var reply = new ConsensusProposalMsg(proposalNotNull);
var sender = _gossip.Peers.First(
peer => peer.PublicKey.Equals(proposalClaimMsg.ValidatorPublicKey));

_gossip.PublishMessage(reply, new[] { sender });
}
}
catch (InvalidOperationException)
{
_logger.Debug(
"Cannot respond received ConsensusProposalClaimMsg message " +
"{Message} since there is no corresponding peer in the table",
proposalClaimMsg);
}

break;

case ConsensusBootstrapMsg bootstrapMsg:
try
{
Expand Down
8 changes: 8 additions & 0 deletions Libplanet.Net/Consensus/Context.Mutate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,14 @@ private void ProcessGenericUponRules()
hash3,
ToString());
Proposal = null;
BroadcastMessage(
new ConsensusProposalClaimMsg(
new ProposalClaimMetadata(
Height,
Round,
hash3,
DateTimeOffset.UtcNow,
_privateKey.PublicKey).Sign(_privateKey)));
}
}

Expand Down
65 changes: 65 additions & 0 deletions Libplanet.Net/Messages/ConsensusProposalClaimMsg.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using Libplanet.Blocks;
using Libplanet.Consensus;

namespace Libplanet.Net.Messages
{
/// <summary>
/// A message class for claiming a <see cref="Proposal"/>.
/// </summary>
public class ConsensusProposalClaimMsg : ConsensusMsg
{
/// <summary>
/// Initializes a new instance of the <see cref="ConsensusProposalClaimMsg"/> class.
/// </summary>
/// <param name="proposalClaim">A <see cref="ProposalClaim"/> of given height,
/// round and <see cref="BlockHash"/>.</param>
public ConsensusProposalClaimMsg(ProposalClaim proposalClaim)
: base(proposalClaim.ValidatorPublicKey, proposalClaim.Height, proposalClaim.Round)
{
ProposalClaim = proposalClaim;
}

/// <summary>
/// Initializes a new instance of the <see cref="ConsensusProposalClaimMsg"/> class
/// with marshalled message.
/// </summary>
/// <param name="dataframes">A marshalled message.</param>
public ConsensusProposalClaimMsg(byte[][] dataframes)
: this(new ProposalClaim(dataframes[0]))
{
}

/// <summary>
/// A <see cref="ProposalClaim"/> of the message.
/// </summary>
public ProposalClaim ProposalClaim { get; }

/// <inheritdoc cref="MessageContent.DataFrames"/>
public override IEnumerable<byte[]> DataFrames =>
new List<byte[]> { ProposalClaim.ToByteArray() };

/// <inheritdoc cref="MessageContent.MessageType"/>
public override MessageType Type => MessageType.ConsensusProposalClaimMsg;

/// <inheritdoc cref="Equals(ConsensusMsg?)"/>
public override bool Equals(ConsensusMsg? other)
{
return other is ConsensusProposalClaimMsg message &&
message.ProposalClaim.Equals(ProposalClaim);
}

/// <inheritdoc cref="Equals(object?)"/>
public override bool Equals(object? obj)
{
return obj is ConsensusMsg other && Equals(other);
}

/// <inheritdoc cref="ConsensusMsg.GetHashCode"/>
public override int GetHashCode()
{
return HashCode.Combine(Type, ProposalClaim);
}
}
}
146 changes: 146 additions & 0 deletions Libplanet/Consensus/ProposalClaim.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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.Blocks;
using Libplanet.Crypto;

namespace Libplanet.Consensus
{
/// <summary>
/// Represents a <see cref="ProposalClaim"/> from a validator for consensus. It contains an
/// essential information <see cref="ProposalClaimMetadata"/> to claim a <see cref="Proposal"/>
/// for a consensus in a height, round, <see cref="BlockHash"/>, and its signature to verify.
/// The signature is verified in
/// constructor, so the instance of <see cref="ProposalClaim"/> should be valid.
/// </summary>
public class ProposalClaim : IEquatable<ProposalClaim>
{
// FIXME: This should be private. Left as internal for testing reasons.
internal static readonly byte[] SignatureKey = { 0x53 }; // 'S'
private static Codec _codec = new Codec();

private readonly ProposalClaimMetadata _metadata;

/// <summary>
/// Instantiates a <see cref="ProposalClaim"/> with given <paramref name="metadata"/>
/// and its <paramref name="signature"/>.
/// </summary>
/// <param name="metadata">A <see cref="ProposalClaimMetadata"/> to claim.</param>
/// <param name="signature">A signature signed with <paramref name="metadata"/>.
/// </param>
/// <exception cref="ArgumentNullException">Thrown if given <paramref name="signature"/> is
/// empty.</exception>
/// <exception cref="ArgumentException">Thrown if given <paramref name="signature"/> is
/// invalid and cannot be verified with <paramref name="metadata"/>.</exception>
public ProposalClaim(ProposalClaimMetadata metadata, ImmutableArray<byte> signature)
{
_metadata = metadata;
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 ProposalClaim(byte[] marshaled)
: this((Dictionary)_codec.Decode(marshaled))
{
}

#pragma warning disable SA1118 // The parameter spans multiple lines
public ProposalClaim(Dictionary encoded)
: this(
new ProposalClaimMetadata(encoded),
encoded.ContainsKey(SignatureKey)
? encoded.GetValue<Binary>(SignatureKey).ToImmutableArray()
: ImmutableArray<byte>.Empty)
{
}
#pragma warning restore SA1118

/// <inheritdoc cref="ProposalClaimMetadata.Height"/>
public long Height => _metadata.Height;

/// <inheritdoc cref="ProposalClaimMetadata.Round"/>
public int Round => _metadata.Round;

/// <inheritdoc cref="ProposalClaimMetadata.BlockHash"/>
public BlockHash BlockHash => _metadata.BlockHash;

/// <inheritdoc cref="ProposalClaimMetadata.Timestamp"/>
public DateTimeOffset Timestamp => _metadata.Timestamp;

/// <inheritdoc cref="ProposalClaimMetadata.ValidatorPublicKey"/>
public PublicKey ValidatorPublicKey => _metadata.ValidatorPublicKey;

/// <summary>
/// A signature that signed with <see cref="ProposalMetadata"/>.
/// </summary>
public ImmutableArray<byte> Signature { get; }

/// <summary>
/// A Bencodex-encoded value of <see cref="ProposalClaim"/>.
/// </summary>
[JsonIgnore]
public Dictionary Encoded =>
!Signature.IsEmpty
? _metadata.Encoded.Add(SignatureKey, Signature)
: _metadata.Encoded;

/// <summary>
/// <see cref="byte"/> encoded <see cref="Proposal"/> data.
/// </summary>
public ImmutableArray<byte> ByteArray => ToByteArray().ToImmutableArray();

public byte[] ToByteArray() => _codec.Encode(Encoded);

/// <summary>
/// Verifies whether the <see cref="ProposalMetadata"/> is properly signed by
/// <see cref="Validator"/>.
/// </summary>
/// <returns><see langword="true"/> if the <see cref="Signature"/> is not empty
/// and is a valid signature signed by <see cref="Validator"/>.</returns>
[Pure]
public bool Verify() =>
!Signature.IsDefaultOrEmpty &&
ValidatorPublicKey.Verify(
_metadata.ByteArray.ToImmutableArray(),
Signature);

/// <inheritdoc/>
[Pure]
public bool Equals(ProposalClaim? other)
{
return other is ProposalClaim proposalClaim &&
_metadata.Equals(proposalClaim._metadata) &&
Signature.SequenceEqual(proposalClaim.Signature);
}

/// <inheritdoc/>
[Pure]
public override bool Equals(object? obj)
{
return obj is ProposalClaim other && Equals(other);
}

/// <inheritdoc/>
[Pure]
public override int GetHashCode()
{
return HashCode.Combine(
_metadata.GetHashCode(),
ByteUtil.CalculateHashCode(Signature.ToArray()));
}
}
}
Loading

0 comments on commit 3b417c1

Please sign in to comment.