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)); + } +}