diff --git a/src/Neo/Builders/BlockBuilder.cs b/src/Neo/Builders/BlockBuilder.cs new file mode 100644 index 0000000000..a00a1b27b7 --- /dev/null +++ b/src/Neo/Builders/BlockBuilder.cs @@ -0,0 +1,166 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BlockBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography; +using Neo.Extensions.Factories; +using Neo.Network.P2P.Payloads; +using System; +using System.Linq; + +namespace Neo.Builders +{ + public class BlockBuilder + { + private BlockBuilder() { } + + private readonly Block _block = new() + { + Header = new() + { + Nonce = RandomNumberFactory.NextUInt64(), + Timestamp = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + MerkleRoot = new(), + NextConsensus = new(), + PrevHash = new(), + Witness = new() + { + InvocationScript = Memory.Empty, + VerificationScript = Memory.Empty, + }, + }, + Transactions = [], + }; + + public static BlockBuilder CreateEmpty() => + new(); + + public static BlockBuilder CreateNext(Block prevBlock) => + new BlockBuilder() + .AddPrevHash(prevBlock.Hash) + .AddIndex(prevBlock.Index + 1); + + public static BlockBuilder CreateNext(Block prevBlock, uint millisecondsPerBlock) => + new BlockBuilder() + .AddPrevHash(prevBlock.Hash) + .AddIndex(prevBlock.Index + 1) + .AddTimestamp(config => config += millisecondsPerBlock); + + public static BlockBuilder CreateNext(Block prevBlock, ProtocolSettings protocolSettings) => + new BlockBuilder() + .AddPrevHash(prevBlock.Hash) + .AddIndex(prevBlock.Index + 1) + .AddTimestamp(config => config += protocolSettings.MillisecondsPerBlock); + + public BlockBuilder AddIndex(uint index) + { + _block.Header.Index = index; + + return this; + } + + public BlockBuilder AddPrimaryIndex(byte index) + { + _block.Header.PrimaryIndex = index; + + return this; + } + + public BlockBuilder AddNextConsensus(UInt160 hash) + { + _block.Header.NextConsensus = hash; + + return this; + } + + public BlockBuilder AddNonce(ulong nonce) + { + _block.Header.Nonce = nonce; + + return this; + } + + public BlockBuilder AddPrevHash(UInt256 hash) + { + _block.Header.PrevHash = hash; + + return this; + } + + public BlockBuilder AddTimestamp(ulong timestamp) + { + _block.Header.Timestamp = timestamp; + + return this; + } + + public BlockBuilder AddTimestamp(Action config) + { + var timestamp = _block.Header.Timestamp; + config(timestamp); + _block.Header.Timestamp = timestamp; + + return this; + } + + public BlockBuilder AddTimestamp(DateTimeOffset timestamp) + { + _block.Header.Timestamp = (ulong)timestamp.ToUnixTimeMilliseconds(); + + return this; + } + + public BlockBuilder AddVersion(uint version) + { + _block.Header.Version = version; + + return this; + } + + public BlockBuilder AddWitness(Action config) + { + var wb = WitnessBuilder.CreateEmpty(); + config(wb); + _block.Header.Witness = wb.Build(); + + return this; + } + + public BlockBuilder AddWitness(Witness witness) + { + _block.Header.Witness = witness; + + return this; + } + + public BlockBuilder AddTransaction(Action config) + { + + var tx = TransactionBuilder.CreateEmpty(); + config(tx); + _block.Transactions = [.. _block.Transactions, tx.Build()]; + + return this; + } + + public BlockBuilder AddTransaction(Transaction tx) + { + _block.Transactions = [.. _block.Transactions, tx]; + + return this; + } + + public Block Build() + { + _block.Header.MerkleRoot = MerkleTree.ComputeRoot([.. _block.Transactions.Select(static s => s.Hash)]); + return _block; + } + } +} diff --git a/tests/Neo.UnitTests/Builders/UT_BlockBuilder.cs b/tests/Neo.UnitTests/Builders/UT_BlockBuilder.cs new file mode 100644 index 0000000000..60c2a56b4e --- /dev/null +++ b/tests/Neo.UnitTests/Builders/UT_BlockBuilder.cs @@ -0,0 +1,156 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_BlockBuilder.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Builders; +using Neo.Extensions.Factories; + +namespace Neo.UnitTests.Builders +{ + [TestClass] + public class UT_BlockBuilder + { + [TestMethod] + public void TestCreateBuilder() + { + var builder = BlockBuilder.CreateEmpty(); + + Assert.IsNotNull(builder); + } + + [TestMethod] + public void TestEmptyBlock() + { + var block = BlockBuilder.CreateEmpty().Build(); + + Assert.IsNotNull(block); + Assert.IsNotNull(block.Hash); + } + + [TestMethod] + public void TestBlockVersion() + { + byte expectedVersion = 1; + var actualBlock = BlockBuilder.CreateEmpty() + .AddVersion(expectedVersion) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedVersion, actualBlock.Version); + } + + [TestMethod] + public void TestBlockIndex() + { + byte expectedIndex = 1; + var actualBlock = BlockBuilder.CreateEmpty() + .AddIndex(expectedIndex) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedIndex, actualBlock.Index); + } + + [TestMethod] + public void TestBlockPrimaryIndex() + { + byte expectedPrimaryIndex = 1; + var actualBlock = BlockBuilder.CreateEmpty() + .AddPrimaryIndex(expectedPrimaryIndex) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedPrimaryIndex, actualBlock.PrimaryIndex); + } + + [TestMethod] + public void TestBlockNextConsensus() + { + var expectedNextConsensus = UInt160.Zero; + var actualBlock = BlockBuilder.CreateEmpty() + .AddNextConsensus(expectedNextConsensus) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedNextConsensus, actualBlock.NextConsensus); + } + + [TestMethod] + public void TestBlockNonce() + { + var expectedNonce = RandomNumberFactory.NextUInt64(); + var actualBlock = BlockBuilder.CreateEmpty() + .AddNonce(expectedNonce) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedNonce, actualBlock.Nonce); + } + + [TestMethod] + public void TestBlockPrevHash() + { + var expectedPrevHash = UInt256.Zero; + var actualBlock = BlockBuilder.CreateEmpty() + .AddPrevHash(expectedPrevHash) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedPrevHash, actualBlock.PrevHash); + } + + [TestMethod] + public void TestBlockTimestamp() + { + var expectedTimestamp = RandomNumberFactory.NextUInt64(); + var actualBlock = BlockBuilder.CreateEmpty() + .AddTimestamp(expectedTimestamp) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedTimestamp, actualBlock.Timestamp); + } + + [TestMethod] + public void TestBlockTransaction() + { + var expectedTransaction = TransactionBuilder.CreateEmpty().Build(); + var actualBlock = BlockBuilder.CreateEmpty() + .AddTransaction(expectedTransaction) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedTransaction, actualBlock.Transactions[0]); + } + + [TestMethod] + public void TestBlockWitness() + { + var expectedWitness = WitnessBuilder.CreateEmpty().Build(); + var actualBlock = BlockBuilder.CreateEmpty() + .AddWitness(expectedWitness) + .Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedWitness, actualBlock.Witness); + } + + [TestMethod] + public void TestBlockMerkleRoot() + { + var expectedMerkleRoot = UInt256.Zero; + var actualBlock = BlockBuilder.CreateEmpty().Build(); + + Assert.IsNotNull(actualBlock); + Assert.AreEqual(expectedMerkleRoot, actualBlock.MerkleRoot); + } + } +}