Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified native/linux-x64/libbitcoinkernel.so
Binary file not shown.
Binary file modified native/osx-x64/libbitcoinkernel.dylib
Binary file not shown.
48 changes: 47 additions & 1 deletion src/BitcoinKernel.Core/BlockProcessing/BlockTreeEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace BitcoinKernel.Core.BlockProcessing;
/// <summary>
/// Represents an entry in the block tree (block index).
/// </summary>
public sealed class BlockTreeEntry
public sealed class BlockTreeEntry : IEquatable<BlockTreeEntry>
{
private readonly IntPtr _handle;

Expand Down Expand Up @@ -49,4 +49,50 @@ public int GetHeight()
{
return NativeMethods.BlockTreeEntryGetHeight(_handle);
}

/// <summary>
/// Determines whether two block tree entries are equal.
/// Two block tree entries are equal when they point to the same block.
/// </summary>
public bool Equals(BlockTreeEntry? other)
{
if (other is null)
return false;
if (ReferenceEquals(this, other))
return true;
return NativeMethods.BlockTreeEntryEquals(_handle, other._handle) == 1;
}

/// <inheritdoc/>
public override bool Equals(object? obj) => Equals(obj as BlockTreeEntry);

/// <inheritdoc/>
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);
}

/// <summary>
/// Determines whether two block tree entries are equal.
/// </summary>
public static bool operator ==(BlockTreeEntry? left, BlockTreeEntry? right)
{
if (left is null)
return right is null;
return left.Equals(right);
}

/// <summary>
/// Determines whether two block tree entries are not equal.
/// </summary>
public static bool operator !=(BlockTreeEntry? left, BlockTreeEntry? right) => !(left == right);
}
8 changes: 7 additions & 1 deletion src/BitcoinKernel.Interop/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);


/// <summary>
/// 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.
/// </summary>
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_block_tree_entry_equals")]
public static extern int BlockTreeEntryEquals(IntPtr entry1, IntPtr entry2);

#endregion

Expand Down
152 changes: 152 additions & 0 deletions tests/BitcoinKernel.Core.Tests/BlockTreeEntryTests.cs
Original file line number Diff line number Diff line change
@@ -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<byte[]> ReadBlockData()
{
var blockData = new List<byte[]>();
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));
}
}
Loading