diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1520b30..00fa6e9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -127,7 +127,7 @@ jobs:
path: native/${{ matrix.rid }}
- name: Setup .NET
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: ${{ matrix.dotnet-version }}
@@ -138,13 +138,13 @@ jobs:
run: dotnet build --configuration Release --no-restore
- name: Test with dotnet
- run: dotnet test --no-restore --logger trx --results-directory "TestResults-${{ matrix.dotnet-version }}"
+ run: dotnet test --no-restore --report-xunit-trx --results-directory "TestResults-${{ matrix.os }}"
- name: Upload dotnet test results
uses: actions/upload-artifact@v7
with:
- name: dotnet-results--${{ matrix.os }}
- path: TestResults--${{ matrix.os }}
+ name: dotnet-results-${{ matrix.os }}
+ path: TestResults-${{ matrix.os }}
if: ${{ always() }}
lint:
@@ -156,7 +156,7 @@ jobs:
uses: actions/checkout@v7
- name: Setup .NET
- uses: actions/setup-dotnet@v5.4.0
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
@@ -175,7 +175,7 @@ jobs:
uses: actions/checkout@v7
- name: Setup .NET
- uses: actions/setup-dotnet@v5.4.0
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
@@ -204,7 +204,7 @@ jobs:
rid: linux-x64
env:
- TEST_VERSION: '0.0.4-alpha.pr14.5'
+ TEST_VERSION: '0.0.4-alpha.pr17.1'
TEST_REPO: 'stringintech/kernel-bindings-tests'
TEST_DIR: '.conformance-tests'
@@ -219,7 +219,7 @@ jobs:
path: native/${{ matrix.rid }}
- name: Setup .NET
- uses: actions/setup-dotnet@v5.4.0
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index 5bb5b73..fd2ebb2 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -16,7 +16,7 @@ jobs:
fetch-depth: 0
- name: Setup .NET
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27e2a51..ebcacfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.32.0]
+
+### Added
+- Bindings for the Bitcoin Core v32 kernel API; CI builds `libbitcoinkernel` from `v32.0rc2` (`bc795e60`)
+- `TransactionInput.GetWitnessStack()` and the new `WitnessStack` type for reading witness items
+- `TransactionInput.GetScriptSig()` returning the raw script sig bytes
+- `ChainParameters.CreateSignet(byte[] challenge)` for signets with a custom challenge
+- `ChainstateManagerOptions.SetDatabaseCacheBytes(ulong)` to size the total database cache (default 450 MiB)
+- `MockTime.Set(...)` and `MockTime.Reset()` to override the kernel clock in tests
+
+### Changed
+- Package version jumps from 0.3.0 to 0.32.0 so the minor version matches the targeted Bitcoin Core release
+- Test project moved from xunit 2.9.3 to xunit.v3 4.0.1; `global.json` opts `dotnet test` into Microsoft.Testing.Platform.
+- Replaced the VSTest-only `coverlet.collector` with `Microsoft.Testing.Extensions.CodeCoverage`
+
## [0.3.0] - 2026-05-26
### Breaking Changes
diff --git a/README.md b/README.md
index 97c4bb7..905d88e 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,9 @@ This library uses [libbitcoinkernel](https://github.com/bitcoin/bitcoin/tree/mas
| Package | Version | Description |
|---------|---------|-------------|
-| **BitcoinKernel** | 0.3.0 | Managed wrappers and native bindings |
+| **BitcoinKernel** | 0.32.0 | Managed wrappers and native bindings |
+
+The minor version tracks the Bitcoin Core release whose kernel API the package binds, so 0.32.x targets Bitcoin Core v32.
```bash
dotnet add package BitcoinKernel
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..3140116
--- /dev/null
+++ b/global.json
@@ -0,0 +1,5 @@
+{
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/native/BITCOIN_COMMIT b/native/BITCOIN_COMMIT
index 9f5a5e1..7039743 100644
--- a/native/BITCOIN_COMMIT
+++ b/native/BITCOIN_COMMIT
@@ -1 +1 @@
-ba48852f9e758df8e67ce5f51c0e3e2b68713ab4
+bc795e60dbb2c6e9c9556949731912429290626a
diff --git a/native/linux-x64/libbitcoinkernel.so b/native/linux-x64/libbitcoinkernel.so
index 2527e43..bd74e70 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 6071804..0498144 100755
Binary files a/native/osx-x64/libbitcoinkernel.dylib and b/native/osx-x64/libbitcoinkernel.dylib differ
diff --git a/src/BitcoinKernel.Interop/NativeMethods.cs b/src/BitcoinKernel.Interop/NativeMethods.cs
index d897c0c..ae57c8d 100644
--- a/src/BitcoinKernel.Interop/NativeMethods.cs
+++ b/src/BitcoinKernel.Interop/NativeMethods.cs
@@ -90,6 +90,14 @@ static NativeMethods()
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_parameters_create")]
public static extern IntPtr ChainParametersCreate(ChainType chain_type);
+ ///
+ /// Creates signet chain parameters with a user-provided challenge.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chain_parameters_create_signet")]
+ public static extern IntPtr ChainParametersCreateSignet(
+ byte[] challenge,
+ nuint challenge_len);
+
///
/// Destroys chain parameters.
///
@@ -224,6 +232,16 @@ public static extern void ChainstateManagerOptionsUpdateChainstateDbInMemory(
IntPtr options,
int chainstate_db_in_memory);
+ ///
+ /// Sets the total database cache in bytes. The kernel splits it between the
+ /// block tree db, the chainstate db and the in-memory coins cache.
+ /// Returns 0 on success, non-zero if the value is rejected.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_chainstate_manager_options_set_database_cache_bytes")]
+ public static extern int ChainstateManagerOptionsSetDatabaseCacheBytes(
+ IntPtr options,
+ ulong database_cache_bytes);
+
#endregion
#region Block Operations
@@ -923,6 +941,54 @@ public static extern IntPtr TransactionSpentOutputsGetCoinAt(
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_input_get_sequence")]
public static extern uint TransactionInputGetSequence(IntPtr transaction_input);
+ ///
+ /// Gets the witness stack of a transaction input.
+ /// The returned stack is unowned and only valid for the lifetime of the input.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_input_get_witness_stack")]
+ public static extern IntPtr TransactionInputGetWitnessStack(IntPtr transaction_input);
+
+ ///
+ /// Serializes the script sig of a transaction input.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_transaction_input_get_script_sig")]
+ public static extern int TransactionInputGetScriptSig(
+ IntPtr transaction_input,
+ WriteBytes writer,
+ IntPtr user_data);
+
+ #endregion
+
+ #region WitnessStack Operations
+
+ ///
+ /// Gets the number of items in a witness stack.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_witness_stack_count_items")]
+ public static extern nuint WitnessStackCountItems(IntPtr witness_stack);
+
+ ///
+ /// Serializes the witness stack item at the specified index.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_witness_stack_get_item_at")]
+ public static extern int WitnessStackGetItemAt(
+ IntPtr witness_stack,
+ nuint index,
+ WriteBytes writer,
+ IntPtr user_data);
+
+ ///
+ /// Copies a witness stack.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_witness_stack_copy")]
+ public static extern IntPtr WitnessStackCopy(IntPtr witness_stack);
+
+ ///
+ /// Destroys a witness stack.
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_witness_stack_destroy")]
+ public static extern void WitnessStackDestroy(IntPtr witness_stack);
+
#endregion
#region TransactionOutPoint Operations
@@ -981,5 +1047,17 @@ public static extern IntPtr TransactionSpentOutputsGetCoinAt(
#endregion
+ #region Testing
+
+ ///
+ /// Overrides the kernel's clock with a fixed Unix timestamp, or restores the
+ /// system clock when timestamp is 0. Affects all kernel time reads globally.
+ /// Returns 0 on success, non-zero if timestamp is outside [0, 4294967295].
+ ///
+ [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "btck_set_mock_time")]
+ public static extern int SetMockTime(long timestamp);
+
+ #endregion
+
}
diff --git a/src/BitcoinKernel/BitcoinKernel.csproj b/src/BitcoinKernel/BitcoinKernel.csproj
index 18808ea..8fb64d4 100644
--- a/src/BitcoinKernel/BitcoinKernel.csproj
+++ b/src/BitcoinKernel/BitcoinKernel.csproj
@@ -8,7 +8,7 @@
BitcoinKernel
- 0.3.0
+ 0.32.0
JanB84
.NET bindings and managed wrappers for libbitcoinkernel. Provides direct access to Bitcoin Core consensus and validation logic with automatic memory management.
MIT
diff --git a/src/BitcoinKernel/Chain/ChainParameters.cs b/src/BitcoinKernel/Chain/ChainParameters.cs
index 52f66ff..cebd746 100644
--- a/src/BitcoinKernel/Chain/ChainParameters.cs
+++ b/src/BitcoinKernel/Chain/ChainParameters.cs
@@ -1,3 +1,4 @@
+using BitcoinKernel.Exceptions;
using BitcoinKernel.Interop;
using BitcoinKernel.Interop.Enums;
@@ -16,6 +17,27 @@ public ChainParameters(ChainType chainType)
throw new InvalidOperationException($"Failed to create chain parameters for chain type: {chainType}. The native library may not be loaded correctly.");
}
+ private ChainParameters(IntPtr handle)
+ {
+ _handle = handle;
+ }
+
+ ///
+ /// Creates signet chain parameters with a custom challenge script.
+ /// Blocks must satisfy the challenge to be valid.
+ ///
+ /// The raw signet challenge script bytes.
+ public static ChainParameters CreateSignet(byte[] challenge)
+ {
+ ArgumentNullException.ThrowIfNull(challenge);
+
+ var handle = NativeMethods.ChainParametersCreateSignet(challenge, (nuint)challenge.Length);
+ if (handle == IntPtr.Zero)
+ throw new ChainParametersException(ChainType.SIGNET, "Failed to create signet chain parameters from the given challenge.");
+
+ return new ChainParameters(handle);
+ }
+
internal IntPtr Handle
{
get
diff --git a/src/BitcoinKernel/Chain/ChainStateManager.cs b/src/BitcoinKernel/Chain/ChainStateManager.cs
index 2604c1a..d083d7a 100644
--- a/src/BitcoinKernel/Chain/ChainStateManager.cs
+++ b/src/BitcoinKernel/Chain/ChainStateManager.cs
@@ -310,6 +310,23 @@ public ChainstateManagerOptions SetChainstateDbInMemory(bool inMemory)
return this;
}
+ ///
+ /// Sets the total database cache in bytes. The kernel splits it between the
+ /// block tree db, the chainstate db and the in-memory coins cache.
+ /// Defaults to 450 MiB when not set. Values below 4 MiB are rejected, and so
+ /// are values above 1 GiB on 32-bit systems.
+ ///
+ public ChainstateManagerOptions SetDatabaseCacheBytes(ulong databaseCacheBytes)
+ {
+ ThrowIfDisposed();
+ int result = NativeMethods.ChainstateManagerOptionsSetDatabaseCacheBytes(_handle, databaseCacheBytes);
+
+ if (result != 0)
+ throw new KernelException($"Failed to set database cache to {databaseCacheBytes} bytes (error code: {result})");
+
+ return this;
+ }
+
private void ThrowIfDisposed()
{
if (_disposed)
diff --git a/src/BitcoinKernel/MockTime.cs b/src/BitcoinKernel/MockTime.cs
new file mode 100644
index 0000000..43bfaa0
--- /dev/null
+++ b/src/BitcoinKernel/MockTime.cs
@@ -0,0 +1,40 @@
+using BitcoinKernel.Exceptions;
+using BitcoinKernel.Interop;
+
+namespace BitcoinKernel;
+
+///
+/// Overrides the clock the kernel reads, for tests. The override is process-wide
+/// and affects every context, so gate its use (for example to regtest) yourself.
+///
+public static class MockTime
+{
+ ///
+ /// Fixes the kernel clock at the given Unix timestamp in seconds.
+ ///
+ /// Between 1 and 4294967295, the largest block header timestamp.
+ public static void Set(long unixSeconds)
+ {
+ if (unixSeconds < 1 || unixSeconds > uint.MaxValue)
+ throw new ArgumentOutOfRangeException(nameof(unixSeconds), $"Mock time must be between 1 and {uint.MaxValue}");
+
+ Apply(unixSeconds);
+ }
+
+ ///
+ /// Fixes the kernel clock at the given time, truncated to whole seconds.
+ ///
+ public static void Set(DateTimeOffset time) => Set(time.ToUnixTimeSeconds());
+
+ ///
+ /// Restores the system clock.
+ ///
+ public static void Reset() => Apply(0);
+
+ private static void Apply(long unixSeconds)
+ {
+ int result = NativeMethods.SetMockTime(unixSeconds);
+ if (result != 0)
+ throw new KernelException($"Failed to set mock time to {unixSeconds} (error code: {result})");
+ }
+}
diff --git a/src/BitcoinKernel/Primitives/TransactionInput.cs b/src/BitcoinKernel/Primitives/TransactionInput.cs
index 2e5d929..604bc9e 100644
--- a/src/BitcoinKernel/Primitives/TransactionInput.cs
+++ b/src/BitcoinKernel/Primitives/TransactionInput.cs
@@ -55,6 +55,47 @@ public OutPoint GetOutPoint()
return new OutPoint(outPointPtr, ownsHandle: false);
}
+ ///
+ /// Gets the witness stack of this input. The returned stack is a
+ /// non-owning view whose lifetime is tied to this input.
+ ///
+ public WitnessStack GetWitnessStack()
+ {
+ ThrowIfDisposed();
+ var witnessPtr = NativeMethods.TransactionInputGetWitnessStack(_handle);
+ if (witnessPtr == IntPtr.Zero)
+ throw new TransactionException("Failed to get witness stack from transaction input");
+
+ return new WitnessStack(witnessPtr, ownsHandle: false);
+ }
+
+ ///
+ /// Gets the raw script sig bytes of this input. Empty for inputs that
+ /// carry no script sig, such as native segwit spends.
+ ///
+ public byte[] GetScriptSig()
+ {
+ ThrowIfDisposed();
+ var bytes = new List();
+ NativeMethods.WriteBytes writer = (data, len, _) =>
+ {
+ // An empty script sig may arrive as a null pointer with len 0.
+ if (len == 0)
+ return 0;
+
+ var buffer = new byte[len];
+ System.Runtime.InteropServices.Marshal.Copy(data, buffer, 0, (int)len);
+ bytes.AddRange(buffer);
+ return 0;
+ };
+
+ int result = NativeMethods.TransactionInputGetScriptSig(_handle, writer, IntPtr.Zero);
+ if (result != 0)
+ throw new TransactionException("Failed to serialize script sig");
+
+ return bytes.ToArray();
+ }
+
///
/// Creates an owned copy of this transaction input.
///
diff --git a/src/BitcoinKernel/Primitives/WitnessStack.cs b/src/BitcoinKernel/Primitives/WitnessStack.cs
new file mode 100644
index 0000000..f10254c
--- /dev/null
+++ b/src/BitcoinKernel/Primitives/WitnessStack.cs
@@ -0,0 +1,126 @@
+using System.Runtime.InteropServices;
+using BitcoinKernel.Exceptions;
+using BitcoinKernel.Interop;
+
+namespace BitcoinKernel.Primitives;
+
+///
+/// Represents the witness stack of a transaction input.
+///
+public sealed class WitnessStack : IDisposable
+{
+ private IntPtr _handle;
+ private bool _disposed;
+ private readonly bool _ownsHandle;
+
+ internal WitnessStack(IntPtr handle, bool ownsHandle = true)
+ {
+ _handle = handle != IntPtr.Zero
+ ? handle
+ : throw new ArgumentException("Invalid witness stack handle", nameof(handle));
+ _ownsHandle = ownsHandle;
+ }
+
+ internal IntPtr Handle
+ {
+ get
+ {
+ ThrowIfDisposed();
+ return _handle;
+ }
+ }
+
+ ///
+ /// Gets the number of items on the witness stack.
+ ///
+ public int Count
+ {
+ get
+ {
+ ThrowIfDisposed();
+ return (int)NativeMethods.WitnessStackCountItems(_handle);
+ }
+ }
+
+ ///
+ /// Gets the raw bytes of the witness stack item at the specified index.
+ ///
+ public byte[] GetItem(int index)
+ {
+ ThrowIfDisposed();
+ if (index < 0 || index >= Count)
+ throw new ArgumentOutOfRangeException(nameof(index));
+
+ var bytes = new List();
+ NativeMethods.WriteBytes writer = (data, len, _) =>
+ {
+ // Empty items (e.g. the CHECKMULTISIG dummy) arrive as a null pointer with len 0.
+ if (len == 0)
+ return 0;
+
+ var buffer = new byte[len];
+ Marshal.Copy(data, buffer, 0, (int)len);
+ bytes.AddRange(buffer);
+ return 0;
+ };
+
+ int result = NativeMethods.WitnessStackGetItemAt(_handle, (nuint)index, writer, IntPtr.Zero);
+ if (result != 0)
+ throw new TransactionException($"Failed to serialize witness stack item at index {index}");
+
+ return bytes.ToArray();
+ }
+
+ ///
+ /// Enumerates the raw bytes of every item on the witness stack, bottom to top.
+ ///
+ public IEnumerable EnumerateItems()
+ {
+ ThrowIfDisposed();
+
+ int count = Count;
+ for (int i = 0; i < count; i++)
+ {
+ yield return GetItem(i);
+ }
+ }
+
+ ///
+ /// Creates an owned copy of this witness stack.
+ ///
+ public WitnessStack Copy()
+ {
+ ThrowIfDisposed();
+ var copy = NativeMethods.WitnessStackCopy(_handle);
+ if (copy == IntPtr.Zero)
+ throw new TransactionException("Failed to copy witness stack");
+
+ return new WitnessStack(copy);
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(WitnessStack));
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ if (_handle != IntPtr.Zero && _ownsHandle)
+ {
+ NativeMethods.WitnessStackDestroy(_handle);
+ _handle = IntPtr.Zero;
+ }
+ _disposed = true;
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ ~WitnessStack()
+ {
+ if (_ownsHandle)
+ Dispose();
+ }
+}
diff --git a/tests/BitcoinKernel.Tests/BitcoinKernel.Tests.csproj b/tests/BitcoinKernel.Tests/BitcoinKernel.Tests.csproj
index 92d1416..bcaa373 100644
--- a/tests/BitcoinKernel.Tests/BitcoinKernel.Tests.csproj
+++ b/tests/BitcoinKernel.Tests/BitcoinKernel.Tests.csproj
@@ -8,13 +8,15 @@
false
true
+
+ Exe
-
-
-
-
+
+
+
+
diff --git a/tests/BitcoinKernel.Tests/ChainParametersTests.cs b/tests/BitcoinKernel.Tests/ChainParametersTests.cs
new file mode 100644
index 0000000..fb005de
--- /dev/null
+++ b/tests/BitcoinKernel.Tests/ChainParametersTests.cs
@@ -0,0 +1,39 @@
+using BitcoinKernel.Chain;
+using Xunit;
+
+namespace BitcoinKernel.Tests;
+
+public class ChainParametersTests
+{
+ // 1-of-2 multisig challenge used by the default public signet.
+ private const string DefaultSignetChallengeHex =
+ "512103ad5e0edad18cb1f0fc0d28a3d4f1f3e445640337489abb10404f2d1e086be430210359ef5021964fe22d6f8e05b2463c9540ce96883fe3b278760f048f5189f2e6c452ae";
+
+ [Fact]
+ public void CreateSignet_WithChallenge_CanBackAContext()
+ {
+ using var chainParams = ChainParameters.CreateSignet(Convert.FromHexString(DefaultSignetChallengeHex));
+ using var options = new KernelContextOptions().SetChainParams(chainParams);
+ using var context = new KernelContext(options);
+
+ Assert.NotNull(context);
+ }
+
+ // The kernel does not parse the challenge when building the parameters; it only
+ // matters once a block's signet solution is checked against it.
+ [Theory]
+ [InlineData("")]
+ [InlineData("ff")]
+ public void CreateSignet_WithEmptyOrNonScriptChallenge_Succeeds(string challengeHex)
+ {
+ using var chainParams = ChainParameters.CreateSignet(Convert.FromHexString(challengeHex));
+
+ Assert.NotNull(chainParams);
+ }
+
+ [Fact]
+ public void CreateSignet_WithNullChallenge_Throws()
+ {
+ Assert.Throws(() => ChainParameters.CreateSignet(null!));
+ }
+}
diff --git a/tests/BitcoinKernel.Tests/ChainstateManagerOptionsTests.cs b/tests/BitcoinKernel.Tests/ChainstateManagerOptionsTests.cs
new file mode 100644
index 0000000..53c55a3
--- /dev/null
+++ b/tests/BitcoinKernel.Tests/ChainstateManagerOptionsTests.cs
@@ -0,0 +1,72 @@
+using BitcoinKernel.Chain;
+using BitcoinKernel.Exceptions;
+using BitcoinKernel.Interop.Enums;
+using Xunit;
+
+namespace BitcoinKernel.Tests;
+
+public class ChainstateManagerOptionsTests : IDisposable
+{
+ private const ulong MiB = 1024 * 1024;
+
+ private readonly ChainParameters _chainParams;
+ private readonly KernelContextOptions _contextOptions;
+ private readonly KernelContext _context;
+ private readonly string _dataDir;
+
+ public ChainstateManagerOptionsTests()
+ {
+ _chainParams = new ChainParameters(ChainType.REGTEST);
+ _contextOptions = new KernelContextOptions().SetChainParams(_chainParams);
+ _context = new KernelContext(_contextOptions);
+ _dataDir = Path.Combine(Path.GetTempPath(), $"btck_options_test_{Guid.NewGuid()}");
+ }
+
+ public void Dispose()
+ {
+ _context.Dispose();
+ _contextOptions.Dispose();
+ _chainParams.Dispose();
+ if (Directory.Exists(_dataDir))
+ Directory.Delete(_dataDir, recursive: true);
+ }
+
+ private ChainstateManagerOptions CreateOptions() =>
+ new(_context, _dataDir, Path.Combine(_dataDir, "blocks"));
+
+ [Fact]
+ public void SetDatabaseCacheBytes_ValidSize_ReturnsSameInstance()
+ {
+ using var options = CreateOptions();
+
+ Assert.Same(options, options.SetDatabaseCacheBytes(64 * MiB));
+ }
+
+ [Fact]
+ public void SetDatabaseCacheBytes_AtMinimum_IsAccepted()
+ {
+ using var options = CreateOptions();
+
+ options.SetDatabaseCacheBytes(4 * MiB);
+ }
+
+ [Fact]
+ public void SetDatabaseCacheBytes_OptionsCreateChainstateManager()
+ {
+ using var options = CreateOptions()
+ .SetDatabaseCacheBytes(16 * MiB)
+ .SetBlockTreeDbInMemory(true)
+ .SetChainstateDbInMemory(true);
+ using var manager = new ChainstateManager(_context, _chainParams, options);
+
+ Assert.Equal(0, manager.GetActiveChain().Height);
+ }
+
+ [Fact]
+ public void SetDatabaseCacheBytes_BelowMinimum_Throws()
+ {
+ using var options = CreateOptions();
+
+ Assert.Throws(() => options.SetDatabaseCacheBytes(4 * MiB - 1));
+ }
+}
diff --git a/tests/BitcoinKernel.Tests/MockTimeTests.cs b/tests/BitcoinKernel.Tests/MockTimeTests.cs
new file mode 100644
index 0000000..ef81707
--- /dev/null
+++ b/tests/BitcoinKernel.Tests/MockTimeTests.cs
@@ -0,0 +1,84 @@
+using BitcoinKernel.Chain;
+using BitcoinKernel.Interop.Enums;
+using BitcoinKernel.Primitives;
+using Xunit;
+
+namespace BitcoinKernel.Tests;
+
+///
+/// Mock time is process-wide, so these tests must not overlap with any other
+/// test that validates blocks or headers.
+///
+[CollectionDefinition(nameof(MockTimeCollection), DisableParallelization = true)]
+public class MockTimeCollection;
+
+[Collection(nameof(MockTimeCollection))]
+public class MockTimeTests
+{
+ // Before the 2024-04-27 timestamp of the first test block.
+ private const long BeforeTestBlocks = 1_700_000_000;
+
+ [Fact]
+ public void SetAndReset_ValidTimestamp_Succeeds()
+ {
+ try
+ {
+ MockTime.Set(BeforeTestBlocks);
+ MockTime.Set(DateTimeOffset.FromUnixTimeSeconds(uint.MaxValue));
+ }
+ finally
+ {
+ MockTime.Reset();
+ }
+ }
+
+ [Theory]
+ [InlineData(0L)]
+ [InlineData(-1L)]
+ [InlineData((long)uint.MaxValue + 1)]
+ public void Set_OutOfRange_Throws(long unixSeconds)
+ {
+ Assert.Throws(() => MockTime.Set(unixSeconds));
+ }
+
+ [Fact]
+ public void Set_BeforeHeaderTimestamp_RejectsHeaderAsTimeFuture()
+ {
+ var dataDir = Path.Combine(Path.GetTempPath(), $"test_mocktime_{Guid.NewGuid()}");
+ try
+ {
+ using var chainParams = new ChainParameters(ChainType.REGTEST);
+ using var contextOptions = new KernelContextOptions().SetChainParams(chainParams);
+ using var context = new KernelContext(contextOptions);
+ using var options = new ChainstateManagerOptions(context, dataDir, Path.Combine(dataDir, "blocks"));
+ using var manager = new ChainstateManager(context, chainParams, options);
+ using var header = BlockHeader.FromBytes(ReadFirstBlock().Take(80).ToArray());
+
+ MockTime.Set(BeforeTestBlocks);
+ Assert.False(manager.ProcessBlockHeader(header, out var rejected));
+ using (rejected)
+ {
+ Assert.Equal(BlockValidationResult.TIME_FUTURE, rejected.ValidationResult);
+ }
+
+ MockTime.Reset();
+ Assert.True(manager.ProcessBlockHeader(header, out var accepted));
+ accepted.Dispose();
+ }
+ finally
+ {
+ MockTime.Reset();
+ if (Directory.Exists(dataDir))
+ Directory.Delete(dataDir, recursive: true);
+ }
+ }
+
+ private static byte[] ReadFirstBlock()
+ {
+ var testAssemblyDir = Path.GetDirectoryName(typeof(MockTimeTests).Assembly.Location);
+ var projectDir = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(testAssemblyDir)));
+ var blockDataFile = Path.Combine(projectDir!, "TestData", "block_data.txt");
+
+ return Convert.FromHexString(File.ReadLines(blockDataFile).First(l => !string.IsNullOrWhiteSpace(l)).Trim());
+ }
+}
diff --git a/tests/BitcoinKernel.Tests/TransactionInputTests.cs b/tests/BitcoinKernel.Tests/TransactionInputTests.cs
new file mode 100644
index 0000000..183fa12
--- /dev/null
+++ b/tests/BitcoinKernel.Tests/TransactionInputTests.cs
@@ -0,0 +1,137 @@
+using BitcoinKernel.Primitives;
+using Xunit;
+
+namespace BitcoinKernel.Tests;
+
+public class TransactionInputTests
+{
+ // P2PKH spend: 107-byte script sig, no witness.
+ private const string LegacyTxHex =
+ "02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb088ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700";
+
+ // P2WSH 2-of-3 multisig spend: empty script sig, witness of
+ // [dummy, sig, sig, witness script].
+ private const string SegwitTxHex =
+ "010000000001011f97548fbbe7a0db7588a66e18d803d0089315aa7d4cc28360b6ec50ef36718a0100000000ffffffff02df1776000000000017a9146c002a686959067f4866b8fb493ad7970290ab728757d29f0000000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004730440220565d170eed95ff95027a69b313758450ba84a01224e1f7f130dda46e94d13f8602207bdd20e307f062594022f12ed5017bbf4a055a06aea91c10110a0e3bb23117fc014730440220647d2dc5b15f60bc37dc42618a370b2a1490293f9e5c8464f53ec4fe1dfe067302203598773895b4b16d37485cbe21b337f4e4b650739880098c592553add7dd4355016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000";
+
+ // P2SH-wrapped P2WPKH spend: script sig pushes the witness program, witness
+ // holds [sig, pubkey].
+ private const string P2shSegwitTxHex =
+ "01000000000101d9fd94d0ff0026d307c994d0003180a5f248146efb6371d040c5973f5f66d9df0400000017160014b31b31a6cb654cfab3c50567bcf124f48a0beaecffffffff012cbd1c000000000017a914233b74bf0823fa58bbbd26dfc3bb4ae715547167870247304402206f60569cac136c114a58aedd80f6fa1c51b49093e7af883e605c212bdafcd8d202200e91a55f408a021ad2631bc29a67bd6915b2d7e9ef0265627eabd7f7234455f6012103e7e802f50344303c76d12c089c8724c1b230e3b745693bbe16aad536293d15e300000000";
+
+ [Fact]
+ public void GetScriptSig_LegacyInput_ReturnsScriptSig()
+ {
+ using var tx = Transaction.FromHex(LegacyTxHex);
+ using var input = tx.GetInputAt(0);
+
+ var scriptSig = input.GetScriptSig();
+
+ Assert.Equal(0x6b, scriptSig.Length);
+ Assert.Equal(
+ "483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617",
+ Convert.ToHexString(scriptSig).ToLowerInvariant());
+ }
+
+ [Fact]
+ public void GetScriptSig_SegwitInput_ReturnsEmpty()
+ {
+ using var tx = Transaction.FromHex(SegwitTxHex);
+ using var input = tx.GetInputAt(0);
+
+ Assert.Empty(input.GetScriptSig());
+ }
+
+ [Fact]
+ public void GetWitnessStack_LegacyInput_IsEmpty()
+ {
+ using var tx = Transaction.FromHex(LegacyTxHex);
+ using var input = tx.GetInputAt(0);
+ using var witness = input.GetWitnessStack();
+
+ Assert.Equal(0, witness.Count);
+ Assert.Empty(witness.EnumerateItems());
+ }
+
+ [Fact]
+ public void GetWitnessStack_SegwitInput_ReturnsItems()
+ {
+ using var tx = Transaction.FromHex(SegwitTxHex);
+ using var input = tx.GetInputAt(0);
+ using var witness = input.GetWitnessStack();
+
+ Assert.Equal(4, witness.Count);
+
+ var items = witness.EnumerateItems().ToList();
+ Assert.Empty(items[0]);
+ Assert.Equal(0x47, items[1].Length);
+ Assert.Equal(0x30, items[1][0]);
+ Assert.Equal(0x47, items[2].Length);
+ Assert.Equal(0x69, items[3].Length);
+ Assert.Equal(0xae, items[3][^1]); // OP_CHECKMULTISIG
+ }
+
+ [Fact]
+ public void P2shSegwitInput_HasScriptSigAndWitness()
+ {
+ using var tx = Transaction.FromHex(P2shSegwitTxHex);
+ using var input = tx.GetInputAt(0);
+ using var witness = input.GetWitnessStack();
+
+ Assert.Equal(
+ "160014b31b31a6cb654cfab3c50567bcf124f48a0beaec",
+ Convert.ToHexString(input.GetScriptSig()).ToLowerInvariant());
+
+ Assert.Equal(2, witness.Count);
+ Assert.Equal(0x47, witness.GetItem(0).Length);
+ Assert.Equal(
+ "03e7e802f50344303c76d12c089c8724c1b230e3b745693bbe16aad536293d15e3",
+ Convert.ToHexString(witness.GetItem(1)).ToLowerInvariant());
+ }
+
+ [Fact]
+ public void WitnessStack_AfterDispose_Throws()
+ {
+ using var tx = Transaction.FromHex(SegwitTxHex);
+ using var input = tx.GetInputAt(0);
+ var witness = input.GetWitnessStack();
+
+ witness.Dispose();
+ witness.Dispose(); // second call is a no-op
+
+ Assert.Throws(() => witness.Count);
+ Assert.Throws(() => witness.GetItem(0));
+ Assert.Throws(() => witness.Copy());
+ }
+
+ [Fact]
+ public void GetWitnessStack_GetItem_OutOfRange_Throws()
+ {
+ using var tx = Transaction.FromHex(SegwitTxHex);
+ using var input = tx.GetInputAt(0);
+ using var witness = input.GetWitnessStack();
+
+ Assert.Throws(() => witness.GetItem(-1));
+ Assert.Throws(() => witness.GetItem(4));
+ }
+
+ [Fact]
+ public void WitnessStack_Copy_OutlivesTransaction()
+ {
+ WitnessStack copy;
+ byte[] lastItem;
+ using (var tx = Transaction.FromHex(SegwitTxHex))
+ using (var input = tx.GetInputAt(0))
+ using (var witness = input.GetWitnessStack())
+ {
+ copy = witness.Copy();
+ lastItem = witness.GetItem(3);
+ }
+
+ using (copy)
+ {
+ Assert.Equal(4, copy.Count);
+ Assert.Equal(lastItem, copy.GetItem(3));
+ }
+ }
+}