Skip to content

Commit

Permalink
[Cirrus] Add IsExpired status to Poll (#707)
Browse files Browse the repository at this point in the history
* Fix merge error

* Changes based on feedback

* Update comment

* Update src/Stratis.Sidechains.Networks/CirrusTest.cs

* Update src/Stratis.Bitcoin.Features.PoA/Voting/VotingManager.cs

Co-authored-by: Francois de la Rouviere <fassadlr@gmail.com>
  • Loading branch information
quantumagi and fassadlr authored Sep 20, 2021
1 parent d0dfd1d commit c6ae67a
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 49 deletions.
5 changes: 5 additions & 0 deletions src/Stratis.Bitcoin.Features.PoA/PoAConsensusOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public class PoAConsensusOptions : ConsensusOptions
/// </summary>
public int InterFluxV2MainChainActivationHeight { get; set; }

/// <summary>
/// Logic related to release 1.1.0.0 will activate at this height, this includes Poll Expiry and the Join Federation Voting Request consensus rule.
/// </summary>
public int Release1100ActivationHeight { get; set; }

/// <summary>Initializes values for networks that use block size rules.</summary>
public PoAConsensusOptions(
uint maxBlockBaseSize,
Expand Down
29 changes: 29 additions & 0 deletions src/Stratis.Bitcoin.Features.PoA/Voting/Poll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,35 @@ public Poll()
/// </summary>
public bool IsPending => this.PollVotedInFavorBlockData == null;

/// <summary><c>true</c> if poll has expired; <c>false</c> otherwise.</summary>
/// <remarks><para>A poll is flagged as "Expired" by setting <see cref="Poll.PollVotedInFavorBlockData.Height"/> to zero.</para>
/// <para>The hash field is set to non-zero to avoid the whole field being deserialized as null. See <see cref="Poll.ReadWrite"/>.</para></remarks>
public bool IsExpired
{
get
{
return this.PollVotedInFavorBlockData != null && this.PollVotedInFavorBlockData.Height == 0;
}

set
{
if (!value)
{
if (this.PollVotedInFavorBlockData != null)
{
Guard.Assert(this.IsExpired);
this.PollVotedInFavorBlockData = null;
}
return;
}

this.PollVotedInFavorBlockData = new HashHeightPair(1 /* A non-zero value */, 0);
}
}

/// <summary><c>true</c> if poll has been approved; <c>false</c> otherwise.</summary>
public bool IsApproved => this.PollVotedInFavorBlockData != null && this.PollVotedInFavorBlockData.Height != 0;

/// <summary><c>true</c> if poll wasn't executed yet; <c>false</c> otherwise.</summary>
public bool IsExecuted => this.PollExecutedBlockData != null;

Expand Down
179 changes: 141 additions & 38 deletions src/Stratis.Bitcoin.Features.PoA/Voting/VotingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ namespace Stratis.Bitcoin.Features.PoA.Voting
{
public sealed class VotingManager : IDisposable
{
// Polls are expired once the tip reaches a block this far beyond the poll start block.
// I.e. if (Math.Max(startblock + PollExpiryBlocks, PollExpiryActivationHeight) <= tip) (See IsPollExpiredAt)
private const int PollExpiryBlocks = 50_000;

private readonly PoAConsensusOptions poaConsensusOptions;
private readonly IBlockRepository blockRepository;
private readonly ChainIndexer chainIndexer;
Expand Down Expand Up @@ -127,7 +131,7 @@ public void ScheduleVote(VotingData votingData)
if (!this.scheduledVotingData.Any(v => v == votingData))
this.scheduledVotingData.Add(votingData);

this.CleanFinishedPollsLocked();
this.SanitizeScheduledPollsLocked();
}

this.logger.LogDebug("Vote was scheduled with key: {0}.", votingData.Key);
Expand All @@ -140,7 +144,7 @@ public List<VotingData> GetScheduledVotes()

lock (this.locker)
{
this.CleanFinishedPollsLocked();
this.SanitizeScheduledPollsLocked();

return new List<VotingData>(this.scheduledVotingData);
}
Expand All @@ -154,7 +158,7 @@ public List<VotingData> GetAndCleanScheduledVotes()

lock (this.locker)
{
this.CleanFinishedPollsLocked();
this.SanitizeScheduledPollsLocked();

List<VotingData> votingData = this.scheduledVotingData;

Expand All @@ -167,24 +171,33 @@ public List<VotingData> GetAndCleanScheduledVotes()
}
}

/// <summary>Checks pending polls against finished polls and removes pending polls that will make no difference and basically are redundant.</summary>
/// <summary>Performs sanity checks against scheduled votes and removes any conflicts.</summary>
/// <remarks>All access should be protected by <see cref="locker"/>.</remarks>
private void CleanFinishedPollsLocked()
private void SanitizeScheduledPollsLocked()
{
// We take polls that are not pending (collected enough votes in favor) but not executed yet (maxReorg blocks
// didn't pass since the vote that made the poll pass). We can't just take not pending polls because of the
// following scenario: federation adds a hash or fed member or does any other revertable action, then reverts
// the action (removes the hash) and then reapplies it again. To allow for this scenario we have to exclude
// executed polls here.
List<Poll> finishedPolls = this.polls.Where(x => !x.IsPending && !x.IsExecuted).ToList();

for (int i = this.scheduledVotingData.Count - 1; i >= 0; i--)
// Sanitize the scheduled votes.
// Remove scheduled votes that are in pending polls or non-executed approved polls.
lock (this.locker)
{
VotingData currentScheduledData = this.scheduledVotingData[i];
List<Poll> pendingPolls = this.GetPendingPolls().ToList();
List<Poll> approvedPolls = this.GetApprovedPolls().Where(x => !x.IsExecuted).ToList();

bool IsTooOldToVoteOn(Poll poll) => poll.IsPending && (this.chainIndexer.Tip.Height - poll.PollStartBlockData.Height) >= PollExpiryBlocks;

bool IsValid(VotingData currentScheduledData)
{
// Remove scheduled voting data that can be found in pending polls.
if (pendingPolls.Any(x => x.VotingData == currentScheduledData && IsTooOldToVoteOn(x)))
return false;

// Remove scheduled voting data that can be found in finished polls that were not yet executed.
if (approvedPolls.Any(x => x.VotingData == currentScheduledData))
return false;

// Remove scheduled voting data that can be found in finished polls that were not yet executed.
if (finishedPolls.Any(x => x.VotingData == currentScheduledData))
this.scheduledVotingData.RemoveAt(i);
return true;
}

this.scheduledVotingData = this.scheduledVotingData.Where(d => IsValid(d)).ToList();
}
}

Expand All @@ -207,7 +220,7 @@ public List<Poll> GetApprovedPolls()

lock (this.locker)
{
return new List<Poll>(this.polls.Where(x => !x.IsPending));
return new List<Poll>(this.polls.Where(x => x.IsApproved));
}
}

Expand All @@ -222,6 +235,17 @@ public List<Poll> GetExecutedPolls()
}
}

/// <summary>Provides a collection of polls that are expired.</summary>
public List<Poll> GetExpiredPolls()
{
this.EnsureInitialized();

lock (this.locker)
{
return new List<Poll>(this.polls.Where(x => x.IsExpired));
}
}

/// <summary>
/// Tells us whether we have already voted to boot a federation member.
/// </summary>
Expand Down Expand Up @@ -427,6 +451,14 @@ private bool IsVotingOnMultisigMember(VotingData votingData)
return this.federationManager.IsMultisigMember(member.PubKey);
}

private bool IsPollExpiredAt(Poll poll, ChainedHeader chainedHeader)
{
if (chainedHeader == null)
return false;

return Math.Max(poll.PollStartBlockData.Height + PollExpiryBlocks, this.poaConsensusOptions.Release1100ActivationHeight) <= chainedHeader.Height;
}

private void ProcessBlock(DBreeze.Transactions.Transaction transaction, ChainedHeaderBlock chBlock)
{
long flagFall = DateTime.Now.Ticks;
Expand All @@ -437,9 +469,21 @@ private void ProcessBlock(DBreeze.Transactions.Transaction transaction, ChainedH
{
bool pollsRepositoryModified = false;

foreach (Poll poll in this.GetPendingPolls().Where(x => this.IsPollExpiredAt(x, chBlock.ChainedHeader)).ToList())
{
this.logger.LogDebug("Expiring poll '{0}'.", poll);

// Flag the poll as expired. The "PollVotedInFavorBlockData" will always be null at this point due to the "GetPendingPolls" filter above.
// The value of the hash is not significant but we set it to a non-zero value to prevent the field from being de-serialized as null.
poll.IsExpired = true;
this.polls.OnPendingStatusChanged(poll);
this.PollsRepository.UpdatePoll(transaction, poll);
pollsRepositoryModified = true;
}

foreach (Poll poll in this.GetApprovedPolls())
{
if (chBlock.ChainedHeader.Height != (poll.PollVotedInFavorBlockData.Height + this.network.Consensus.MaxReorgLength))
if (poll.IsExpired || chBlock.ChainedHeader.Height != (poll.PollVotedInFavorBlockData.Height + this.network.Consensus.MaxReorgLength))
continue;

this.logger.LogDebug("Applying poll '{0}'.", poll);
Expand Down Expand Up @@ -615,6 +659,17 @@ private void UnProcessBlock(DBreeze.Transactions.Transaction transaction, Chaine
pollsRepositoryModified = true;
}

foreach (Poll poll in this.polls.Where(x => x.IsExpired && !IsPollExpiredAt(x, chBlock.ChainedHeader.Previous)).ToList())
{
this.logger.LogDebug("Reverting poll expiry '{0}'.", poll);

// Revert back to null as this field would have been when the poll was expired.
poll.IsExpired = false;
this.polls.OnPendingStatusChanged(poll);
this.PollsRepository.UpdatePoll(transaction, poll);
pollsRepositoryModified = true;
}

if (this.federationManager.GetMultisigMinersApplicabilityHeight() == chBlock.ChainedHeader.Height)
this.federationManager.UpdateMultisigMiners(false);
}
Expand Down Expand Up @@ -819,27 +874,75 @@ private void OnBlockDisconnected(BlockDisconnected blockDisconnected)
[NoTrace]
private void AddComponentStats(StringBuilder log)
{
log.AppendLine();
log.AppendLine(">> Voting & Poll Data");

// Use the polls list directly as opposed to the locked versions of them for console reporting.
List<Poll> pendingPolls = this.polls.Where(x => x.IsPending).ToList();
List<Poll> approvedPolls = this.polls.Where(x => !x.IsPending).ToList();
List<Poll> executedPolls = this.polls.Where(x => x.IsExecuted).ToList();

double avgBlockProcessingTime;
if (this.blocksProcessed == 0)
avgBlockProcessingTime = double.NaN;
else
avgBlockProcessingTime = Math.Round((double)(new TimeSpan(this.blocksProcessingTime).Milliseconds) / this.blocksProcessed, 2);

double avgBlockProcessingThroughput = Math.Round(this.blocksProcessed / (new TimeSpan(this.blocksProcessingTime).TotalSeconds), 2);

log.AppendLine("Polls Repository Height".PadRight(LoggingConfiguration.ColumnLength) + $": {(this.PollsRepository.CurrentTip?.Height ?? 0)}".PadRight(10) + $"(Hash: {(this.PollsRepository.CurrentTip?.Hash.ToString())})");
log.AppendLine("Blocks Processed".PadRight(LoggingConfiguration.ColumnLength) + $": Count : {this.blocksProcessed}".PadRight(20) + $"Avg Time: { avgBlockProcessingTime } ms".PadRight(20) + $"Throughput: { avgBlockProcessingThroughput } per second");
log.AppendLine("Member Polls".PadRight(LoggingConfiguration.ColumnLength) + $": Pending: {pendingPolls.MemberPolls().Count}".PadRight(20) + $"Approved: {approvedPolls.MemberPolls().Count}".PadRight(20) + $"Executed : {executedPolls.MemberPolls().Count}");
log.AppendLine("Whitelist Polls".PadRight(LoggingConfiguration.ColumnLength) + $": Pending: {pendingPolls.WhitelistPolls().Count}".PadRight(20) + $"Approved: {approvedPolls.WhitelistPolls().Count}".PadRight(20) + $"Executed : {executedPolls.WhitelistPolls().Count}");
log.AppendLine("Scheduled Votes".PadRight(LoggingConfiguration.ColumnLength) + ": " + this.scheduledVotingData.Count);
log.AppendLine("Scheduled votes will be added to the next block this node mines.");
lock (this.locker)
{
double avgBlockProcessingTime;
if (this.blocksProcessed == 0)
avgBlockProcessingTime = double.NaN;
else
avgBlockProcessingTime = Math.Round((double)(new TimeSpan(this.blocksProcessingTime).Milliseconds) / this.blocksProcessed, 2);

double avgBlockProcessingThroughput = Math.Round(this.blocksProcessed / (new TimeSpan(this.blocksProcessingTime).TotalSeconds), 2);

log.AppendLine("Polls Repository Height".PadRight(LoggingConfiguration.ColumnLength) + $": {(this.PollsRepository.CurrentTip?.Height ?? 0)}".PadRight(10) + $"(Hash: {(this.PollsRepository.CurrentTip?.Hash.ToString())})");
log.AppendLine("Blocks Processed".PadRight(LoggingConfiguration.ColumnLength) + $": {this.blocksProcessed}".PadRight(18) + $"Avg Time: { avgBlockProcessingTime } ms".PadRight(20) + $"Throughput: { avgBlockProcessingThroughput } per second");
log.AppendLine();

log.AppendLine(
"Expired Member Polls".PadRight(24) + ": " + GetExpiredPolls().MemberPolls().Count.ToString().PadRight(16) +
"Expired Whitelist Polls".PadRight(30) + ": " + GetExpiredPolls().WhitelistPolls().Count);
log.AppendLine(
"Pending Member Polls".PadRight(24) + ": " + GetPendingPolls().MemberPolls().Count.ToString().PadRight(16) +
"Pending Whitelist Polls".PadRight(30) + ": " + GetPendingPolls().WhitelistPolls().Count);
log.AppendLine(
"Approved Member Polls".PadRight(24) + ": " + GetApprovedPolls().MemberPolls().Where(x => !x.IsExecuted).Count().ToString().PadRight(16) +
"Approved Whitelist Polls".PadRight(30) + ": " + GetApprovedPolls().WhitelistPolls().Where(x => !x.IsExecuted).Count());
log.AppendLine(
"Executed Member Polls".PadRight(24) + ": " + GetExecutedPolls().MemberPolls().Count.ToString().PadRight(16) +
"Executed Whitelist Polls".PadRight(30) + ": " + GetExecutedPolls().WhitelistPolls().Count);
log.AppendLine(
"Scheduled Votes".PadRight(24) + ": " + this.scheduledVotingData.Count.ToString().PadRight(16) +
"Scheduled votes will be added to the next block this node mines.");

if (this.nodeStats.DisplayBenchStats)
{
long tipHeight = this.chainIndexer.Tip.Height;

List<Poll> pendingPolls = GetPendingPolls().OrderByDescending(p => p.PollStartBlockData.Height).ToList();
if (pendingPolls.Count != 0)
{
log.AppendLine();
log.AppendLine("--- Pending Add/Kick Member Polls ---");
foreach (Poll poll in pendingPolls.Where(p => !p.IsExecuted && (p.VotingData.Key == VoteKey.AddFederationMember || p.VotingData.Key == VoteKey.KickFederationMember)))
{
IFederationMember federationMember = ((PoAConsensusFactory)(this.network.Consensus.ConsensusFactory)).DeserializeFederationMember(poll.VotingData.Data);
string expiresIn = $", Expires In = {(Math.Max(this.poaConsensusOptions.Release1100ActivationHeight, poll.PollStartBlockData.Height + PollExpiryBlocks) - tipHeight)}";
log.Append($"{poll.VotingData.Key.ToString().PadLeft(22)}, PubKey = { federationMember.PubKey.ToHex() }, In Favor = {poll.PubKeysHexVotedInFavor.Count}{expiresIn}");
bool exists = this.federationManager.GetFederationMembers().Any(m => m.PubKey == federationMember.PubKey);
if (poll.VotingData.Key == VoteKey.AddFederationMember && exists)
log.Append(" (Already exists)");
if (poll.VotingData.Key == VoteKey.KickFederationMember && !exists)
log.Append(" (Does not exist)");
log.AppendLine();
}
}

List<Poll> approvedPolls = GetApprovedPolls().Where(p => !p.IsExecuted).OrderByDescending(p => p.PollVotedInFavorBlockData.Height).ToList();
if (approvedPolls.Count != 0)
{
log.AppendLine();
log.AppendLine("--- Approved Polls ---");
foreach (Poll poll in approvedPolls)
{
log.AppendLine($"{poll.VotingData.Key.ToString().PadLeft(22)}, Applied In = ({(poll.PollStartBlockData.Height - (tipHeight - this.network.Consensus.MaxReorgLength))})");
}
}
}
}

log.AppendLine();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ public override Task RunAsync(RuleContext context)

foreach (Transaction transaction in context.ValidationContext.BlockToValidate.Transactions)
{
CheckTransaction(transaction);
CheckTransaction(transaction, context.ValidationContext.ChainedHeaderToValidate.Height);
}

return Task.CompletedTask;
}

public void CheckTransaction(Transaction transaction)
public void CheckTransaction(Transaction transaction, int height)
{
if (transaction.IsCoinBase || transaction.IsCoinStake)
return;
Expand All @@ -73,13 +73,16 @@ public void CheckTransaction(Transaction transaction)
}

// Prohibit re-use of collateral addresses.
Script script = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(request.CollateralMainchainAddress);
string collateralAddress = script.GetDestinationAddress(this.counterChainNetwork).ToString();
CollateralFederationMember owner = this.federationManager.CollateralAddressOwner(this.votingManager, VoteKey.AddFederationMember, collateralAddress);
if (owner != null)
if (height >= ((PoAConsensusOptions)(this.network.Consensus.Options)).Release1100ActivationHeight)
{
this.Logger.LogTrace("(-)[INVALID_COLLATERAL_REUSE]");
PoAConsensusErrors.VotingRequestInvalidCollateralReuse.Throw();
Script script = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(request.CollateralMainchainAddress);
string collateralAddress = script.GetDestinationAddress(this.counterChainNetwork).ToString();
CollateralFederationMember owner = this.federationManager.CollateralAddressOwner(this.votingManager, VoteKey.AddFederationMember, collateralAddress);
if (owner != null)
{
this.Logger.LogTrace("(-)[INVALID_COLLATERAL_REUSE]");
PoAConsensusErrors.VotingRequestInvalidCollateralReuse.Throw();
}
}
}
}
Expand Down
Loading

0 comments on commit c6ae67a

Please sign in to comment.