diff --git a/native/osx-x64/libbitcoinkernel.dylib b/native/osx-x64/libbitcoinkernel.dylib index 638aac4..38ef57c 100755 Binary files a/native/osx-x64/libbitcoinkernel.dylib and b/native/osx-x64/libbitcoinkernel.dylib differ diff --git a/src/BitcoinKernel.Core/Abstractions/Block.cs b/src/BitcoinKernel.Core/Abstractions/Block.cs index e4170c3..993f7ed 100644 --- a/src/BitcoinKernel.Core/Abstractions/Block.cs +++ b/src/BitcoinKernel.Core/Abstractions/Block.cs @@ -72,6 +72,21 @@ public byte[] GetHash() return blockHash.ToBytes(); } + /// + /// Gets the block header from this block. + /// + public BlockHeader GetHeader() + { + ThrowIfDisposed(); + var headerPtr = NativeMethods.BlockGetHeader(_handle); + if (headerPtr == IntPtr.Zero) + { + throw new BlockException("Failed to get block header"); + } + + return new BlockHeader(headerPtr); + } + /// /// Serializes the block to bytes. /// diff --git a/src/BitcoinKernel.Core/Abstractions/BlockHeader.cs b/src/BitcoinKernel.Core/Abstractions/BlockHeader.cs new file mode 100644 index 0000000..5abdc7c --- /dev/null +++ b/src/BitcoinKernel.Core/Abstractions/BlockHeader.cs @@ -0,0 +1,157 @@ +using BitcoinKernel.Core.Exceptions; +using BitcoinKernel.Interop; + +namespace BitcoinKernel.Core.Abstractions; + +/// +/// Represents a block header containing metadata about a block. +/// +public sealed class BlockHeader : IDisposable +{ + private IntPtr _handle; + private bool _disposed; + private readonly bool _ownsHandle; + + internal BlockHeader(IntPtr handle, bool ownsHandle = true) + { + _handle = handle != IntPtr.Zero + ? handle + : throw new ArgumentException("Invalid block header handle", nameof(handle)); + _ownsHandle = ownsHandle; + } + + /// + /// Creates a block header from raw serialized data (80 bytes). + /// + public static BlockHeader FromBytes(byte[] rawHeaderData) + { + ArgumentNullException.ThrowIfNull(rawHeaderData, nameof(rawHeaderData)); + if (rawHeaderData.Length != 80) + throw new ArgumentException("Block header must be exactly 80 bytes", nameof(rawHeaderData)); + + IntPtr headerPtr = NativeMethods.BlockHeaderCreate(rawHeaderData, (UIntPtr)rawHeaderData.Length); + + if (headerPtr == IntPtr.Zero) + { + throw new BlockException("Failed to create block header from raw data"); + } + + return new BlockHeader(headerPtr); + } + + internal IntPtr Handle + { + get + { + ThrowIfDisposed(); + return _handle; + } + } + + /// + /// Gets the block hash of this header. + /// + public byte[] GetHash() + { + ThrowIfDisposed(); + var hashPtr = NativeMethods.BlockHeaderGetHash(_handle); + if (hashPtr == IntPtr.Zero) + { + throw new BlockException("Failed to get block hash from header"); + } + + using var blockHash = new BlockHash(hashPtr); + return blockHash.ToBytes(); + } + + /// + /// Gets the previous block hash from this header. + /// + public byte[] GetPrevHash() + { + ThrowIfDisposed(); + var hashPtr = NativeMethods.BlockHeaderGetPrevHash(_handle); + if (hashPtr == IntPtr.Zero) + { + throw new BlockException("Failed to get previous block hash from header"); + } + + // The hash pointer is unowned and only valid for the lifetime of the header + var bytes = new byte[32]; + NativeMethods.BlockHashToBytes(hashPtr, bytes); + return bytes; + } + + /// + /// Gets the timestamp from this header (Unix epoch seconds). + /// + public uint Timestamp + { + get + { + ThrowIfDisposed(); + return NativeMethods.BlockHeaderGetTimestamp(_handle); + } + } + + /// + /// Gets the nBits difficulty target from this header (compact format). + /// + public uint Bits + { + get + { + ThrowIfDisposed(); + return NativeMethods.BlockHeaderGetBits(_handle); + } + } + + /// + /// Gets the version from this header. + /// + public int Version + { + get + { + ThrowIfDisposed(); + return NativeMethods.BlockHeaderGetVersion(_handle); + } + } + + /// + /// Gets the nonce from this header. + /// + public uint Nonce + { + get + { + ThrowIfDisposed(); + return NativeMethods.BlockHeaderGetNonce(_handle); + } + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(BlockHeader)); + } + + public void Dispose() + { + if (!_disposed) + { + if (_handle != IntPtr.Zero && _ownsHandle) + { + NativeMethods.BlockHeaderDestroy(_handle); + _handle = IntPtr.Zero; + } + _disposed = true; + } + } + + ~BlockHeader() + { + if (_ownsHandle) + Dispose(); + } +} diff --git a/src/BitcoinKernel.Core/Abstractions/BlockIndex.cs b/src/BitcoinKernel.Core/Abstractions/BlockIndex.cs index 5b9ec1c..a0cdd1c 100644 --- a/src/BitcoinKernel.Core/Abstractions/BlockIndex.cs +++ b/src/BitcoinKernel.Core/Abstractions/BlockIndex.cs @@ -51,4 +51,16 @@ public byte[] GetBlockHash() ? new BlockIndex(prevPtr, ownsHandle: false) : null; } -} \ No newline at end of file + + /// + /// Gets the block header associated with this block index. + /// + public BlockHeader GetBlockHeader() + { + var headerPtr = NativeMethods.BlockTreeEntryGetBlockHeader(_handle); + if (headerPtr == IntPtr.Zero) + throw new InvalidOperationException("Failed to get block header from block index"); + + return new BlockHeader(headerPtr); + } +} diff --git a/src/BitcoinKernel.Core/Abstractions/BlockValidationState.cs b/src/BitcoinKernel.Core/Abstractions/BlockValidationState.cs new file mode 100644 index 0000000..240a4a4 --- /dev/null +++ b/src/BitcoinKernel.Core/Abstractions/BlockValidationState.cs @@ -0,0 +1,105 @@ +using BitcoinKernel.Core.Exceptions; +using BitcoinKernel.Interop; +using BitcoinKernel.Interop.Enums; + +namespace BitcoinKernel.Core.Abstractions; + +/// +/// Represents the validation state of a block. +/// +public sealed class BlockValidationState : IDisposable +{ + private IntPtr _handle; + private bool _disposed; + + /// + /// Creates a new block validation state. + /// + public BlockValidationState() + { + _handle = NativeMethods.BlockValidationStateCreate(); + if (_handle == IntPtr.Zero) + { + throw new BlockException("Failed to create block validation state"); + } + } + + internal BlockValidationState(IntPtr handle) + { + if (handle == IntPtr.Zero) + { + throw new ArgumentException("Invalid block validation state handle", nameof(handle)); + } + + _handle = handle; + } + + internal IntPtr Handle + { + get + { + ThrowIfDisposed(); + return _handle; + } + } + + /// + /// Gets the validation mode (valid, invalid, or internal error). + /// + public ValidationMode ValidationMode + { + get + { + ThrowIfDisposed(); + return NativeMethods.BlockValidationStateGetValidationMode(_handle); + } + } + + /// + /// Gets the block validation result (detailed error reason if invalid). + /// + public Interop.Enums.BlockValidationResult ValidationResult + { + get + { + ThrowIfDisposed(); + return NativeMethods.BlockValidationStateGetBlockValidationResult(_handle); + } + } + + /// + /// Creates a copy of this block validation state. + /// + public BlockValidationState Copy() + { + ThrowIfDisposed(); + var copyPtr = NativeMethods.BlockValidationStateCopy(_handle); + if (copyPtr == IntPtr.Zero) + { + throw new BlockException("Failed to copy block validation state"); + } + return new BlockValidationState(copyPtr); + } + + private void ThrowIfDisposed() + { + if (_disposed) + throw new ObjectDisposedException(nameof(BlockValidationState)); + } + + + public void Dispose() + { + if (!_disposed) + { + if (_handle != IntPtr.Zero) + { + NativeMethods.BlockValidationStateDestroy(_handle); + _handle = IntPtr.Zero; + } + _disposed = true; + } + } + + ~BlockValidationState() => Dispose(); +} diff --git a/src/BitcoinKernel.Core/Chain/ChainStateManager.cs b/src/BitcoinKernel.Core/Chain/ChainStateManager.cs index cde0974..000c639 100644 --- a/src/BitcoinKernel.Core/Chain/ChainStateManager.cs +++ b/src/BitcoinKernel.Core/Chain/ChainStateManager.cs @@ -64,6 +64,40 @@ public Abstractions.Chain GetActiveChain() return new Abstractions.Chain(chainPtr); } + /// + /// Gets the block index with the most cumulative proof of work. + /// + public BlockIndex GetBestBlockIndex() + { + ThrowIfDisposed(); + IntPtr indexPtr = NativeMethods.ChainstateManagerGetBestEntry(_handle); + if (indexPtr == IntPtr.Zero) + throw new KernelException("Failed to get best block index"); + + return new BlockIndex(indexPtr, ownsHandle: false); + } + + /// + /// Processes and validates a block header. + /// + /// The block header to process. + /// The validation state result. + /// True if processing was successful, false otherwise. + public bool ProcessBlockHeader(BlockHeader header, out BlockValidationState validationState) + { + ThrowIfDisposed(); + ArgumentNullException.ThrowIfNull(header); + + using var state = new BlockValidationState(); + int result = NativeMethods.ChainstateManagerProcessBlockHeader( + _handle, + header.Handle, + state.Handle); + + validationState = state.Copy(); + return result == 0; + } + /// /// Processes a block through validation. /// diff --git a/src/BitcoinKernel.Core/ScriptVerification/ScriptVerifier.cs b/src/BitcoinKernel.Core/ScriptVerification/ScriptVerifier.cs index 7aab83f..68a2c9e 100644 --- a/src/BitcoinKernel.Core/ScriptVerification/ScriptVerifier.cs +++ b/src/BitcoinKernel.Core/ScriptVerification/ScriptVerifier.cs @@ -56,11 +56,15 @@ public static bool VerifyScript( } // Create spent outputs - var kernelSpentOutputs = spentOutputs.Any() - ? spentOutputs.Select(utxo => utxo.Handle).ToArray() - : null; - - + IntPtr precomputedDataPtr = IntPtr.Zero; + if (spentOutputs.Any()) + { + var kernelSpentOutputs = spentOutputs.Select(utxo => utxo.Handle).ToArray(); + precomputedDataPtr = NativeMethods.PrecomputedTransactionDataCreate( + transaction.Handle, + kernelSpentOutputs, + (nuint)spentOutputs.Count); + } // Verify script // Allocate memory for status byte @@ -71,8 +75,7 @@ public static bool VerifyScript( scriptPubkey.Handle, amount, transaction.Handle, - kernelSpentOutputs, - (nuint)spentOutputs.Count, + precomputedDataPtr, inputIndex, (uint)flags, statusPtr); @@ -95,6 +98,10 @@ public static bool VerifyScript( finally { Marshal.FreeHGlobal(statusPtr); + if (precomputedDataPtr != IntPtr.Zero) + { + NativeMethods.PrecomputedTransactionDataDestroy(precomputedDataPtr); + } } return true; } diff --git a/src/BitcoinKernel.Interop/NativeMethods.cs b/src/BitcoinKernel.Interop/NativeMethods.cs index 128c686..17e7669 100644 --- a/src/BitcoinKernel.Interop/NativeMethods.cs +++ b/src/BitcoinKernel.Interop/NativeMethods.cs @@ -137,6 +137,22 @@ public static extern IntPtr ChainstateManagerGetBlockTreeEntryByHash( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chainstate_manager_get_active_chain")] public static extern IntPtr ChainstateManagerGetActiveChain(IntPtr manager); + /// + /// Gets the block tree entry with the most cumulative proof of work. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chainstate_manager_get_best_entry")] + public static extern IntPtr ChainstateManagerGetBestEntry(IntPtr manager); + + /// + /// Processes and validates a block header. + /// Returns 0 on success. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chainstate_manager_process_block_header")] + public static extern int ChainstateManagerProcessBlockHeader( + IntPtr manager, + IntPtr header, + IntPtr block_validation_state); + /// /// Imports blocks from an array of file paths. /// Returns 0 on success. @@ -240,6 +256,12 @@ public static extern IntPtr BlockRead( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_get_hash")] public static extern IntPtr BlockGetHash(IntPtr block); + /// + /// Gets the block header from a block. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_get_header")] + public static extern IntPtr BlockGetHeader(IntPtr block); + /// /// Serializes the block to bytes. /// @@ -273,6 +295,12 @@ public static extern int BlockToBytes( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_tree_entry_get_previous")] public static extern IntPtr BlockTreeEntryGetPrevious(IntPtr block_tree_entry); + /// + /// Gets the block header from a block tree entry. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_tree_entry_get_block_header")] + public static extern IntPtr BlockTreeEntryGetBlockHeader(IntPtr block_tree_entry); + /// /// Checks if two block tree entries are equal. Two block tree entries are equal when they /// point to the same block. @@ -281,6 +309,18 @@ public static extern int BlockToBytes( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_tree_entry_equals")] public static extern int BlockTreeEntryEquals(IntPtr entry1, IntPtr entry2); + /// + /// Copies a block (reference counted). + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_copy")] + public static extern IntPtr BlockCopy(IntPtr block); + + /// + /// Gets a transaction at the specified index in a block. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_get_transaction_at")] + public static extern IntPtr BlockGetTransactionAt(IntPtr block, nuint index); + #endregion #region BlockHash Operations @@ -311,7 +351,66 @@ public static extern int BlockToBytes( #endregion + #region Block Header Operations + /// + /// Creates a block header from raw serialized data (80 bytes). + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_create")] + public static extern IntPtr BlockHeaderCreate( + byte[] raw_block_header, + UIntPtr raw_block_header_len); + + /// + /// Copies a block header. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_copy")] + public static extern IntPtr BlockHeaderCopy(IntPtr header); + + /// + /// Gets the block hash from a block header. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_get_hash")] + public static extern IntPtr BlockHeaderGetHash(IntPtr header); + + /// + /// Gets the previous block hash from a block header. + /// The returned hash is unowned and only valid for the lifetime of the block header. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_get_prev_hash")] + public static extern IntPtr BlockHeaderGetPrevHash(IntPtr header); + + /// + /// Gets the timestamp from a block header (Unix epoch seconds). + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_get_timestamp")] + public static extern uint BlockHeaderGetTimestamp(IntPtr header); + + /// + /// Gets the nBits difficulty target from a block header (compact format). + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_get_bits")] + public static extern uint BlockHeaderGetBits(IntPtr header); + + /// + /// Gets the version from a block header. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_get_version")] + public static extern int BlockHeaderGetVersion(IntPtr header); + + /// + /// Gets the nonce from a block header. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_get_nonce")] + public static extern uint BlockHeaderGetNonce(IntPtr header); + + /// + /// Destroys a block header. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_header_destroy")] + public static extern void BlockHeaderDestroy(IntPtr header); + + #endregion #region Chain Operations @@ -321,6 +420,19 @@ public static extern int BlockToBytes( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_get_height")] public static extern int ChainGetHeight(IntPtr chain); + /// + /// Gets a block tree entry by height. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_get_by_height")] + public static extern IntPtr ChainGetByHeight(IntPtr chain, int height); + + /// + /// Checks if a block tree entry is in the chain. + /// Returns 1 if in chain, 0 otherwise. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_contains")] + public static extern int ChainContains(IntPtr chain, IntPtr block_tree_entry); + #endregion #region Transaction Operations @@ -410,6 +522,31 @@ public static extern int TransactionToBytes( #endregion + #region PrecomputedTransactionData Operations + + /// + /// Creates precomputed transaction data for script verification. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_precomputed_transaction_data_create")] + public static extern IntPtr PrecomputedTransactionDataCreate( + IntPtr tx_to, + IntPtr[] spent_outputs, + nuint spent_outputs_len); + + /// + /// Copies precomputed transaction data. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_precomputed_transaction_data_copy")] + public static extern IntPtr PrecomputedTransactionDataCopy(IntPtr precomputed_txdata); + + /// + /// Destroys precomputed transaction data. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_precomputed_transaction_data_destroy")] + public static extern void PrecomputedTransactionDataDestroy(IntPtr precomputed_txdata); + + #endregion + #region ScriptPubkey Operations /// @@ -433,8 +570,7 @@ public static extern int ScriptPubkeyVerify( IntPtr script_pubkey, long amount, IntPtr tx_to, - IntPtr[]? spent_outputs, - nuint spent_outputs_len, + IntPtr precomputed_txdata, uint input_index, uint flags, IntPtr status); @@ -517,6 +653,12 @@ public static extern IntPtr LoggingConnectionCreate( #region Block Validation State + /// + /// Creates a new block validation state. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_validation_state_create")] + public static extern IntPtr BlockValidationStateCreate(); + /// /// Gets the validation mode from a block validation state. /// @@ -529,6 +671,18 @@ public static extern IntPtr LoggingConnectionCreate( [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_validation_state_get_block_validation_result")] public static extern BlockValidationResult BlockValidationStateGetBlockValidationResult(IntPtr validation_state); + /// + /// Copies a block validation state. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_validation_state_copy")] + public static extern IntPtr BlockValidationStateCopy(IntPtr validation_state); + + /// + /// Destroys a block validation state. + /// + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_validation_state_destroy")] + public static extern void BlockValidationStateDestroy(IntPtr validation_state); + #endregion #region Txid Operations @@ -595,38 +749,7 @@ public static extern IntPtr LoggingConnectionCreate( #endregion - #region Block Operations (Additional) - /// - /// Copies a block (reference counted). - /// - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_copy")] - public static extern IntPtr BlockCopy(IntPtr block); - - /// - /// Gets a transaction at the specified index in a block. - /// - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_get_transaction_at")] - public static extern IntPtr BlockGetTransactionAt(IntPtr block, nuint index); - - #endregion - - #region Chain Operations (Additional) - - /// - /// Gets a block tree entry by height. - /// - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_get_by_height")] - public static extern IntPtr ChainGetByHeight(IntPtr chain, int height); - - /// - /// Checks if a block tree entry is in the chain. - /// Returns 1 if in chain, 0 otherwise. - /// - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_contains")] - public static extern int ChainContains(IntPtr chain, IntPtr block_tree_entry); - - #endregion #region BlockSpentOutputs Operations diff --git a/tests/BitcoinKernel.Core.Tests/BlockHeaderTests.cs b/tests/BitcoinKernel.Core.Tests/BlockHeaderTests.cs new file mode 100644 index 0000000..59b9e0c --- /dev/null +++ b/tests/BitcoinKernel.Core.Tests/BlockHeaderTests.cs @@ -0,0 +1,371 @@ +using BitcoinKernel.Core; +using BitcoinKernel.Core.Abstractions; +using BitcoinKernel.Core.BlockProcessing; +using BitcoinKernel.Core.Chain; +using BitcoinKernel.Core.Exceptions; +using BitcoinKernel.Interop.Enums; +using Xunit; + +namespace BitcoinKernel.Core.Tests; + +public class BlockHeaderTests : IDisposable +{ + private KernelContext? _context; + private ChainParameters? _chainParams; + private ChainstateManager? _chainstateManager; + private BlockProcessor? _blockProcessor; + private string? _tempDir; + + public void Dispose() + { + _chainstateManager?.Dispose(); + _chainParams?.Dispose(); + _context?.Dispose(); + + if (!string.IsNullOrEmpty(_tempDir) && Directory.Exists(_tempDir)) + { + Directory.Delete(_tempDir, true); + } + } + + private void SetupContext() + { + _chainParams = new ChainParameters(ChainType.REGTEST); + var contextOptions = new KernelContextOptions().SetChainParams(_chainParams); + _context = new KernelContext(contextOptions); + + _tempDir = Path.Combine(Path.GetTempPath(), $"test_blockheader_{Guid.NewGuid()}"); + Directory.CreateDirectory(_tempDir); + + var options = new ChainstateManagerOptions(_context, _tempDir, Path.Combine(_tempDir, "blocks")); + _chainstateManager = new ChainstateManager(_context, _chainParams, options); + _blockProcessor = new BlockProcessor(_chainstateManager); + } + + private void SetupWithBlocks() + { + SetupContext(); + + // Process test blocks + foreach (var rawBlock in ReadBlockData()) + { + using var block = Block.FromBytes(rawBlock); + _chainstateManager!.ProcessBlock(block); + } + } + + private static List ReadBlockData() + { + var blockData = new List(); + var testAssemblyDir = Path.GetDirectoryName(typeof(BlockHeaderTests).Assembly.Location); + var projectDir = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(testAssemblyDir))); + var blockDataFile = Path.Combine(projectDir!, "TestData", "block_data.txt"); + + foreach (var line in File.ReadLines(blockDataFile)) + { + if (!string.IsNullOrWhiteSpace(line)) + { + blockData.Add(Convert.FromHexString(line.Trim())); + } + } + + return blockData; + } + + [Fact] + public void FromBytes_ValidHeader_ShouldCreateBlockHeader() + { + // Get the first 80 bytes from a block (the header) + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + using var header = BlockHeader.FromBytes(headerBytes); + + Assert.NotNull(header); + } + + [Fact] + public void FromBytes_NullData_ShouldThrowArgumentNullException() + { + Assert.Throws(() => BlockHeader.FromBytes(null!)); + } + + [Fact] + public void FromBytes_InvalidLength_ShouldThrowArgumentException() + { + var invalidBytes = new byte[79]; // Wrong length + + var exception = Assert.Throws(() => BlockHeader.FromBytes(invalidBytes)); + Assert.Contains("80 bytes", exception.Message); + } + + [Fact] + public void GetHash_ShouldReturnValidHash() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + using var header = BlockHeader.FromBytes(headerBytes); + var hash = header.GetHash(); + + Assert.NotNull(hash); + Assert.Equal(32, hash.Length); + } + + [Fact] + public void GetPrevHash_ShouldReturnValidHash() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[1].Take(80).ToArray(); // Second block has a previous hash + + using var header = BlockHeader.FromBytes(headerBytes); + var prevHash = header.GetPrevHash(); + + Assert.NotNull(prevHash); + Assert.Equal(32, prevHash.Length); + } + + [Fact] + public void Timestamp_ShouldReturnValidValue() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + using var header = BlockHeader.FromBytes(headerBytes); + var timestamp = header.Timestamp; + + Assert.True(timestamp > 0); + } + + [Fact] + public void Bits_ShouldReturnValidValue() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + using var header = BlockHeader.FromBytes(headerBytes); + var bits = header.Bits; + + Assert.True(bits > 0); + } + + [Fact] + public void Version_ShouldReturnValidValue() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + using var header = BlockHeader.FromBytes(headerBytes); + var version = header.Version; + + Assert.True(version > 0); + } + + [Fact] + public void Nonce_ShouldReturnValidValue() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + using var header = BlockHeader.FromBytes(headerBytes); + var nonce = header.Nonce; + + // Nonce can be any value including 0 + Assert.True(nonce >= 0); + } + + [Fact] + public void Block_GetHeader_ShouldReturnValidHeader() + { + var blockData = ReadBlockData(); + + using var block = Block.FromBytes(blockData[0]); + using var header = block.GetHeader(); + + Assert.NotNull(header); + Assert.True(header.Timestamp > 0); + Assert.True(header.Version > 0); + } + + [Fact] + public void Block_GetHeader_HashShouldMatchBlockHash() + { + var blockData = ReadBlockData(); + + using var block = Block.FromBytes(blockData[0]); + var blockHash = block.GetHash(); + + using var header = block.GetHeader(); + var headerHash = header.GetHash(); + + Assert.Equal(blockHash, headerHash); + } + + [Fact] + public void BlockIndex_GetBlockHeader_ShouldReturnValidHeader() + { + SetupWithBlocks(); + + var chain = _chainstateManager!.GetActiveChain(); + var tip = chain.GetTip(); + + using var header = tip.GetBlockHeader(); + + Assert.NotNull(header); + Assert.True(header.Timestamp > 0); + Assert.True(header.Version > 0); + } + + [Fact] + public void BlockIndex_GetBlockHeader_HashShouldMatchIndexHash() + { + SetupWithBlocks(); + + var chain = _chainstateManager!.GetActiveChain(); + var tip = chain.GetTip(); + var tipHash = tip.GetBlockHash(); + + using var header = tip.GetBlockHeader(); + var headerHash = header.GetHash(); + + Assert.Equal(tipHash, headerHash); + } + + [Fact] + public void ChainstateManager_GetBestBlockIndex_ShouldReturnTip() + { + SetupWithBlocks(); + + var bestIndex = _chainstateManager!.GetBestBlockIndex(); + var chain = _chainstateManager.GetActiveChain(); + var tip = chain.GetTip(); + + Assert.Equal(tip.Height, bestIndex.Height); + Assert.Equal(tip.GetBlockHash(), bestIndex.GetBlockHash()); + } + + [Fact] + public void ChainstateManager_ProcessBlockHeader_ValidHeader_ShouldSucceed() + { + SetupContext(); + + // Process the first block to establish genesis + var blockData = ReadBlockData(); + using var genesisBlock = Block.FromBytes(blockData[0]); + _chainstateManager!.ProcessBlock(genesisBlock); + + // Get the second block's header and process it + var secondBlockHeaderBytes = blockData[1].Take(80).ToArray(); + using var secondHeader = BlockHeader.FromBytes(secondBlockHeaderBytes); + + var success = _chainstateManager.ProcessBlockHeader(secondHeader, out var validationState); + + Assert.True(success); + Assert.Equal(ValidationMode.VALID, validationState.ValidationMode); + + validationState.Dispose(); + } + + [Fact] + public void ChainstateManager_ProcessBlockHeader_InvalidHeader_ShouldFail() + { + SetupContext(); + + // Create an invalid header (all zeros won't be valid) + var invalidHeaderBytes = new byte[80]; + using var invalidHeader = BlockHeader.FromBytes(invalidHeaderBytes); + + var success = _chainstateManager!.ProcessBlockHeader(invalidHeader, out var validationState); + + Assert.False(success); + Assert.NotEqual(ValidationMode.VALID, validationState.ValidationMode); + + validationState.Dispose(); + } + + [Fact] + public void Dispose_ShouldAllowMultipleCalls() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + var header = BlockHeader.FromBytes(headerBytes); + + header.Dispose(); + header.Dispose(); // Should not throw + } + + [Fact] + public void AccessAfterDispose_ShouldThrowObjectDisposedException() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + var header = BlockHeader.FromBytes(headerBytes); + header.Dispose(); + + Assert.Throws(() => header.Timestamp); + } + + [Fact] + public void GetHash_AfterDispose_ShouldThrowObjectDisposedException() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + var header = BlockHeader.FromBytes(headerBytes); + header.Dispose(); + + Assert.Throws(() => header.GetHash()); + } + + [Fact] + public void MultipleHeaders_FromSameBlock_ShouldHaveSameProperties() + { + var blockData = ReadBlockData(); + var headerBytes = blockData[0].Take(80).ToArray(); + + using var header1 = BlockHeader.FromBytes(headerBytes); + using var header2 = BlockHeader.FromBytes(headerBytes); + + Assert.Equal(header1.Version, header2.Version); + Assert.Equal(header1.Timestamp, header2.Timestamp); + Assert.Equal(header1.Bits, header2.Bits); + Assert.Equal(header1.Nonce, header2.Nonce); + Assert.Equal(header1.GetHash(), header2.GetHash()); + } + + [Fact] + public void BlockHeader_PrevHashForGenesisBlock_ShouldBeNonNull() + { + var blockData = ReadBlockData(); + var genesisHeaderBytes = blockData[0].Take(80).ToArray(); + + using var header = BlockHeader.FromBytes(genesisHeaderBytes); + var prevHash = header.GetPrevHash(); + + // Genesis block's previous hash should be readable (32 bytes) + // Note: Regtest genesis may not have all zeros like mainnet + Assert.NotNull(prevHash); + Assert.Equal(32, prevHash.Length); + } + + [Fact] + public void BlockHeader_ChainedHeaders_PrevHashShouldMatchPreviousBlockHash() + { + var blockData = ReadBlockData(); + + // Get first two blocks + var firstHeaderBytes = blockData[0].Take(80).ToArray(); + var secondHeaderBytes = blockData[1].Take(80).ToArray(); + + using var firstHeader = BlockHeader.FromBytes(firstHeaderBytes); + using var secondHeader = BlockHeader.FromBytes(secondHeaderBytes); + + var firstBlockHash = firstHeader.GetHash(); + var secondPrevHash = secondHeader.GetPrevHash(); + + // Second block's prevHash should match first block's hash + Assert.Equal(firstBlockHash, secondPrevHash); + } +} diff --git a/tests/BitcoinKernel.Core.Tests/BlockValidationStateTests.cs b/tests/BitcoinKernel.Core.Tests/BlockValidationStateTests.cs new file mode 100644 index 0000000..3c46c89 --- /dev/null +++ b/tests/BitcoinKernel.Core.Tests/BlockValidationStateTests.cs @@ -0,0 +1,83 @@ +using BitcoinKernel.Core.Abstractions; +using BitcoinKernel.Interop.Enums; +using Xunit; + +namespace BitcoinKernel.Core.Tests; + +public class BlockValidationStateTests +{ + [Fact] + public void Create_ShouldCreateValidState() + { + using var state = new BlockValidationState(); + + Assert.NotNull(state); + } + + [Fact] + public void ValidationMode_NewState_ShouldReturnValid() + { + using var state = new BlockValidationState(); + + var mode = state.ValidationMode; + + Assert.Equal(ValidationMode.VALID, mode); + } + + [Fact] + public void ValidationResult_NewState_ShouldReturnUnset() + { + using var state = new BlockValidationState(); + + var result = state.ValidationResult; + + Assert.Equal(Interop.Enums.BlockValidationResult.UNSET, result); + } + + [Fact] + public void Copy_ShouldCreateIndependentCopy() + { + using var original = new BlockValidationState(); + using var copy = original.Copy(); + + Assert.NotNull(copy); + Assert.Equal(original.ValidationMode, copy.ValidationMode); + Assert.Equal(original.ValidationResult, copy.ValidationResult); + } + + [Fact] + public void Dispose_ShouldAllowMultipleCalls() + { + var state = new BlockValidationState(); + + state.Dispose(); + state.Dispose(); // Should not throw + } + + [Fact] + public void AccessAfterDispose_ShouldThrowObjectDisposedException() + { + var state = new BlockValidationState(); + state.Dispose(); + + Assert.Throws(() => state.ValidationMode); + } + + [Fact] + public void Copy_AfterDispose_ShouldThrowObjectDisposedException() + { + var state = new BlockValidationState(); + state.Dispose(); + + Assert.Throws(() => state.Copy()); + } + + [Fact] + public void ValidationResult_AfterDispose_ShouldThrowObjectDisposedException() + { + var state = new BlockValidationState(); + state.Dispose(); + + Assert.Throws(() => state.ValidationResult); + } +} diff --git a/tests/BitcoinKernel.Core.Tests/ScriptVerificationTests.cs b/tests/BitcoinKernel.Core.Tests/ScriptVerificationTests.cs index 8fee2c7..35b8534 100644 --- a/tests/BitcoinKernel.Core.Tests/ScriptVerificationTests.cs +++ b/tests/BitcoinKernel.Core.Tests/ScriptVerificationTests.cs @@ -93,6 +93,17 @@ public void ScriptVerifyTest_NativeSegwitTransaction_InvalidSegwit() 18393430, 0, expectSuccess: false); } + [Fact] + public void ScriptVerifyTest_TaprootTransaction() + { + VerifyTest( + "5120339ce7e165e67d93adb3fef88a6d4beed33f01fa876f05a225242b82a631abc0", + "01000000000101d1f1c1f8cdf6759167b90f52c9ad358a369f95284e841d7a2536cef31c0549580100000000fdffffff020000000000000000316a2f49206c696b65205363686e6f7272207369677320616e6420492063616e6e6f74206c69652e204062697462756734329e06010000000000225120a37c3903c8d0db6512e2b40b0dffa05e5a3ab73603ce8c9c4b7771e5412328f90140a60c383f71bac0ec919b1d7dbc3eb72dd56e7aa99583615564f9f99b8ae4e837b758773a5b2e4c51348854c8389f008e05029db7f464a5ff2e01d5e6e626174affd30a00", + 88480, 0, expectSuccess: true); + } + + + private void VerifyTest(string scriptPubkeyHex, string transactionHex, long amount, int inputIndex, bool expectSuccess) { // Create array of spent output hex strings (all the same for this test)