Skip to content
This repository was archived by the owner on Aug 16, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public class TestPoANetwork : PoANetwork

public TestPoANetwork()
{
this.Name = "PoATest";

this.FederationKey1 = new Mnemonic("lava frown leave wedding virtual ghost sibling able mammal liar wide wisdom").DeriveExtKey().PrivateKey;
this.FederationKey2 = new Mnemonic("idle power swim wash diesel blouse photo among eager reward govern menu").DeriveExtKey().PrivateKey;
this.FederationKey3 = new Mnemonic("high neither night category fly wasp inner kitchen phone current skate hair").DeriveExtKey().PrivateKey;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Threading.Tasks;
using Stratis.Bitcoin.Features.PoA.IntegrationTests.Common;
using Stratis.Bitcoin.Features.PoA.Voting;
using Stratis.Bitcoin.IntegrationTests.Common;
using Stratis.Bitcoin.IntegrationTests.Common.EnvironmentMockUpHelpers;
using Xunit;

namespace Stratis.Bitcoin.Features.PoA.IntegrationTests
{
public class EnableVoteKickingTests
{

[Fact]
public async Task EnableAutoKick()
{
using (PoANodeBuilder builder = PoANodeBuilder.CreatePoANodeBuilder(this))
{
const int idleTimeSeconds = 5 * 60;

// Have a network that mimics Cirrus where voting is on and kicking is off.
var votingNetwork = new TestPoANetwork();
var oldOptions = (PoAConsensusOptions)votingNetwork.Consensus.Options;
votingNetwork.Consensus.Options = new PoAConsensusOptions(maxBlockBaseSize: oldOptions.MaxBlockBaseSize,
maxStandardVersion: oldOptions.MaxStandardVersion,
maxStandardTxWeight: oldOptions.MaxStandardTxWeight,
maxBlockSigopsCost: oldOptions.MaxBlockSigopsCost,
maxStandardTxSigopsCost: oldOptions.MaxStandardTxSigopsCost,
genesisFederationMembers: oldOptions.GenesisFederationMembers,
targetSpacingSeconds: 60,
votingEnabled: true,
autoKickIdleMembers: false,
federationMemberMaxIdleTimeSeconds: oldOptions.FederationMemberMaxIdleTimeSeconds);

CoreNode node1 = builder.CreatePoANode(votingNetwork, votingNetwork.FederationKey1).Start();
CoreNode node2 = builder.CreatePoANode(votingNetwork, votingNetwork.FederationKey2).Start();
TestHelper.Connect(node1, node2);

// Mine a block on this network from each node. Confirm it's alive.
await node1.MineBlocksAsync(1);
CoreNodePoAExtensions.WaitTillSynced(node1, node2);
await node2.MineBlocksAsync(1);
CoreNodePoAExtensions.WaitTillSynced(node1, node2);

// Edit the consensus options so that kicking is turned on.
votingNetwork.Consensus.Options = new PoAConsensusOptions(maxBlockBaseSize: oldOptions.MaxBlockBaseSize,
maxStandardVersion: oldOptions.MaxStandardVersion,
maxStandardTxWeight: oldOptions.MaxStandardTxWeight,
maxBlockSigopsCost: oldOptions.MaxBlockSigopsCost,
maxStandardTxSigopsCost: oldOptions.MaxStandardTxSigopsCost,
genesisFederationMembers: oldOptions.GenesisFederationMembers,
targetSpacingSeconds: 60,
votingEnabled: true,
autoKickIdleMembers: true,
federationMemberMaxIdleTimeSeconds: idleTimeSeconds);

node1.Restart();

// Lets get our 2 nodes to actively mine some blocks.
// In doing so, their test datetimeprovider will increment by minutes at a time.
for (int i = 0; i < 5; i++)
{
await node1.MineBlocksAsync(1);
CoreNodePoAExtensions.WaitTillSynced(node1, node2);

await node2.MineBlocksAsync(1);
CoreNodePoAExtensions.WaitTillSynced(node1, node2);
}

// Enough time has passed - Check that our new node wants to vote the third fed member out, who has not mined at all.
// Check that we have a single active poll to vote him out.
var activePolls = node1.FullNode.NodeService<VotingManager>().GetPendingPolls();
Assert.Single(activePolls);
Assert.Equal(VoteKey.KickFederationMember, activePolls[0].VotingData.Key);
byte[] lastMemberBytes = (votingNetwork.Consensus.ConsensusFactory as PoAConsensusFactory).SerializeFederationMember(votingNetwork.ConsensusOptions.GenesisFederationMembers[2]);
Assert.Equal(lastMemberBytes, activePolls[0].VotingData.Data);
}
}
}
}
7 changes: 5 additions & 2 deletions src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,13 @@ public override Task InitializeAsync()

if (options.VotingEnabled)
{
this.votingManager.Initialize();

// If we are kicking members, we need to initialize this component before the VotingManager.
// The VotingManager may tally votes and execute federation changes, but the IdleKicker needs to know who the current block is from.
// The IdleKicker can much more easily find out who the block is from if it receives the block first.
if (options.AutoKickIdleMembers)
this.idleFederationMembersKicker.Initialize();

this.votingManager.Initialize();
}

this.miner.InitializeMining();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ public void Initialize()
}
else
{
this.logger.LogDebug("No saved data found. Initializing federation data with genesis timestamps.");
this.logger.LogDebug("No saved data found. Initializing federation data with current timestamp.");

this.fedPubKeysByLastActiveTime = new Dictionary<PubKey, uint>();

// Initialize with 0.
// Initialize with current timestamp. If we were to initialise with 0, then everyone would be wrong instantly!
foreach (IFederationMember federationMember in this.federationManager.GetFederationMembers())
this.fedPubKeysByLastActiveTime.Add(federationMember.PubKey, 0);
this.fedPubKeysByLastActiveTime.Add(federationMember.PubKey, (uint) this.timeProvider.GetAdjustedTimeAsUnixTimestamp());

this.SaveMembersByLastActiveTime();
}
Expand Down Expand Up @@ -126,17 +126,7 @@ private void OnBlockConnected(BlockConnected blockConnectedData)

foreach (KeyValuePair<PubKey, uint> fedMemberToActiveTime in this.fedPubKeysByLastActiveTime)
{
uint inactiveForSeconds;

if (fedMemberToActiveTime.Value == 0)
{
// Fed member was never active, count from first block after genesis.
inactiveForSeconds = tip.Header.Time - blockConnectedData.ConnectedBlock.ChainedHeader.GetAncestor(1).Header.Time;
}
else
{
inactiveForSeconds = tip.Header.Time - fedMemberToActiveTime.Value;
}
uint inactiveForSeconds = tip.Header.Time - fedMemberToActiveTime.Value;

if (inactiveForSeconds > this.federationMemberMaxIdleTimeSeconds && this.federationManager.IsFederationMember &&
!FederationVotingController.IsMultisigMember(this.network, fedMemberToActiveTime.Key))
Expand All @@ -145,11 +135,7 @@ private void OnBlockConnected(BlockConnected blockConnectedData)

byte[] federationMemberBytes = this.consensusFactory.SerializeFederationMember(memberToKick);

List<Poll> finishedPolls = this.votingManager.GetFinishedPolls();

bool alreadyKicking = finishedPolls.Any(x => !x.IsExecuted &&
x.VotingData.Key == VoteKey.KickFederationMember && x.VotingData.Data.SequenceEqual(federationMemberBytes) &&
x.PubKeysHexVotedInFavor.Contains(this.federationManager.CurrentFederationKey.PubKey.ToHex()));
bool alreadyKicking = this.AlreadyVotingFor(federationMemberBytes);

if (!alreadyKicking)
{
Expand All @@ -158,7 +144,7 @@ private void OnBlockConnected(BlockConnected blockConnectedData)
this.votingManager.ScheduleVote(new VotingData()
{
Key = VoteKey.KickFederationMember,
Data = fedMemberToActiveTime.Key.ToBytes()
Data = federationMemberBytes
});
}
else
Expand All @@ -169,6 +155,43 @@ private void OnBlockConnected(BlockConnected blockConnectedData)
}
}

/// <summary>
/// Tells us whether we have already voted to boot a federation member.
/// </summary>
private bool AlreadyVotingFor(byte[] federationMemberBytes)
{
List<Poll> finishedPolls = this.votingManager.GetFinishedPolls();

if(finishedPolls.Any(x => !x.IsExecuted &&
x.VotingData.Key == VoteKey.KickFederationMember && x.VotingData.Data.SequenceEqual(federationMemberBytes) &&
x.PubKeysHexVotedInFavor.Contains(this.federationManager.CurrentFederationKey.PubKey.ToHex())))
{
// We've already voted in a finished poll.
return true;
}

List<Poll> pendingPolls = this.votingManager.GetPendingPolls();

if (pendingPolls.Any(x => x.VotingData.Key == VoteKey.KickFederationMember &&
x.VotingData.Data.SequenceEqual(federationMemberBytes) &&
x.PubKeysHexVotedInFavor.Contains(this.federationManager.CurrentFederationKey.PubKey.ToHex())))
{
// We've already voted in a pending poll.
return true;
}


List<VotingData> scheduledVotes = this.votingManager.GetScheduledVotes();

if (scheduledVotes.Any(x => x.Key == VoteKey.KickFederationMember && x.Data.SequenceEqual(federationMemberBytes)))
{
// We have the vote queued to be put out next time we mine a block.
return true;
}

return false;
}

private void SaveMembersByLastActiveTime()
{
var dataToSave = new Dictionary<string, uint>();
Expand Down
2 changes: 1 addition & 1 deletion src/Stratis.Sidechains.Networks/CirrusMain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ internal CirrusMain()
genesisFederationMembers: genesisFederationMembers,
targetSpacingSeconds: 16,
votingEnabled: true,
autoKickIdleMembers: false
autoKickIdleMembers: true
)
{
EnforceMinProtocolVersionAtBlockHeight = 384675, // setting the value to zero makes the functionality inactive
Expand Down
2 changes: 1 addition & 1 deletion src/Stratis.Sidechains.Networks/CirrusRegTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public CirrusRegTest()
genesisFederationMembers: genesisFederationMembers,
targetSpacingSeconds: 16,
votingEnabled: true,
autoKickIdleMembers: false
autoKickIdleMembers: true
);

var buriedDeployments = new BuriedDeploymentsArray
Expand Down
2 changes: 1 addition & 1 deletion src/Stratis.Sidechains.Networks/CirrusTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ internal CirrusTest()
genesisFederationMembers: genesisFederationMembers,
targetSpacingSeconds: 16,
votingEnabled: true,
autoKickIdleMembers: false
autoKickIdleMembers: true
)
{
EnforceMinProtocolVersionAtBlockHeight = 505900, // setting the value to zero makes the functionality inactive
Expand Down