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)