diff --git a/src/neo/Consensus/ConsensusContext.cs b/src/neo/Consensus/ConsensusContext.cs index 763fa2e77f..b968f4dc47 100644 --- a/src/neo/Consensus/ConsensusContext.cs +++ b/src/neo/Consensus/ConsensusContext.cs @@ -35,7 +35,7 @@ internal class ConsensusContext : IDisposable, ISerializable public ConsensusPayload[] LastChangeViewPayloads; // LastSeenMessage array stores the height of the last seen message, for each validator. // if this node never heard from validator i, LastSeenMessage[i] will be -1. - public int[] LastSeenMessage; + public Dictionary LastSeenMessage { get; private set; } /// /// Store all verified unsorted transactions' senders' fee currently in the consensus context. @@ -55,7 +55,17 @@ internal class ConsensusContext : IDisposable, ISerializable public bool WatchOnly => MyIndex < 0; public Header PrevHeader => Snapshot.GetHeader(Block.PrevHash); public int CountCommitted => CommitPayloads.Count(p => p != null); - public int CountFailed => LastSeenMessage.Count(p => p < (((int)Block.Index) - 1)); + public int CountFailed => LastSeenMessage?.Count(p => p.Value < (((int)Block.Index) - 1)) ?? 0; + public bool ValidatorsChanged + { + get + { + if (Snapshot.Height == 0) return false; + TrimmedBlock currentBlock = Snapshot.Blocks[Snapshot.CurrentBlockHash]; + TrimmedBlock previousBlock = Snapshot.Blocks[currentBlock.PrevHash]; + return currentBlock.NextConsensus != previousBlock.NextConsensus; + } + } #region Consensus States public bool RequestSentOrReceived => PreparationPayloads[Block.ConsensusData.PrimaryIndex] != null; @@ -384,11 +394,17 @@ public void Reset(byte viewNumber) ChangeViewPayloads = new ConsensusPayload[Validators.Length]; LastChangeViewPayloads = new ConsensusPayload[Validators.Length]; CommitPayloads = new ConsensusPayload[Validators.Length]; - if (LastSeenMessage == null) + if (ValidatorsChanged || LastSeenMessage is null) { - LastSeenMessage = new int[Validators.Length]; - for (int i = 0; i < Validators.Length; i++) - LastSeenMessage[i] = -1; + var previous_last_seen_message = LastSeenMessage; + LastSeenMessage = new Dictionary(); + foreach (var validator in Validators) + { + if (previous_last_seen_message != null && previous_last_seen_message.TryGetValue(validator, out int value)) + LastSeenMessage[validator] = value; + else + LastSeenMessage[validator] = (int)Snapshot.Height; + } } keyPair = null; for (int i = 0; i < Validators.Length; i++) @@ -418,7 +434,7 @@ public void Reset(byte viewNumber) Block.Transactions = null; TransactionHashes = null; PreparationPayloads = new ConsensusPayload[Validators.Length]; - if (MyIndex >= 0) LastSeenMessage[MyIndex] = (int)Block.Index; + if (MyIndex >= 0) LastSeenMessage[Validators[MyIndex]] = (int)Block.Index; } public void Save() diff --git a/src/neo/Consensus/ConsensusService.cs b/src/neo/Consensus/ConsensusService.cs index 9caca4b651..fbcf0437c1 100644 --- a/src/neo/Consensus/ConsensusService.cs +++ b/src/neo/Consensus/ConsensusService.cs @@ -291,7 +291,7 @@ private void OnConsensusPayload(ConsensusPayload payload) { return; } - context.LastSeenMessage[payload.ValidatorIndex] = (int)payload.BlockIndex; + context.LastSeenMessage[context.Validators[payload.ValidatorIndex]] = (int)payload.BlockIndex; foreach (IP2PPlugin plugin in Plugin.P2PPlugins) if (!plugin.OnConsensusMessage(payload)) return; diff --git a/tests/neo.UnitTests/Consensus/UT_Consensus.cs b/tests/neo.UnitTests/Consensus/UT_Consensus.cs index 86eb6bc8c2..5bcf9f031c 100644 --- a/tests/neo.UnitTests/Consensus/UT_Consensus.cs +++ b/tests/neo.UnitTests/Consensus/UT_Consensus.cs @@ -46,7 +46,6 @@ public void ConsensusService_SingleNodeActors_OnStart_PrepReq_PrepResponses_Comm Console.WriteLine($"\n(UT-Consensus) Wallet is: {mockWallet.Object.GetAccount(UInt160.Zero).GetKey().PublicKey}"); var mockContext = new Mock(mockWallet.Object, Blockchain.Singleton.Store); - mockContext.Object.LastSeenMessage = new int[] { 0, 0, 0, 0, 0, 0, 0 }; KeyPair[] kp_array = new KeyPair[7] { @@ -136,6 +135,10 @@ public void ConsensusService_SingleNodeActors_OnStart_PrepReq_PrepResponses_Comm // As we may expect, as soon as consensus start it sends a RecoveryRequest of this aforementioned type var askingForInitialRecovery = subscriber.ExpectMsg(); Console.WriteLine($"Recovery Message I: {askingForInitialRecovery}"); + foreach (var validator in mockContext.Object.Validators) + { + mockContext.Object.LastSeenMessage[validator] = 0; + } // Ensuring cast of type ConsensusPayload from the received message from subscriber ConsensusPayload initialRecoveryPayload = (ConsensusPayload)askingForInitialRecovery.Inventory; // Ensuring casting of type RecoveryRequest @@ -166,7 +169,11 @@ public void ConsensusService_SingleNodeActors_OnStart_PrepReq_PrepResponses_Comm mockContext.Object.ChangeViewPayloads[mockContext.Object.MyIndex] = null; Console.WriteLine("Forcing Failed nodes for recovery request... "); mockContext.Object.CountFailed.Should().Be(0); - mockContext.Object.LastSeenMessage = new int[] { -1, -1, -1, -1, -1, -1, -1 }; + mockContext.Object.LastSeenMessage.Clear(); + foreach (var validator in mockContext.Object.Validators) + { + mockContext.Object.LastSeenMessage[validator] = -1; + } mockContext.Object.CountFailed.Should().Be(7); Console.WriteLine("\nWaiting for recovery due to failed nodes... "); var backupOnRecoveryDueToFailedNodes = subscriber.ExpectMsg(); @@ -266,6 +273,12 @@ public void ConsensusService_SingleNodeActors_OnStart_PrepReq_PrepResponses_Comm kp_array[6].PublicKey }; Console.WriteLine($"Generated keypairs PKey:"); + //refresh LastSeenMessage + mockContext.Object.LastSeenMessage.Clear(); + foreach (var item in mockContext.Object.Validators) + { + mockContext.Object.LastSeenMessage[item] = -1; + } for (int i = 0; i < mockContext.Object.Validators.Length; i++) Console.WriteLine($"{mockContext.Object.Validators[i]}/{Contract.CreateSignatureContract(mockContext.Object.Validators[i]).ScriptHash}"); var updatedContract = Contract.CreateMultiSigContract(mockContext.Object.M, mockContext.Object.Validators); @@ -308,7 +321,7 @@ public void ConsensusService_SingleNodeActors_OnStart_PrepReq_PrepResponses_Comm Console.WriteLine("\nAsserting CountCommitted is 2..."); mockContext.Object.CountCommitted.Should().Be(2); Console.WriteLine($"\nAsserting CountFailed is 1..."); - mockContext.Object.CountFailed.Should().Be(1); + mockContext.Object.CountFailed.Should().Be(6); Console.WriteLine("\nCN6 simulation time"); TellConsensusPayload(actorConsensus, GetCommitPayloadModifiedAndSignedCopy(commitPayload, 5, kp_array[5], updatedBlockHashData)); @@ -318,7 +331,7 @@ public void ConsensusService_SingleNodeActors_OnStart_PrepReq_PrepResponses_Comm Console.WriteLine("\nAsserting CountCommitted is 3..."); mockContext.Object.CountCommitted.Should().Be(3); Console.WriteLine($"\nAsserting CountFailed is 0..."); - mockContext.Object.CountFailed.Should().Be(0); + mockContext.Object.CountFailed.Should().Be(5); Console.WriteLine("\nCN5 simulation time"); TellConsensusPayload(actorConsensus, GetCommitPayloadModifiedAndSignedCopy(commitPayload, 4, kp_array[4], updatedBlockHashData)); @@ -381,10 +394,14 @@ public void ConsensusService_SingleNodeActors_OnStart_PrepReq_PrepResponses_Comm Console.WriteLine("\nAsserting CountCommitted is 0..."); mockContext.Object.CountCommitted.Should().Be(0); Console.WriteLine($"\nAsserting CountFailed is 0..."); - mockContext.Object.CountFailed.Should().Be(0); + mockContext.Object.CountFailed.Should().Be(3); Console.WriteLine($"\nModifying CountFailed and asserting 7..."); // This will ensure a non-deterministic behavior after last recovery - mockContext.Object.LastSeenMessage = new int[] { -1, -1, -1, -1, -1, -1, -1 }; + mockContext.Object.LastSeenMessage.Clear(); + foreach (var validator in mockContext.Object.Validators) + { + mockContext.Object.LastSeenMessage[validator] = -1; + } mockContext.Object.CountFailed.Should().Be(7); TellConsensusPayload(actorConsensus, rmPayload);