diff --git a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoANetwork.cs b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoANetwork.cs index bcff4fb67f3..01391cbb1f5 100644 --- a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoANetwork.cs +++ b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests.Common/TestPoANetwork.cs @@ -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; diff --git a/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs new file mode 100644 index 00000000000..c4e5e54c0e7 --- /dev/null +++ b/src/Stratis.Bitcoin.Features.PoA.IntegrationTests/EnableVoteKickingTests.cs @@ -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().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); + } + } + } +} diff --git a/src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs b/src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs index 205dfd75faa..3ed8d2edbb7 100644 --- a/src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs +++ b/src/Stratis.Bitcoin.Features.PoA/PoAFeature.cs @@ -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(); diff --git a/src/Stratis.Bitcoin.Features.PoA/Voting/IdleFederationMembersKicker.cs b/src/Stratis.Bitcoin.Features.PoA/Voting/IdleFederationMembersKicker.cs index 6d93682334f..bc342dfd5ee 100644 --- a/src/Stratis.Bitcoin.Features.PoA/Voting/IdleFederationMembersKicker.cs +++ b/src/Stratis.Bitcoin.Features.PoA/Voting/IdleFederationMembersKicker.cs @@ -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(); - // 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(); } @@ -126,17 +126,7 @@ private void OnBlockConnected(BlockConnected blockConnectedData) foreach (KeyValuePair 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)) @@ -145,11 +135,7 @@ private void OnBlockConnected(BlockConnected blockConnectedData) byte[] federationMemberBytes = this.consensusFactory.SerializeFederationMember(memberToKick); - List 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) { @@ -158,7 +144,7 @@ private void OnBlockConnected(BlockConnected blockConnectedData) this.votingManager.ScheduleVote(new VotingData() { Key = VoteKey.KickFederationMember, - Data = fedMemberToActiveTime.Key.ToBytes() + Data = federationMemberBytes }); } else @@ -169,6 +155,43 @@ private void OnBlockConnected(BlockConnected blockConnectedData) } } + /// + /// Tells us whether we have already voted to boot a federation member. + /// + private bool AlreadyVotingFor(byte[] federationMemberBytes) + { + List 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 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 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(); diff --git a/src/Stratis.Sidechains.Networks/CirrusMain.cs b/src/Stratis.Sidechains.Networks/CirrusMain.cs index b7a17516c2e..d8edccbc34b 100644 --- a/src/Stratis.Sidechains.Networks/CirrusMain.cs +++ b/src/Stratis.Sidechains.Networks/CirrusMain.cs @@ -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 diff --git a/src/Stratis.Sidechains.Networks/CirrusRegTest.cs b/src/Stratis.Sidechains.Networks/CirrusRegTest.cs index 05c9a13d5ff..8102ad97f2e 100644 --- a/src/Stratis.Sidechains.Networks/CirrusRegTest.cs +++ b/src/Stratis.Sidechains.Networks/CirrusRegTest.cs @@ -92,7 +92,7 @@ public CirrusRegTest() genesisFederationMembers: genesisFederationMembers, targetSpacingSeconds: 16, votingEnabled: true, - autoKickIdleMembers: false + autoKickIdleMembers: true ); var buriedDeployments = new BuriedDeploymentsArray diff --git a/src/Stratis.Sidechains.Networks/CirrusTest.cs b/src/Stratis.Sidechains.Networks/CirrusTest.cs index afba81b5b4a..530132f0923 100644 --- a/src/Stratis.Sidechains.Networks/CirrusTest.cs +++ b/src/Stratis.Sidechains.Networks/CirrusTest.cs @@ -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