diff --git a/CHANGES.md b/CHANGES.md index 2567893ccea..fec40f0b0f4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,14 @@ To be released. ### Backward-incompatible API changes + - Added the parameter `protocolVersion` to `Block(long, long, BigInteger, + Nonce, Address?, HashDigest?, DateTimeOffset, + IEnumerable> transactions, HashDigest?, + HashDigest?)` constructor. [[#1142], [#1146]] + - Added the parameter to `protocolVersion` to `Block.Mine()` method. + [[#1142], [#1146]] + - Added the first parameter `protocolVersion` to `BlockHeader()` constructor. + [[#1142], [#1146]] - Added `stagePolicy` as the second parameter to `BlockChain()` constructor. [[#1130], [#1131]] - Removed `IBlockStatesStore` interface. [[#1117]] @@ -30,6 +38,10 @@ To be released. ### Added APIs + - Added `Block.CurrentProtocolVersion` constant. [[#1142], [#1146]] + - Added `Block.ProtocolVersion` property. [[#1142], [#1146]] + - Added `Block.Header` property. [[#1070], [#1102]] + - Added `BlockHeader.ProtocolVersion` property. [[#1142], [#1146]] - Added `IStagePolicy` interface. [[#1130], [#1131]] - Added `VolatileStagePolicy` class. [[#1130], [#1131], [#1136]] - Added `ITransport` interface. [[#1052]] @@ -38,11 +50,10 @@ To be released. - Added `BlockExceedingTransactionsException` class. [[#1104], [#1110]] - Added `BlockChain.GetStagedTransactionIds()` method. [[#1089]] - (Libplanet.RocksDBStore) Added `maxTotalWalSize`, `keepLogFileNum` and - `maxLogFileSize` parameters into `RocksDBStore` constructor. + `maxLogFileSize` parameters into `RocksDBStore()` constructor. [[#1065], [#1102], [#1132]] - Added `Swarm.BlockDemand` property. [[#1068], [#1102]] - Added `BlockDemand` struct. [[#1068], [#1102]] - - Added `Block.Header` property. [[#1070], [#1102]] - Added `TurnClient.PublicAddress` property. [[#1074], [#1102]] - Added `TurnClient.EndPoint` property. [[#1074], [#1102]] - Added `TurnClient.BehindNAT` property. [[#1074], [#1102]] @@ -70,6 +81,7 @@ To be released. class. [[#1119]] - Added `Libplanet.Blockchain.Renderers.Debug.InvalidRenderException` class. [[#1119]] + - Added `InvalidBlockProtocolVersionException` class. [[#1142], [#1146]] - Added `InvalidBlockTxHashException` class. [[#1116]] - Removed `Swarm.TraceTable()` method. [[#1120]] - Added `Swarm.PeerStates` property. [[#1120]] @@ -82,6 +94,9 @@ To be released. - Upgraded *Bencodex* package (which is a dependency) so that Libplanet gets benefits from its recent optimizations. [[#1081], [#1084], [#1086], [#1101]] + - Since `BlockHeader.ProtocolVersion` was added, the existing blocks are + considered protocol compliant with the protocol version zero. + [[#1142], [#1146]] - When a `BlockChain` follows `VolatileStagePolicy`, which is Libplanet's the only built-in `IStagePolicy` implementation at the moment, as its `StagePolicy`, its staged transactions are no longer @@ -98,7 +113,6 @@ To be released. - `Swarm` became not to fill states from trusted peers, because now states can be validated rather than trusted due to MPT. [[#1117]] - `HashDigest` became serializable. [[#795], [#1125]] - ### Bug fixes - Fixed a bug where `BlockChain.MineBlock()` was not automatically @@ -146,6 +160,8 @@ To be released. [#1136]: https://github.com/planetarium/libplanet/pull/1136 [#1137]: https://github.com/planetarium/libplanet/pull/1137 [#1141]: https://github.com/planetarium/libplanet/pull/1141 +[#1142]: https://github.com/planetarium/libplanet/issues/1142 +[#1146]: https://github.com/planetarium/libplanet/pull/1146 Version 0.10.2 diff --git a/Libplanet.Tests/Blockchain/BlockChainTest.ValidateNextBlock.cs b/Libplanet.Tests/Blockchain/BlockChainTest.ValidateNextBlock.cs index 096bee226ab..f79b0113917 100644 --- a/Libplanet.Tests/Blockchain/BlockChainTest.ValidateNextBlock.cs +++ b/Libplanet.Tests/Blockchain/BlockChainTest.ValidateNextBlock.cs @@ -28,6 +28,46 @@ public void ValidateNextBlock() Assert.Equal(_blockChain.Tip, validNextBlock); } + [Fact] + private void ValidateNextBlockProtocolVersion() + { + Block block1 = Block.Mine( + 1, + 1024, + _fx.GenesisBlock.TotalDifficulty, + _fx.GenesisBlock.Miner.Value, + _fx.GenesisBlock.Hash, + _fx.GenesisBlock.Timestamp.AddDays(1), + _emptyTransaction, + protocolVersion: 1 + ).AttachStateRootHash(_fx.StateStore, _policy.BlockAction); + _blockChain.Append(block1); + + Block block2 = Block.Mine( + 2, + 1024, + block1.TotalDifficulty, + _fx.GenesisBlock.Miner.Value, + block1.Hash, + _fx.GenesisBlock.Timestamp.AddDays(1), + _emptyTransaction, + protocolVersion: 0 + ).AttachStateRootHash(_fx.StateStore, _policy.BlockAction); + Assert.Throws(() => _blockChain.Append(block2)); + + Block block3 = Block.Mine( + 2, + 1024, + block1.TotalDifficulty, + _fx.GenesisBlock.Miner.Value, + block1.Hash, + _fx.GenesisBlock.Timestamp.AddDays(1), + _emptyTransaction, + protocolVersion: Block.CurrentProtocolVersion + 1 + ).AttachStateRootHash(_fx.StateStore, _policy.BlockAction); + Assert.Throws(() => _blockChain.Append(block3)); + } + [Fact] private void ValidateNextBlockInvalidIndex() { diff --git a/Libplanet.Tests/Blocks/BlockHeaderTest.cs b/Libplanet.Tests/Blocks/BlockHeaderTest.cs index 6d6d89b0978..9dc9e78dc9a 100644 --- a/Libplanet.Tests/Blocks/BlockHeaderTest.cs +++ b/Libplanet.Tests/Blocks/BlockHeaderTest.cs @@ -12,6 +12,32 @@ public class BlockHeaderTest : IClassFixture public BlockHeaderTest(BlockFixture fixture) => _fx = fixture; + [Fact] + public void ValidateProtocolVersion() + { + var header = new BlockHeader( + protocolVersion: -1, + index: 0, + difficulty: _fx.Next.Difficulty, + totalDifficulty: _fx.Next.TotalDifficulty, + nonce: _fx.Next.Nonce.ByteArray, + miner: _fx.Next.Miner?.ByteArray ?? ImmutableArray.Empty, + hash: _fx.Next.Hash.ByteArray, + txHash: _fx.Next.TxHash?.ByteArray ?? ImmutableArray.Empty, + previousHash: _fx.Next.PreviousHash?.ByteArray ?? ImmutableArray.Empty, + timestamp: _fx.Next.Timestamp.ToString( + BlockHeader.TimestampFormat, + CultureInfo.InvariantCulture + ), + preEvaluationHash: TestUtils.GetRandomBytes(32).ToImmutableArray(), + stateRootHash: ImmutableArray.Empty + ); + + Assert.Throws(() => + header.Validate(DateTimeOffset.UtcNow) + ); + } + [Fact] public void ValidateTimestamp() { @@ -19,6 +45,7 @@ public void ValidateTimestamp() string future = (now + TimeSpan.FromSeconds(16)) .ToString(BlockHeader.TimestampFormat, CultureInfo.InvariantCulture); var header = new BlockHeader( + protocolVersion: 0, index: 0, difficulty: 0, totalDifficulty: 0, @@ -43,6 +70,7 @@ public void ValidateTimestamp() public void ValidateNonce() { var header = new BlockHeader( + protocolVersion: 0, index: _fx.Next.Index, difficulty: long.MaxValue, totalDifficulty: _fx.Genesis.TotalDifficulty + long.MaxValue, @@ -67,6 +95,7 @@ public void ValidateNonce() public void ValidateIndex() { var header = new BlockHeader( + protocolVersion: 0, index: -1, difficulty: _fx.Next.Difficulty, totalDifficulty: _fx.Next.TotalDifficulty, @@ -92,6 +121,7 @@ public void ValidateDifficulty() { DateTimeOffset now = DateTimeOffset.UtcNow; var genesisHeader = new BlockHeader( + protocolVersion: 0, index: 0, difficulty: 1000, totalDifficulty: 1000, @@ -109,6 +139,7 @@ public void ValidateDifficulty() genesisHeader.Validate(DateTimeOffset.UtcNow)); var header1 = new BlockHeader( + protocolVersion: 0, index: 10, difficulty: 0, totalDifficulty: 1000, @@ -126,6 +157,7 @@ public void ValidateDifficulty() header1.Validate(DateTimeOffset.UtcNow)); var header2 = new BlockHeader( + protocolVersion: 0, index: 10, difficulty: 1000, totalDifficulty: 10, @@ -148,6 +180,7 @@ public void ValidatePreviousHash() { DateTimeOffset now = DateTimeOffset.UtcNow; var genesisHeader = new BlockHeader( + protocolVersion: 0, index: 0, difficulty: 0, totalDifficulty: 0, @@ -165,6 +198,7 @@ public void ValidatePreviousHash() genesisHeader.Validate(DateTimeOffset.UtcNow)); var header = new BlockHeader( + protocolVersion: 0, index: 10, difficulty: 1000, totalDifficulty: 1500, diff --git a/Libplanet/Blockchain/BlockChain.cs b/Libplanet/Blockchain/BlockChain.cs index 0667c7ddeb0..220fc1dffc4 100644 --- a/Libplanet/Blockchain/BlockChain.cs +++ b/Libplanet/Blockchain/BlockChain.cs @@ -1935,6 +1935,27 @@ HashDigest blockHash private InvalidBlockException ValidateNextBlock(Block nextBlock) { + int actualProtocolVersion = nextBlock.ProtocolVersion; + const int currentProtocolVersion = Block.CurrentProtocolVersion; + if (actualProtocolVersion > currentProtocolVersion) + { + string message = + $"The protocol version ({actualProtocolVersion}) of the block " + + $"#{nextBlock.Index} {nextBlock.Hash} is not supported by this node." + + $"The highest supported protocol version is {currentProtocolVersion}."; + throw new InvalidBlockProtocolVersionException( + actualProtocolVersion, + message + ); + } + else if (Tip is { } tip && actualProtocolVersion < tip.ProtocolVersion) + { + string message = + "The protocol version is disallowed to be downgraded from the topmost block " + + $"in the chain ({actualProtocolVersion} < {tip.ProtocolVersion})."; + throw new InvalidBlockProtocolVersionException(actualProtocolVersion, message); + } + InvalidBlockException e = Policy.ValidateNextBlock(this, nextBlock); if (!(e is null)) diff --git a/Libplanet/Blocks/Block.cs b/Libplanet/Blocks/Block.cs index c726d87beb6..bd1dce0eb6c 100644 --- a/Libplanet/Blocks/Block.cs +++ b/Libplanet/Blocks/Block.cs @@ -21,6 +21,8 @@ namespace Libplanet.Blocks public class Block where T : IAction, new() { + public const int CurrentProtocolVersion = 0; + private int _bytesLength; /// @@ -51,6 +53,8 @@ public class Block /// Automatically determined if null is passed (which is default). /// The of the states on the block. /// + /// The protocol version. + /// by default. /// public Block( long index, @@ -62,8 +66,10 @@ public Block( DateTimeOffset timestamp, IEnumerable> transactions, HashDigest? preEvaluationHash = null, - HashDigest? stateRootHash = null) + HashDigest? stateRootHash = null, + int protocolVersion = CurrentProtocolVersion) { + ProtocolVersion = protocolVersion; Index = index; Difficulty = difficulty; TotalDifficulty = totalDifficulty; @@ -145,33 +151,39 @@ public Block( private Block(RawBlock rb) : this( +#pragma warning disable SA1118 + rb.Header.ProtocolVersion, new HashDigest(rb.Header.Hash), rb.Header.Index, rb.Header.Difficulty, rb.Header.TotalDifficulty, new Nonce(rb.Header.Nonce.ToArray()), rb.Header.Miner.Any() ? new Address(rb.Header.Miner) : (Address?)null, -#pragma warning disable MEN002 // Line is too long - rb.Header.PreviousHash.Any() ? new HashDigest(rb.Header.PreviousHash) : (HashDigest?)null, -#pragma warning restore MEN002 // Line is too long + rb.Header.PreviousHash.Any() + ? new HashDigest(rb.Header.PreviousHash) + : (HashDigest?)null, DateTimeOffset.ParseExact( rb.Header.Timestamp, BlockHeader.TimestampFormat, CultureInfo.InvariantCulture).ToUniversalTime(), -#pragma warning disable MEN002 // Line is too long - rb.Header.TxHash.Any() ? new HashDigest(rb.Header.TxHash) : (HashDigest?)null, -#pragma warning restore MEN002 // Line is too long + rb.Header.TxHash.Any() + ? new HashDigest(rb.Header.TxHash) + : (HashDigest?)null, rb.Transactions .Select(tx => Transaction.Deserialize(tx.ToArray())) .ToList(), -#pragma warning disable MEN002 // Line is too long - rb.Header.PreEvaluationHash.Any() ? new HashDigest(rb.Header.PreEvaluationHash) : (HashDigest?)null, - rb.Header.StateRootHash.Any() ? new HashDigest(rb.Header.StateRootHash) : (HashDigest?)null) -#pragma warning restore MEN002 // Line is too long + rb.Header.PreEvaluationHash.Any() + ? new HashDigest(rb.Header.PreEvaluationHash) + : (HashDigest?)null, + rb.Header.StateRootHash.Any() + ? new HashDigest(rb.Header.StateRootHash) + : (HashDigest?)null) +#pragma warning restore SA1118 { } private Block( + int protocolVersion, HashDigest hash, long index, long difficulty, @@ -186,6 +198,7 @@ private Block( HashDigest? stateRootHash ) { + ProtocolVersion = protocolVersion; Index = index; Difficulty = difficulty; TotalDifficulty = totalDifficulty; @@ -205,6 +218,12 @@ private Block( Transactions = transactions.ToImmutableArray(); } + /// + /// The protocol version number. + /// + [IgnoreDuringEquals] + public int ProtocolVersion { get; } + /// /// is derived from a serialized /// after are evaluated. @@ -288,6 +307,7 @@ public BlockHeader Header // FIXME: When hash is not assigned, should throw an exception. return new BlockHeader( + protocolVersion: ProtocolVersion, index: Index, timestamp: timestampAsString, nonce: Nonce.ToByteArray().ToImmutableArray(), @@ -324,6 +344,7 @@ public BlockHeader Header /// The when mining started. /// s that are going to be included /// in the block. + /// The protocol version. /// /// A cancellation token used to propagate notification that this /// operation should be canceled. @@ -336,6 +357,7 @@ public static Block Mine( HashDigest? previousHash, DateTimeOffset timestamp, IEnumerable> transactions, + int protocolVersion = CurrentProtocolVersion, CancellationToken cancellationToken = default(CancellationToken)) { var txs = transactions.OrderBy(tx => tx.Id).ToImmutableArray(); @@ -347,7 +369,8 @@ public static Block Mine( miner, previousHash, timestamp, - txs); + txs, + protocolVersion: protocolVersion); // Poor man' way to optimize stamp... // FIXME: We need to rather reorganize the serialization layout. @@ -658,6 +681,12 @@ private byte[] SerializeForHash(HashDigest? stateRootHash = null) .Add("difficulty", Difficulty) .Add("nonce", Nonce.ToByteArray()); + if (ProtocolVersion != 0) + { + // TODO: unit test + dict = dict.Add("protocol_version", ProtocolVersion); + } + if (!(Miner is null)) { dict = dict.Add("reward_beneficiary", Miner.Value.ToByteArray()); diff --git a/Libplanet/Blocks/BlockHeader.cs b/Libplanet/Blocks/BlockHeader.cs index 7edb9c1928e..016b747882a 100644 --- a/Libplanet/Blocks/BlockHeader.cs +++ b/Libplanet/Blocks/BlockHeader.cs @@ -17,6 +17,8 @@ public readonly struct BlockHeader { internal const string TimestampFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ"; + private static readonly byte[] ProtocolVersionKey = { 0x76 }; // 'v' + private static readonly byte[] IndexKey = { 0x69 }; // 'i' private static readonly byte[] TimestampKey = { 0x74 }; // 't' @@ -45,6 +47,8 @@ public readonly struct BlockHeader /// /// Creates a instance. /// + /// The protocol version. Goes to the . /// The height of the block. Goes to the . /// /// The time this block is created. @@ -71,6 +75,7 @@ public readonly struct BlockHeader /// The of the states on the block. /// public BlockHeader( + int protocolVersion, long index, string timestamp, ImmutableArray nonce, @@ -83,6 +88,7 @@ public BlockHeader( ImmutableArray preEvaluationHash, ImmutableArray stateRootHash) { + ProtocolVersion = protocolVersion; Index = index; Timestamp = timestamp; Nonce = nonce; @@ -98,6 +104,10 @@ public BlockHeader( public BlockHeader(Bencodex.Types.Dictionary dict) { + // TODO: unit test + ProtocolVersion = dict.ContainsKey(ProtocolVersionKey) + ? (int)dict.GetValue(ProtocolVersionKey) + : 0; Index = dict.GetValue(IndexKey); Timestamp = dict.GetValue(TimestampKey); Difficulty = dict.GetValue(DifficultyKey); @@ -129,6 +139,11 @@ public BlockHeader(Bencodex.Types.Dictionary dict) : ImmutableArray.Empty; } + /// + /// The protocol version number. + /// + public int ProtocolVersion { get; } + public long Index { get; } public string Timestamp { get; } @@ -196,6 +211,12 @@ public Bencodex.Types.Dictionary ToBencodex() .Add(NonceKey, Nonce.ToArray()) .Add(HashKey, Hash.ToArray()); + if (ProtocolVersion != 0) + { + // TODO: unit test + dict = dict.Add(ProtocolVersionKey, ProtocolVersion); + } + if (Miner.Any()) { dict = dict.Add(MinerKey, Miner.ToArray()); @@ -226,6 +247,14 @@ public Bencodex.Types.Dictionary ToBencodex() internal void Validate(DateTimeOffset currentTime) { + if (ProtocolVersion < 0) + { + throw new InvalidBlockProtocolVersionException( + ProtocolVersion, + $"A block's protocol version cannot be less than zero: {ProtocolVersion}." + ); + } + DateTimeOffset ts = DateTimeOffset.ParseExact( Timestamp, TimestampFormat, diff --git a/Libplanet/Blocks/InvalidBlockProtocolVersionException.cs b/Libplanet/Blocks/InvalidBlockProtocolVersionException.cs new file mode 100644 index 00000000000..88e9850fde4 --- /dev/null +++ b/Libplanet/Blocks/InvalidBlockProtocolVersionException.cs @@ -0,0 +1,49 @@ +#nullable enable +using System; +using System.Runtime.Serialization; + +namespace Libplanet.Blocks +{ + /// + /// The exception that is thrown when a 's + /// (or a 's + /// ) is invalid. + /// + [Serializable] + public sealed class InvalidBlockProtocolVersionException : InvalidBlockException, ISerializable + { + /// + /// Initializes a new instance of class. + /// + /// The actual block protocol version which is invalid. + /// + /// The message that describes the error. + public InvalidBlockProtocolVersionException(int actualProtocolVersion, string message) + : base(message) + { + ActualProtocolVersion = actualProtocolVersion; + } + + private InvalidBlockProtocolVersionException( + SerializationInfo info, + StreamingContext context + ) + : base(info.GetString(nameof(Message)) ?? string.Empty) + { + ActualProtocolVersion = info.GetInt32(nameof(ActualProtocolVersion)); + } + + /// + /// The actual block protocol version which is invalid. + /// + public int ActualProtocolVersion { get; set; } + + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(Message), Message); + info.AddValue(nameof(ActualProtocolVersion), ActualProtocolVersion); + } + } +}