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