diff --git a/native/linux-x64/libbitcoinkernel.so b/native/linux-x64/libbitcoinkernel.so
index 286b492..948ec97 100755
Binary files a/native/linux-x64/libbitcoinkernel.so and b/native/linux-x64/libbitcoinkernel.so differ
diff --git a/native/osx-x64/libbitcoinkernel.dylib b/native/osx-x64/libbitcoinkernel.dylib
index a1143f5..638aac4 100755
Binary files a/native/osx-x64/libbitcoinkernel.dylib and b/native/osx-x64/libbitcoinkernel.dylib differ
diff --git a/src/BitcoinKernel.Core/BlockProcessing/BlockTreeEntry.cs b/src/BitcoinKernel.Core/BlockProcessing/BlockTreeEntry.cs
index a638c65..4f481f2 100644
--- a/src/BitcoinKernel.Core/BlockProcessing/BlockTreeEntry.cs
+++ b/src/BitcoinKernel.Core/BlockProcessing/BlockTreeEntry.cs
@@ -7,7 +7,7 @@ namespace BitcoinKernel.Core.BlockProcessing;
///
/// Represents an entry in the block tree (block index).
///
-public sealed class BlockTreeEntry
+public sealed class BlockTreeEntry : IEquatable
{
private readonly IntPtr _handle;
@@ -49,4 +49,50 @@ public int GetHeight()
{
return NativeMethods.BlockTreeEntryGetHeight(_handle);
}
+
+ ///
+ /// Determines whether two block tree entries are equal.
+ /// Two block tree entries are equal when they point to the same block.
+ ///
+ public bool Equals(BlockTreeEntry? other)
+ {
+ if (other is null)
+ return false;
+ if (ReferenceEquals(this, other))
+ return true;
+ return NativeMethods.BlockTreeEntryEquals(_handle, other._handle) == 1;
+ }
+
+ ///
+ public override bool Equals(object? obj) => Equals(obj as BlockTreeEntry);
+
+ ///
+ public override int GetHashCode()
+ {
+ // Use the block hash bytes to compute hash code
+ // Read directly from native without wrapping in a BlockHash that would dispose
+ var hashPtr = NativeMethods.BlockTreeEntryGetBlockHash(_handle);
+ if (hashPtr == IntPtr.Zero)
+ {
+ return 0;
+ }
+ var hashBytes = new byte[32];
+ NativeMethods.BlockHashToBytes(hashPtr, hashBytes);
+ return BitConverter.ToInt32(hashBytes, 0);
+ }
+
+ ///
+ /// Determines whether two block tree entries are equal.
+ ///
+ public static bool operator ==(BlockTreeEntry? left, BlockTreeEntry? right)
+ {
+ if (left is null)
+ return right is null;
+ return left.Equals(right);
+ }
+
+ ///
+ /// Determines whether two block tree entries are not equal.
+ ///
+ public static bool operator !=(BlockTreeEntry? left, BlockTreeEntry? right) => !(left == right);
}
\ No newline at end of file
diff --git a/src/BitcoinKernel.Interop/NativeMethods.cs b/src/BitcoinKernel.Interop/NativeMethods.cs
index 96ffc22..128c686 100644
--- a/src/BitcoinKernel.Interop/NativeMethods.cs
+++ b/src/BitcoinKernel.Interop/NativeMethods.cs
@@ -273,7 +273,13 @@ 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);
-
+ ///
+ /// Checks if two block tree entries are equal. Two block tree entries are equal when they
+ /// point to the same block.
+ /// Returns 1 if equal, 0 otherwise.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_tree_entry_equals")]
+ public static extern int BlockTreeEntryEquals(IntPtr entry1, IntPtr entry2);
#endregion
diff --git a/tests/BitcoinKernel.Core.Tests/BlockTreeEntryTests.cs b/tests/BitcoinKernel.Core.Tests/BlockTreeEntryTests.cs
new file mode 100644
index 0000000..729f7c2
--- /dev/null
+++ b/tests/BitcoinKernel.Core.Tests/BlockTreeEntryTests.cs
@@ -0,0 +1,152 @@
+using BitcoinKernel.Core.BlockProcessing;
+using BitcoinKernel.Core.Chain;
+using BitcoinKernel.Interop.Enums;
+
+
+namespace BitcoinKernel.Core.Tests;
+
+public class BlockTreeEntryTests : 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 SetupWithBlocks()
+ {
+ _chainParams = new ChainParameters(ChainType.REGTEST);
+ var contextOptions = new KernelContextOptions().SetChainParams(_chainParams);
+ _context = new KernelContext(contextOptions);
+
+ _tempDir = Path.Combine(Path.GetTempPath(), $"test_blocktreeentry_{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);
+
+ // Process test blocks
+ foreach (var rawBlock in ReadBlockData())
+ {
+ using var block = Abstractions.Block.FromBytes(rawBlock);
+ _chainstateManager.ProcessBlock(block);
+ }
+ }
+
+ private static List ReadBlockData()
+ {
+ var blockData = new List();
+ var testAssemblyDir = Path.GetDirectoryName(typeof(BlockTreeEntryTests).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 Equals_SameBlock_ReturnsTrue()
+ {
+ SetupWithBlocks();
+ var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash();
+
+ var entry1 = _blockProcessor!.GetBlockTreeEntry(tipHash);
+ var entry2 = _blockProcessor.GetBlockTreeEntry(tipHash);
+
+ Assert.True(entry1!.Equals(entry2));
+ }
+
+ [Fact]
+ public void Equals_DifferentBlocks_ReturnsFalse()
+ {
+ SetupWithBlocks();
+ var chain = _chainstateManager!.GetActiveChain();
+
+ var tipEntry = _blockProcessor!.GetBlockTreeEntry(chain.GetTip().GetBlockHash());
+ var genesisEntry = _blockProcessor.GetBlockTreeEntry(chain.GetBlockByHeight(0)!.GetBlockHash());
+
+ Assert.False(tipEntry!.Equals(genesisEntry));
+ }
+
+ [Fact]
+ public void Equals_WithNull_ReturnsFalse()
+ {
+ SetupWithBlocks();
+ var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash();
+
+ var entry = _blockProcessor!.GetBlockTreeEntry(tipHash);
+
+ Assert.False(entry!.Equals(null));
+ }
+
+ [Fact]
+ public void GetHashCode_EqualEntries_ReturnsSameHashCode()
+ {
+ SetupWithBlocks();
+ var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash();
+
+ var entry1 = _blockProcessor!.GetBlockTreeEntry(tipHash);
+ var entry2 = _blockProcessor.GetBlockTreeEntry(tipHash);
+
+ Assert.Equal(entry1!.GetHashCode(), entry2!.GetHashCode());
+ }
+
+ [Fact]
+ public void OperatorEquals_SameBlock_ReturnsTrue()
+ {
+ SetupWithBlocks();
+ var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash();
+
+ var entry1 = _blockProcessor!.GetBlockTreeEntry(tipHash);
+ var entry2 = _blockProcessor.GetBlockTreeEntry(tipHash);
+
+ Assert.True(entry1 == entry2);
+ }
+
+ [Fact]
+ public void OperatorNotEquals_DifferentBlocks_ReturnsTrue()
+ {
+ SetupWithBlocks();
+ var chain = _chainstateManager!.GetActiveChain();
+
+ var tipEntry = _blockProcessor!.GetBlockTreeEntry(chain.GetTip().GetBlockHash());
+ var genesisEntry = _blockProcessor.GetBlockTreeEntry(chain.GetBlockByHeight(0)!.GetBlockHash());
+
+ Assert.True(tipEntry != genesisEntry);
+ }
+
+ [Fact]
+ public void GetPrevious_EqualEntries_ReturnEqualPrevious()
+ {
+ SetupWithBlocks();
+ var tipHash = _chainstateManager!.GetActiveChain().GetTip().GetBlockHash();
+
+ var entry1 = _blockProcessor!.GetBlockTreeEntry(tipHash);
+ var entry2 = _blockProcessor.GetBlockTreeEntry(tipHash);
+
+ var prev1 = entry1!.GetPrevious();
+ var prev2 = entry2!.GetPrevious();
+
+ Assert.True(prev1!.Equals(prev2));
+ }
+}