diff --git a/neo.UnitTests/TestBlockchain.cs b/neo.UnitTests/TestBlockchain.cs new file mode 100644 index 000000000..b456dc820 --- /dev/null +++ b/neo.UnitTests/TestBlockchain.cs @@ -0,0 +1,65 @@ +using Moq; +using Neo.Cryptography.ECC; +using Neo.IO.Wrappers; +using Neo.Ledger; +using Neo.Persistence; +using System; + +namespace Neo.UnitTests +{ + public static class TestBlockchain + { + private static NeoSystem TheNeoSystem; + + public static NeoSystem InitializeMockNeoSystem() + { + if (TheNeoSystem == null) + { + var mockSnapshot = new Mock(); + mockSnapshot.SetupGet(p => p.Blocks).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.Transactions).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.Accounts).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.UnspentCoins).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.SpentCoins).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.Validators).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.Assets).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.Contracts).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.Storages).Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.HeaderHashList) + .Returns(new TestDataCache()); + mockSnapshot.SetupGet(p => p.ValidatorsCount).Returns(new TestMetaDataCache()); + mockSnapshot.SetupGet(p => p.BlockHashIndex).Returns(new TestMetaDataCache()); + mockSnapshot.SetupGet(p => p.HeaderHashIndex).Returns(new TestMetaDataCache()); + + var mockStore = new Mock(); + + var defaultTx = TestUtils.CreateRandomHashInvocationMockTransaction().Object; + mockStore.Setup(p => p.GetBlocks()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetTransactions()).Returns(new TestDataCache( + new TransactionState + { + BlockIndex = 1, + Transaction = defaultTx + })); + + mockStore.Setup(p => p.GetAccounts()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetUnspentCoins()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetSpentCoins()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetValidators()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetAssets()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetContracts()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetStorages()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetHeaderHashList()).Returns(new TestDataCache()); + mockStore.Setup(p => p.GetValidatorsCount()).Returns(new TestMetaDataCache()); + mockStore.Setup(p => p.GetBlockHashIndex()).Returns(new TestMetaDataCache()); + mockStore.Setup(p => p.GetHeaderHashIndex()).Returns(new TestMetaDataCache()); + mockStore.Setup(p => p.GetSnapshot()).Returns(mockSnapshot.Object); + + Console.WriteLine("initialize NeoSystem"); + TheNeoSystem = new NeoSystem(mockStore.Object); // new Mock(mockStore.Object); + } + + return TheNeoSystem; + } + } +} \ No newline at end of file diff --git a/neo.UnitTests/TestUtils.cs b/neo.UnitTests/TestUtils.cs index 1e73f84ce..a7c479889 100644 --- a/neo.UnitTests/TestUtils.cs +++ b/neo.UnitTests/TestUtils.cs @@ -1,14 +1,21 @@ -using Neo.Cryptography.ECC; +using Moq; +using Neo.Cryptography.ECC; +using Neo.IO; using Neo.Network.P2P.Payloads; +using Neo.Persistence; using Neo.VM; using System; +using System.Collections.Generic; +using System.IO; namespace Neo.UnitTests { public static class TestUtils { + public static readonly Random TestRandom = new Random(1337); // use fixed seed for guaranteed determinism + public static byte[] GetByteArray(int length, byte firstByte) - { + { byte[] array = new byte[length]; array[0] = firstByte; for (int i = 1; i < length; i++) @@ -95,5 +102,50 @@ private static void setupBlockBaseWithValues(BlockBase bb, UInt256 val256, out U }; bb.Witness = scriptVal; } - } + + public static Mock CreateRandomHashInvocationMockTransaction() + { + var mockTx = new Mock + { + CallBase = true + }; + mockTx.Setup(p => p.Verify(It.IsAny(), It.IsAny>())).Returns(true); + var tx = mockTx.Object; + var randomBytes = new byte[16]; + TestRandom.NextBytes(randomBytes); + tx.Script = randomBytes; + tx.Attributes = new TransactionAttribute[0]; + tx.Inputs = new CoinReference[0]; + tx.Outputs = new TransactionOutput[0]; + tx.Witnesses = new Witness[0]; + + return mockTx; + } + + public static Mock CreateRandomMockMinerTransaction() + { + var mockTx = new Mock + { + CallBase = true + }; + var tx = mockTx.Object; + tx.Attributes = new TransactionAttribute[0]; + tx.Inputs = new CoinReference[0]; + tx.Outputs = new TransactionOutput[0]; + tx.Witnesses = new Witness[0]; + tx.Nonce = (uint)TestRandom.Next(); + return mockTx; + } + + public static T CopyMsgBySerialization(T serializableObj, T newObj) where T : ISerializable + { + using (MemoryStream ms = new MemoryStream(serializableObj.ToArray(), false)) + using (BinaryReader reader = new BinaryReader(ms)) + { + newObj.Deserialize(reader); + } + + return newObj; + } + } } diff --git a/neo.UnitTests/UT_Consensus.cs b/neo.UnitTests/UT_Consensus.cs index d9bd62a57..b1608e187 100644 --- a/neo.UnitTests/UT_Consensus.cs +++ b/neo.UnitTests/UT_Consensus.cs @@ -4,11 +4,18 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Neo.Consensus; +using Neo.Cryptography; using Neo.IO; using Neo.Ledger; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using ECPoint = Neo.Cryptography.ECC.ECPoint; namespace Neo.UnitTests { @@ -16,6 +23,12 @@ namespace Neo.UnitTests [TestClass] public class ConsensusTests : TestKit { + [TestInitialize] + public void TestSetup() + { + TestBlockchain.InitializeMockNeoSystem(); + } + [TestCleanup] public void Cleanup() { @@ -28,6 +41,7 @@ public void ConsensusService_Primary_Sends_PrepareRequest_After_OnStart() TestProbe subscriber = CreateTestProbe(); var mockConsensusContext = new Mock(); + var mockStore = new Mock(); // context.Reset(): do nothing //mockConsensusContext.Setup(mr => mr.Reset()).Verifiable(); // void @@ -38,9 +52,8 @@ public void ConsensusService_Primary_Sends_PrepareRequest_After_OnStart() mockConsensusContext.SetupProperty(mr => mr.Nonce); mockConsensusContext.SetupProperty(mr => mr.NextConsensus); mockConsensusContext.Object.NextConsensus = UInt160.Zero; - mockConsensusContext.Setup(mr => mr.GetPrimaryIndex(It.IsAny())).Returns(2); - mockConsensusContext.SetupProperty(mr => mr.State); // allows get and set to update mock state on Initialize method - mockConsensusContext.Object.State = ConsensusState.Initial; + mockConsensusContext.SetupGet(mr => mr.PreparationPayloads).Returns(new ConsensusPayload[7]); + mockConsensusContext.SetupGet(mr => mr.CommitPayloads).Returns(new ConsensusPayload[7]); int timeIndex = 0; var timeValues = new[] { @@ -99,21 +112,16 @@ public void ConsensusService_Primary_Sends_PrepareRequest_After_OnStart() Nonce = mockConsensusContext.Object.Nonce, NextConsensus = mockConsensusContext.Object.NextConsensus, TransactionHashes = new UInt256[0], - MinerTransaction = minerTx, //(MinerTransaction)Transactions[TransactionHashes[0]], - Signature = new byte[64]//Signatures[MyIndex] + MinerTransaction = minerTx //(MinerTransaction)Transactions[TransactionHashes[0]], }; - ConsensusMessage mprep = prep; - byte[] prepData = mprep.ToArray(); - ConsensusPayload prepPayload = new ConsensusPayload { Version = 0, PrevHash = mockConsensusContext.Object.PrevHash, BlockIndex = mockConsensusContext.Object.BlockIndex, ValidatorIndex = (ushort)mockConsensusContext.Object.MyIndex, - Timestamp = mockConsensusContext.Object.Timestamp, - Data = prepData + ConsensusMessage = prep }; mockConsensusContext.Setup(mr => mr.MakePrepareRequest()).Returns(prepPayload); @@ -123,11 +131,10 @@ public void ConsensusService_Primary_Sends_PrepareRequest_After_OnStart() // ============================================================================ TestActorRef actorConsensus = ActorOfAsTestActorRef( - Akka.Actor.Props.Create(() => new ConsensusService(subscriber, subscriber, mockConsensusContext.Object)) + Akka.Actor.Props.Create(() => new ConsensusService(subscriber, subscriber, mockStore.Object, mockConsensusContext.Object)) ); Console.WriteLine("will trigger OnPersistCompleted!"); - actorConsensus.Tell(new Blockchain.PersistCompleted { Block = new Block @@ -142,8 +149,10 @@ public void ConsensusService_Primary_Sends_PrepareRequest_After_OnStart() } }); - //Console.WriteLine("will start consensus!"); - //actorConsensus.Tell(new ConsensusService.Start()); + // OnPersist will not launch timer, we need OnStart + + Console.WriteLine("will start consensus!"); + actorConsensus.Tell(new ConsensusService.Start()); Console.WriteLine("OnTimer should expire!"); Console.WriteLine("Waiting for subscriber message!"); @@ -162,5 +171,457 @@ public void ConsensusService_Primary_Sends_PrepareRequest_After_OnStart() Assert.AreEqual(1, 1); } + + [TestMethod] + public void TestSerializeAndDeserializeConsensusContext() + { + var consensusContext = new ConsensusContext(null); + consensusContext.PrevHash = UInt256.Parse("0xd42561e3d30e15be6400b6df2f328e02d2bf6354c41dce433bc57687c82144bf"); + consensusContext.BlockIndex = 1; + consensusContext.ViewNumber = 2; + consensusContext.Validators = new ECPoint[7] + { + ECPoint.Parse("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", Cryptography.ECC.ECCurve.Secp256r1), + ECPoint.Parse("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", Cryptography.ECC.ECCurve.Secp256r1), + ECPoint.Parse("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", Cryptography.ECC.ECCurve.Secp256r1), + ECPoint.Parse("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", Cryptography.ECC.ECCurve.Secp256r1), + ECPoint.Parse("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", Cryptography.ECC.ECCurve.Secp256r1), + ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", Cryptography.ECC.ECCurve.Secp256r1), + ECPoint.Parse("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", Cryptography.ECC.ECCurve.Secp256r1) + }; + consensusContext.MyIndex = -1; + consensusContext.PrimaryIndex = 6; + consensusContext.Timestamp = 4244941711; + consensusContext.Nonce = UInt64.MaxValue; + consensusContext.NextConsensus = UInt160.Parse("5555AAAA5555AAAA5555AAAA5555AAAA5555AAAA"); + var testTx1 = TestUtils.CreateRandomHashInvocationMockTransaction().Object; + var testTx2 = TestUtils.CreateRandomHashInvocationMockTransaction().Object; + + int txCountToInlcude = 256; + consensusContext.TransactionHashes = new UInt256[txCountToInlcude]; + + Transaction[] txs = new Transaction[txCountToInlcude]; + txs[0] = TestUtils.CreateRandomMockMinerTransaction().Object; + consensusContext.TransactionHashes[0] = txs[0].Hash; + for (int i = 1; i < txCountToInlcude; i++) + { + txs[i] = TestUtils.CreateRandomHashInvocationMockTransaction().Object; + consensusContext.TransactionHashes[i] = txs[i].Hash; + } + // consensusContext.TransactionHashes = new UInt256[2] {testTx1.Hash, testTx2.Hash}; + consensusContext.Transactions = txs.ToDictionary(p => p.Hash); + + consensusContext.PreparationPayloads = new ConsensusPayload[consensusContext.Validators.Length]; + var prepareRequestMessage = new PrepareRequest + { + Nonce = consensusContext.Nonce, + NextConsensus = consensusContext.NextConsensus, + TransactionHashes = consensusContext.TransactionHashes, + MinerTransaction = (MinerTransaction)consensusContext.Transactions[consensusContext.TransactionHashes[0]], + Timestamp = 23 + }; + consensusContext.PreparationPayloads[6] = MakeSignedPayload(consensusContext, prepareRequestMessage, 6, new[] { (byte)'3', (byte)'!' }); + consensusContext.PreparationPayloads[0] = MakeSignedPayload(consensusContext, new PrepareResponse { PreparationHash = consensusContext.PreparationPayloads[6].Hash }, 0, new[] { (byte)'t', (byte)'e' }); + consensusContext.PreparationPayloads[1] = MakeSignedPayload(consensusContext, new PrepareResponse { PreparationHash = consensusContext.PreparationPayloads[6].Hash }, 1, new[] { (byte)'s', (byte)'t' }); + consensusContext.PreparationPayloads[2] = null; + consensusContext.PreparationPayloads[3] = MakeSignedPayload(consensusContext, new PrepareResponse { PreparationHash = consensusContext.PreparationPayloads[6].Hash }, 3, new[] { (byte)'1', (byte)'2' }); + consensusContext.PreparationPayloads[4] = null; + consensusContext.PreparationPayloads[5] = null; + + consensusContext.CommitPayloads = new ConsensusPayload[consensusContext.Validators.Length]; + using (SHA256 sha256 = SHA256.Create()) + { + consensusContext.CommitPayloads[3] = MakeSignedPayload(consensusContext, new Commit { Signature = sha256.ComputeHash(testTx1.Hash.ToArray()) }, 3, new[] { (byte)'3', (byte)'4' }); + consensusContext.CommitPayloads[6] = MakeSignedPayload(consensusContext, new Commit { Signature = sha256.ComputeHash(testTx2.Hash.ToArray()) }, 3, new[] { (byte)'6', (byte)'7' }); + } + + consensusContext.Timestamp = TimeProvider.Current.UtcNow.ToTimestamp(); + + consensusContext.ChangeViewPayloads = new ConsensusPayload[consensusContext.Validators.Length]; + consensusContext.ChangeViewPayloads[0] = MakeSignedPayload(consensusContext, new ChangeView { ViewNumber = 1, NewViewNumber = 2, Timestamp = 6 }, 0, new[] { (byte)'A' }); + consensusContext.ChangeViewPayloads[1] = MakeSignedPayload(consensusContext, new ChangeView { ViewNumber = 1, NewViewNumber = 2, Timestamp = 5 }, 1, new[] { (byte)'B' }); + consensusContext.ChangeViewPayloads[2] = null; + consensusContext.ChangeViewPayloads[3] = MakeSignedPayload(consensusContext, new ChangeView { ViewNumber = 1, NewViewNumber = 2, Timestamp = uint.MaxValue }, 3, new[] { (byte)'C' }); + consensusContext.ChangeViewPayloads[4] = null; + consensusContext.ChangeViewPayloads[5] = null; + consensusContext.ChangeViewPayloads[6] = MakeSignedPayload(consensusContext, new ChangeView { ViewNumber = 1, NewViewNumber = 2, Timestamp = 1 }, 6, new[] { (byte)'D' }); + + consensusContext.LastChangeViewPayloads = new ConsensusPayload[consensusContext.Validators.Length]; + + var copiedContext = TestUtils.CopyMsgBySerialization(consensusContext, new ConsensusContext(null)); + + copiedContext.PrevHash.Should().Be(consensusContext.PrevHash); + copiedContext.BlockIndex.Should().Be(consensusContext.BlockIndex); + copiedContext.ViewNumber.Should().Be(consensusContext.ViewNumber); + copiedContext.Validators.ShouldAllBeEquivalentTo(consensusContext.Validators); + copiedContext.MyIndex.Should().Be(consensusContext.MyIndex); + copiedContext.PrimaryIndex.Should().Be(consensusContext.PrimaryIndex); + copiedContext.Timestamp.Should().Be(consensusContext.Timestamp); + copiedContext.Nonce.Should().Be(consensusContext.Nonce); + copiedContext.NextConsensus.Should().Be(consensusContext.NextConsensus); + copiedContext.TransactionHashes.ShouldAllBeEquivalentTo(consensusContext.TransactionHashes); + copiedContext.Transactions.ShouldAllBeEquivalentTo(consensusContext.Transactions); + copiedContext.Transactions.Values.ShouldAllBeEquivalentTo(consensusContext.Transactions.Values); + copiedContext.PreparationPayloads.ShouldAllBeEquivalentTo(consensusContext.PreparationPayloads); + copiedContext.CommitPayloads.ShouldAllBeEquivalentTo(consensusContext.CommitPayloads); + copiedContext.ChangeViewPayloads.ShouldAllBeEquivalentTo(consensusContext.ChangeViewPayloads); + } + + [TestMethod] + public void TestSerializeAndDeserializeRecoveryMessageWithChangeViewsAndNoPrepareRequest() + { + var msg = new RecoveryMessage + { + ChangeViewMessages = new Dictionary() + { + { + 0, + new RecoveryMessage.ChangeViewPayloadCompact + { + ValidatorIndex = 0, + OriginalViewNumber = 9, + Timestamp = 6, + InvocationScript = new[] { (byte)'A' } + } + }, + { + 1, + new RecoveryMessage.ChangeViewPayloadCompact + { + ValidatorIndex = 1, + OriginalViewNumber = 7, + Timestamp = 5, + InvocationScript = new[] { (byte)'B' } + } + }, + { + 3, + new RecoveryMessage.ChangeViewPayloadCompact + { + ValidatorIndex = 3, + OriginalViewNumber = 5, + Timestamp = 3, + InvocationScript = new[] { (byte)'C' } + } + }, + { + 6, + new RecoveryMessage.ChangeViewPayloadCompact + { + ValidatorIndex = 6, + OriginalViewNumber = 2, + Timestamp = 1, + InvocationScript = new[] { (byte)'D' } + } + } + }, + PreparationHash = new UInt256(Crypto.Default.Hash256(new[] { (byte)'a' })), + PreparationMessages = new Dictionary() + { + { + 0, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 0, + InvocationScript = new[] { (byte)'t', (byte)'e' } + } + }, + { + 3, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 3, + InvocationScript = new[] { (byte)'1', (byte)'2' } + } + }, + { + 6, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 6, + InvocationScript = new[] { (byte)'3', (byte)'!' } + } + } + }, + CommitMessages = new Dictionary() + }; + + // msg.TransactionHashes = null; + // msg.Nonce = 0; + // msg.NextConsensus = null; + // msg.MinerTransaction = (MinerTransaction) null; + msg.PrepareRequestMessage.Should().Be(null); + + var copiedMsg = TestUtils.CopyMsgBySerialization(msg, new RecoveryMessage()); ; + + copiedMsg.ChangeViewMessages.ShouldAllBeEquivalentTo(msg.ChangeViewMessages); + copiedMsg.PreparationHash.Should().Be(msg.PreparationHash); + copiedMsg.PreparationMessages.ShouldAllBeEquivalentTo(msg.PreparationMessages); + copiedMsg.CommitMessages.Count.Should().Be(0); + } + + [TestMethod] + public void TestSerializeAndDeserializeRecoveryMessageWithChangeViewsAndPrepareRequest() + { + Transaction[] txs = new Transaction[5]; + txs[0] = TestUtils.CreateRandomMockMinerTransaction().Object; + for (int i = 1; i < txs.Length; i++) + txs[i] = TestUtils.CreateRandomHashInvocationMockTransaction().Object; + var msg = new RecoveryMessage + { + ChangeViewMessages = new Dictionary() + { + { + 0, + new RecoveryMessage.ChangeViewPayloadCompact + { + ValidatorIndex = 0, + OriginalViewNumber = 9, + Timestamp = 6, + InvocationScript = new[] { (byte)'A' } + } + }, + { + 1, + new RecoveryMessage.ChangeViewPayloadCompact + { + ValidatorIndex = 1, + OriginalViewNumber = 7, + Timestamp = 5, + InvocationScript = new[] { (byte)'B' } + } + }, + { + 3, + new RecoveryMessage.ChangeViewPayloadCompact + { + ValidatorIndex = 3, + OriginalViewNumber = 5, + Timestamp = 3, + InvocationScript = new[] { (byte)'C' } + } + }, + { + 6, + new RecoveryMessage.ChangeViewPayloadCompact + { + ValidatorIndex = 6, + OriginalViewNumber = 2, + Timestamp = 1, + InvocationScript = new[] { (byte)'D' } + } + } + }, + PrepareRequestMessage = new PrepareRequest + { + TransactionHashes = txs.Select(p => p.Hash).ToArray(), + Nonce = ulong.MaxValue, + NextConsensus = UInt160.Parse("5555AAAA5555AAAA5555AAAA5555AAAA5555AAAA"), + MinerTransaction = (MinerTransaction)txs[0] + }, + PreparationHash = new UInt256(Crypto.Default.Hash256(new[] { (byte)'a' })), + PreparationMessages = new Dictionary() + { + { + 0, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 0, + InvocationScript = new[] { (byte)'t', (byte)'e' } + } + }, + { + 1, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 1, + InvocationScript = new[] { (byte)'s', (byte)'t' } + } + }, + { + 3, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 3, + InvocationScript = new[] { (byte)'1', (byte)'2' } + } + } + }, + CommitMessages = new Dictionary() + }; + + var copiedMsg = TestUtils.CopyMsgBySerialization(msg, new RecoveryMessage()); ; + + copiedMsg.ChangeViewMessages.ShouldAllBeEquivalentTo(msg.ChangeViewMessages); + copiedMsg.PrepareRequestMessage.ShouldBeEquivalentTo(msg.PrepareRequestMessage); + copiedMsg.PreparationHash.Should().Be(null); + copiedMsg.PreparationMessages.ShouldAllBeEquivalentTo(msg.PreparationMessages); + copiedMsg.CommitMessages.Count.Should().Be(0); + } + + [TestMethod] + public void TestSerializeAndDeserializeRecoveryMessageWithoutChangeViewsWithoutCommits() + { + Transaction[] txs = new Transaction[5]; + txs[0] = TestUtils.CreateRandomMockMinerTransaction().Object; + for (int i = 1; i < txs.Length; i++) + txs[i] = TestUtils.CreateRandomHashInvocationMockTransaction().Object; + var msg = new RecoveryMessage + { + ChangeViewMessages = new Dictionary(), + PrepareRequestMessage = new PrepareRequest + { + TransactionHashes = txs.Select(p => p.Hash).ToArray(), + Nonce = ulong.MaxValue, + NextConsensus = UInt160.Parse("5555AAAA5555AAAA5555AAAA5555AAAA5555AAAA"), + MinerTransaction = (MinerTransaction)txs[0] + }, + PreparationMessages = new Dictionary() + { + { + 0, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 0, + InvocationScript = new[] { (byte)'t', (byte)'e' } + } + }, + { + 1, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 1, + InvocationScript = new[] { (byte)'s', (byte)'t' } + } + }, + { + 3, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 3, + InvocationScript = new[] { (byte)'1', (byte)'2' } + } + }, + { + 6, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 6, + InvocationScript = new[] { (byte)'3', (byte)'!' } + } + } + }, + CommitMessages = new Dictionary() + }; + + var copiedMsg = TestUtils.CopyMsgBySerialization(msg, new RecoveryMessage()); ; + + copiedMsg.ChangeViewMessages.Count.Should().Be(0); + copiedMsg.PrepareRequestMessage.ShouldBeEquivalentTo(msg.PrepareRequestMessage); + copiedMsg.PreparationHash.Should().Be(null); + copiedMsg.PreparationMessages.ShouldAllBeEquivalentTo(msg.PreparationMessages); + copiedMsg.CommitMessages.Count.Should().Be(0); + } + + [TestMethod] + public void TestSerializeAndDeserializeRecoveryMessageWithoutChangeViewsWithCommits() + { + Transaction[] txs = new Transaction[5]; + txs[0] = TestUtils.CreateRandomMockMinerTransaction().Object; + for (int i = 1; i < txs.Length; i++) + txs[i] = TestUtils.CreateRandomHashInvocationMockTransaction().Object; + var msg = new RecoveryMessage + { + ChangeViewMessages = new Dictionary(), + PrepareRequestMessage = new PrepareRequest + { + TransactionHashes = txs.Select(p => p.Hash).ToArray(), + Nonce = ulong.MaxValue, + NextConsensus = UInt160.Parse("5555AAAA5555AAAA5555AAAA5555AAAA5555AAAA"), + MinerTransaction = (MinerTransaction)txs[0] + }, + PreparationMessages = new Dictionary() + { + { + 0, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 0, + InvocationScript = new[] { (byte)'t', (byte)'e' } + } + }, + { + 1, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 1, + InvocationScript = new[] { (byte)'s', (byte)'t' } + } + }, + { + 3, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 3, + InvocationScript = new[] { (byte)'1', (byte)'2' } + } + }, + { + 6, + new RecoveryMessage.PreparationPayloadCompact + { + ValidatorIndex = 6, + InvocationScript = new[] { (byte)'3', (byte)'!' } + } + } + }, + CommitMessages = new Dictionary + { + { + 1, + new RecoveryMessage.CommitPayloadCompact + { + ValidatorIndex = 1, + Signature = new byte[64] { (byte)'1', (byte)'2', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + InvocationScript = new[] { (byte)'1', (byte)'2' } + } + }, + { + 6, + new RecoveryMessage.CommitPayloadCompact + { + ValidatorIndex = 6, + Signature = new byte[64] { (byte)'3', (byte)'D', (byte)'R', (byte)'I', (byte)'N', (byte)'K', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + InvocationScript = new[] { (byte)'6', (byte)'7' } + } + } + } + }; + + var copiedMsg = TestUtils.CopyMsgBySerialization(msg, new RecoveryMessage()); ; + + copiedMsg.ChangeViewMessages.Count.Should().Be(0); + copiedMsg.PrepareRequestMessage.ShouldBeEquivalentTo(msg.PrepareRequestMessage); + copiedMsg.PreparationHash.Should().Be(null); + copiedMsg.PreparationMessages.ShouldAllBeEquivalentTo(msg.PreparationMessages); + copiedMsg.CommitMessages.ShouldAllBeEquivalentTo(msg.CommitMessages); + } + + private static ConsensusPayload MakeSignedPayload(IConsensusContext context, ConsensusMessage message, ushort validatorIndex, byte[] witnessInvocationScript) + { + return new ConsensusPayload + { + Version = ConsensusContext.Version, + PrevHash = context.PrevHash, + BlockIndex = context.BlockIndex, + ValidatorIndex = validatorIndex, + ConsensusMessage = message, + Witness = new Witness + { + InvocationScript = witnessInvocationScript, + VerificationScript = Contract.CreateSignatureRedeemScript(context.Validators[validatorIndex]) + } + }; + } } } diff --git a/neo.UnitTests/UT_MemoryPool.cs b/neo.UnitTests/UT_MemoryPool.cs index 4f78b9b03..41f318285 100644 --- a/neo.UnitTests/UT_MemoryPool.cs +++ b/neo.UnitTests/UT_MemoryPool.cs @@ -1,24 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using Neo.Ledger; -using FluentAssertions; -using Neo.Cryptography.ECC; -using Neo.IO.Wrappers; using Neo.Network.P2P.Payloads; -using Neo.Persistence; +using System; +using System.Collections.Generic; +using System.Linq; namespace Neo.UnitTests { [TestClass] public class UT_MemoryPool { - private static NeoSystem TheNeoSystem; - - private readonly Random _random = new Random(1337); // use fixed seed for guaranteed determinism - private MemoryPool _unit; [TestInitialize] @@ -27,59 +19,7 @@ public void TestSetup() // protect against external changes on TimeProvider TimeProvider.ResetToDefault(); - if (TheNeoSystem == null) - { - var mockSnapshot = new Mock(); - mockSnapshot.SetupGet(p => p.Blocks).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.Transactions).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.Accounts).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.UnspentCoins).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.SpentCoins).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.Validators).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.Assets).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.Contracts).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.Storages).Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.HeaderHashList) - .Returns(new TestDataCache()); - mockSnapshot.SetupGet(p => p.ValidatorsCount).Returns(new TestMetaDataCache()); - mockSnapshot.SetupGet(p => p.BlockHashIndex).Returns(new TestMetaDataCache()); - mockSnapshot.SetupGet(p => p.HeaderHashIndex).Returns(new TestMetaDataCache()); - - var mockStore = new Mock(); - - var defaultTx = CreateRandomHashInvocationMockTransaction().Object; - defaultTx.Outputs = new TransactionOutput[1]; - defaultTx.Outputs[0] = new TransactionOutput - { - AssetId = Blockchain.UtilityToken.Hash, - Value = new Fixed8(1000000), - ScriptHash = UInt160.Zero // doesn't matter for our purposes. - }; - - mockStore.Setup(p => p.GetBlocks()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetTransactions()).Returns(new TestDataCache( - new TransactionState - { - BlockIndex = 1, - Transaction = defaultTx - })); - - mockStore.Setup(p => p.GetAccounts()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetUnspentCoins()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetSpentCoins()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetValidators()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetAssets()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetContracts()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetStorages()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetHeaderHashList()).Returns(new TestDataCache()); - mockStore.Setup(p => p.GetValidatorsCount()).Returns(new TestMetaDataCache()); - mockStore.Setup(p => p.GetBlockHashIndex()).Returns(new TestMetaDataCache()); - mockStore.Setup(p => p.GetHeaderHashIndex()).Returns(new TestMetaDataCache()); - mockStore.Setup(p => p.GetSnapshot()).Returns(mockSnapshot.Object); - - Console.WriteLine("initialize NeoSystem"); - TheNeoSystem = new NeoSystem(mockStore.Object); // new Mock(mockStore.Object); - } + NeoSystem TheNeoSystem = TestBlockchain.InitializeMockNeoSystem(); // Create a MemoryPool with capacity of 100 _unit = new MemoryPool(TheNeoSystem, 100); @@ -92,33 +32,18 @@ public void TestSetup() _unit.Count.ShouldBeEquivalentTo(0); } - private Mock CreateRandomHashInvocationMockTransaction() - { - var mockTx = new Mock(); - mockTx.CallBase = true; - mockTx.Setup(p => p.Verify(It.IsAny(), It.IsAny>())).Returns(true); - var tx = mockTx.Object; - var randomBytes = new byte[16]; - _random.NextBytes(randomBytes); - tx.Script = randomBytes; - tx.Attributes = new TransactionAttribute[0]; - tx.Inputs = new CoinReference[0]; - tx.Outputs = new TransactionOutput[0]; - tx.Witnesses = new Witness[0]; - - return mockTx; - } + long LongRandom(long min, long max, Random rand) { // Only returns positive random long values. - long longRand = (long) rand.NextBigInteger(63); + long longRand = (long)rand.NextBigInteger(63); return longRand % (max - min) + min; } private Transaction CreateMockTransactionWithFee(long fee) { - var mockTx = CreateRandomHashInvocationMockTransaction(); + var mockTx = TestUtils.CreateRandomHashInvocationMockTransaction(); mockTx.SetupGet(p => p.NetworkFee).Returns(new Fixed8(fee)); var tx = mockTx.Object; if (fee > 0) @@ -136,21 +61,21 @@ private Transaction CreateMockTransactionWithFee(long fee) private Transaction CreateMockHighPriorityTransaction() { - return CreateMockTransactionWithFee(LongRandom(100000, 100000000, _random)); + return CreateMockTransactionWithFee(LongRandom(100000, 100000000, TestUtils.TestRandom)); } private Transaction CreateMockLowPriorityTransaction() { - long rNetFee = LongRandom(0, 100000, _random); + long rNetFee = LongRandom(0, 100000, TestUtils.TestRandom); // [0,0.001] GAS a fee lower than the threshold of 0.001 GAS (not enough to be a high priority TX) return CreateMockTransactionWithFee(rNetFee); } - private void AddTransactions(int count, bool isHighPriority=false) + private void AddTransactions(int count, bool isHighPriority = false) { for (int i = 0; i < count; i++) { - var txToAdd = isHighPriority ? CreateMockHighPriorityTransaction(): CreateMockLowPriorityTransaction(); + var txToAdd = isHighPriority ? CreateMockHighPriorityTransaction() : CreateMockLowPriorityTransaction(); Console.WriteLine($"created tx: {txToAdd.Hash}"); _unit.TryAdd(txToAdd.Hash, txToAdd); } @@ -349,7 +274,7 @@ public void VerifySortOrderAndThatHighetFeeTransactionsAreReverifiedFirst() verifiedTxs.Length.ShouldBeEquivalentTo(2); verifiedTxs[0].ShouldBeEquivalentTo(maxHighPriorityTransaction); verifiedTxs[1].ShouldBeEquivalentTo(maxLowPriorityTransaction); - var blockWith2Tx = new Block { Transactions = new Transaction[2] { maxHighPriorityTransaction, maxLowPriorityTransaction }}; + var blockWith2Tx = new Block { Transactions = new Transaction[2] { maxHighPriorityTransaction, maxLowPriorityTransaction } }; // verify and remove the 2 transactions from the verified pool _unit.UpdatePoolForBlockPersisted(blockWith2Tx, Blockchain.Singleton.GetSnapshot()); _unit.SortedHighPrioTxCount.ShouldBeEquivalentTo(0); @@ -397,5 +322,21 @@ public void CapacityTestWithUnverifiedHighProirtyTransactions() AddHighPriorityTransactions(1); _unit.CanTransactionFitInPool(CreateMockLowPriorityTransaction()).ShouldBeEquivalentTo(false); } + + [TestMethod] + public void TestInvalidateAll() + { + AddHighPriorityTransactions(30); + AddLowPriorityTransactions(60); + _unit.UnverifiedSortedHighPrioTxCount.ShouldBeEquivalentTo(0); + _unit.UnverifiedSortedLowPrioTxCount.ShouldBeEquivalentTo(0); + _unit.SortedHighPrioTxCount.ShouldBeEquivalentTo(30); + _unit.SortedLowPrioTxCount.ShouldBeEquivalentTo(60); + _unit.InvalidateAllTransactions(); + _unit.UnverifiedSortedHighPrioTxCount.ShouldBeEquivalentTo(30); + _unit.UnverifiedSortedLowPrioTxCount.ShouldBeEquivalentTo(60); + _unit.SortedHighPrioTxCount.ShouldBeEquivalentTo(0); + _unit.SortedLowPrioTxCount.ShouldBeEquivalentTo(0); + } } } diff --git a/neo/Consensus/ChangeView.cs b/neo/Consensus/ChangeView.cs index 2bf141215..589e0f79d 100644 --- a/neo/Consensus/ChangeView.cs +++ b/neo/Consensus/ChangeView.cs @@ -1,13 +1,20 @@ -using System; -using System.IO; +using System.IO; namespace Neo.Consensus { internal class ChangeView : ConsensusMessage { public byte NewViewNumber; + /// + /// Timestamp of when the ChangeView message was created. This allows receiving nodes to ensure + // they only respond once to a specific ChangeView request (it thus prevents replay of the ChangeView + // message from repeatedly broadcasting RecoveryMessages). + /// + public uint Timestamp; - public override int Size => base.Size + sizeof(byte); + public override int Size => base.Size + + sizeof(byte) //NewViewNumber + + sizeof(uint); //Timestamp public ChangeView() : base(ConsensusMessageType.ChangeView) @@ -18,13 +25,14 @@ public override void Deserialize(BinaryReader reader) { base.Deserialize(reader); NewViewNumber = reader.ReadByte(); - if (NewViewNumber == 0) throw new FormatException(); + Timestamp = reader.ReadUInt32(); } public override void Serialize(BinaryWriter writer) { base.Serialize(writer); writer.Write(NewViewNumber); + writer.Write(Timestamp); } } } diff --git a/neo/Consensus/Commit.cs b/neo/Consensus/Commit.cs new file mode 100644 index 000000000..cac24aff6 --- /dev/null +++ b/neo/Consensus/Commit.cs @@ -0,0 +1,25 @@ +using System.IO; + +namespace Neo.Consensus +{ + internal class Commit : ConsensusMessage + { + public byte[] Signature; + + public override int Size => base.Size + Signature.Length; + + public Commit() : base(ConsensusMessageType.Commit) { } + + public override void Deserialize(BinaryReader reader) + { + base.Deserialize(reader); + Signature = reader.ReadBytes(64); + } + + public override void Serialize(BinaryWriter writer) + { + base.Serialize(writer); + writer.Write(Signature); + } + } +} diff --git a/neo/Consensus/ConsensusContext.cs b/neo/Consensus/ConsensusContext.cs index 924709fc9..5c3c9da85 100644 --- a/neo/Consensus/ConsensusContext.cs +++ b/neo/Consensus/ConsensusContext.cs @@ -9,6 +9,7 @@ using Neo.Wallets; using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace Neo.Consensus @@ -16,9 +17,8 @@ namespace Neo.Consensus internal class ConsensusContext : IConsensusContext { public const uint Version = 0; - public ConsensusState State { get; set; } - public UInt256 PrevHash { get; set; } public uint BlockIndex { get; set; } + public UInt256 PrevHash { get; set; } public byte ViewNumber { get; set; } public ECPoint[] Validators { get; set; } public int MyIndex { get; set; } @@ -28,73 +28,106 @@ internal class ConsensusContext : IConsensusContext public UInt160 NextConsensus { get; set; } public UInt256[] TransactionHashes { get; set; } public Dictionary Transactions { get; set; } - public byte[][] Signatures { get; set; } - public byte[] ExpectedView { get; set; } - private Snapshot snapshot; + public ConsensusPayload[] PreparationPayloads { get; set; } + public ConsensusPayload[] CommitPayloads { get; set; } + public ConsensusPayload[] ChangeViewPayloads { get; set; } + public ConsensusPayload[] LastChangeViewPayloads { get; set; } + public Block Block { get; set; } + public Snapshot Snapshot { get; private set; } private KeyPair keyPair; private readonly Wallet wallet; - public int M => Validators.Length - (Validators.Length - 1) / 3; - public Header PrevHeader => snapshot.GetHeader(PrevHash); - public bool TransactionExists(UInt256 hash) => snapshot.ContainsTransaction(hash); - public bool VerifyTransaction(Transaction tx) => tx.Verify(snapshot, Transactions.Values); + public int Size => throw new NotImplementedException(); public ConsensusContext(Wallet wallet) { this.wallet = wallet; } - public void ChangeView(byte view_number) - { - State &= ConsensusState.SignatureSent; - ViewNumber = view_number; - PrimaryIndex = GetPrimaryIndex(view_number); - if (State == ConsensusState.Initial) - { - TransactionHashes = null; - Signatures = new byte[Validators.Length][]; - } - if (MyIndex >= 0) - ExpectedView[MyIndex] = view_number; - _header = null; - } - public Block CreateBlock() { - Block block = MakeHeader(); - if (block == null) return null; - Contract contract = Contract.CreateMultiSigContract(M, Validators); - ContractParametersContext sc = new ContractParametersContext(block); - for (int i = 0, j = 0; i < Validators.Length && j < M; i++) - if (Signatures[i] != null) + if (Block is null) + { + Block = MakeHeader(); + if (Block == null) return null; + Contract contract = Contract.CreateMultiSigContract(this.M(), Validators); + ContractParametersContext sc = new ContractParametersContext(Block); + for (int i = 0, j = 0; i < Validators.Length && j < this.M(); i++) { - sc.AddSignature(contract, Validators[i], Signatures[i]); + if (CommitPayloads[i] == null) continue; + sc.AddSignature(contract, Validators[i], CommitPayloads[i].GetDeserializedMessage().Signature); j++; } - sc.Verifiable.Witnesses = sc.GetWitnesses(); - block.Transactions = TransactionHashes.Select(p => Transactions[p]).ToArray(); - return block; + sc.Verifiable.Witnesses = sc.GetWitnesses(); + Block.Transactions = TransactionHashes.Select(p => Transactions[p]).ToArray(); + } + return Block; } - public void Dispose() + public void Deserialize(BinaryReader reader) { - snapshot?.Dispose(); + Reset(0); + if (reader.ReadUInt32() != Version) throw new FormatException(); + if (reader.ReadUInt32() != BlockIndex) throw new InvalidOperationException(); + ViewNumber = reader.ReadByte(); + PrimaryIndex = reader.ReadUInt32(); + Timestamp = reader.ReadUInt32(); + Nonce = reader.ReadUInt64(); + NextConsensus = reader.ReadSerializable(); + if (NextConsensus.Equals(UInt160.Zero)) + NextConsensus = null; + TransactionHashes = reader.ReadSerializableArray(); + if (TransactionHashes.Length == 0) + TransactionHashes = null; + Transaction[] transactions = new Transaction[reader.ReadVarInt()]; + if (transactions.Length == 0) + { + Transactions = null; + } + else + { + for (int i = 0; i < transactions.Length; i++) + transactions[i] = Transaction.DeserializeFrom(reader); + Transactions = transactions.ToDictionary(p => p.Hash); + } + PreparationPayloads = new ConsensusPayload[reader.ReadVarInt()]; + for (int i = 0; i < PreparationPayloads.Length; i++) + PreparationPayloads[i] = reader.ReadBoolean() ? reader.ReadSerializable() : null; + CommitPayloads = new ConsensusPayload[reader.ReadVarInt()]; + for (int i = 0; i < CommitPayloads.Length; i++) + CommitPayloads[i] = reader.ReadBoolean() ? reader.ReadSerializable() : null; + ChangeViewPayloads = new ConsensusPayload[reader.ReadVarInt()]; + for (int i = 0; i < ChangeViewPayloads.Length; i++) + ChangeViewPayloads[i] = reader.ReadBoolean() ? reader.ReadSerializable() : null; + LastChangeViewPayloads = new ConsensusPayload[reader.ReadVarInt()]; + for (int i = 0; i < LastChangeViewPayloads.Length; i++) + LastChangeViewPayloads[i] = reader.ReadBoolean() ? reader.ReadSerializable() : null; } - public uint GetPrimaryIndex(byte view_number) + public void Dispose() { - int p = ((int)BlockIndex - view_number) % Validators.Length; - return p >= 0 ? (uint)p : (uint)(p + Validators.Length); + Snapshot?.Dispose(); } - public ConsensusPayload MakeChangeView() + public ConsensusPayload MakeChangeView(byte newViewNumber) { - return MakeSignedPayload(new ChangeView + return ChangeViewPayloads[MyIndex] = MakeSignedPayload(new ChangeView { - NewViewNumber = ExpectedView[MyIndex] + NewViewNumber = newViewNumber, + Timestamp = TimeProvider.Current.UtcNow.ToTimestamp() }); } + public ConsensusPayload MakeCommit() + { + if (CommitPayloads[MyIndex] == null) + CommitPayloads[MyIndex] = MakeSignedPayload(new Commit + { + Signature = MakeHeader()?.Sign(keyPair) + }); + return CommitPayloads[MyIndex]; + } + private Block _header = null; public Block MakeHeader() { @@ -125,18 +158,12 @@ private ConsensusPayload MakeSignedPayload(ConsensusMessage message) PrevHash = PrevHash, BlockIndex = BlockIndex, ValidatorIndex = (ushort)MyIndex, - Timestamp = Timestamp, - Data = message.ToArray() + ConsensusMessage = message }; SignPayload(payload); return payload; } - public void SignHeader() - { - Signatures[MyIndex] = MakeHeader()?.Sign(keyPair); - } - private void SignPayload(ConsensusPayload payload) { ContractParametersContext sc; @@ -154,63 +181,149 @@ private void SignPayload(ConsensusPayload payload) public ConsensusPayload MakePrepareRequest() { - return MakeSignedPayload(new PrepareRequest + Fill(); + return PreparationPayloads[MyIndex] = MakeSignedPayload(new PrepareRequest { + Timestamp = Timestamp, Nonce = Nonce, NextConsensus = NextConsensus, TransactionHashes = TransactionHashes, - MinerTransaction = (MinerTransaction)Transactions[TransactionHashes[0]], - Signature = Signatures[MyIndex] + MinerTransaction = (MinerTransaction)Transactions[TransactionHashes[0]] }); } - public ConsensusPayload MakePrepareResponse(byte[] signature) + public ConsensusPayload MakeRecoveryMessage() { - return MakeSignedPayload(new PrepareResponse + PrepareRequest prepareRequestMessage = null; + if (TransactionHashes != null) { - Signature = signature + prepareRequestMessage = new PrepareRequest + { + ViewNumber = ViewNumber, + TransactionHashes = TransactionHashes, + Nonce = Nonce, + NextConsensus = NextConsensus, + MinerTransaction = (MinerTransaction)Transactions[TransactionHashes[0]], + Timestamp = Timestamp + }; + } + return MakeSignedPayload(new RecoveryMessage() + { + ChangeViewMessages = LastChangeViewPayloads.Where(p => p != null).Select(p => RecoveryMessage.ChangeViewPayloadCompact.FromPayload(p)).Take(this.M()).ToDictionary(p => (int)p.ValidatorIndex), + PrepareRequestMessage = prepareRequestMessage, + // We only need a PreparationHash set if we don't have the PrepareRequest information. + PreparationHash = TransactionHashes == null ? PreparationPayloads.Where(p => p != null).GroupBy(p => p.GetDeserializedMessage().PreparationHash, (k, g) => new { Hash = k, Count = g.Count() }).OrderByDescending(p => p.Count).Select(p => p.Hash).FirstOrDefault() : null, + PreparationMessages = PreparationPayloads.Where(p => p != null).Select(p => RecoveryMessage.PreparationPayloadCompact.FromPayload(p)).ToDictionary(p => (int)p.ValidatorIndex), + CommitMessages = this.CommitSent() + ? CommitPayloads.Where(p => p != null).Select(p => RecoveryMessage.CommitPayloadCompact.FromPayload(p)).ToDictionary(p => (int)p.ValidatorIndex) + : new Dictionary() }); } - public void Reset() + public ConsensusPayload MakePrepareResponse() { - snapshot?.Dispose(); - snapshot = Blockchain.Singleton.GetSnapshot(); - State = ConsensusState.Initial; - PrevHash = snapshot.CurrentBlockHash; - BlockIndex = snapshot.Height + 1; - ViewNumber = 0; - Validators = snapshot.GetValidators(); - MyIndex = -1; - PrimaryIndex = BlockIndex % (uint)Validators.Length; - TransactionHashes = null; - Signatures = new byte[Validators.Length][]; - ExpectedView = new byte[Validators.Length]; - keyPair = null; - for (int i = 0; i < Validators.Length; i++) + return PreparationPayloads[MyIndex] = MakeSignedPayload(new PrepareResponse + { + PreparationHash = PreparationPayloads[PrimaryIndex].Hash + }); + } + + public void Reset(byte viewNumber) + { + if (viewNumber == 0) { - WalletAccount account = wallet.GetAccount(Validators[i]); - if (account?.HasKey == true) + Block = null; + Snapshot?.Dispose(); + Snapshot = Blockchain.Singleton.GetSnapshot(); + PrevHash = Snapshot.CurrentBlockHash; + BlockIndex = Snapshot.Height + 1; + Validators = Snapshot.GetValidators(); + MyIndex = -1; + ChangeViewPayloads = new ConsensusPayload[Validators.Length]; + LastChangeViewPayloads = new ConsensusPayload[Validators.Length]; + keyPair = null; + for (int i = 0; i < Validators.Length; i++) { + WalletAccount account = wallet?.GetAccount(Validators[i]); + if (account?.HasKey != true) continue; MyIndex = i; keyPair = account.GetKey(); break; } } + else + { + for (int i = 0; i < LastChangeViewPayloads.Length; i++) + if (ChangeViewPayloads[i]?.GetDeserializedMessage().NewViewNumber == viewNumber) + LastChangeViewPayloads[i] = ChangeViewPayloads[i]; + else + LastChangeViewPayloads[i] = null; + } + ViewNumber = viewNumber; + PrimaryIndex = this.GetPrimaryIndex(viewNumber); + Timestamp = 0; + TransactionHashes = null; + PreparationPayloads = new ConsensusPayload[Validators.Length]; + CommitPayloads = new ConsensusPayload[Validators.Length]; _header = null; } - public void Fill() + public void Serialize(BinaryWriter writer) { - IEnumerable mem_pool = Blockchain.Singleton.MemPool.GetSortedVerifiedTransactions(); + writer.Write(Version); + writer.Write(BlockIndex); + writer.Write(ViewNumber); + writer.Write(PrimaryIndex); + writer.Write(Timestamp); + writer.Write(Nonce); + writer.Write(NextConsensus ?? UInt160.Zero); + writer.Write(TransactionHashes ?? new UInt256[0]); + writer.Write(Transactions?.Values.ToArray() ?? new Transaction[0]); + writer.WriteVarInt(PreparationPayloads.Length); + foreach (var payload in PreparationPayloads) + { + bool hasPayload = !(payload is null); + writer.Write(hasPayload); + if (!hasPayload) continue; + writer.Write(payload); + } + writer.WriteVarInt(CommitPayloads.Length); + foreach (var payload in CommitPayloads) + { + bool hasPayload = !(payload is null); + writer.Write(hasPayload); + if (!hasPayload) continue; + writer.Write(payload); + } + writer.WriteVarInt(ChangeViewPayloads.Length); + foreach (var payload in ChangeViewPayloads) + { + bool hasPayload = !(payload is null); + writer.Write(hasPayload); + if (!hasPayload) continue; + writer.Write(payload); + } + writer.WriteVarInt(LastChangeViewPayloads.Length); + foreach (var payload in LastChangeViewPayloads) + { + bool hasPayload = !(payload is null); + writer.Write(hasPayload); + if (!hasPayload) continue; + writer.Write(payload); + } + } + + private void Fill() + { + IEnumerable memoryPoolTransactions = Blockchain.Singleton.MemPool.GetSortedVerifiedTransactions(); foreach (IPolicyPlugin plugin in Plugin.Policies) - mem_pool = plugin.FilterForBlock(mem_pool); - List transactions = mem_pool.ToList(); - Fixed8 amount_netfee = Block.CalculateNetFee(transactions); - TransactionOutput[] outputs = amount_netfee == Fixed8.Zero ? new TransactionOutput[0] : new[] { new TransactionOutput + memoryPoolTransactions = plugin.FilterForBlock(memoryPoolTransactions); + List transactions = memoryPoolTransactions.ToList(); + Fixed8 amountNetFee = Block.CalculateNetFee(transactions); + TransactionOutput[] outputs = amountNetFee == Fixed8.Zero ? new TransactionOutput[0] : new[] { new TransactionOutput { AssetId = Blockchain.UtilityToken.Hash, - Value = amount_netfee, + Value = amountNetFee, ScriptHash = wallet.GetChangeAddress() } }; while (true) @@ -224,7 +337,7 @@ public void Fill() Outputs = outputs, Witnesses = new Witness[0] }; - if (!snapshot.ContainsTransaction(tx.Hash)) + if (!Snapshot.ContainsTransaction(tx.Hash)) { Nonce = nonce; transactions.Insert(0, tx); @@ -233,8 +346,8 @@ public void Fill() } TransactionHashes = transactions.Select(p => p.Hash).ToArray(); Transactions = transactions.ToDictionary(p => p.Hash); - NextConsensus = Blockchain.GetConsensusAddress(snapshot.GetValidators(transactions).ToArray()); - Timestamp = Math.Max(TimeProvider.Current.UtcNow.ToTimestamp(), PrevHeader.Timestamp + 1); + NextConsensus = Blockchain.GetConsensusAddress(Snapshot.GetValidators(transactions).ToArray()); + Timestamp = Math.Max(TimeProvider.Current.UtcNow.ToTimestamp(), this.PrevHeader().Timestamp + 1); } private static ulong GetNonce() @@ -244,17 +357,5 @@ private static ulong GetNonce() rand.NextBytes(nonce); return nonce.ToUInt64(0); } - - public bool VerifyRequest() - { - if (!State.HasFlag(ConsensusState.RequestReceived)) - return false; - if (!Blockchain.GetConsensusAddress(snapshot.GetValidators(Transactions.Values).ToArray()).Equals(NextConsensus)) - return false; - Transaction tx_gen = Transactions.Values.FirstOrDefault(p => p.Type == TransactionType.MinerTransaction); - Fixed8 amount_netfee = Block.CalculateNetFee(Transactions.Values); - if (tx_gen?.Outputs.Sum(p => p.Value) != amount_netfee) return false; - return true; - } } } diff --git a/neo/Consensus/ConsensusMessageType.cs b/neo/Consensus/ConsensusMessageType.cs index b57dbc321..4fee3acdb 100644 --- a/neo/Consensus/ConsensusMessageType.cs +++ b/neo/Consensus/ConsensusMessageType.cs @@ -6,9 +6,15 @@ internal enum ConsensusMessageType : byte { [ReflectionCache(typeof(ChangeView))] ChangeView = 0x00, + [ReflectionCache(typeof(PrepareRequest))] PrepareRequest = 0x20, [ReflectionCache(typeof(PrepareResponse))] PrepareResponse = 0x21, + [ReflectionCache(typeof(Commit))] + Commit = 0x30, + + [ReflectionCache(typeof(RecoveryMessage))] + RecoveryMessage = 0x41, } } diff --git a/neo/Consensus/ConsensusService.cs b/neo/Consensus/ConsensusService.cs index 6cd80b38f..a8e23ec1b 100644 --- a/neo/Consensus/ConsensusService.cs +++ b/neo/Consensus/ConsensusService.cs @@ -6,6 +6,7 @@ using Neo.Ledger; using Neo.Network.P2P; using Neo.Network.P2P.Payloads; +using Neo.Persistence; using Neo.Plugins; using Neo.Wallets; using System; @@ -16,32 +17,40 @@ namespace Neo.Consensus { public sealed class ConsensusService : UntypedActor { - public class Start { } + public class Start { public bool IgnoreRecoveryLogs; } public class SetViewNumber { public byte ViewNumber; } internal class Timer { public uint Height; public byte ViewNumber; } private readonly IConsensusContext context; private readonly IActorRef localNode; private readonly IActorRef taskManager; + private readonly Store store; private ICancelable timer_token; private DateTime block_received_time; + private bool started = false; + /// + /// This will be cleared every block (so it will not grow out of control, but is used to prevent repeatedly + /// responding to the same message. + /// + private readonly HashSet knownHashes = new HashSet(); + private bool isRecovering = false; - public ConsensusService(IActorRef localNode, IActorRef taskManager, Wallet wallet) - : this(localNode, taskManager, new ConsensusContext(wallet)) + public ConsensusService(IActorRef localNode, IActorRef taskManager, Store store, Wallet wallet) + : this(localNode, taskManager, store, new ConsensusContext(wallet)) { } - public ConsensusService(IActorRef localNode, IActorRef taskManager, IConsensusContext context) + public ConsensusService(IActorRef localNode, IActorRef taskManager, Store store, IConsensusContext context) { this.localNode = localNode; this.taskManager = taskManager; + this.store = store; this.context = context; } - private bool AddTransaction(Transaction tx, bool verify) { - if (verify && !context.VerifyTransaction(tx)) + if (verify && !tx.Verify(context.Snapshot, context.Transactions.Values)) { Log($"Invalid transaction: {tx.Hash}{Environment.NewLine}{tx.ToArray().ToHexString()}", LogLevel.Warning); RequestChangeView(); @@ -56,13 +65,15 @@ private bool AddTransaction(Transaction tx, bool verify) context.Transactions[tx.Hash] = tx; if (context.TransactionHashes.Length == context.Transactions.Count) { - if (context.VerifyRequest()) + if (VerifyRequest()) { + // if we are the primary for this view, but acting as a backup because we recovered our own + // previously sent prepare request, then we don't want to send a prepare response. + if (context.MyIndex == context.PrimaryIndex) return true; + Log($"send prepare response"); - context.State |= ConsensusState.SignatureSent; - context.SignHeader(); - localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakePrepareResponse(context.Signatures[context.MyIndex]) }); - CheckSignatures(); + localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakePrepareResponse() }); + CheckPreparations(); } else { @@ -83,49 +94,76 @@ private void ChangeTimer(TimeSpan delay) }, ActorRefs.NoSender); } - private void CheckExpectedView(byte view_number) + private void CheckCommits() { - if (context.ViewNumber == view_number) return; - if (context.ExpectedView.Count(p => p == view_number) >= context.M) + if (context.CommitPayloads.Count(p => p != null) >= context.M() && context.TransactionHashes.All(p => context.Transactions.ContainsKey(p))) { - InitializeConsensus(view_number); + Block block = context.CreateBlock(); + Log($"relay block: {block.Hash}"); + localNode.Tell(new LocalNode.Relay { Inventory = block }); } } - private void CheckSignatures() + private void CheckExpectedView(byte viewNumber) { - if (context.Signatures.Count(p => p != null) >= context.M && context.TransactionHashes.All(p => context.Transactions.ContainsKey(p))) + if (context.ViewNumber == viewNumber) return; + if (context.ChangeViewPayloads.Count(p => p != null && p.GetDeserializedMessage().NewViewNumber == viewNumber) >= context.M()) { - Block block = context.CreateBlock(); - Log($"relay block: {block.Hash}"); - localNode.Tell(new LocalNode.Relay { Inventory = block }); - context.State |= ConsensusState.BlockSent; + ChangeView message = context.ChangeViewPayloads[context.MyIndex]?.GetDeserializedMessage(); + if (message is null || message.NewViewNumber < viewNumber) + localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakeChangeView(viewNumber) }); + InitializeConsensus(viewNumber); } } - private void InitializeConsensus(byte view_number) + private void CheckPreparations() { - if (view_number == 0) - context.Reset(); - else - context.ChangeView(view_number); + if (context.PreparationPayloads.Count(p => p != null) >= context.M() && context.TransactionHashes.All(p => context.Transactions.ContainsKey(p))) + { + ConsensusPayload payload = context.MakeCommit(); + Log($"send commit"); + context.Save(store); + localNode.Tell(new LocalNode.SendDirectly { Inventory = payload }); + // Set timer, so we will resend the commit in case of a networking issue + ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock)); + CheckCommits(); + } + } + + private byte GetLastExpectedView(int validatorIndex) + { + var lastPreparationPayload = context.PreparationPayloads[validatorIndex]; + if (lastPreparationPayload != null) + return lastPreparationPayload.GetDeserializedMessage().ViewNumber; + + return context.ChangeViewPayloads[validatorIndex]?.GetDeserializedMessage().NewViewNumber ?? (byte)0; + } + + private void InitializeConsensus(byte viewNumber) + { + context.Reset(viewNumber); if (context.MyIndex < 0) return; - if (view_number > 0) - Log($"changeview: view={view_number} primary={context.Validators[context.GetPrimaryIndex((byte)(view_number - 1u))]}", LogLevel.Warning); - Log($"initialize: height={context.BlockIndex} view={view_number} index={context.MyIndex} role={(context.MyIndex == context.PrimaryIndex ? ConsensusState.Primary : ConsensusState.Backup)}"); - if (context.MyIndex == context.PrimaryIndex) + if (viewNumber > 0) + Log($"changeview: view={viewNumber} primary={context.Validators[context.GetPrimaryIndex((byte)(viewNumber - 1u))]}", LogLevel.Warning); + Log($"initialize: height={context.BlockIndex} view={viewNumber} index={context.MyIndex} role={(context.IsPrimary() ? "Primary" : "Backup")}"); + if (context.IsPrimary()) { - context.State |= ConsensusState.Primary; - TimeSpan span = TimeProvider.Current.UtcNow - block_received_time; - if (span >= Blockchain.TimePerBlock) - ChangeTimer(TimeSpan.Zero); + if (isRecovering) + { + ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (viewNumber + 1))); + } else - ChangeTimer(Blockchain.TimePerBlock - span); + { + TimeSpan span = TimeProvider.Current.UtcNow - block_received_time; + if (span >= Blockchain.TimePerBlock) + ChangeTimer(TimeSpan.Zero); + else + ChangeTimer(Blockchain.TimePerBlock - span); + } } else { - context.State = ConsensusState.Backup; - ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (view_number + 1))); + ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (viewNumber + 1))); } } @@ -136,19 +174,63 @@ private void Log(string message, LogLevel level = LogLevel.Info) private void OnChangeViewReceived(ConsensusPayload payload, ChangeView message) { - if (message.NewViewNumber <= context.ExpectedView[payload.ValidatorIndex]) + // We keep track of the payload hashes received in this block, and don't respond with recovery + // in response to the same payload that we already responded to previously. + // ChangeView messages include a Timestamp when the change view is sent, thus if a node restarts + // and issues a change view for the same view, it will have a different hash and will correctly respond + // again; however replay attacks of the ChangeView message from arbitrary nodes will not trigger an + // additonal recovery message response. + if (!knownHashes.Add(payload.Hash)) return; + if (message.NewViewNumber <= context.ViewNumber) + { + bool shouldSendRecovery = false; + // Limit recovery to sending from `f` nodes when the request is from a lower view number. + int allowedRecoveryNodeCount = context.F(); + for (int i = 0; i < allowedRecoveryNodeCount; i++) + { + var eligibleResponders = context.Validators.Length - 1; + var chosenIndex = (payload.ValidatorIndex + i + message.NewViewNumber) % eligibleResponders; + if (chosenIndex >= payload.ValidatorIndex) chosenIndex++; + if (chosenIndex != context.MyIndex) continue; + shouldSendRecovery = true; + break; + } + + if (!shouldSendRecovery) return; + + Log($"send recovery from view: {message.ViewNumber} to view: {context.ViewNumber}"); + localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakeRecoveryMessage() }); + } + + var expectedView = GetLastExpectedView(payload.ValidatorIndex); + if (message.NewViewNumber <= expectedView) return; + Log($"{nameof(OnChangeViewReceived)}: height={payload.BlockIndex} view={message.ViewNumber} index={payload.ValidatorIndex} nv={message.NewViewNumber}"); - context.ExpectedView[payload.ValidatorIndex] = message.NewViewNumber; + context.ChangeViewPayloads[payload.ValidatorIndex] = payload; CheckExpectedView(message.NewViewNumber); } + private void OnCommitReceived(ConsensusPayload payload, Commit commit) + { + if (context.CommitPayloads[payload.ValidatorIndex] != null) return; + Log($"{nameof(OnCommitReceived)}: height={payload.BlockIndex} view={commit.ViewNumber} index={payload.ValidatorIndex}"); + byte[] hashData = context.MakeHeader()?.GetHashData(); + if (hashData == null) + { + context.CommitPayloads[payload.ValidatorIndex] = payload; + } + else if (Crypto.Default.VerifySignature(hashData, commit.Signature, context.Validators[payload.ValidatorIndex].EncodePoint(false))) + { + context.CommitPayloads[payload.ValidatorIndex] = payload; + CheckCommits(); + } + } + private void OnConsensusPayload(ConsensusPayload payload) { - if (context.State.HasFlag(ConsensusState.BlockSent)) return; - if (payload.ValidatorIndex == context.MyIndex) return; - if (payload.Version != ConsensusContext.Version) - return; + if (context.BlockSent()) return; + if (payload.Version != ConsensusContext.Version) return; if (payload.PrevHash != context.PrevHash || payload.BlockIndex != context.BlockIndex) { if (context.BlockIndex < payload.BlockIndex) @@ -158,27 +240,26 @@ private void OnConsensusPayload(ConsensusPayload payload) return; } if (payload.ValidatorIndex >= context.Validators.Length) return; - ConsensusMessage message; - try - { - message = ConsensusMessage.DeserializeFrom(payload.Data); - } - catch - { + ConsensusMessage message = payload.ConsensusMessage; + if (message.ViewNumber != context.ViewNumber && message.Type != ConsensusMessageType.ChangeView && + message.Type != ConsensusMessageType.RecoveryMessage) return; - } - if (message.ViewNumber != context.ViewNumber && message.Type != ConsensusMessageType.ChangeView) - return; - switch (message.Type) + switch (message) { - case ConsensusMessageType.ChangeView: - OnChangeViewReceived(payload, (ChangeView)message); + case ChangeView view: + OnChangeViewReceived(payload, view); + break; + case PrepareRequest request: + OnPrepareRequestReceived(payload, request); break; - case ConsensusMessageType.PrepareRequest: - OnPrepareRequestReceived(payload, (PrepareRequest)message); + case PrepareResponse response: + OnPrepareResponseReceived(payload, response); break; - case ConsensusMessageType.PrepareResponse: - OnPrepareResponseReceived(payload, (PrepareResponse)message); + case Commit commit: + OnCommitReceived(payload, commit); + break; + case RecoveryMessage recovery: + OnRecoveryMessageReceived(payload, recovery); break; } } @@ -187,38 +268,79 @@ private void OnPersistCompleted(Block block) { Log($"persist block: {block.Hash}"); block_received_time = TimeProvider.Current.UtcNow; + knownHashes.Clear(); InitializeConsensus(0); } + private void OnRecoveryMessageReceived(ConsensusPayload payload, RecoveryMessage message) + { + if (message.ViewNumber < context.ViewNumber) return; + Log($"{nameof(OnRecoveryMessageReceived)}: height={payload.BlockIndex} view={message.ViewNumber} index={payload.ValidatorIndex}"); + isRecovering = true; + try + { + if (message.ViewNumber > context.ViewNumber) + { + if (context.CommitSent()) return; + ConsensusPayload[] changeViewPayloads = message.GetChangeViewPayloads(context, payload); + foreach (ConsensusPayload changeViewPayload in changeViewPayloads) + ReverifyAndProcessPayload(changeViewPayload); + } + if (message.ViewNumber != context.ViewNumber) return; + if (!context.CommitSent()) + { + if (!context.RequestSentOrReceived()) + { + ConsensusPayload prepareRequestPayload = message.GetPrepareRequestPayload(context, payload); + if (prepareRequestPayload != null) + ReverifyAndProcessPayload(prepareRequestPayload); + else if (context.IsPrimary()) + SendPrepareRequest(); + } + ConsensusPayload[] prepareResponsePayloads = message.GetPrepareResponsePayloads(context, payload); + foreach (ConsensusPayload prepareResponsePayload in prepareResponsePayloads) + ReverifyAndProcessPayload(prepareResponsePayload); + } + ConsensusPayload[] commitPayloads = message.GetCommitPayloadsFromRecoveryMessage(context, payload); + foreach (ConsensusPayload commitPayload in commitPayloads) + ReverifyAndProcessPayload(commitPayload); + } + finally + { + isRecovering = false; + } + } + private void OnPrepareRequestReceived(ConsensusPayload payload, PrepareRequest message) { - if (context.State.HasFlag(ConsensusState.RequestReceived)) return; + if (context.RequestSentOrReceived()) return; if (payload.ValidatorIndex != context.PrimaryIndex) return; Log($"{nameof(OnPrepareRequestReceived)}: height={payload.BlockIndex} view={message.ViewNumber} index={payload.ValidatorIndex} tx={message.TransactionHashes.Length}"); - if (!context.State.HasFlag(ConsensusState.Backup)) return; - if (payload.Timestamp <= context.PrevHeader.Timestamp || payload.Timestamp > TimeProvider.Current.UtcNow.AddMinutes(10).ToTimestamp()) + if (message.Timestamp <= context.PrevHeader().Timestamp || message.Timestamp > TimeProvider.Current.UtcNow.AddMinutes(10).ToTimestamp()) { - Log($"Timestamp incorrect: {payload.Timestamp}", LogLevel.Warning); + Log($"Timestamp incorrect: {message.Timestamp}", LogLevel.Warning); return; } - if (message.TransactionHashes.Any(p => context.TransactionExists(p))) + if (message.TransactionHashes.Any(p => context.Snapshot.ContainsTransaction(p))) { Log($"Invalid request: transaction already exists", LogLevel.Warning); return; } - context.State |= ConsensusState.RequestReceived; - context.Timestamp = payload.Timestamp; + context.Timestamp = message.Timestamp; context.Nonce = message.Nonce; context.NextConsensus = message.NextConsensus; context.TransactionHashes = message.TransactionHashes; context.Transactions = new Dictionary(); + for (int i = 0; i < context.PreparationPayloads.Length; i++) + if (context.PreparationPayloads[i] != null) + if (!context.PreparationPayloads[i].GetDeserializedMessage().PreparationHash.Equals(payload.Hash)) + context.PreparationPayloads[i] = null; + context.PreparationPayloads[payload.ValidatorIndex] = payload; byte[] hashData = context.MakeHeader().GetHashData(); - if (!Crypto.Default.VerifySignature(hashData, message.Signature, context.Validators[payload.ValidatorIndex].EncodePoint(false))) return; - for (int i = 0; i < context.Signatures.Length; i++) - if (context.Signatures[i] != null) - if (!Crypto.Default.VerifySignature(hashData, context.Signatures[i], context.Validators[i].EncodePoint(false))) - context.Signatures[i] = null; - context.Signatures[payload.ValidatorIndex] = message.Signature; + for (int i = 0; i < context.CommitPayloads.Length; i++) + if (context.CommitPayloads[i] != null) + if (!Crypto.Default.VerifySignature(hashData, context.CommitPayloads[i].GetDeserializedMessage().Signature, context.Validators[i].EncodePoint(false))) + context.CommitPayloads[i] = null; Dictionary mempoolVerified = Blockchain.Singleton.MemPool.GetVerifiedTransactions().ToDictionary(p => p.Hash); List unverified = new List(); @@ -231,7 +353,6 @@ private void OnPrepareRequestReceived(ConsensusPayload payload, PrepareRequest m } else { - if (Blockchain.Singleton.MemPool.TryGetValue(hash, out tx)) unverified.Add(tx); } @@ -252,84 +373,106 @@ private void OnPrepareRequestReceived(ConsensusPayload payload, PrepareRequest m private void OnPrepareResponseReceived(ConsensusPayload payload, PrepareResponse message) { - if (context.Signatures[payload.ValidatorIndex] != null) return; + if (context.PreparationPayloads[payload.ValidatorIndex] != null) return; + if (context.PreparationPayloads[context.PrimaryIndex] != null && !message.PreparationHash.Equals(context.PreparationPayloads[context.PrimaryIndex].Hash)) + return; Log($"{nameof(OnPrepareResponseReceived)}: height={payload.BlockIndex} view={message.ViewNumber} index={payload.ValidatorIndex}"); - byte[] hashData = context.MakeHeader()?.GetHashData(); - if (hashData == null) - { - context.Signatures[payload.ValidatorIndex] = message.Signature; - } - else if (Crypto.Default.VerifySignature(hashData, message.Signature, context.Validators[payload.ValidatorIndex].EncodePoint(false))) - { - context.Signatures[payload.ValidatorIndex] = message.Signature; - CheckSignatures(); - } + context.PreparationPayloads[payload.ValidatorIndex] = payload; + if (context.CommitSent()) return; + if (context.RequestSentOrReceived()) + CheckPreparations(); } protected override void OnReceive(object message) { - switch (message) + if (message is Start options) { - case Start _: - OnStart(); - break; - case SetViewNumber setView: - InitializeConsensus(setView.ViewNumber); - break; - case Timer timer: - OnTimer(timer); - break; - case ConsensusPayload payload: - OnConsensusPayload(payload); - break; - case Transaction transaction: - OnTransaction(transaction); - break; - case Blockchain.PersistCompleted completed: - OnPersistCompleted(completed.Block); - break; + if (started) return; + OnStart(options); + } + else + { + if (!started) return; + switch (message) + { + case SetViewNumber setView: + InitializeConsensus(setView.ViewNumber); + break; + case Timer timer: + OnTimer(timer); + break; + case ConsensusPayload payload: + OnConsensusPayload(payload); + break; + case Transaction transaction: + OnTransaction(transaction); + break; + case Blockchain.PersistCompleted completed: + OnPersistCompleted(completed.Block); + break; + } } } - private void OnStart() + private void OnStart(Start options) { Log("OnStart"); + started = true; + if (!options.IgnoreRecoveryLogs && context.Load(store)) + { + if (context.Transactions != null) + { + Sender.Ask(new Blockchain.FillMemoryPool + { + Transactions = context.Transactions.Values + }).Wait(); + } + if (context.CommitSent()) + { + CheckPreparations(); + return; + } + } InitializeConsensus(0); + // Issue a ChangeView with NewViewNumber of 0 to request recovery messages on start-up. + if (context.BlockIndex == Blockchain.Singleton.HeaderHeight + 1) + localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakeChangeView(0) }); } private void OnTimer(Timer timer) { - if (context.State.HasFlag(ConsensusState.BlockSent)) return; + if (context.BlockSent()) return; if (timer.Height != context.BlockIndex || timer.ViewNumber != context.ViewNumber) return; - Log($"timeout: height={timer.Height} view={timer.ViewNumber} state={context.State}"); - if (context.State.HasFlag(ConsensusState.Primary) && !context.State.HasFlag(ConsensusState.RequestSent)) + Log($"timeout: height={timer.Height} view={timer.ViewNumber}"); + if (context.IsPrimary() && !context.RequestSentOrReceived()) + { + SendPrepareRequest(); + } + else if ((context.IsPrimary() && context.RequestSentOrReceived()) || context.IsBackup()) { - Log($"send prepare request: height={timer.Height} view={timer.ViewNumber}"); - context.State |= ConsensusState.RequestSent; - if (!context.State.HasFlag(ConsensusState.SignatureSent)) + if (context.CommitSent()) { - context.Fill(); - context.SignHeader(); + // Re-send commit periodically by sending recover message in case of a network issue. + Log($"send recovery to resend commit"); + localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakeRecoveryMessage() }); + ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << 1)); } - localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakePrepareRequest() }); - if (context.TransactionHashes.Length > 1) + else { - foreach (InvPayload payload in InvPayload.CreateGroup(InventoryType.TX, context.TransactionHashes.Skip(1).ToArray())) - localNode.Tell(Message.Create("inv", payload)); + RequestChangeView(); } - ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (timer.ViewNumber + 1))); - } - else if ((context.State.HasFlag(ConsensusState.Primary) && context.State.HasFlag(ConsensusState.RequestSent)) || context.State.HasFlag(ConsensusState.Backup)) - { - RequestChangeView(); } } private void OnTransaction(Transaction transaction) { if (transaction.Type == TransactionType.MinerTransaction) return; - if (!context.State.HasFlag(ConsensusState.Backup) || !context.State.HasFlag(ConsensusState.RequestReceived) || context.State.HasFlag(ConsensusState.SignatureSent) || context.State.HasFlag(ConsensusState.ViewChanging) || context.State.HasFlag(ConsensusState.BlockSent)) + if (!context.IsBackup() || !context.RequestSentOrReceived() || context.ResponseSent() || context.BlockSent()) return; + // If we are changing view but we already have enough preparation payloads to commit in the current view, + // we must keep on accepting transactions in the current view to be able to create the block. + if (context.ViewChanging() && + context.PreparationPayloads.Count(p => p != null) < context.M()) return; if (context.Transactions.ContainsKey(transaction.Hash)) return; if (!context.TransactionHashes.Contains(transaction.Hash)) return; AddTransaction(transaction, true); @@ -338,23 +481,54 @@ private void OnTransaction(Transaction transaction) protected override void PostStop() { Log("OnStop"); + started = false; context.Dispose(); base.PostStop(); } - public static Props Props(IActorRef localNode, IActorRef taskManager, Wallet wallet) + public static Props Props(IActorRef localNode, IActorRef taskManager, Store store, Wallet wallet) { - return Akka.Actor.Props.Create(() => new ConsensusService(localNode, taskManager, wallet)).WithMailbox("consensus-service-mailbox"); + return Akka.Actor.Props.Create(() => new ConsensusService(localNode, taskManager, store, wallet)).WithMailbox("consensus-service-mailbox"); } private void RequestChangeView() { - context.State |= ConsensusState.ViewChanging; - context.ExpectedView[context.MyIndex]++; - Log($"request change view: height={context.BlockIndex} view={context.ViewNumber} nv={context.ExpectedView[context.MyIndex]} state={context.State}"); - ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (context.ExpectedView[context.MyIndex] + 1))); - localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakeChangeView() }); - CheckExpectedView(context.ExpectedView[context.MyIndex]); + byte expectedView = context.ChangeViewPayloads[context.MyIndex]?.GetDeserializedMessage().NewViewNumber ?? 0; + if (expectedView < context.ViewNumber) expectedView = context.ViewNumber; + expectedView++; + Log($"request change view: height={context.BlockIndex} view={context.ViewNumber} nv={expectedView}"); + ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (expectedView + 1))); + localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakeChangeView(expectedView) }); + CheckExpectedView(expectedView); + } + + private void ReverifyAndProcessPayload(ConsensusPayload payload) + { + if (!payload.Verify(context.Snapshot)) return; + OnConsensusPayload(payload); + } + + private void SendPrepareRequest() + { + Log($"send prepare request: height={context.BlockIndex} view={context.ViewNumber}"); + localNode.Tell(new LocalNode.SendDirectly { Inventory = context.MakePrepareRequest() }); + + if (context.TransactionHashes.Length > 1) + { + foreach (InvPayload payload in InvPayload.CreateGroup(InventoryType.TX, context.TransactionHashes.Skip(1).ToArray())) + localNode.Tell(Message.Create("inv", payload)); + } + ChangeTimer(TimeSpan.FromSeconds(Blockchain.SecondsPerBlock << (context.ViewNumber + 1))); + } + + private bool VerifyRequest() + { + if (!Blockchain.GetConsensusAddress(context.Snapshot.GetValidators(context.Transactions.Values).ToArray()).Equals(context.NextConsensus)) + return false; + Transaction minerTx = context.Transactions.Values.FirstOrDefault(p => p.Type == TransactionType.MinerTransaction); + Fixed8 amountNetFee = Block.CalculateNetFee(context.Transactions.Values); + if (minerTx?.Outputs.Sum(p => p.Value) != amountNetFee) return false; + return true; } } diff --git a/neo/Consensus/ConsensusState.cs b/neo/Consensus/ConsensusState.cs deleted file mode 100644 index 7d3d2aa3c..000000000 --- a/neo/Consensus/ConsensusState.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Neo.Consensus -{ - [Flags] - public enum ConsensusState : byte - { - Initial = 0x00, - Primary = 0x01, - Backup = 0x02, - RequestSent = 0x04, - RequestReceived = 0x08, - SignatureSent = 0x10, - BlockSent = 0x20, - ViewChanging = 0x40, - } -} diff --git a/neo/Consensus/Helper.cs b/neo/Consensus/Helper.cs new file mode 100644 index 000000000..363a3e1f9 --- /dev/null +++ b/neo/Consensus/Helper.cs @@ -0,0 +1,68 @@ +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Neo.Consensus +{ + internal static class Helper + { + /// + /// Prefix for saving consensus state. + /// + public const byte CN_Context = 0xf4; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int F(this IConsensusContext context) => (context.Validators.Length - 1) / 3; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int M(this IConsensusContext context) => context.Validators.Length - context.F(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPrimary(this IConsensusContext context) => context.MyIndex == context.PrimaryIndex; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBackup(this IConsensusContext context) => context.MyIndex >= 0 && context.MyIndex != context.PrimaryIndex; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Header PrevHeader(this IConsensusContext context) => context.Snapshot.GetHeader(context.PrevHash); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool RequestSentOrReceived(this IConsensusContext context) => context.PreparationPayloads[context.PrimaryIndex] != null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ResponseSent(this IConsensusContext context) => context.PreparationPayloads[context.MyIndex] != null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CommitSent(this IConsensusContext context) => context.CommitPayloads[context.MyIndex] != null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool BlockSent(this IConsensusContext context) => context.Block != null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ViewChanging(this IConsensusContext context) => context.ChangeViewPayloads[context.MyIndex]?.GetDeserializedMessage().NewViewNumber > context.ViewNumber; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetPrimaryIndex(this IConsensusContext context, byte viewNumber) + { + int p = ((int)context.BlockIndex - viewNumber) % context.Validators.Length; + return p >= 0 ? (uint)p : (uint)(p + context.Validators.Length); + } + + public static void Save(this IConsensusContext context, Store store) + { + store.PutSync(CN_Context, new byte[0], context.ToArray()); + } + + public static bool Load(this IConsensusContext context, Store store) + { + byte[] data = store.Get(CN_Context, new byte[0]); + if (data is null) return false; + using (MemoryStream ms = new MemoryStream(data, false)) + using (BinaryReader reader = new BinaryReader(ms)) + { + try + { + context.Deserialize(reader); + } + catch + { + return false; + } + return true; + } + } + } +} diff --git a/neo/Consensus/IConsensusContext.cs b/neo/Consensus/IConsensusContext.cs index 893b36085..5ce891e71 100644 --- a/neo/Consensus/IConsensusContext.cs +++ b/neo/Consensus/IConsensusContext.cs @@ -1,14 +1,15 @@ using Neo.Cryptography.ECC; +using Neo.IO; using Neo.Network.P2P.Payloads; +using Neo.Persistence; using System; using System.Collections.Generic; namespace Neo.Consensus { - public interface IConsensusContext : IDisposable + public interface IConsensusContext : IDisposable, ISerializable { //public const uint Version = 0; - ConsensusState State { get; set; } UInt256 PrevHash { get; } uint BlockIndex { get; } byte ViewNumber { get; } @@ -20,38 +21,28 @@ public interface IConsensusContext : IDisposable UInt160 NextConsensus { get; set; } UInt256[] TransactionHashes { get; set; } Dictionary Transactions { get; set; } - byte[][] Signatures { get; set; } - byte[] ExpectedView { get; set; } - - int M { get; } - - Header PrevHeader { get; } - - bool TransactionExists(UInt256 hash); - bool VerifyTransaction(Transaction tx); - - void ChangeView(byte view_number); + ConsensusPayload[] PreparationPayloads { get; set; } + ConsensusPayload[] CommitPayloads { get; set; } + ConsensusPayload[] ChangeViewPayloads { get; set; } + Block Block { get; set; } + Snapshot Snapshot { get; } Block CreateBlock(); //void Dispose(); - uint GetPrimaryIndex(byte view_number); + ConsensusPayload MakeChangeView(byte newViewNumber); - ConsensusPayload MakeChangeView(); + ConsensusPayload MakeCommit(); Block MakeHeader(); - void SignHeader(); - ConsensusPayload MakePrepareRequest(); - ConsensusPayload MakePrepareResponse(byte[] signature); - - void Reset(); + ConsensusPayload MakeRecoveryMessage(); - void Fill(); + ConsensusPayload MakePrepareResponse(); - bool VerifyRequest(); + void Reset(byte viewNumber); } } diff --git a/neo/Consensus/PrepareRequest.cs b/neo/Consensus/PrepareRequest.cs index 354205555..7a422da13 100644 --- a/neo/Consensus/PrepareRequest.cs +++ b/neo/Consensus/PrepareRequest.cs @@ -8,13 +8,18 @@ namespace Neo.Consensus { internal class PrepareRequest : ConsensusMessage { + public uint Timestamp; public ulong Nonce; public UInt160 NextConsensus; public UInt256[] TransactionHashes; public MinerTransaction MinerTransaction; - public byte[] Signature; - public override int Size => base.Size + sizeof(ulong) + NextConsensus.Size + TransactionHashes.GetVarSize() + MinerTransaction.Size + Signature.Length; + public override int Size => base.Size + + sizeof(uint) //Timestamp + + sizeof(ulong) //Nonce + + NextConsensus.Size //NextConsensus + + TransactionHashes.GetVarSize() //TransactionHashes + + MinerTransaction.Size; //MinerTransaction public PrepareRequest() : base(ConsensusMessageType.PrepareRequest) @@ -24,25 +29,25 @@ public PrepareRequest() public override void Deserialize(BinaryReader reader) { base.Deserialize(reader); + Timestamp = reader.ReadUInt32(); Nonce = reader.ReadUInt64(); NextConsensus = reader.ReadSerializable(); - TransactionHashes = reader.ReadSerializableArray(); + TransactionHashes = reader.ReadSerializableArray(Block.MaxTransactionsPerBlock); if (TransactionHashes.Distinct().Count() != TransactionHashes.Length) throw new FormatException(); MinerTransaction = reader.ReadSerializable(); if (MinerTransaction.Hash != TransactionHashes[0]) throw new FormatException(); - Signature = reader.ReadBytes(64); } public override void Serialize(BinaryWriter writer) { base.Serialize(writer); + writer.Write(Timestamp); writer.Write(Nonce); writer.Write(NextConsensus); writer.Write(TransactionHashes); writer.Write(MinerTransaction); - writer.Write(Signature); } } } diff --git a/neo/Consensus/PrepareResponse.cs b/neo/Consensus/PrepareResponse.cs index 184e4e7e1..5e94e7aab 100644 --- a/neo/Consensus/PrepareResponse.cs +++ b/neo/Consensus/PrepareResponse.cs @@ -1,12 +1,13 @@ -using System.IO; +using Neo.IO; +using System.IO; namespace Neo.Consensus { internal class PrepareResponse : ConsensusMessage { - public byte[] Signature; + public UInt256 PreparationHash; - public override int Size => base.Size + Signature.Length; + public override int Size => base.Size + PreparationHash.Size; public PrepareResponse() : base(ConsensusMessageType.PrepareResponse) @@ -16,13 +17,13 @@ public PrepareResponse() public override void Deserialize(BinaryReader reader) { base.Deserialize(reader); - Signature = reader.ReadBytes(64); + PreparationHash = reader.ReadSerializable(); } public override void Serialize(BinaryWriter writer) { base.Serialize(writer); - writer.Write(Signature); + writer.Write(PreparationHash); } } } diff --git a/neo/Consensus/RecoveryMessage.ChangeViewPayloadCompact.cs b/neo/Consensus/RecoveryMessage.ChangeViewPayloadCompact.cs new file mode 100644 index 000000000..373e7e75a --- /dev/null +++ b/neo/Consensus/RecoveryMessage.ChangeViewPayloadCompact.cs @@ -0,0 +1,51 @@ +using Neo.IO; +using Neo.Network.P2P.Payloads; +using System.IO; + +namespace Neo.Consensus +{ + partial class RecoveryMessage + { + public class ChangeViewPayloadCompact : ISerializable + { + public ushort ValidatorIndex; + public byte OriginalViewNumber; + public uint Timestamp; + public byte[] InvocationScript; + + int ISerializable.Size => + sizeof(ushort) + //ValidatorIndex + sizeof(byte) + //OriginalViewNumber + sizeof(uint) + //Timestamp + InvocationScript.GetVarSize(); //InvocationScript + + void ISerializable.Deserialize(BinaryReader reader) + { + ValidatorIndex = reader.ReadUInt16(); + OriginalViewNumber = reader.ReadByte(); + Timestamp = reader.ReadUInt32(); + InvocationScript = reader.ReadVarBytes(1024); + } + + public static ChangeViewPayloadCompact FromPayload(ConsensusPayload payload) + { + ChangeView message = payload.GetDeserializedMessage(); + return new ChangeViewPayloadCompact + { + ValidatorIndex = payload.ValidatorIndex, + OriginalViewNumber = message.ViewNumber, + Timestamp = message.Timestamp, + InvocationScript = payload.Witness.InvocationScript + }; + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(ValidatorIndex); + writer.Write(OriginalViewNumber); + writer.Write(Timestamp); + writer.WriteVarBytes(InvocationScript); + } + } + } +} diff --git a/neo/Consensus/RecoveryMessage.CommitPayloadCompact.cs b/neo/Consensus/RecoveryMessage.CommitPayloadCompact.cs new file mode 100644 index 000000000..58dd0e34e --- /dev/null +++ b/neo/Consensus/RecoveryMessage.CommitPayloadCompact.cs @@ -0,0 +1,46 @@ +using Neo.IO; +using Neo.Network.P2P.Payloads; +using System.IO; + +namespace Neo.Consensus +{ + partial class RecoveryMessage + { + public class CommitPayloadCompact : ISerializable + { + public ushort ValidatorIndex; + public byte[] Signature; + public byte[] InvocationScript; + + int ISerializable.Size => + sizeof(ushort) + //ValidatorIndex + Signature.Length + //Signature + InvocationScript.GetVarSize(); //InvocationScript + + void ISerializable.Deserialize(BinaryReader reader) + { + ValidatorIndex = reader.ReadUInt16(); + Signature = reader.ReadBytes(64); + InvocationScript = reader.ReadVarBytes(1024); + } + + public static CommitPayloadCompact FromPayload(ConsensusPayload payload) + { + Commit message = payload.GetDeserializedMessage(); + return new CommitPayloadCompact + { + ValidatorIndex = payload.ValidatorIndex, + Signature = message.Signature, + InvocationScript = payload.Witness.InvocationScript + }; + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(ValidatorIndex); + writer.Write(Signature); + writer.WriteVarBytes(InvocationScript); + } + } + } +} diff --git a/neo/Consensus/RecoveryMessage.PreparationPayloadCompact.cs b/neo/Consensus/RecoveryMessage.PreparationPayloadCompact.cs new file mode 100644 index 000000000..8c0e52f59 --- /dev/null +++ b/neo/Consensus/RecoveryMessage.PreparationPayloadCompact.cs @@ -0,0 +1,40 @@ +using Neo.IO; +using Neo.Network.P2P.Payloads; +using System.IO; + +namespace Neo.Consensus +{ + partial class RecoveryMessage + { + public class PreparationPayloadCompact : ISerializable + { + public ushort ValidatorIndex; + public byte[] InvocationScript; + + int ISerializable.Size => + sizeof(ushort) + //ValidatorIndex + InvocationScript.GetVarSize(); //InvocationScript + + void ISerializable.Deserialize(BinaryReader reader) + { + ValidatorIndex = reader.ReadUInt16(); + InvocationScript = reader.ReadVarBytes(1024); + } + + public static PreparationPayloadCompact FromPayload(ConsensusPayload payload) + { + return new PreparationPayloadCompact + { + ValidatorIndex = payload.ValidatorIndex, + InvocationScript = payload.Witness.InvocationScript + }; + } + + void ISerializable.Serialize(BinaryWriter writer) + { + writer.Write(ValidatorIndex); + writer.WriteVarBytes(InvocationScript); + } + } + } +} diff --git a/neo/Consensus/RecoveryMessage.cs b/neo/Consensus/RecoveryMessage.cs new file mode 100644 index 000000000..731d8258e --- /dev/null +++ b/neo/Consensus/RecoveryMessage.cs @@ -0,0 +1,148 @@ +using Neo.IO; +using Neo.Ledger; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Neo.Consensus +{ + internal partial class RecoveryMessage : ConsensusMessage + { + public Dictionary ChangeViewMessages; + public PrepareRequest PrepareRequestMessage; + /// The PreparationHash in case the PrepareRequest hasn't been received yet. + /// This can be null if the PrepareRequest information is present, since it can be derived in that case. + public UInt256 PreparationHash; + public Dictionary PreparationMessages; + public Dictionary CommitMessages; + + public RecoveryMessage() : base(ConsensusMessageType.RecoveryMessage) + { + } + + public override void Deserialize(BinaryReader reader) + { + base.Deserialize(reader); + ChangeViewMessages = reader.ReadSerializableArray(Blockchain.MaxValidators).ToDictionary(p => (int)p.ValidatorIndex); + if (reader.ReadBoolean()) + PrepareRequestMessage = reader.ReadSerializable(); + else + { + int preparationHashSize = UInt256.Zero.Size; + if (preparationHashSize == (int)reader.ReadVarInt((ulong)preparationHashSize)) + PreparationHash = new UInt256(reader.ReadBytes(preparationHashSize)); + } + + PreparationMessages = reader.ReadSerializableArray(Blockchain.MaxValidators).ToDictionary(p => (int)p.ValidatorIndex); + CommitMessages = reader.ReadSerializableArray(Blockchain.MaxValidators).ToDictionary(p => (int)p.ValidatorIndex); + } + + internal ConsensusPayload[] GetChangeViewPayloads(IConsensusContext context, ConsensusPayload payload) + { + return ChangeViewMessages.Values.Select(p => new ConsensusPayload + { + Version = payload.Version, + PrevHash = payload.PrevHash, + BlockIndex = payload.BlockIndex, + ValidatorIndex = p.ValidatorIndex, + ConsensusMessage = new ChangeView + { + ViewNumber = p.OriginalViewNumber, + NewViewNumber = ViewNumber, + Timestamp = p.Timestamp + }, + Witness = new Witness + { + InvocationScript = p.InvocationScript, + VerificationScript = Contract.CreateSignatureRedeemScript(context.Validators[p.ValidatorIndex]) + } + }).ToArray(); + } + + internal ConsensusPayload[] GetCommitPayloadsFromRecoveryMessage(IConsensusContext context, ConsensusPayload payload) + { + return CommitMessages.Values.Select(p => new ConsensusPayload + { + Version = payload.Version, + PrevHash = payload.PrevHash, + BlockIndex = payload.BlockIndex, + ValidatorIndex = p.ValidatorIndex, + ConsensusMessage = new Commit + { + ViewNumber = ViewNumber, + Signature = p.Signature + }, + Witness = new Witness + { + InvocationScript = p.InvocationScript, + VerificationScript = Contract.CreateSignatureRedeemScript(context.Validators[p.ValidatorIndex]) + } + }).ToArray(); + } + + internal ConsensusPayload GetPrepareRequestPayload(IConsensusContext context, ConsensusPayload payload) + { + if (PrepareRequestMessage == null) return null; + if (!PreparationMessages.TryGetValue((int)context.PrimaryIndex, out RecoveryMessage.PreparationPayloadCompact compact)) + return null; + return new ConsensusPayload + { + Version = payload.Version, + PrevHash = payload.PrevHash, + BlockIndex = payload.BlockIndex, + ValidatorIndex = (ushort)context.PrimaryIndex, + ConsensusMessage = PrepareRequestMessage, + Witness = new Witness + { + InvocationScript = compact.InvocationScript, + VerificationScript = Contract.CreateSignatureRedeemScript(context.Validators[context.PrimaryIndex]) + } + }; + } + + internal ConsensusPayload[] GetPrepareResponsePayloads(IConsensusContext context, ConsensusPayload payload) + { + UInt256 preparationHash = PreparationHash ?? context.PreparationPayloads[context.PrimaryIndex]?.Hash; + if (preparationHash is null) return new ConsensusPayload[0]; + return PreparationMessages.Values.Where(p => p.ValidatorIndex != context.PrimaryIndex).Select(p => new ConsensusPayload + { + Version = payload.Version, + PrevHash = payload.PrevHash, + BlockIndex = payload.BlockIndex, + ValidatorIndex = p.ValidatorIndex, + ConsensusMessage = new PrepareResponse + { + ViewNumber = ViewNumber, + PreparationHash = preparationHash + }, + Witness = new Witness + { + InvocationScript = p.InvocationScript, + VerificationScript = Contract.CreateSignatureRedeemScript(context.Validators[p.ValidatorIndex]) + } + }).ToArray(); + } + + public override void Serialize(BinaryWriter writer) + { + base.Serialize(writer); + writer.Write(ChangeViewMessages.Values.ToArray()); + bool hasPrepareRequestMessage = PrepareRequestMessage != null; + writer.Write(hasPrepareRequestMessage); + if (hasPrepareRequestMessage) + writer.Write(PrepareRequestMessage); + else + { + if (PreparationHash == null) + writer.WriteVarInt(0); + else + writer.WriteVarBytes(PreparationHash.ToArray()); + } + + writer.Write(PreparationMessages.Values.ToArray()); + writer.Write(CommitMessages.Values.ToArray()); + } + } +} diff --git a/neo/Ledger/Blockchain.cs b/neo/Ledger/Blockchain.cs index 82dac883a..68dbda9ee 100644 --- a/neo/Ledger/Blockchain.cs +++ b/neo/Ledger/Blockchain.cs @@ -24,10 +24,12 @@ public class ApplicationExecuted { public Transaction Transaction; public Applic public class PersistCompleted { public Block Block; } public class Import { public IEnumerable Blocks; } public class ImportCompleted { } + public class FillMemoryPool { public IEnumerable Transactions; } + public class FillCompleted { } public static readonly uint SecondsPerBlock = ProtocolSettings.Default.SecondsPerBlock; public const uint DecrementInterval = 2000000; - public const uint MaxValidators = 1024; + public const int MaxValidators = 1024; public static readonly uint[] GenerationAmount = { 8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; public static readonly TimeSpan TimePerBlock = TimeSpan.FromSeconds(SecondsPerBlock); public static readonly ECPoint[] StandbyValidators = ProtocolSettings.Default.StandbyValidators.OfType().Select(p => ECPoint.DecodePoint(p.HexToBytes(), ECCurve.Secp256r1)).ToArray(); @@ -253,6 +255,33 @@ private void AddUnverifiedBlockToCache(Block block) blocks.AddLast(block); } + private void OnFillMemoryPool(IEnumerable transactions) + { + // Invalidate all the transactions in the memory pool, to avoid any failures when adding new transactions. + MemPool.InvalidateAllTransactions(); + + // Add the transactions to the memory pool + foreach (var tx in transactions) + { + if (tx.Type == TransactionType.MinerTransaction) + continue; + if (Store.ContainsTransaction(tx.Hash)) + continue; + if (!Plugin.CheckPolicy(tx)) + continue; + // First remove the tx if it is unverified in the pool. + MemPool.TryRemoveUnVerified(tx.Hash, out _); + // Verify the the transaction + if (!tx.Verify(currentSnapshot, MemPool.GetVerifiedTransactions())) + continue; + // Add to the memory pool + MemPool.TryAdd(tx.Hash, tx); + } + // Transactions originally in the pool will automatically be reverified based on their priority. + + Sender.Tell(new FillCompleted()); + } + private RelayResultReason OnNewBlock(Block block) { if (block.Index <= Height) @@ -406,6 +435,9 @@ protected override void OnReceive(object message) case Import import: OnImport(import.Blocks); break; + case FillMemoryPool fill: + OnFillMemoryPool(fill.Transactions); + break; case Header[] headers: OnNewHeaders(headers); break; diff --git a/neo/Ledger/MemoryPool.cs b/neo/Ledger/MemoryPool.cs index edeb66f0f..9c367fd8f 100644 --- a/neo/Ledger/MemoryPool.cs +++ b/neo/Ledger/MemoryPool.cs @@ -342,7 +342,7 @@ private bool TryRemoveVerified(UInt256 hash, out PoolItem item) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryRemoveUnVerified(UInt256 hash, out PoolItem item) + internal bool TryRemoveUnVerified(UInt256 hash, out PoolItem item) { if (!_unverifiedTransactions.TryGetValue(hash, out item)) return false; @@ -354,6 +354,27 @@ private bool TryRemoveUnVerified(UInt256 hash, out PoolItem item) return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InvalidateVerifiedTransactions() + { + foreach (PoolItem item in _sortedHighPrioTransactions) + { + if (_unverifiedTransactions.TryAdd(item.Tx.Hash, item)) + _unverifiedSortedHighPriorityTransactions.Add(item); + } + + foreach (PoolItem item in _sortedLowPrioTransactions) + { + if (_unverifiedTransactions.TryAdd(item.Tx.Hash, item)) + _unverifiedSortedLowPriorityTransactions.Add(item); + } + + // Clear the verified transactions now, since they all must be reverified. + _unsortedTransactions.Clear(); + _sortedHighPrioTransactions.Clear(); + _sortedLowPrioTransactions.Clear(); + } + // Note: this must only be called from a single thread (the Blockchain actor) internal void UpdatePoolForBlockPersisted(Block block, Snapshot snapshot) { @@ -368,22 +389,7 @@ internal void UpdatePoolForBlockPersisted(Block block, Snapshot snapshot) } // Add all the previously verified transactions back to the unverified transactions - foreach (PoolItem item in _sortedHighPrioTransactions) - { - if (_unverifiedTransactions.TryAdd(item.Tx.Hash, item)) - _unverifiedSortedHighPriorityTransactions.Add(item); - } - - foreach (PoolItem item in _sortedLowPrioTransactions) - { - if (_unverifiedTransactions.TryAdd(item.Tx.Hash, item)) - _unverifiedSortedLowPriorityTransactions.Add(item); - } - - // Clear the verified transactions now, since they all must be reverified. - _unsortedTransactions.Clear(); - _sortedHighPrioTransactions.Clear(); - _sortedLowPrioTransactions.Clear(); + InvalidateVerifiedTransactions(); } finally { @@ -406,7 +412,19 @@ internal void UpdatePoolForBlockPersisted(Block block, Snapshot snapshot) _maxLowPriorityTxPerBlock, MaxSecondsToReverifyLowPrioTx, snapshot); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void InvalidateAllTransactions() + { + _txRwLock.EnterWriteLock(); + try + { + InvalidateVerifiedTransactions(); + } + finally + { + _txRwLock.ExitWriteLock(); + } + } + private int ReverifyTransactions(SortedSet verifiedSortedTxPool, SortedSet unverifiedSortedTxPool, int count, double secondsTimeout, Snapshot snapshot) { diff --git a/neo/Ledger/TrimmedBlock.cs b/neo/Ledger/TrimmedBlock.cs index b5b9eb0db..1bf5a371d 100644 --- a/neo/Ledger/TrimmedBlock.cs +++ b/neo/Ledger/TrimmedBlock.cs @@ -57,7 +57,7 @@ public Header Header public override void Deserialize(BinaryReader reader) { base.Deserialize(reader); - Hashes = reader.ReadSerializableArray(); + Hashes = reader.ReadSerializableArray(Block.MaxTransactionsPerBlock); } public override void Serialize(BinaryWriter writer) diff --git a/neo/NeoSystem.cs b/neo/NeoSystem.cs index 480c7730d..b34a0f52d 100644 --- a/neo/NeoSystem.cs +++ b/neo/NeoSystem.cs @@ -14,9 +14,6 @@ namespace Neo { public class NeoSystem : IDisposable { - private Peer.Start start_message = null; - private bool suspend = false; - public ActorSystem ActorSystem { get; } = ActorSystem.Create(nameof(NeoSystem), $"akka {{ log-dead-letters = off }}" + $"blockchain-mailbox {{ mailbox-type: \"{typeof(BlockchainMailbox).AssemblyQualifiedName}\" }}" + @@ -30,8 +27,13 @@ public class NeoSystem : IDisposable public IActorRef Consensus { get; private set; } public RpcServer RpcServer { get; private set; } + private readonly Store store; + private Peer.Start start_message = null; + private bool suspend = false; + public NeoSystem(Store store) { + this.store = store; this.Blockchain = ActorSystem.ActorOf(Ledger.Blockchain.Props(this, store)); this.LocalNode = ActorSystem.ActorOf(Network.P2P.LocalNode.Props(this)); this.TaskManager = ActorSystem.ActorOf(Network.P2P.TaskManager.Props(this)); @@ -65,10 +67,10 @@ internal void ResumeNodeStartup() } } - public void StartConsensus(Wallet wallet) + public void StartConsensus(Wallet wallet, Store consensus_store = null, bool ignoreRecoveryLogs = false) { - Consensus = ActorSystem.ActorOf(ConsensusService.Props(this.LocalNode, this.TaskManager, wallet)); - Consensus.Tell(new ConsensusService.Start()); + Consensus = ActorSystem.ActorOf(ConsensusService.Props(this.LocalNode, this.TaskManager, consensus_store ?? store, wallet)); + Consensus.Tell(new ConsensusService.Start { IgnoreRecoveryLogs = ignoreRecoveryLogs }, Blockchain); } public void StartNode(int port = 0, int wsPort = 0, int minDesiredConnections = Peer.DefaultMinDesiredConnections, diff --git a/neo/Network/P2P/Payloads/Block.cs b/neo/Network/P2P/Payloads/Block.cs index bc1f25704..a73b5bec2 100644 --- a/neo/Network/P2P/Payloads/Block.cs +++ b/neo/Network/P2P/Payloads/Block.cs @@ -11,6 +11,8 @@ namespace Neo.Network.P2P.Payloads { public class Block : BlockBase, IInventory, IEquatable { + public const int MaxTransactionsPerBlock = ushort.MaxValue; + public Transaction[] Transactions; private Header _header = null; @@ -51,7 +53,7 @@ public static Fixed8 CalculateNetFee(IEnumerable transactions) public override void Deserialize(BinaryReader reader) { base.Deserialize(reader); - Transactions = new Transaction[reader.ReadVarInt(0x10000)]; + Transactions = new Transaction[reader.ReadVarInt(MaxTransactionsPerBlock)]; if (Transactions.Length == 0) throw new FormatException(); HashSet hashes = new HashSet(); for (int i = 0; i < Transactions.Length; i++) diff --git a/neo/Network/P2P/Payloads/ConsensusPayload.cs b/neo/Network/P2P/Payloads/ConsensusPayload.cs index b961d4194..1a8129309 100644 --- a/neo/Network/P2P/Payloads/ConsensusPayload.cs +++ b/neo/Network/P2P/Payloads/ConsensusPayload.cs @@ -1,4 +1,6 @@ -using Neo.Cryptography; +#pragma warning disable CS0612 +using Neo.Consensus; +using Neo.Cryptography; using Neo.Cryptography.ECC; using Neo.IO; using Neo.Persistence; @@ -15,12 +17,32 @@ public class ConsensusPayload : IInventory public UInt256 PrevHash; public uint BlockIndex; public ushort ValidatorIndex; - public uint Timestamp; + [Obsolete] //This field will be removed from future version and should not be used. + private uint Timestamp; public byte[] Data; public Witness Witness; + private ConsensusMessage _deserializedMessage = null; + internal ConsensusMessage ConsensusMessage + { + get + { + if (_deserializedMessage is null) + _deserializedMessage = ConsensusMessage.DeserializeFrom(Data); + return _deserializedMessage; + } + set + { + if (!ReferenceEquals(_deserializedMessage, value)) + { + _deserializedMessage = value; + Data = value?.ToArray(); + } + } + } + private UInt256 _hash = null; - UInt256 IInventory.Hash + public UInt256 Hash { get { @@ -47,7 +69,19 @@ Witness[] IVerifiable.Witnesses } } - public int Size => sizeof(uint) + PrevHash.Size + sizeof(uint) + sizeof(ushort) + sizeof(uint) + Data.GetVarSize() + 1 + Witness.Size; + public int Size => + sizeof(uint) + //Version + PrevHash.Size + //PrevHash + sizeof(uint) + //BlockIndex + sizeof(ushort) + //ValidatorIndex + sizeof(uint) + //Timestamp + Data.GetVarSize() + //Data + 1 + Witness.Size; //Witness + + internal T GetDeserializedMessage() where T : ConsensusMessage + { + return (T)ConsensusMessage; + } void ISerializable.Deserialize(BinaryReader reader) { @@ -103,3 +137,4 @@ public bool Verify(Snapshot snapshot) } } } +#pragma warning restore CS0612 diff --git a/neo/Persistence/LevelDB/LevelDBStore.cs b/neo/Persistence/LevelDB/LevelDBStore.cs index 1a54d5cbd..b74034f1c 100644 --- a/neo/Persistence/LevelDB/LevelDBStore.cs +++ b/neo/Persistence/LevelDB/LevelDBStore.cs @@ -35,6 +35,13 @@ public void Dispose() db.Dispose(); } + public override byte[] Get(byte prefix, byte[] key) + { + if (!db.TryGet(ReadOptions.Default, SliceBuilder.Begin(prefix).Add(key), out Slice slice)) + return null; + return slice.ToArray(); + } + public override DataCache GetAccounts() { return new DbCache(db, null, null, Prefixes.ST_Account); @@ -104,5 +111,15 @@ public override MetaDataCache GetHeaderHashIndex() { return new DbMetaDataCache(db, null, null, Prefixes.IX_CurrentHeader); } + + public override void Put(byte prefix, byte[] key, byte[] value) + { + db.Put(WriteOptions.Default, SliceBuilder.Begin(prefix).Add(key), value); + } + + public override void PutSync(byte prefix, byte[] key, byte[] value) + { + db.Put(new WriteOptions { Sync = true }, SliceBuilder.Begin(prefix).Add(key), value); + } } } diff --git a/neo/Persistence/LevelDB/Prefixes.cs b/neo/Persistence/LevelDB/Prefixes.cs index a92363f38..5f7e3ea90 100644 --- a/neo/Persistence/LevelDB/Prefixes.cs +++ b/neo/Persistence/LevelDB/Prefixes.cs @@ -19,5 +19,10 @@ internal static class Prefixes public const byte IX_CurrentHeader = 0xc1; public const byte SYS_Version = 0xf0; + + /* Prefixes 0xf1 to 0xff are reserved for external use. + * + * Note: The saved consensus state uses the Prefix 0xf4 + */ } } diff --git a/neo/Persistence/Store.cs b/neo/Persistence/Store.cs index 5936fbcfb..fd7e8b381 100644 --- a/neo/Persistence/Store.cs +++ b/neo/Persistence/Store.cs @@ -21,6 +21,7 @@ public abstract class Store : IPersistence MetaDataCache IPersistence.BlockHashIndex => GetBlockHashIndex(); MetaDataCache IPersistence.HeaderHashIndex => GetHeaderHashIndex(); + public abstract byte[] Get(byte prefix, byte[] key); public abstract DataCache GetBlocks(); public abstract DataCache GetTransactions(); public abstract DataCache GetAccounts(); @@ -34,6 +35,8 @@ public abstract class Store : IPersistence public abstract MetaDataCache GetValidatorsCount(); public abstract MetaDataCache GetBlockHashIndex(); public abstract MetaDataCache GetHeaderHashIndex(); + public abstract void Put(byte prefix, byte[] key, byte[] value); + public abstract void PutSync(byte prefix, byte[] key, byte[] value); public abstract Snapshot GetSnapshot(); }