diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1264b21..c550f26 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -96,7 +96,7 @@ jobs:
platform: linux_amd64
env:
- TEST_VERSION: '0.0.2'
+ TEST_VERSION: '0.0.3-alpha.4'
TEST_REPO: 'stringintech/kernel-bindings-tests'
TEST_DIR: '.conformance-tests'
diff --git a/src/BitcoinKernel.Core/ScriptVerification/PrecomputedTransactionData.cs b/src/BitcoinKernel.Core/ScriptVerification/PrecomputedTransactionData.cs
new file mode 100644
index 0000000..19d6ebd
--- /dev/null
+++ b/src/BitcoinKernel.Core/ScriptVerification/PrecomputedTransactionData.cs
@@ -0,0 +1,69 @@
+using BitcoinKernel.Core.Abstractions;
+using BitcoinKernel.Core.Exceptions;
+using BitcoinKernel.Interop;
+
+namespace BitcoinKernel.Core.ScriptVerification;
+
+///
+/// Holds precomputed transaction data used to accelerate repeated script verification
+/// across multiple inputs of the same transaction.
+/// Required when btck_ScriptVerificationFlags_TAPROOT is set.
+///
+public sealed class PrecomputedTransactionData : IDisposable
+{
+ private IntPtr _handle;
+ private bool _disposed;
+
+ ///
+ /// Creates precomputed transaction data for the given transaction.
+ ///
+ /// The transaction being verified.
+ ///
+ /// The outputs being spent by the transaction inputs. Required when the TAPROOT
+ /// verification flag is set. Must match the transaction input count if provided.
+ ///
+ public PrecomputedTransactionData(Transaction transaction, IReadOnlyList? spentOutputs = null)
+ {
+ ArgumentNullException.ThrowIfNull(transaction);
+
+ IntPtr[] handles = spentOutputs is { Count: > 0 }
+ ? spentOutputs.Select(o => o.Handle).ToArray()
+ : Array.Empty();
+
+ _handle = NativeMethods.PrecomputedTransactionDataCreate(
+ transaction.Handle,
+ handles,
+ (nuint)handles.Length);
+
+ if (_handle == IntPtr.Zero)
+ throw new KernelException("Failed to create precomputed transaction data");
+ }
+
+ internal IntPtr Handle
+ {
+ get
+ {
+ ThrowIfDisposed();
+ return _handle;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed && _handle != IntPtr.Zero)
+ {
+ NativeMethods.PrecomputedTransactionDataDestroy(_handle);
+ _handle = IntPtr.Zero;
+ _disposed = true;
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ ~PrecomputedTransactionData() => Dispose();
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(PrecomputedTransactionData));
+ }
+}
diff --git a/src/BitcoinKernel.Core/ScriptVerification/ScriptVerifier.cs b/src/BitcoinKernel.Core/ScriptVerification/ScriptVerifier.cs
index 68a2c9e..ca63fbc 100644
--- a/src/BitcoinKernel.Core/ScriptVerification/ScriptVerifier.cs
+++ b/src/BitcoinKernel.Core/ScriptVerification/ScriptVerifier.cs
@@ -15,6 +15,55 @@ namespace BitcoinKernel.Core.ScriptVerification;
public static class ScriptVerifier
{
+ ///
+ /// Verifies a script pubkey using externally-managed precomputed transaction data.
+ /// Use this overload when the is created
+ /// separately and reused across multiple inputs of the same transaction.
+ ///
+ /// The output script to verify against.
+ /// The amount of the output being spent.
+ /// The transaction containing the input to verify.
+ /// Optional externally-managed precomputed transaction data. Required for Taproot.
+ /// The index of the transaction input to verify.
+ /// Script verification flags to use.
+ /// True if the script is valid, false if invalid.
+ /// Thrown when verification fails with an error status.
+ public static bool VerifyScript(
+ ScriptPubKey scriptPubkey,
+ long amount,
+ Transaction transaction,
+ PrecomputedTransactionData? precomputedTxData,
+ uint inputIndex,
+ ScriptVerificationFlags flags = ScriptVerificationFlags.All)
+ {
+ IntPtr precomputedPtr = precomputedTxData?.Handle ?? IntPtr.Zero;
+
+ IntPtr statusPtr = Marshal.AllocHGlobal(1);
+ try
+ {
+ int result = NativeMethods.ScriptPubkeyVerify(
+ scriptPubkey.Handle,
+ amount,
+ transaction.Handle,
+ precomputedPtr,
+ inputIndex,
+ (uint)flags,
+ statusPtr);
+
+ byte statusCode = Marshal.ReadByte(statusPtr);
+ var status = (ScriptVerifyStatus)statusCode;
+
+ if (status != ScriptVerifyStatus.OK)
+ throw new ScriptVerificationException(status, $"Script verification failed: {status}");
+
+ return result != 0;
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(statusPtr);
+ }
+ }
+
///
/// Verifies a script pubkey against a transaction input, throwing an exception on error.
///
diff --git a/tools/kernel-bindings-test-handler/Handlers/MethodDispatcher.cs b/tools/kernel-bindings-test-handler/Handlers/MethodDispatcher.cs
new file mode 100644
index 0000000..08749a0
--- /dev/null
+++ b/tools/kernel-bindings-test-handler/Handlers/MethodDispatcher.cs
@@ -0,0 +1,427 @@
+using BitcoinKernel.Core;
+using BitcoinKernel.Core.Abstractions;
+using BitcoinKernel.Core.Chain;
+using BitcoinKernel.Core.Exceptions;
+using BitcoinKernel.Core.ScriptVerification;
+using BitcoinKernel.Interop.Enums;
+using BitcoinKernel.TestHandler.Protocol;
+using BitcoinKernel.TestHandler.Registry;
+
+namespace BitcoinKernel.TestHandler.Handlers;
+
+///
+/// Routes all incoming method calls to the appropriate handler and manages the object registry.
+/// Uses BitcoinKernel.Core managed types throughout.
+///
+public sealed class MethodDispatcher : IDisposable
+{
+ private readonly ObjectRegistry _registry = new();
+ private bool _disposed;
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ _registry.Dispose();
+ }
+ }
+
+ // ── Helpers ───────────────────────────────────────────────────────────────
+
+ private T Get(string refName) => _registry.Get(refName);
+
+ private T GetVal(string refName) => _registry.Get>(refName).Value;
+
+ private static Response RefError(string id) => Responses.EmptyError(id);
+
+ // ── Context ───────────────────────────────────────────────────────────────
+
+ public Response ContextCreate(string id, string? refName, BtckContextCreateParams p)
+ {
+ if (refName == null) return RefError(id);
+
+ try
+ {
+ var chainType = ParseChainType(p.ChainParameters?.ChainType ?? string.Empty);
+ using var chainParams = new ChainParameters(chainType);
+ using var options = new KernelContextOptions().SetChainParams(chainParams);
+ var context = new KernelContext(options);
+ _registry.Register(refName, context);
+ return Responses.Ref(id, refName);
+ }
+ catch
+ {
+ return Responses.EmptyError(id);
+ }
+ }
+
+ public Response ContextDestroy(string id, BtckContextDestroyParams p)
+ {
+ if (p.Context?.Ref is { } r) _registry.Destroy(r);
+ return Responses.Null(id);
+ }
+
+ // ── Chainstate Manager ────────────────────────────────────────────────────
+
+ public Response ChainstateManagerCreate(string id, string? refName, BtckChainstateManagerCreateParams p)
+ {
+ if (refName == null) return RefError(id);
+ if (p.Context?.Ref is not { } ctxRef) return RefError(id);
+
+ try
+ {
+ var context = Get(ctxRef);
+
+ var tempDir = Path.Combine(Path.GetTempPath(), $"btck_handler_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+ var blocksDir = Path.Combine(tempDir, "blocks");
+ Directory.CreateDirectory(blocksDir);
+
+ using var chainParams = new ChainParameters(ChainType.REGTEST);
+ using var managerOptions = new ChainstateManagerOptions(context, tempDir, blocksDir)
+ .SetBlockTreeDbInMemory(true)
+ .SetChainstateDbInMemory(true);
+
+ var manager = new ChainstateManager(context, chainParams, managerOptions);
+ _registry.Register(refName, new ChainstateManagerWithTempDir(manager, tempDir));
+ return Responses.Ref(id, refName);
+ }
+ catch
+ {
+ return Responses.EmptyError(id);
+ }
+ }
+
+ public Response ChainstateManagerGetActiveChain(string id, string? refName, BtckChainstateManagerGetActiveChainParams p)
+ {
+ if (refName == null) return RefError(id);
+ if (p.ChainstateManager?.Ref is not { } csmRef) return RefError(id);
+
+ var manager = Get(csmRef).Manager;
+ var chain = manager.GetActiveChain();
+ _registry.Register(refName, new NonOwningRef(chain));
+ return Responses.Ref(id, refName);
+ }
+
+ public Response ChainstateManagerProcessBlock(string id, BtckChainstateManagerProcessBlockParams p)
+ {
+ if (p.ChainstateManager?.Ref is not { } csmRef) return RefError(id);
+ if (p.Block?.Ref is not { } blockRef) return RefError(id);
+
+ try
+ {
+ var manager = Get(csmRef).Manager;
+ var block = Get(blockRef);
+ bool isNew = manager.ProcessBlock(block);
+ return Responses.Ok(id, new { new_block = isNew });
+ }
+ catch
+ {
+ return Responses.EmptyError(id);
+ }
+ }
+
+ public Response ChainstateManagerDestroy(string id, BtckChainstateManagerDestroyParams p)
+ {
+ if (p.ChainstateManager?.Ref is { } r) _registry.Destroy(r);
+ return Responses.Null(id);
+ }
+
+ // ── Chain ─────────────────────────────────────────────────────────────────
+
+ public Response ChainGetHeight(string id, BtckChainGetHeightParams p)
+ {
+ if (p.Chain?.Ref is not { } chainRef) return RefError(id);
+ return Responses.Ok(id, GetVal(chainRef).Height);
+ }
+
+ public Response ChainGetByHeight(string id, string? refName, BtckChainGetByHeightParams p)
+ {
+ if (refName == null) return RefError(id);
+ if (p.Chain?.Ref is not { } chainRef) return RefError(id);
+
+ var blockIndex = GetVal(chainRef).GetBlockByHeight(p.BlockHeight);
+ if (blockIndex == null) return Responses.EmptyError(id);
+
+ _registry.Register(refName, new NonOwningRef(blockIndex));
+ return Responses.Ref(id, refName);
+ }
+
+ public Response ChainContains(string id, BtckChainContainsParams p)
+ {
+ if (p.Chain?.Ref is not { } chainRef) return RefError(id);
+ if (p.BlockTreeEntry?.Ref is not { } bteRef) return RefError(id);
+
+ bool contains = GetVal(chainRef).Contains(GetVal(bteRef));
+ return Responses.Ok(id, contains);
+ }
+
+ // ── Block ─────────────────────────────────────────────────────────────────
+
+ public Response BlockCreate(string id, string? refName, BtckBlockCreateParams p)
+ {
+ if (refName == null) return RefError(id);
+
+ try
+ {
+ var block = Block.FromBytes(Convert.FromHexString(p.RawBlock));
+ _registry.Register(refName, block);
+ return Responses.Ref(id, refName);
+ }
+ catch
+ {
+ return Responses.EmptyError(id);
+ }
+ }
+
+ public Response BlockTreeEntryGetBlockHash(string id, BtckBlockTreeEntryGetBlockHashParams p)
+ {
+ if (p.BlockTreeEntry?.Ref is not { } bteRef) return RefError(id);
+
+ var hashBytes = GetVal(bteRef).GetBlockHash();
+ // Reverse bytes to get display (big-endian) order
+ return Responses.Ok(id, Convert.ToHexString(hashBytes.Reverse().ToArray()).ToLowerInvariant());
+ }
+
+ // ── Script Pubkey ─────────────────────────────────────────────────────────
+
+ public Response ScriptPubkeyCreate(string id, string? refName, BtckScriptPubkeyCreateParams p)
+ {
+ if (refName == null) return RefError(id);
+
+ try
+ {
+ var spk = ScriptPubKey.FromHex(p.ScriptPubKeyHex);
+ _registry.Register(refName, spk);
+ return Responses.Ref(id, refName);
+ }
+ catch
+ {
+ return Responses.EmptyError(id);
+ }
+ }
+
+ public Response ScriptPubkeyDestroy(string id, BtckScriptPubkeyDestroyParams p)
+ {
+ if (p.ScriptPubKey?.Ref is { } r) _registry.Destroy(r);
+ return Responses.Null(id);
+ }
+
+ public Response ScriptPubkeyVerify(string id, BtckScriptPubkeyVerifyParams p)
+ {
+ if (p.ScriptPubKey?.Ref is not { } spkRef) return RefError(id);
+ if (p.TxTo?.Ref is not { } txRef) return RefError(id);
+
+ try
+ {
+ var scriptPubKey = Get(spkRef);
+ var transaction = Get(txRef);
+
+ PrecomputedTransactionData? precomputed = p.PrecomputedTxData?.Ref is { } precompRef
+ ? Get(precompRef)
+ : null;
+
+ var flags = ParseFlags(p.Flags);
+
+ bool valid = ScriptVerifier.VerifyScript(
+ scriptPubKey,
+ p.Amount,
+ transaction,
+ precomputed,
+ p.InputIndex,
+ flags);
+
+ return Responses.Ok(id, valid);
+ }
+ catch (ScriptVerificationException ex) when (ex.Status != ScriptVerifyStatus.OK)
+ {
+ return Responses.CodedError(id, "btck_ScriptVerifyStatus", MapScriptVerifyStatus(ex.Status));
+ }
+ catch (KeyNotFoundException)
+ {
+ return Responses.EmptyError(id);
+ }
+ }
+
+ // ── Transaction ───────────────────────────────────────────────────────────
+
+ public Response TransactionCreate(string id, string? refName, BtckTransactionCreateParams p)
+ {
+ if (refName == null) return RefError(id);
+
+ try
+ {
+ var tx = Transaction.FromHex(p.RawTransaction);
+ _registry.Register(refName, tx);
+ return Responses.Ref(id, refName);
+ }
+ catch
+ {
+ return Responses.EmptyError(id);
+ }
+ }
+
+ public Response TransactionDestroy(string id, BtckTransactionDestroyParams p)
+ {
+ if (p.Transaction?.Ref is { } r) _registry.Destroy(r);
+ return Responses.Null(id);
+ }
+
+ // ── Transaction Output ────────────────────────────────────────────────────
+
+ public Response TransactionOutputCreate(string id, string? refName, BtckTransactionOutputCreateParams p)
+ {
+ if (refName == null) return Responses.Null(id);
+ if (p.ScriptPubKey?.Ref is not { } spkRef) return Responses.Null(id);
+
+ try
+ {
+ var spk = Get(spkRef);
+ var txOut = new TxOut(spk, p.Amount);
+ _registry.Register(refName, txOut);
+ return Responses.Ref(id, refName);
+ }
+ catch
+ {
+ return Responses.Null(id);
+ }
+ }
+
+ public Response TransactionOutputDestroy(string id, BtckTransactionOutputDestroyParams p)
+ {
+ if (p.TransactionOutput?.Ref is { } r) _registry.Destroy(r);
+ return Responses.Null(id);
+ }
+
+ // ── Precomputed Transaction Data ──────────────────────────────────────────
+
+ public Response PrecomputedTransactionDataCreate(string id, string? refName, BtckPrecomputedTransactionDataCreateParams p)
+ {
+ if (refName == null) return RefError(id);
+ if (p.TxTo?.Ref is not { } txRef) return RefError(id);
+
+ try
+ {
+ var transaction = Get(txRef);
+
+ List? spentOutputs = null;
+ if (p.SpentOutputs is { Count: > 0 })
+ {
+ spentOutputs = p.SpentOutputs
+ .Select(r => Get(r.Ref))
+ .ToList();
+ }
+
+ var precomputed = new PrecomputedTransactionData(transaction, spentOutputs);
+ _registry.Register(refName, precomputed);
+ return Responses.Ref(id, refName);
+ }
+ catch
+ {
+ return Responses.EmptyError(id);
+ }
+ }
+
+ public Response PrecomputedTransactionDataDestroy(string id, BtckPrecomputedTransactionDataDestroyParams p)
+ {
+ if (p.PrecomputedTxData?.Ref is { } r) _registry.Destroy(r);
+ return Responses.Null(id);
+ }
+
+ // ── Parsing helpers ───────────────────────────────────────────────────────
+
+ private static ChainType ParseChainType(string s) => s switch
+ {
+ "btck_ChainType_MAINNET" => ChainType.MAINNET,
+ "btck_ChainType_TESTNET" => ChainType.TESTNET,
+ "btck_ChainType_TESTNET_4" => ChainType.TESTNET_4,
+ "btck_ChainType_SIGNET" => ChainType.SIGNET,
+ "btck_ChainType_REGTEST" => ChainType.REGTEST,
+ _ => throw new ArgumentException($"Unknown chain type: {s}")
+ };
+
+ private static ScriptVerificationFlags ParseFlags(System.Text.Json.JsonElement? flags)
+ {
+ if (flags == null) return ScriptVerificationFlags.None;
+
+ var el = flags.Value;
+ if (el.ValueKind == System.Text.Json.JsonValueKind.Number)
+ return (ScriptVerificationFlags)el.GetUInt32();
+
+ if (el.ValueKind == System.Text.Json.JsonValueKind.Array)
+ {
+ var combined = ScriptVerificationFlags.None;
+ foreach (var item in el.EnumerateArray())
+ if (item.ValueKind == System.Text.Json.JsonValueKind.String)
+ combined |= ParseFlagString(item.GetString() ?? string.Empty);
+ return combined;
+ }
+
+ if (el.ValueKind == System.Text.Json.JsonValueKind.String)
+ return ParseFlagString(el.GetString() ?? string.Empty);
+
+ return ScriptVerificationFlags.None;
+ }
+
+ private static ScriptVerificationFlags ParseFlagString(string s)
+ {
+ const string prefix = "btck_ScriptVerificationFlags_";
+ if (s.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ s = s[prefix.Length..];
+
+ return s.ToUpperInvariant() switch
+ {
+ "NONE" => ScriptVerificationFlags.None,
+ "P2SH" => ScriptVerificationFlags.P2SH,
+ "DERSIG" => ScriptVerificationFlags.DerSig,
+ "NULLDUMMY" => ScriptVerificationFlags.NullDummy,
+ "CHECKLOCKTIMEVERIFY" => ScriptVerificationFlags.CheckLockTimeVerify,
+ "CHECKSEQUENCEVERIFY" => ScriptVerificationFlags.CheckSequenceVerify,
+ "WITNESS" => ScriptVerificationFlags.Witness,
+ "TAPROOT" => ScriptVerificationFlags.Taproot,
+ _ => throw new ArgumentException($"Unknown script verification flag: {s}")
+ };
+ }
+
+ private static string MapScriptVerifyStatus(ScriptVerifyStatus status) => status switch
+ {
+ ScriptVerifyStatus.ERROR_INVALID_FLAGS_COMBINATION => "ERROR_INVALID_FLAGS_COMBINATION",
+ ScriptVerifyStatus.ERROR_SPENT_OUTPUTS_REQUIRED => "ERROR_SPENT_OUTPUTS_REQUIRED",
+ ScriptVerifyStatus.ERROR_TX_INPUT_INDEX => "ERROR_TX_INPUT_INDEX",
+ ScriptVerifyStatus.ERROR_SPENT_OUTPUTS_MISMATCH => "ERROR_SPENT_OUTPUTS_MISMATCH",
+ ScriptVerifyStatus.ERROR_INVALID_FLAGS => "ERROR_INVALID_FLAGS",
+ _ => "ERROR_UNKNOWN"
+ };
+}
+
+///
+/// Wraps a together with the temp directory it owns,
+/// so both are cleaned up when the entry is removed from the registry.
+///
+internal sealed class ChainstateManagerWithTempDir : IDisposable
+{
+ private readonly string _tempDir;
+ private bool _disposed;
+
+ internal ChainstateManagerWithTempDir(ChainstateManager manager, string tempDir)
+ {
+ Manager = manager;
+ _tempDir = tempDir;
+ }
+
+ internal ChainstateManager Manager { get; }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ Manager.Dispose();
+ try
+ {
+ if (Directory.Exists(_tempDir))
+ Directory.Delete(_tempDir, recursive: true);
+ }
+ catch { /* best-effort */ }
+ }
+}
diff --git a/tools/kernel-bindings-test-handler/Handlers/ScriptVerifyHandler.cs b/tools/kernel-bindings-test-handler/Handlers/ScriptVerifyHandler.cs
deleted file mode 100644
index 7e0c29f..0000000
--- a/tools/kernel-bindings-test-handler/Handlers/ScriptVerifyHandler.cs
+++ /dev/null
@@ -1,194 +0,0 @@
-using BitcoinKernel.Core;
-using BitcoinKernel.Core.Abstractions;
-using BitcoinKernel.Core.Exceptions;
-using BitcoinKernel.Core.ScriptVerification;
-using BitcoinKernel.Interop.Enums;
-using BitcoinKernel.TestHandler.Protocol;
-
-namespace BitcoinKernel.TestHandler.Handlers;
-
-///
-/// Handles script_pubkey.verify method requests.
-///
-public class ScriptVerifyHandler
-{
- private readonly KernelContext _context;
-
- public ScriptVerifyHandler(KernelContext context)
- {
- _context = context;
- }
-
- ///
- /// Handles a script verification request.
- ///
- public Response Handle(string requestId, BtckScriptPubkeyVerifyParams parameters)
- {
- try
- {
- // Parse input data
- var scriptPubKey = ScriptPubKey.FromHex(parameters.ScriptPubKeyHex);
- var transaction = Transaction.FromHex(parameters.TxHex);
-
- // Parse spent outputs if provided
- var spentOutputs = new List();
- if (parameters.SpentOutputs != null && parameters.SpentOutputs.Any())
- {
- foreach (var output in parameters.SpentOutputs)
- {
- var outputScriptPubKey = ScriptPubKey.FromHex(output.ScriptPubKeyHex);
- spentOutputs.Add(new TxOut(outputScriptPubKey, output.Value));
- }
- }
-
- // Parse flags
- var flags = ParseFlags(parameters.Flags);
-
- // Verify the script
- ScriptVerifier.VerifyScript(
- scriptPubKey,
- parameters.Amount,
- transaction,
- parameters.InputIndex,
- spentOutputs,
- flags
- );
-
- // Success
- return new Response
- {
- Id = requestId,
- Result = true
- };
- }
- catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "inputIndex")
- {
- return new Response
- {
- Id = requestId,
- Result = null,
- Error = new ErrorResponse
- {
- Code = new ErrorCode
- {
- Type = "btck_ScriptVerifyStatus",
- Member = "TxInputIndex"
- }
- }
- };
- }
- catch (ScriptVerificationException ex)
- {
- // If status is OK, the script just failed verification (result: false)
- // If status is not OK, it's an actual error condition
- if (ex.Status == ScriptVerifyStatus.OK)
- {
- return new Response
- {
- Id = requestId,
- Result = false
- };
- }
-
- return new Response
- {
- Id = requestId,
- Result = null,
- Error = new ErrorResponse
- {
- Code = new ErrorCode
- {
- Type = "btck_ScriptVerifyStatus",
- Member = MapScriptVerifyStatus(ex.Status)
- }
- }
- };
- }
- }
-
- ///
- /// Parses flags from either uint or string format.
- ///
- private ScriptVerificationFlags ParseFlags(object? flags)
- {
- if (flags == null)
- return ScriptVerificationFlags.None;
-
- // Handle numeric flags
- if (flags is uint or int or long)
- {
- return (ScriptVerificationFlags)Convert.ToUInt32(flags);
- }
-
- // Handle System.Text.Json JsonElement
- if (flags.GetType().Name == "JsonElement")
- {
- var jsonElement = (System.Text.Json.JsonElement)flags;
- if (jsonElement.ValueKind == System.Text.Json.JsonValueKind.Number)
- {
- return (ScriptVerificationFlags)jsonElement.GetUInt32();
- }
- else if (jsonElement.ValueKind == System.Text.Json.JsonValueKind.Array)
- {
- // Handle array of string flags - combine them with OR
- ScriptVerificationFlags combinedFlags = ScriptVerificationFlags.None;
- foreach (var element in jsonElement.EnumerateArray())
- {
- if (element.ValueKind == System.Text.Json.JsonValueKind.String)
- {
- combinedFlags |= ParseFlagString(element.GetString() ?? string.Empty);
- }
- }
- return combinedFlags;
- }
- }
-
- // Handle string flags
- if (flags is string flagStr)
- {
- return ParseFlagString(flagStr);
- }
-
- return ScriptVerificationFlags.None;
- }
-
- ///
- /// Parses a string flag name to ScriptVerificationFlags.
- ///
- private ScriptVerificationFlags ParseFlagString(string flagStr)
- {
- // Handle btck_ prefixed format (e.g., "btck_ScriptVerificationFlags_WITNESS")
- if (flagStr.StartsWith("btck_ScriptVerificationFlags_", StringComparison.OrdinalIgnoreCase))
- {
- flagStr = flagStr.Substring("btck_ScriptVerificationFlags_".Length);
- }
-
- return flagStr.ToUpperInvariant() switch
- {
- "NONE" => ScriptVerificationFlags.None,
- "P2SH" => ScriptVerificationFlags.P2SH,
- "DERSIG" => ScriptVerificationFlags.DerSig,
- "NULLDUMMY" => ScriptVerificationFlags.NullDummy,
- "CHECKLOCKTIMEVERIFY" => ScriptVerificationFlags.CheckLockTimeVerify,
- "CHECKSEQUENCEVERIFY" => ScriptVerificationFlags.CheckSequenceVerify,
- "WITNESS" => ScriptVerificationFlags.Witness,
- "TAPROOT" => ScriptVerificationFlags.Taproot,
- "ALL" => ScriptVerificationFlags.All,
- "ALL_PRE_TAPROOT" => ScriptVerificationFlags.AllPreTaproot,
- _ => throw new ArgumentException($"Unknown flag: {flagStr}")
- };
- }
-
- ///
- /// Maps ScriptVerifyStatus to error variant strings.
- ///
- private string MapScriptVerifyStatus(ScriptVerifyStatus status)
- {
- return status switch
- {
- ScriptVerifyStatus.ERROR_INVALID_FLAGS_COMBINATION => "ERROR_INVALID_FLAGS_COMBINATION",
- ScriptVerifyStatus.ERROR_SPENT_OUTPUTS_REQUIRED => "ERROR_SPENT_OUTPUTS_REQUIRED",
- _ => "ERROR_INVALID"
- };
- }
-}
diff --git a/tools/kernel-bindings-test-handler/Program.cs b/tools/kernel-bindings-test-handler/Program.cs
index 8c4d7ae..f7afa58 100644
--- a/tools/kernel-bindings-test-handler/Program.cs
+++ b/tools/kernel-bindings-test-handler/Program.cs
@@ -1,5 +1,4 @@
using System.Text.Json;
-using BitcoinKernel.Core;
using BitcoinKernel.TestHandler.Handlers;
using BitcoinKernel.TestHandler.Protocol;
@@ -7,26 +6,23 @@ namespace BitcoinKernel.TestHandler;
///
/// Test handler for Bitcoin Kernel conformance tests.
-/// Implements the JSON-RPC-like protocol for testing bindings.
+/// Implements the JSON-based protocol for testing bindings.
+/// Reads JSON requests line-by-line from stdin, writes JSON responses to stdout.
///
class Program
{
static async Task Main(string[] args)
{
- // Initialize kernel context
- using var context = new KernelContext();
- var scriptVerifyHandler = new ScriptVerifyHandler(context);
-
- // Configure JSON serialization options
var jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
WriteIndented = false
};
+ using var dispatcher = new MethodDispatcher();
+
try
{
- // Read requests line-by-line from stdin
string? line;
while ((line = await Console.In.ReadLineAsync()) != null)
{
@@ -36,44 +32,21 @@ static async Task Main(string[] args)
Response response;
try
{
- // Parse the request
var request = JsonSerializer.Deserialize(line, jsonOptions);
-
if (request == null)
{
- response = new Response
- {
- Id = "unknown",
- Error = new ErrorResponse
- {
- Code = new ErrorCode
- {
- Type = "InvalidRequest"
- }
- }
- };
+ response = new Response { Id = "unknown", Result = null, Error = new ErrorResponse() };
}
else
{
- response = HandleRequest(request, scriptVerifyHandler, jsonOptions);
+ response = Dispatch(request, dispatcher, jsonOptions);
}
}
catch (JsonException)
{
- response = new Response
- {
- Id = "unknown",
- Error = new ErrorResponse
- {
- Code = new ErrorCode
- {
- Type = "InvalidRequest"
- }
- }
- };
+ response = new Response { Id = "unknown", Result = null, Error = new ErrorResponse() };
}
- // Write response to stdout
var responseJson = JsonSerializer.Serialize(response, jsonOptions);
await Console.Out.WriteLineAsync(responseJson);
await Console.Out.FlushAsync();
@@ -88,77 +61,115 @@ static async Task Main(string[] args)
}
}
- ///
- /// Routes the request to the appropriate handler.
- ///
- private static Response HandleRequest(Request request, ScriptVerifyHandler scriptVerifyHandler, JsonSerializerOptions jsonOptions)
+ private static Response Dispatch(Request request, MethodDispatcher dispatcher, JsonSerializerOptions opts)
{
+ var id = request.Id;
+
try
{
- switch (request.Method)
+ return request.Method switch
{
- case "btck_script_pubkey_verify":
- if (request.Params == null)
- {
- return new Response
- {
- Id = request.Id,
- Error = new ErrorResponse
- {
- Code = new ErrorCode
- {
- Type = "InvalidParams"
- }
- }
- };
- }
+ // ── Context ──────────────────────────────────────────────────
+ "btck_context_create" =>
+ dispatcher.ContextCreate(id, request.Ref,
+ Deserialize(request.Params, opts)),
- var btckScriptPubkeyVerifyParams = JsonSerializer.Deserialize(request.Params.Value, jsonOptions);
+ "btck_context_destroy" =>
+ dispatcher.ContextDestroy(id,
+ Deserialize(request.Params, opts)),
- if (btckScriptPubkeyVerifyParams == null)
- {
- return new Response
- {
- Id = request.Id,
- Error = new ErrorResponse
- {
- Code = new ErrorCode
- {
- Type = "InvalidParams"
- }
- }
- };
- }
+ // ── Chainstate Manager ────────────────────────────────────────
+ "btck_chainstate_manager_create" =>
+ dispatcher.ChainstateManagerCreate(id, request.Ref,
+ Deserialize(request.Params, opts)),
- return scriptVerifyHandler.Handle(request.Id, btckScriptPubkeyVerifyParams);
+ "btck_chainstate_manager_get_active_chain" =>
+ dispatcher.ChainstateManagerGetActiveChain(id, request.Ref,
+ Deserialize(request.Params, opts)),
- default:
- return new Response
- {
- Id = request.Id,
- Error = new ErrorResponse
- {
- Code = new ErrorCode
- {
- Type = "MethodNotFound"
- }
- }
- };
- }
+ "btck_chainstate_manager_process_block" =>
+ dispatcher.ChainstateManagerProcessBlock(id,
+ Deserialize(request.Params, opts)),
+
+ "btck_chainstate_manager_destroy" =>
+ dispatcher.ChainstateManagerDestroy(id,
+ Deserialize(request.Params, opts)),
+
+ // ── Chain ─────────────────────────────────────────────────────
+ "btck_chain_get_height" =>
+ dispatcher.ChainGetHeight(id,
+ Deserialize(request.Params, opts)),
+
+ "btck_chain_get_by_height" =>
+ dispatcher.ChainGetByHeight(id, request.Ref,
+ Deserialize(request.Params, opts)),
+
+ "btck_chain_contains" =>
+ dispatcher.ChainContains(id,
+ Deserialize(request.Params, opts)),
+
+ // ── Block ─────────────────────────────────────────────────────
+ "btck_block_create" =>
+ dispatcher.BlockCreate(id, request.Ref,
+ Deserialize(request.Params, opts)),
+
+ "btck_block_tree_entry_get_block_hash" =>
+ dispatcher.BlockTreeEntryGetBlockHash(id,
+ Deserialize(request.Params, opts)),
+
+ // ── Script Pubkey ─────────────────────────────────────────────
+ "btck_script_pubkey_create" =>
+ dispatcher.ScriptPubkeyCreate(id, request.Ref,
+ Deserialize(request.Params, opts)),
+
+ "btck_script_pubkey_destroy" =>
+ dispatcher.ScriptPubkeyDestroy(id,
+ Deserialize(request.Params, opts)),
+
+ "btck_script_pubkey_verify" =>
+ dispatcher.ScriptPubkeyVerify(id,
+ Deserialize(request.Params, opts)),
+
+ // ── Transaction ───────────────────────────────────────────────
+ "btck_transaction_create" =>
+ dispatcher.TransactionCreate(id, request.Ref,
+ Deserialize(request.Params, opts)),
+
+ "btck_transaction_destroy" =>
+ dispatcher.TransactionDestroy(id,
+ Deserialize(request.Params, opts)),
+
+ // ── Transaction Output ────────────────────────────────────────
+ "btck_transaction_output_create" =>
+ dispatcher.TransactionOutputCreate(id, request.Ref,
+ Deserialize(request.Params, opts)),
+
+ "btck_transaction_output_destroy" =>
+ dispatcher.TransactionOutputDestroy(id,
+ Deserialize(request.Params, opts)),
+
+ // ── Precomputed Transaction Data ──────────────────────────────
+ "btck_precomputed_transaction_data_create" =>
+ dispatcher.PrecomputedTransactionDataCreate(id, request.Ref,
+ Deserialize(request.Params, opts)),
+
+ "btck_precomputed_transaction_data_destroy" =>
+ dispatcher.PrecomputedTransactionDataDestroy(id,
+ Deserialize(request.Params, opts)),
+
+ // ── Unknown ───────────────────────────────────────────────────
+ _ => new Response { Id = id, Result = null, Error = new ErrorResponse() }
+ };
}
catch (Exception)
{
- return new Response
- {
- Id = request.Id,
- Error = new ErrorResponse
- {
- Code = new ErrorCode
- {
- Type = "InternalError"
- }
- }
- };
+ return new Response { Id = id, Result = null, Error = new ErrorResponse() };
}
}
+
+ private static T Deserialize(JsonElement? element, JsonSerializerOptions opts) where T : new()
+ {
+ if (element == null) return new T();
+ return JsonSerializer.Deserialize(element.Value, opts) ?? new T();
+ }
}
diff --git a/tools/kernel-bindings-test-handler/Protocol/Request.cs b/tools/kernel-bindings-test-handler/Protocol/Request.cs
index 79f9cd8..46d37c1 100644
--- a/tools/kernel-bindings-test-handler/Protocol/Request.cs
+++ b/tools/kernel-bindings-test-handler/Protocol/Request.cs
@@ -16,40 +16,192 @@ public class Request
[JsonPropertyName("params")]
public JsonElement? Params { get; set; }
+
+ ///
+ /// Reference name for storing the returned object in the registry.
+ ///
+ [JsonPropertyName("ref")]
+ public string? Ref { get; set; }
}
///
-/// Parameters for btck_script_pubkey_verify method.
+/// Represents a reference to an object stored in the registry. Used both as a
+/// parameter value and as a result value.
///
-public class BtckScriptPubkeyVerifyParams
+public class RefType
+{
+ [JsonPropertyName("ref")]
+ public string Ref { get; set; } = string.Empty;
+}
+
+// ── Context ──────────────────────────────────────────────────────────────────
+
+public class BtckContextCreateParams
+{
+ [JsonPropertyName("chain_parameters")]
+ public ChainParametersParam? ChainParameters { get; set; }
+}
+
+public class ChainParametersParam
+{
+ [JsonPropertyName("chain_type")]
+ public string ChainType { get; set; } = string.Empty;
+}
+
+public class BtckContextDestroyParams
+{
+ [JsonPropertyName("context")]
+ public RefType? Context { get; set; }
+}
+
+// ── Chainstate Manager ────────────────────────────────────────────────────────
+
+public class BtckChainstateManagerCreateParams
+{
+ [JsonPropertyName("context")]
+ public RefType? Context { get; set; }
+}
+
+public class BtckChainstateManagerGetActiveChainParams
+{
+ [JsonPropertyName("chainstate_manager")]
+ public RefType? ChainstateManager { get; set; }
+}
+
+public class BtckChainstateManagerProcessBlockParams
+{
+ [JsonPropertyName("chainstate_manager")]
+ public RefType? ChainstateManager { get; set; }
+
+ [JsonPropertyName("block")]
+ public RefType? Block { get; set; }
+}
+
+public class BtckChainstateManagerDestroyParams
+{
+ [JsonPropertyName("chainstate_manager")]
+ public RefType? ChainstateManager { get; set; }
+}
+
+// ── Chain ─────────────────────────────────────────────────────────────────────
+
+public class BtckChainGetHeightParams
+{
+ [JsonPropertyName("chain")]
+ public RefType? Chain { get; set; }
+}
+
+public class BtckChainGetByHeightParams
+{
+ [JsonPropertyName("chain")]
+ public RefType? Chain { get; set; }
+
+ [JsonPropertyName("block_height")]
+ public int BlockHeight { get; set; }
+}
+
+public class BtckChainContainsParams
+{
+ [JsonPropertyName("chain")]
+ public RefType? Chain { get; set; }
+
+ [JsonPropertyName("block_tree_entry")]
+ public RefType? BlockTreeEntry { get; set; }
+}
+
+// ── Block ─────────────────────────────────────────────────────────────────────
+
+public class BtckBlockCreateParams
+{
+ [JsonPropertyName("raw_block")]
+ public string RawBlock { get; set; } = string.Empty;
+}
+
+public class BtckBlockTreeEntryGetBlockHashParams
+{
+ [JsonPropertyName("block_tree_entry")]
+ public RefType? BlockTreeEntry { get; set; }
+}
+
+// ── Script Pubkey ─────────────────────────────────────────────────────────────
+
+public class BtckScriptPubkeyCreateParams
{
[JsonPropertyName("script_pubkey")]
public string ScriptPubKeyHex { get; set; } = string.Empty;
+}
+
+public class BtckScriptPubkeyDestroyParams
+{
+ [JsonPropertyName("script_pubkey")]
+ public RefType? ScriptPubKey { get; set; }
+}
+
+public class BtckScriptPubkeyVerifyParams
+{
+ [JsonPropertyName("script_pubkey")]
+ public RefType? ScriptPubKey { get; set; }
[JsonPropertyName("amount")]
public long Amount { get; set; }
[JsonPropertyName("tx_to")]
- public string TxHex { get; set; } = string.Empty;
+ public RefType? TxTo { get; set; }
+
+ [JsonPropertyName("precomputed_txdata")]
+ public RefType? PrecomputedTxData { get; set; }
[JsonPropertyName("input_index")]
public uint InputIndex { get; set; }
- [JsonPropertyName("spent_outputs")]
- public List? SpentOutputs { get; set; }
-
[JsonPropertyName("flags")]
- public JsonElement? Flags { get; set; } // Can be uint, or array
+ public JsonElement? Flags { get; set; }
}
-///
-/// Represents a spent output.
-///
-public class SpentOutput
+// ── Transaction ───────────────────────────────────────────────────────────────
+
+public class BtckTransactionCreateParams
+{
+ [JsonPropertyName("raw_transaction")]
+ public string RawTransaction { get; set; } = string.Empty;
+}
+
+public class BtckTransactionDestroyParams
+{
+ [JsonPropertyName("transaction")]
+ public RefType? Transaction { get; set; }
+}
+
+// ── Transaction Output ────────────────────────────────────────────────────────
+
+public class BtckTransactionOutputCreateParams
{
[JsonPropertyName("script_pubkey")]
- public string ScriptPubKeyHex { get; set; } = string.Empty;
+ public RefType? ScriptPubKey { get; set; }
[JsonPropertyName("amount")]
- public long Value { get; set; }
+ public long Amount { get; set; }
+}
+
+public class BtckTransactionOutputDestroyParams
+{
+ [JsonPropertyName("transaction_output")]
+ public RefType? TransactionOutput { get; set; }
+}
+
+// ── Precomputed Transaction Data ──────────────────────────────────────────────
+
+public class BtckPrecomputedTransactionDataCreateParams
+{
+ [JsonPropertyName("tx_to")]
+ public RefType? TxTo { get; set; }
+
+ [JsonPropertyName("spent_outputs")]
+ public List? SpentOutputs { get; set; }
+}
+
+public class BtckPrecomputedTransactionDataDestroyParams
+{
+ [JsonPropertyName("precomputed_txdata")]
+ public RefType? PrecomputedTxData { get; set; }
}
diff --git a/tools/kernel-bindings-test-handler/Protocol/Response.cs b/tools/kernel-bindings-test-handler/Protocol/Response.cs
index e75f655..358d641 100644
--- a/tools/kernel-bindings-test-handler/Protocol/Response.cs
+++ b/tools/kernel-bindings-test-handler/Protocol/Response.cs
@@ -1,3 +1,4 @@
+using System.Text.Json;
using System.Text.Json.Serialization;
namespace BitcoinKernel.TestHandler.Protocol;
@@ -11,21 +12,20 @@ public class Response
public string Id { get; set; } = string.Empty;
[JsonPropertyName("result")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object? Result { get; set; }
[JsonPropertyName("error")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public ErrorResponse? Error { get; set; }
+ public object? Error { get; set; }
}
///
-/// Represents an error response.
+/// An error response with a structured error code.
///
public class ErrorResponse
{
[JsonPropertyName("code")]
- public ErrorCode Code { get; set; } = new();
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public ErrorCode? Code { get; set; }
}
///
@@ -39,3 +39,47 @@ public class ErrorCode
[JsonPropertyName("member")]
public string Member { get; set; } = string.Empty;
}
+
+///
+/// Helper to build Response objects.
+///
+public static class Responses
+{
+ ///
+ /// Creates a success response with null result (void operations).
+ ///
+ public static Response Null(string id) =>
+ new() { Id = id, Result = null, Error = null };
+
+ ///
+ /// Creates a success response with a scalar result value.
+ ///
+ public static Response Ok(string id, object result) =>
+ new() { Id = id, Result = result, Error = null };
+
+ ///
+ /// Creates a success response with a reference result.
+ ///
+ public static Response Ref(string id, string refName) =>
+ new() { Id = id, Result = new RefType { Ref = refName }, Error = null };
+
+ ///
+ /// Creates an error response with an empty error object (operation failed, no code details).
+ ///
+ public static Response EmptyError(string id) =>
+ new() { Id = id, Result = null, Error = new ErrorResponse() };
+
+ ///
+ /// Creates an error response with a typed error code.
+ ///
+ public static Response CodedError(string id, string errorType, string errorMember) =>
+ new()
+ {
+ Id = id,
+ Result = null,
+ Error = new ErrorResponse
+ {
+ Code = new ErrorCode { Type = errorType, Member = errorMember }
+ }
+ };
+}
diff --git a/tools/kernel-bindings-test-handler/Registry/NonOwningRef.cs b/tools/kernel-bindings-test-handler/Registry/NonOwningRef.cs
new file mode 100644
index 0000000..9dd0bb1
--- /dev/null
+++ b/tools/kernel-bindings-test-handler/Registry/NonOwningRef.cs
@@ -0,0 +1,20 @@
+namespace BitcoinKernel.TestHandler.Registry;
+
+///
+/// Wraps any object in a no-op IDisposable so it can live in the ObjectRegistry.
+/// Use this for objects whose lifetime is managed elsewhere (e.g. Chain, BlockIndex).
+///
+public sealed class NonOwningRef : IDisposable
+{
+ public T Value { get; }
+
+ public NonOwningRef(T value)
+ {
+ Value = value;
+ }
+
+ public void Dispose()
+ {
+ // Intentionally empty – we do not own the underlying object.
+ }
+}
diff --git a/tools/kernel-bindings-test-handler/Registry/ObjectRegistry.cs b/tools/kernel-bindings-test-handler/Registry/ObjectRegistry.cs
new file mode 100644
index 0000000..7671683
--- /dev/null
+++ b/tools/kernel-bindings-test-handler/Registry/ObjectRegistry.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+
+namespace BitcoinKernel.TestHandler.Registry;
+
+///
+/// Maintains a map of reference names to IDisposable native objects across requests.
+///
+public sealed class ObjectRegistry : IDisposable
+{
+ private readonly Dictionary _store = new();
+ private bool _disposed;
+
+ ///
+ /// Registers an object under the given reference name.
+ /// Any previously registered object under that name is disposed first.
+ ///
+ public void Register(string refName, IDisposable obj)
+ {
+ if (_store.TryGetValue(refName, out var existing))
+ existing.Dispose();
+
+ _store[refName] = obj;
+ }
+
+ ///
+ /// Retrieves an object from the registry by reference name, cast to T.
+ ///
+ /// When the name is not registered.
+ /// When the object is not of type T.
+ public T Get(string refName)
+ {
+ if (!_store.TryGetValue(refName, out var obj))
+ throw new KeyNotFoundException($"Registry: reference '{refName}' not found.");
+
+ return (T)obj;
+ }
+
+ ///
+ /// Removes and disposes the object registered under the given reference name.
+ ///
+ public void Destroy(string refName)
+ {
+ if (_store.Remove(refName, out var obj))
+ obj.Dispose();
+ }
+
+ ///
+ /// Returns true when the given reference name is registered.
+ ///
+ public bool Contains(string refName) => _store.ContainsKey(refName);
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ foreach (var obj in _store.Values)
+ {
+ try { obj.Dispose(); }
+ catch { /* best-effort cleanup */ }
+ }
+
+ _store.Clear();
+ }
+}