From 66dc9764a625f69be00dc68b376de75578721ee6 Mon Sep 17 00:00:00 2001 From: stringintech Date: Mon, 17 Nov 2025 14:33:46 +0330 Subject: [PATCH 1/4] Remove handler implementations Handlers are now expected to be implemented separately (e.g., in binding repos) and pin to specific versions of this test repository --- .github/workflows/ci.yml | 71 ----- .gitmodules | 4 - Makefile | 37 --- README.md | 44 +-- go-handler/.gitignore | 1 - go-handler/chainstate.go | 192 ------------- go-handler/go-bitcoinkernel | 1 - go-handler/go.mod | 7 - go-handler/handler.go | 30 --- go-handler/main.go | 47 ---- go-handler/protocol.go | 51 ---- go-handler/script_pubkey.go | 82 ------ go-handler/state.go | 30 --- rust-handler/.gitignore | 1 - rust-handler/Cargo.lock | 430 ------------------------------ rust-handler/Cargo.toml | 12 - rust-handler/src/chainstate.rs | 233 ---------------- rust-handler/src/handler.rs | 68 ----- rust-handler/src/main.rs | 63 ----- rust-handler/src/protocol.rs | 52 ---- rust-handler/src/script_pubkey.rs | 115 -------- rust-handler/src/state.rs | 32 --- 22 files changed, 8 insertions(+), 1595 deletions(-) delete mode 100644 .github/workflows/ci.yml delete mode 100644 .gitmodules delete mode 100644 Makefile delete mode 100644 go-handler/.gitignore delete mode 100644 go-handler/chainstate.go delete mode 160000 go-handler/go-bitcoinkernel delete mode 100644 go-handler/go.mod delete mode 100644 go-handler/handler.go delete mode 100644 go-handler/main.go delete mode 100644 go-handler/protocol.go delete mode 100644 go-handler/script_pubkey.go delete mode 100644 go-handler/state.go delete mode 100644 rust-handler/.gitignore delete mode 100644 rust-handler/Cargo.lock delete mode 100644 rust-handler/Cargo.toml delete mode 100644 rust-handler/src/chainstate.rs delete mode 100644 rust-handler/src/handler.rs delete mode 100644 rust-handler/src/main.rs delete mode 100644 rust-handler/src/protocol.rs delete mode 100644 rust-handler/src/script_pubkey.rs delete mode 100644 rust-handler/src/state.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 7733639..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: CI - -on: - push: - pull_request: - -jobs: - ubuntu: - name: Build and Test on Ubuntu - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.23.3' - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust-handler - - - name: Install Boost library - run: | - sudo apt-get update - sudo apt-get install -y libboost-all-dev - - - name: Build - run: make build - - - name: Run tests - run: make test - - macos: - name: Build and Test on macOS - runs-on: macos-latest - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.23.3' - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust-handler - - - name: Install Boost library - run: | - brew install boost - - - name: Build - run: make build - - - name: Run tests - run: make test \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 38e631c..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "go-handler/go-bitcoinkernel"] - path = go-handler/go-bitcoinkernel - url = https://github.com/stringintech/go-bitcoinkernel.git - branch = main diff --git a/Makefile b/Makefile deleted file mode 100644 index c64eed9..0000000 --- a/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -.PHONY: all build test test-single clean - -all: build test - -build: - @echo "Building orchestrator binary..." - mkdir -p bin - cd orchestrator && go build -o ./bin/orchestrator - @echo "Building go-handler binary..." - cd go-handler/go-bitcoinkernel && $(MAKE) build-kernel && $(MAKE) build - cd go-handler && go build -o ./bin/go-handler - @echo "Building rust-handler binary..." - cd rust-handler && cargo build --release - @echo "Build complete!" - -test: - @echo "Running all conformance tests with go-handler..." - -./orchestrator/bin/orchestrator -handler ./go-handler/bin/go-handler -testdir testdata - @echo "Running all conformance tests with rust-handler..." - -./orchestrator/bin/orchestrator -handler ./rust-handler/target/release/rust-handler -testdir testdata - -test-single: - @if [ -z "$(TEST)" ]; then \ - echo "Error: TEST variable not set. Usage: make test-single TEST=testdata/chainstate_basic.json"; \ - exit 1; \ - fi - @echo "Running test with go-handler: $(TEST)" - ./bin/orchestrator -handler ./go-handle/bin/go-handler -testfile $(TEST) - @echo "Running test with rust-handler: $(TEST)" - ./bin/orchestrator -handler ./rust-handler/target/release/rust-handler -testfile $(TEST) - -clean: - @echo "Cleaning build artifacts..." - cd go-handler/go-bitcoinkernel && $(MAKE) clean - cd orchestrator && go clean && rm -rf build - cd go-handler && go clean && rm -rf build - cd rust-handler && cargo clean \ No newline at end of file diff --git a/README.md b/README.md index f9c6cad..31c2a65 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Bitcoin Kernel Binding Conformance Tests -This directory contains a language-agnostic conformance testing framework for Bitcoin Kernel bindings. +This repository contains a language-agnostic conformance testing framework for Bitcoin Kernel bindings. ## ⚠️ Work in Progress @@ -29,39 +29,11 @@ The framework ensures that all language bindings (Go, Python, Rust, etc.) behave └─────────┘ ``` -1. [**Orchestrator**](./orchestrator): Spawns handler binary, sends test requests, validates responses -2. **Handler Binary**: Implements protocol, calls binding API, returns results - - [Go handler](./go-handler) for the [Go binding](https://github.com/stringintech/go-bitcoinkernel) - - [Rust handler](./rust-handler) for the [Rust binding](https://github.com/TheCharlatan/rust-bitcoinkernel) -3. [**Test Cases**](./testdata): JSON files defining requests and expected responses +**This repository contains:** +1. [**Orchestrator**](./orchestrator): Spawns handler binary, sends test requests via stdin, validates responses from stdout +2. [**Test Cases**](./testdata): JSON files defining requests and expected responses -## Getting Started - -### Cloning - -Clone the repository with submodules: - -```bash -git clone --recurse-submodules https://github.com/stringintech/kernel-bindings-spec.git -``` -**Note:** go-handler currently depends on go-bitcoinkernel via a submodule. - - -### Building - -Use the provided Makefile to build the project: - -```bash -# Build everything! -make build -``` - -### Running Tests - -```bash -# Run all conformance tests with both Go and Rust handlers -make test - -# Run a specific test file with both Go and Rust handlers -make test-single TEST=testdata/chainstate_basic.json -``` \ No newline at end of file +**Handler binaries** are not hosted in this repository. They must be implemented separately and should: +- Implement the JSON protocol for communication with the orchestrator +- Call the binding API to execute operations +- Pin to a specific version/tag of this test repository \ No newline at end of file diff --git a/go-handler/.gitignore b/go-handler/.gitignore deleted file mode 100644 index e660fd9..0000000 --- a/go-handler/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bin/ diff --git a/go-handler/chainstate.go b/go-handler/chainstate.go deleted file mode 100644 index 6ad6760..0000000 --- a/go-handler/chainstate.go +++ /dev/null @@ -1,192 +0,0 @@ -package main - -import ( - "encoding/hex" - "encoding/json" - "os" - - "github.com/stringintech/go-bitcoinkernel/kernel" -) - -// handleChainstateSetup initializes a chainstate and imports blocks -func handleChainstateSetup(req Request, state *SessionState) Response { - var params struct { - ChainType string `json:"chain_type"` - BlocksHex []string `json:"blocks_hex"` - } - - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - return NewErrorResponse(req.ID, ErrInvalidParams, "Failed to parse params: "+err.Error()) - } - - // Clean up any existing state - state.Cleanup() - - // Create temp directory - tempDir, err := os.MkdirTemp("", "conformance_test_") - if err != nil { - return NewErrorResponse(req.ID, ErrInternalError, "Failed to create temp dir: "+err.Error()) - } - state.tempDir = tempDir - - // Parse chain type - var chainType kernel.ChainType - switch params.ChainType { - case "mainnet": - chainType = kernel.ChainTypeMainnet - case "testnet": - chainType = kernel.ChainTypeTestnet - case "testnet4": - chainType = kernel.ChainTypeTestnet4 - case "signet": - chainType = kernel.ChainTypeSignet - case "regtest": - chainType = kernel.ChainTypeRegtest - default: - state.Cleanup() - return NewErrorResponse(req.ID, ErrInvalidParams, "Unknown chain type: "+params.ChainType) - } - - // Create chain parameters - chainParams, err := kernel.NewChainParameters(chainType) - if err != nil { - state.Cleanup() - return NewErrorResponse(req.ID, ErrKernel, "Failed to create chain parameters: "+err.Error()) - } - defer chainParams.Destroy() - - // Create context options - contextOpts := kernel.NewContextOptions() - contextOpts.SetChainParams(chainParams) - - // Create context - ctx, err := kernel.NewContext(contextOpts) - if err != nil { - state.Cleanup() - return NewErrorResponse(req.ID, ErrKernel, "Failed to create context: "+err.Error()) - } - defer ctx.Destroy() - - // Create chainstate manager options - opts, err := kernel.NewChainstateManagerOptions(ctx, state.tempDir, state.tempDir+"/blocks") - if err != nil { - state.Cleanup() - return NewErrorResponse(req.ID, ErrKernel, "Failed to create options: "+err.Error()) - } - defer opts.Destroy() - - // Configure for in-memory testing - opts.SetWorkerThreads(1) - opts.UpdateBlockTreeDBInMemory(true) - opts.UpdateChainstateDBInMemory(true) - if err := opts.SetWipeDBs(true, true); err != nil { - state.Cleanup() - return NewErrorResponse(req.ID, ErrKernel, "Failed to set wipe DBs: "+err.Error()) - } - - // Create chainstate manager - manager, err := kernel.NewChainstateManager(opts) - if err != nil { - state.Cleanup() - return NewErrorResponse(req.ID, ErrKernel, "Failed to create manager: "+err.Error()) - } - state.chainstateManager = manager - - // Initialize empty databases - if err := manager.ImportBlocks(nil); err != nil { - state.Cleanup() - return NewErrorResponse(req.ID, ErrKernel, "Failed to initialize: "+err.Error()) - } - - // Process blocks - blocksImported := 0 - for i, blockHex := range params.BlocksHex { - blockBytes, err := hex.DecodeString(blockHex) - if err != nil { - return NewErrorResponse(req.ID, ErrInvalidParams, "Invalid block hex at index "+string(rune(i))+": "+err.Error()) - } - - block, err := kernel.NewBlock(blockBytes) - if err != nil { - return NewErrorResponse(req.ID, ErrKernel, "Failed to create block at index "+string(rune(i))+": "+err.Error()) - } - - ok, duplicate := manager.ProcessBlock(block) - block.Destroy() - - if !ok || duplicate { - return NewErrorResponse(req.ID, ErrKernel, "Failed to process block at index "+string(rune(i))) - } - - blocksImported++ - } - - // Get tip height - chain := manager.GetActiveChain() - tipHeight := chain.GetHeight() - - result := map[string]interface{}{ - "blocks_imported": blocksImported, - "tip_height": tipHeight, - } - - return NewSuccessResponse(req.ID, result) -} - -// handleChainstateReadBlock reads a block by height or tip -func handleChainstateReadBlock(req Request, state *SessionState) Response { - if state.chainstateManager == nil { - return NewErrorResponse(req.ID, ErrInternalError, "Chainstate not initialized") - } - - var params struct { - Height *int32 `json:"height,omitempty"` - Tip *bool `json:"tip,omitempty"` - } - - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - return NewErrorResponse(req.ID, ErrInvalidParams, "Failed to parse params: "+err.Error()) - } - - chain := state.chainstateManager.GetActiveChain() - - var blockIndex *kernel.BlockTreeEntry - if params.Tip != nil && *params.Tip { - blockIndex = chain.GetTip() - } else if params.Height != nil { - blockIndex = chain.GetByHeight(*params.Height) - } else { - return NewErrorResponse(req.ID, ErrInvalidParams, "Must specify either height or tip") - } - - if blockIndex == nil { - return NewErrorResponse(req.ID, ErrKernel, "Block not found") - } - - block, err := state.chainstateManager.ReadBlock(blockIndex) - if err != nil { - return NewErrorResponse(req.ID, ErrKernel, "Failed to read block: "+err.Error()) - } - defer block.Destroy() - - blockBytes, err := block.Bytes() - if err != nil { - return NewErrorResponse(req.ID, ErrKernel, "Failed to serialize block: "+err.Error()) - } - - result := map[string]interface{}{ - "block_hex": hex.EncodeToString(blockBytes), - "height": blockIndex.Height(), - } - - return NewSuccessResponse(req.ID, result) -} - -// handleChainstateTeardown cleans up chainstate resources -func handleChainstateTeardown(req Request, state *SessionState) Response { - state.Cleanup() - result := map[string]interface{}{ - "success": true, - } - return NewSuccessResponse(req.ID, result) -} diff --git a/go-handler/go-bitcoinkernel b/go-handler/go-bitcoinkernel deleted file mode 160000 index 4e19882..0000000 --- a/go-handler/go-bitcoinkernel +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4e19882d7ea8717279e3df9592bc68d05ed81b4f diff --git a/go-handler/go.mod b/go-handler/go.mod deleted file mode 100644 index ec06133..0000000 --- a/go-handler/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module github.com/stringintech/go-bitcoinkernel/conformance/go-handler - -go 1.23.3 - -replace github.com/stringintech/go-bitcoinkernel => ./go-bitcoinkernel - -require github.com/stringintech/go-bitcoinkernel v0.0.0-00010101000000-000000000000 diff --git a/go-handler/handler.go b/go-handler/handler.go deleted file mode 100644 index 876f4bc..0000000 --- a/go-handler/handler.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import "fmt" - -// handleRequest dispatches a request to the appropriate handler -func handleRequest(req Request, state *SessionState) (resp Response) { - // Recover from panics and return error response - defer func() { - if r := recover(); r != nil { - resp = NewErrorResponse(req.ID, ErrInternalError, fmt.Sprintf("Internal error (panic): %v", r)) - } - }() - - switch req.Method { - // ScriptPubkey - case "script_pubkey.verify": - return handleScriptPubkeyVerify(req) - - // Chainstate - case "chainstate.setup": - return handleChainstateSetup(req, state) - case "chainstate.read_block": - return handleChainstateReadBlock(req, state) - case "chainstate.teardown": - return handleChainstateTeardown(req, state) - - default: - return NewErrorResponse(req.ID, ErrMethodNotFound, "Unknown method: "+req.Method) - } -} diff --git a/go-handler/main.go b/go-handler/main.go deleted file mode 100644 index 9d01cc8..0000000 --- a/go-handler/main.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "os" -) - -func main() { - // Create session state - state := NewSessionState() - defer state.Cleanup() - - // Read requests from stdin line by line - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - line := scanner.Text() - - // Parse request - var req Request - if err := json.Unmarshal([]byte(line), &req); err != nil { - resp := NewErrorResponse("", ErrInvalidRequest, "Failed to parse JSON: "+err.Error()) - sendResponse(resp) - continue - } - - resp := handleRequest(req, state) - sendResponse(resp) - } - - if err := scanner.Err(); err != nil { - fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err) - os.Exit(1) - } -} - -// sendResponse writes a response to stdout as JSON -func sendResponse(resp Response) { - data, err := json.Marshal(resp) - if err != nil { - fmt.Fprintf(os.Stderr, "Error marshaling response: %v\n", err) - return - } - - fmt.Println(string(data)) -} diff --git a/go-handler/protocol.go b/go-handler/protocol.go deleted file mode 100644 index 53102c9..0000000 --- a/go-handler/protocol.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "encoding/json" -) - -type Request struct { - ID string `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` -} - -type Response struct { - ID string `json:"id"` - Result interface{} `json:"result,omitempty"` - Error *ErrorObj `json:"error,omitempty"` -} - -type ErrorObj struct { - Code string `json:"code"` - Message string `json:"message"` -} - -// Standard error codes -const ( - ErrInvalidRequest = "INVALID_REQUEST" - ErrMethodNotFound = "METHOD_NOT_FOUND" - ErrInvalidParams = "INVALID_PARAMS" - ErrKernel = "KERNEL_ERROR" - ErrScriptVerify = "SCRIPT_VERIFY_ERROR" - ErrInternalError = "INTERNAL_ERROR" -) - -// NewErrorResponse creates an error response -func NewErrorResponse(id, code, message string) Response { - return Response{ - ID: id, - Error: &ErrorObj{ - Code: code, - Message: message, - }, - } -} - -// NewSuccessResponse creates a success response -func NewSuccessResponse(id string, result interface{}) Response { - return Response{ - ID: id, - Result: result, - } -} diff --git a/go-handler/script_pubkey.go b/go-handler/script_pubkey.go deleted file mode 100644 index edcc3a3..0000000 --- a/go-handler/script_pubkey.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "encoding/hex" - "encoding/json" - "errors" - - "github.com/stringintech/go-bitcoinkernel/kernel" -) - -// handleScriptPubkeyVerify verifies a script against a transaction -func handleScriptPubkeyVerify(req Request) Response { - var params struct { - ScriptPubkeyHex string `json:"script_pubkey_hex"` - Amount int64 `json:"amount"` - TxHex string `json:"tx_hex"` - InputIndex uint `json:"input_index"` - Flags string `json:"flags"` - } - - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - return NewErrorResponse(req.ID, ErrInvalidParams, "Failed to parse params: "+err.Error()) - } - - // Decode script pubkey - var scriptBytes []byte - var err error - if params.ScriptPubkeyHex != "" { - scriptBytes, err = hex.DecodeString(params.ScriptPubkeyHex) - if err != nil { - return NewErrorResponse(req.ID, ErrInvalidParams, "Invalid script pubkey hex: "+err.Error()) - } - } - - // Decode transaction - txBytes, err := hex.DecodeString(params.TxHex) - if err != nil { - return NewErrorResponse(req.ID, ErrInvalidParams, "Invalid transaction hex: "+err.Error()) - } - - // Create script pubkey - scriptPubkey := kernel.NewScriptPubkey(scriptBytes) - defer scriptPubkey.Destroy() - - // Create transaction - tx, err := kernel.NewTransaction(txBytes) - if err != nil { - return NewErrorResponse(req.ID, ErrKernel, "Failed to create transaction: "+err.Error()) - } - defer tx.Destroy() - - // Parse flags - var flags kernel.ScriptFlags - switch params.Flags { - case "VERIFY_ALL_NO_TAPROOT": - flags = kernel.ScriptFlags(kernel.ScriptFlagsVerifyAll &^ kernel.ScriptFlagsVerifyTaproot) - case "VERIFY_ALL": - flags = kernel.ScriptFlagsVerifyAll - case "VERIFY_NONE": - flags = kernel.ScriptFlagsVerifyNone - default: - return NewErrorResponse(req.ID, ErrInvalidParams, "Unknown flags: "+params.Flags) - } - - // Verify script - err = scriptPubkey.Verify(params.Amount, tx, nil, params.InputIndex, flags) - - if err != nil { - // Check if it's a script verification error - var scriptVerifyError *kernel.ScriptVerifyError - if errors.As(err, &scriptVerifyError) { - return NewErrorResponse(req.ID, ErrScriptVerify, "Script verification failed") - } - return NewErrorResponse(req.ID, ErrKernel, "Verification error: "+err.Error()) - } - - result := map[string]interface{}{ - "valid": true, - } - - return NewSuccessResponse(req.ID, result) -} diff --git a/go-handler/state.go b/go-handler/state.go deleted file mode 100644 index 2e1040a..0000000 --- a/go-handler/state.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "os" - - "github.com/stringintech/go-bitcoinkernel/kernel" -) - -// SessionState holds stateful resources for the test session -type SessionState struct { - chainstateManager *kernel.ChainstateManager - tempDir string -} - -// NewSessionState creates a new session state -func NewSessionState() *SessionState { - return &SessionState{} -} - -// Cleanup destroys all resources and removes temp directories -func (s *SessionState) Cleanup() { - if s.chainstateManager != nil { - s.chainstateManager.Destroy() - s.chainstateManager = nil - } - if s.tempDir != "" { - os.RemoveAll(s.tempDir) - s.tempDir = "" - } -} diff --git a/rust-handler/.gitignore b/rust-handler/.gitignore deleted file mode 100644 index c41cc9e..0000000 --- a/rust-handler/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target \ No newline at end of file diff --git a/rust-handler/Cargo.lock b/rust-handler/Cargo.lock deleted file mode 100644 index 7fbe116..0000000 --- a/rust-handler/Cargo.lock +++ /dev/null @@ -1,430 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitcoinkernel" -version = "0.0.23" -source = "git+https://github.com/TheCharlatan/rust-bitcoinkernel.git?branch=master#3eef72400f7d1b47d202a8ec9847be013be49c97" -dependencies = [ - "libbitcoinkernel-sys", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "cc" -version = "1.2.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "libbitcoinkernel-sys" -version = "0.0.22" -source = "git+https://github.com/TheCharlatan/rust-bitcoinkernel.git?branch=master#3eef72400f7d1b47d202a8ec9847be013be49c97" -dependencies = [ - "bindgen", - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "libc" -version = "0.2.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rust-handler" -version = "0.1.0" -dependencies = [ - "bitcoinkernel", - "hex", - "serde", - "serde_json", - "tempfile", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "syn" -version = "2.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/rust-handler/Cargo.toml b/rust-handler/Cargo.toml deleted file mode 100644 index d9664b7..0000000 --- a/rust-handler/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "rust-handler" -version = "0.1.0" -edition = "2021" -rust-version = "1.71.0" - -[dependencies] -bitcoinkernel = { git = "https://github.com/TheCharlatan/rust-bitcoinkernel.git", branch = "master" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -hex = "0.4" -tempfile = "3.8" diff --git a/rust-handler/src/chainstate.rs b/rust-handler/src/chainstate.rs deleted file mode 100644 index 217b851..0000000 --- a/rust-handler/src/chainstate.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::protocol::{Response, ERR_INTERNAL, ERR_INVALID_PARAMS, ERR_KERNEL}; -use crate::state::SessionState; -use bitcoinkernel::{Block, ChainType, ChainstateManager, Context}; -use serde::Deserialize; -use serde_json::json; - -#[derive(Debug, Deserialize)] -pub struct SetupParams { - pub chain_type: String, - pub blocks_hex: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct ReadBlockParams { - pub height: Option, - pub tip: Option, -} - -pub fn handle_chainstate_setup( - id: String, - params: serde_json::Value, - state: &mut SessionState, -) -> Response { - let params: SetupParams = match serde_json::from_value(params) { - Ok(p) => p, - Err(e) => { - return Response::error( - id, - ERR_INVALID_PARAMS, - format!("Failed to parse params: {}", e), - ); - } - }; - - // Clean up any existing state - state.cleanup(); - - // Create temp directory - let temp_dir = match tempfile::tempdir() { - Ok(dir) => dir, - Err(e) => { - return Response::error( - id, - ERR_INTERNAL, - format!("Failed to create temp dir: {}", e), - ); - } - }; - - let data_dir = temp_dir.path().to_str().unwrap(); - let blocks_dir = temp_dir.path().join("blocks"); - let blocks_dir_str = blocks_dir.to_str().unwrap(); - - // Parse chain type - let chain_type = match params.chain_type.as_str() { - "mainnet" => ChainType::Mainnet, - "testnet" => ChainType::Testnet, - "testnet4" => ChainType::Testnet4, - "signet" => ChainType::Signet, - "regtest" => ChainType::Regtest, - _ => { - return Response::error( - id, - ERR_INVALID_PARAMS, - format!("Unknown chain type: {}", params.chain_type), - ); - } - }; - - // Create context - let context = match Context::builder().chain_type(chain_type).build() { - Ok(ctx) => ctx, - Err(e) => { - return Response::error(id, ERR_KERNEL, format!("Failed to create context: {}", e)); - } - }; - - // Create chainstate manager - let chainman = match ChainstateManager::builder(&context, data_dir, blocks_dir_str) - .and_then(|builder| { - builder - .worker_threads(1) - .block_tree_db_in_memory(true) - .chainstate_db_in_memory(true) - .wipe_db(true, true) - }) - .and_then(|builder| builder.build()) - { - Ok(cm) => cm, - Err(e) => { - return Response::error( - id, - ERR_KERNEL, - format!("Failed to create chainstate manager: {}", e), - ); - } - }; - - // Initialize empty databases - if let Err(e) = chainman.import_blocks() { - return Response::error(id, ERR_KERNEL, format!("Failed to initialize: {}", e)); - } - - // Process blocks - let mut blocks_imported = 0; - for (i, block_hex) in params.blocks_hex.iter().enumerate() { - let block_bytes = match hex::decode(block_hex) { - Ok(bytes) => bytes, - Err(e) => { - return Response::error( - id, - ERR_INVALID_PARAMS, - format!("Invalid block hex at index {}: {}", i, e), - ); - } - }; - - let block = match Block::new(&block_bytes) { - Ok(b) => b, - Err(e) => { - return Response::error( - id, - ERR_KERNEL, - format!("Failed to create block at index {}: {}", i, e), - ); - } - }; - - let result = chainman.process_block(&block); - - if result.is_rejected() { - return Response::error( - id, - ERR_KERNEL, - format!("Failed to process block at index {}", i), - ); - } - - if result.is_new_block() { - blocks_imported += 1; - } - } - - // Get tip height - let chain = chainman.active_chain(); - let tip_height = chain.height(); - - // Store state - state.context = Some(context); - state.chainstate_manager = Some(chainman); - state.temp_dir = Some(temp_dir); - - Response::success( - id, - json!({ - "blocks_imported": blocks_imported, - "tip_height": tip_height, - }), - ) -} - -pub fn handle_chainstate_read_block( - id: String, - params: serde_json::Value, - state: &SessionState, -) -> Response { - let chainman = match &state.chainstate_manager { - Some(cm) => cm, - None => { - return Response::error(id, ERR_INTERNAL, "Chainstate not initialized".to_string()); - } - }; - - let params: ReadBlockParams = match serde_json::from_value(params) { - Ok(p) => p, - Err(e) => { - return Response::error( - id, - ERR_INVALID_PARAMS, - format!("Failed to parse params: {}", e), - ); - } - }; - - let chain = chainman.active_chain(); - - let block_index = if params.tip.unwrap_or(false) { - chain.tip() - } else if let Some(height) = params.height { - match chain.at_height(height as usize) { - Some(bi) => bi, - None => { - return Response::error(id, ERR_KERNEL, "Block not found".to_string()); - } - } - } else { - return Response::error( - id, - ERR_INVALID_PARAMS, - "Must specify either height or tip".to_string(), - ); - }; - - let height = block_index.height(); - - let block = match chainman.read_block_data(&block_index) { - Ok(b) => b, - Err(e) => { - return Response::error(id, ERR_KERNEL, format!("Failed to read block: {}", e)); - } - }; - - let block_bytes = match block.consensus_encode() { - Ok(bytes) => bytes, - Err(e) => { - return Response::error(id, ERR_KERNEL, format!("Failed to serialize block: {}", e)); - } - }; - let block_hex = hex::encode(block_bytes); - - Response::success( - id, - json!({ - "block_hex": block_hex, - "height": height, - }), - ) -} - -pub fn handle_chainstate_teardown(id: String, state: &mut SessionState) -> Response { - state.cleanup(); - Response::success(id, json!({"success": true})) -} diff --git a/rust-handler/src/handler.rs b/rust-handler/src/handler.rs deleted file mode 100644 index 908b39e..0000000 --- a/rust-handler/src/handler.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::chainstate::{ - handle_chainstate_read_block, handle_chainstate_setup, handle_chainstate_teardown, -}; -use crate::protocol::{Request, Response, ERR_INTERNAL, ERR_METHOD_NOT_FOUND}; -use crate::script_pubkey::handle_script_pubkey_verify; -use crate::state::SessionState; -use std::panic; - -pub fn handle_request(req: Request, state: &mut SessionState) -> Response { - // Catch panics and return error response - let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { - dispatch_request(req, state) - })); - - match result { - Ok(response) => response, - Err(e) => { - let panic_msg = if let Some(s) = e.downcast_ref::<&str>() { - s.to_string() - } else if let Some(s) = e.downcast_ref::() { - s.clone() - } else { - "Unknown panic".to_string() - }; - Response::error( - String::new(), - ERR_INTERNAL, - format!("Internal error (panic): {}", panic_msg), - ) - } - } -} - -fn dispatch_request(req: Request, state: &mut SessionState) -> Response { - match req.method.as_str() { - // ScriptPubkey - "script_pubkey.verify" => handle_script_pubkey_verify(req.id, req.params), - - // Chainstate - "chainstate.setup" => handle_chainstate_setup(req.id, req.params, state), - "chainstate.read_block" => handle_chainstate_read_block(req.id, req.params, state), - "chainstate.teardown" => handle_chainstate_teardown(req.id, state), - - _ => Response::error( - req.id, - ERR_METHOD_NOT_FOUND, - format!("Unknown method: {}", req.method), - ), - } -} - -// Helper for panic::catch_unwind with mutable references -struct AssertUnwindSafe(F); - -impl std::ops::Deref for AssertUnwindSafe { - type Target = F; - fn deref(&self) -> &F { - &self.0 - } -} - -impl std::ops::DerefMut for AssertUnwindSafe { - fn deref_mut(&mut self) -> &mut F { - &mut self.0 - } -} - -impl R, R> std::panic::UnwindSafe for AssertUnwindSafe {} diff --git a/rust-handler/src/main.rs b/rust-handler/src/main.rs deleted file mode 100644 index 6904a12..0000000 --- a/rust-handler/src/main.rs +++ /dev/null @@ -1,63 +0,0 @@ -mod chainstate; -mod handler; -mod protocol; -mod script_pubkey; -mod state; - -use handler::handle_request; -use protocol::{Request, Response, ERR_INVALID_REQUEST}; -use state::SessionState; -use std::io::{self, BufRead, Write}; - -fn main() { - // Create session state - let mut state = SessionState::new(); - - // Read requests from stdin line by line - let stdin = io::stdin(); - let mut stdout = io::stdout(); - - for line in stdin.lock().lines() { - let line = match line { - Ok(l) => l, - Err(e) => { - eprintln!("Error reading stdin: {}", e); - std::process::exit(1); - } - }; - - // Parse request - let req: Request = match serde_json::from_str(&line) { - Ok(r) => r, - Err(e) => { - let resp = Response::error( - String::new(), - ERR_INVALID_REQUEST, - format!("Failed to parse JSON: {}", e), - ); - send_response(&mut stdout, &resp); - continue; - } - }; - - // Handle request - let resp = handle_request(req, &mut state); - send_response(&mut stdout, &resp); - } -} - -fn send_response(stdout: &mut impl Write, resp: &Response) { - match serde_json::to_string(resp) { - Ok(json) => { - if let Err(e) = writeln!(stdout, "{}", json) { - eprintln!("Error writing response: {}", e); - } - if let Err(e) = stdout.flush() { - eprintln!("Error flushing stdout: {}", e); - } - } - Err(e) => { - eprintln!("Error marshaling response: {}", e); - } - } -} diff --git a/rust-handler/src/protocol.rs b/rust-handler/src/protocol.rs deleted file mode 100644 index 69302ab..0000000 --- a/rust-handler/src/protocol.rs +++ /dev/null @@ -1,52 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize)] -pub struct Request { - pub id: String, - pub method: String, - pub params: serde_json::Value, -} - -#[derive(Debug, Serialize)] -pub struct Response { - pub id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -#[derive(Debug, Serialize)] -pub struct ErrorObj { - pub code: String, - pub message: String, -} - -// Standard error codes -pub const ERR_INVALID_REQUEST: &str = "INVALID_REQUEST"; -pub const ERR_METHOD_NOT_FOUND: &str = "METHOD_NOT_FOUND"; -pub const ERR_INVALID_PARAMS: &str = "INVALID_PARAMS"; -pub const ERR_KERNEL: &str = "KERNEL_ERROR"; -pub const ERR_SCRIPT_VERIFY: &str = "SCRIPT_VERIFY_ERROR"; -pub const ERR_INTERNAL: &str = "INTERNAL_ERROR"; - -impl Response { - pub fn success(id: String, result: serde_json::Value) -> Self { - Response { - id, - result: Some(result), - error: None, - } - } - - pub fn error(id: String, code: &str, message: String) -> Self { - Response { - id, - result: None, - error: Some(ErrorObj { - code: code.to_string(), - message, - }), - } - } -} diff --git a/rust-handler/src/script_pubkey.rs b/rust-handler/src/script_pubkey.rs deleted file mode 100644 index 812f1d9..0000000 --- a/rust-handler/src/script_pubkey.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::protocol::{Response, ERR_INVALID_PARAMS, ERR_KERNEL, ERR_SCRIPT_VERIFY}; -use bitcoinkernel::{verify, KernelError, ScriptPubkey, Transaction, TxOut}; -use bitcoinkernel::{VERIFY_ALL, VERIFY_ALL_PRE_TAPROOT, VERIFY_NONE}; -use serde::Deserialize; -use serde_json::json; - -#[derive(Debug, Deserialize)] -pub struct VerifyParams { - pub script_pubkey_hex: String, - pub amount: i64, - pub tx_hex: String, - pub input_index: usize, - pub flags: String, -} - -pub fn handle_script_pubkey_verify( - id: String, - params: serde_json::Value, -) -> Response { - let params: VerifyParams = match serde_json::from_value(params) { - Ok(p) => p, - Err(e) => { - return Response::error( - id, - ERR_INVALID_PARAMS, - format!("Failed to parse params: {}", e), - ); - } - }; - - // Decode script pubkey - let script_bytes = if params.script_pubkey_hex.is_empty() { - vec![] - } else { - match hex::decode(¶ms.script_pubkey_hex) { - Ok(bytes) => bytes, - Err(e) => { - return Response::error( - id, - ERR_INVALID_PARAMS, - format!("Invalid script pubkey hex: {}", e), - ); - } - } - }; - - // Decode transaction - let tx_bytes = match hex::decode(¶ms.tx_hex) { - Ok(bytes) => bytes, - Err(e) => { - return Response::error( - id, - ERR_INVALID_PARAMS, - format!("Invalid transaction hex: {}", e), - ); - } - }; - - // Create script pubkey - let script_pubkey = match ScriptPubkey::new(&script_bytes) { - Ok(s) => s, - Err(e) => { - return Response::error( - id, - ERR_KERNEL, - format!("Failed to create script pubkey: {}", e), - ); - } - }; - - // Create transaction - let tx = match Transaction::new(&tx_bytes) { - Ok(t) => t, - Err(e) => { - return Response::error( - id, - ERR_KERNEL, - format!("Failed to create transaction: {}", e), - ); - } - }; - - // Parse flags - let flags = match params.flags.as_str() { - "VERIFY_ALL_NO_TAPROOT" => VERIFY_ALL_PRE_TAPROOT, - "VERIFY_ALL" => VERIFY_ALL, - "VERIFY_NONE" => VERIFY_NONE, - _ => { - return Response::error( - id, - ERR_INVALID_PARAMS, - format!("Unknown flags: {}", params.flags), - ); - } - }; - - // Verify script - let empty_spent_outputs: Vec = vec![]; - let result = verify( - &script_pubkey, - Some(params.amount), - &tx, - params.input_index, - Some(flags), - &empty_spent_outputs, - ); - - match result { - Ok(()) => Response::success(id, json!({"valid": true})), - Err(KernelError::ScriptVerify(_)) => { - Response::error(id, ERR_SCRIPT_VERIFY, "Script verification failed".to_string()) - } - Err(e) => Response::error(id, ERR_KERNEL, format!("Verification error: {}", e)), - } -} diff --git a/rust-handler/src/state.rs b/rust-handler/src/state.rs deleted file mode 100644 index dcf9aec..0000000 --- a/rust-handler/src/state.rs +++ /dev/null @@ -1,32 +0,0 @@ -use bitcoinkernel::{ChainstateManager, Context}; - -pub struct SessionState { - pub chainstate_manager: Option, - pub context: Option, - pub temp_dir: Option, -} - -impl SessionState { - pub fn new() -> Self { - SessionState { - chainstate_manager: None, - context: None, - temp_dir: None, - } - } - - pub fn cleanup(&mut self) { - // Drop chainstate manager first - self.chainstate_manager = None; - // Then drop context - self.context = None; - // Finally drop temp dir (which will delete it) - self.temp_dir = None; - } -} - -impl Drop for SessionState { - fn drop(&mut self) { - self.cleanup(); - } -} From 62b1729a4d5dc6178413eea0c0e4bbafb6d659e1 Mon Sep 17 00:00:00 2001 From: stringintech Date: Mon, 17 Nov 2025 14:54:15 +0330 Subject: [PATCH 2/4] Restructure project, rename orchestrator to runner, and embed json test data --- README.md | 12 ++++---- {orchestrator => cmd/runner}/main.go | 41 ++++++++++------------------ go.mod | 3 ++ orchestrator/.gitignore | 1 - orchestrator/go.mod | 3 -- {orchestrator => runner}/runner.go | 24 +++++++++++++++- {orchestrator => runner}/types.go | 2 +- testdata/testdata.go | 6 ++++ 8 files changed, 54 insertions(+), 38 deletions(-) rename {orchestrator => cmd/runner}/main.go (63%) create mode 100644 go.mod delete mode 100644 orchestrator/.gitignore delete mode 100644 orchestrator/go.mod rename {orchestrator => runner}/runner.go (91%) rename {orchestrator => runner}/types.go (98%) create mode 100644 testdata/testdata.go diff --git a/README.md b/README.md index 31c2a65..49d56bd 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ The framework ensures that all language bindings (Go, Python, Rust, etc.) behave ``` ┌─────────────┐ ┌──────────────────┐ -│ Orchestrator│────────▶│ Handler Binary │ -│ (Go Test │ stdin │ (Go/Rust/etc) │ -│ Runner) │◀────────│ │ +│ Test Runner │────────▶│ Handler Binary │ +│ (Go CLI) │ stdin │ (Go/Rust/etc) │ +│ │◀────────│ │ └─────────────┘ stdout └──────────────────┘ │ │ │ │ @@ -25,15 +25,15 @@ The framework ensures that all language bindings (Go, Python, Rust, etc.) behave ┌─────────┐ ┌────────────────┐ │ Test │ │ Binding API │ │ Cases │ └────────────────┘ - │ (JSON) │ + │ (JSON) │ └─────────┘ ``` **This repository contains:** -1. [**Orchestrator**](./orchestrator): Spawns handler binary, sends test requests via stdin, validates responses from stdout +1. [**Test Runner**](./cmd/runner/main.go): Spawns handler binary, sends test requests via stdin, validates responses from stdout 2. [**Test Cases**](./testdata): JSON files defining requests and expected responses **Handler binaries** are not hosted in this repository. They must be implemented separately and should: -- Implement the JSON protocol for communication with the orchestrator +- Implement the JSON protocol for communication with the test runner - Call the binding API to execute operations - Pin to a specific version/tag of this test repository \ No newline at end of file diff --git a/orchestrator/main.go b/cmd/runner/main.go similarity index 63% rename from orchestrator/main.go rename to cmd/runner/main.go index fdddad3..d89fcde 100644 --- a/orchestrator/main.go +++ b/cmd/runner/main.go @@ -3,15 +3,16 @@ package main import ( "flag" "fmt" + "io/fs" "os" - "path/filepath" "strings" + + "github.com/stringintech/kernel-bindings-tests/runner" + "github.com/stringintech/kernel-bindings-tests/testdata" ) func main() { handlerPath := flag.String("handler", "", "Path to handler binary") - testDir := flag.String("testdir", "", "Directory containing test JSON files") - testFile := flag.String("testfile", "", "Single test file to run") flag.Parse() if *handlerPath == "" { @@ -20,25 +21,13 @@ func main() { os.Exit(1) } - if *testDir == "" && *testFile == "" { - fmt.Fprintf(os.Stderr, "Error: either -testdir or -testfile must be specified\n") - flag.Usage() + // Collect embedded test files + testFiles, err := fs.Glob(testdata.FS, "*.json") + if err != nil { + fmt.Fprintf(os.Stderr, "Error finding test files: %v\n", err) os.Exit(1) } - // Collect test files - var testFiles []string - if *testFile != "" { - testFiles = []string{*testFile} - } else { - files, err := filepath.Glob(filepath.Join(*testDir, "*.json")) - if err != nil { - fmt.Fprintf(os.Stderr, "Error finding test files: %v\n", err) - os.Exit(1) - } - testFiles = files - } - if len(testFiles) == 0 { fmt.Fprintf(os.Stderr, "No test files found\n") os.Exit(1) @@ -50,25 +39,25 @@ func main() { totalTests := 0 for _, testFile := range testFiles { - fmt.Printf("\n=== Running test suite: %s ===\n", filepath.Base(testFile)) + fmt.Printf("\n=== Running test suite: %s ===\n", testFile) - // Load test suite - suite, err := LoadTestSuite(testFile) + // Load test suite from embedded FS + suite, err := runner.LoadTestSuiteFromFS(testdata.FS, testFile) if err != nil { fmt.Fprintf(os.Stderr, "Error loading test suite: %v\n", err) continue } // Create test runner - runner, err := NewTestRunner(*handlerPath) + testRunner, err := runner.NewTestRunner(*handlerPath) if err != nil { fmt.Fprintf(os.Stderr, "Error creating test runner: %v\n", err) continue } // Run suite - result := runner.RunTestSuite(*suite) - runner.Close() + result := testRunner.RunTestSuite(*suite) + testRunner.Close() printResults(result) @@ -90,7 +79,7 @@ func main() { } } -func printResults(result TestResult) { +func printResults(result runner.TestResult) { fmt.Printf("\nTest Suite: %s\n", result.SuiteName) fmt.Printf("Total: %d, Passed: %d, Failed: %d\n\n", result.TotalTests, result.PassedTests, result.FailedTests) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6cf3000 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/stringintech/kernel-bindings-tests + +go 1.23 diff --git a/orchestrator/.gitignore b/orchestrator/.gitignore deleted file mode 100644 index e660fd9..0000000 --- a/orchestrator/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bin/ diff --git a/orchestrator/go.mod b/orchestrator/go.mod deleted file mode 100644 index 280e707..0000000 --- a/orchestrator/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/stringintech/go-bitcoinkernel/conformance/orchestrator - -go 1.23.3 diff --git a/orchestrator/runner.go b/runner/runner.go similarity index 91% rename from orchestrator/runner.go rename to runner/runner.go index 9d23a29..a3be11b 100644 --- a/orchestrator/runner.go +++ b/runner/runner.go @@ -1,11 +1,13 @@ -package main +package runner import ( "bufio" "bytes" + "embed" "encoding/json" "fmt" "io" + "io/fs" "os" "os/exec" "path/filepath" @@ -261,3 +263,23 @@ func LoadTestSuite(filePath string) (*TestSuite, error) { return &suite, nil } + +// LoadTestSuiteFromFS loads a test suite from an embedded filesystem +func LoadTestSuiteFromFS(fsys embed.FS, filePath string) (*TestSuite, error) { + data, err := fs.ReadFile(fsys, filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + var suite TestSuite + if err := json.Unmarshal(data, &suite); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) + } + + // Set suite name from filename if not specified + if suite.Name == "" { + suite.Name = filepath.Base(filePath) + } + + return &suite, nil +} diff --git a/orchestrator/types.go b/runner/types.go similarity index 98% rename from orchestrator/types.go rename to runner/types.go index fd974e9..05d7268 100644 --- a/orchestrator/types.go +++ b/runner/types.go @@ -1,4 +1,4 @@ -package main +package runner import ( "encoding/json" diff --git a/testdata/testdata.go b/testdata/testdata.go new file mode 100644 index 0000000..4d2220a --- /dev/null +++ b/testdata/testdata.go @@ -0,0 +1,6 @@ +package testdata + +import "embed" + +//go:embed *.json +var FS embed.FS From b1dfae813fd1ddb21cfb55e6cf295dff42ea3226 Mon Sep 17 00:00:00 2001 From: stringintech Date: Mon, 17 Nov 2025 18:23:35 +0330 Subject: [PATCH 3/4] Add mock handler Implement a mock handler that validates the test framework itself. The handler embeds test cases, reads requests from stdin, and returns expected responses from the JSON test files. --- .github/workflows/test.yml | 24 ++++++ .gitignore | 1 + Makefile | 27 +++++++ README.md | 15 +++- cmd/mock-handler/main.go | 158 +++++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/mock-handler/main.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..44135b5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Build + run: make build + + - name: Run tests against mock handler + run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c4f3b37 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: all build test clean runner mock-handler deps lint + +BUILD_DIR := build +RUNNER_BIN := $(BUILD_DIR)/runner +MOCK_HANDLER_BIN := $(BUILD_DIR)/mock-handler + +all: build test + +build: runner mock-handler + +runner: + @echo "Building test runner..." + @mkdir -p $(BUILD_DIR) + go build -o $(RUNNER_BIN) ./cmd/runner + +mock-handler: + @echo "Building mock handler..." + @mkdir -p $(BUILD_DIR) + go build -o $(MOCK_HANDLER_BIN) ./cmd/mock-handler + +test: + @echo "Running conformance tests with mock handler..." + $(RUNNER_BIN) -handler $(MOCK_HANDLER_BIN) + +clean: + @echo "Cleaning build artifacts..." + rm -rf $(BUILD_DIR) diff --git a/README.md b/README.md index 49d56bd..c0a85a2 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,21 @@ The framework ensures that all language bindings (Go, Python, Rust, etc.) behave **This repository contains:** 1. [**Test Runner**](./cmd/runner/main.go): Spawns handler binary, sends test requests via stdin, validates responses from stdout 2. [**Test Cases**](./testdata): JSON files defining requests and expected responses +3. [**Mock Handler**](./cmd/mock-handler/main.go): Validates the runner by echoing expected responses from test cases **Handler binaries** are not hosted in this repository. They must be implemented separately and should: - Implement the JSON protocol for communication with the test runner - Call the binding API to execute operations -- Pin to a specific version/tag of this test repository \ No newline at end of file +- Pin to a specific version/tag of this test repository + +## Getting Started + +Build and test against the mock handler: + +```bash +# Build both runner and mock handler +make build + +# Run tests against the mock handler +make test +``` diff --git a/cmd/mock-handler/main.go b/cmd/mock-handler/main.go new file mode 100644 index 0000000..d789606 --- /dev/null +++ b/cmd/mock-handler/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io/fs" + "os" + + "github.com/stringintech/kernel-bindings-tests/runner" + "github.com/stringintech/kernel-bindings-tests/testdata" +) + +func main() { + // Build a map of test ID -> filename + testIndex, err := buildTestIndex() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to build test index: %v\n", err) + os.Exit(1) + } + + // Read requests from stdin and respond with expected results + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Text() + if err := handleRequest(line, testIndex); err != nil { + fmt.Fprintf(os.Stderr, "Error handling request: %v\n", err) + continue + } + } + + if err := scanner.Err(); err != nil { + fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err) + os.Exit(1) + } +} + +// buildTestIndex creates a map of test ID -> filename without loading full test data +func buildTestIndex() (map[string]string, error) { + testFiles, err := fs.Glob(testdata.FS, "*.json") + if err != nil { + return nil, fmt.Errorf("failed to find test files: %w", err) + } + + index := make(map[string]string) + for _, testFile := range testFiles { + // Read file to extract test IDs only + data, err := fs.ReadFile(testdata.FS, testFile) + if err != nil { + return nil, fmt.Errorf("failed to read test file %s: %w", testFile, err) + } + + // Parse just enough to get test IDs + var suite struct { + Tests []struct { + ID string `json:"id"` + } `json:"tests"` + } + if err := json.Unmarshal(data, &suite); err != nil { + return nil, fmt.Errorf("failed to parse test file %s: %w", testFile, err) + } + + for _, test := range suite.Tests { + index[test.ID] = testFile + } + } + + return index, nil +} + +// handleRequest processes a single request and outputs the expected response +func handleRequest(line string, testIndex map[string]string) error { + // Parse request + var req runner.Request + if err := json.Unmarshal([]byte(line), &req); err != nil { + return fmt.Errorf("failed to parse request: %w", err) + } + + filename, ok := testIndex[req.ID] + if !ok { + resp := runner.Response{ + ID: req.ID, + Error: &runner.ErrorObj{ + Code: "UNKNOWN_TEST", + Message: fmt.Sprintf("No test case found with ID: %s", req.ID), + }, + } + return writeResponse(resp) + } + + // Load the test suite containing this test case + suite, err := runner.LoadTestSuiteFromFS(testdata.FS, filename) + if err != nil { + resp := runner.Response{ + ID: req.ID, + Error: &runner.ErrorObj{ + Code: "LOAD_ERROR", + Message: fmt.Sprintf("Failed to load test suite: %v", err), + }, + } + return writeResponse(resp) + } + + // Find the specific test case + var testCase *runner.TestCase + for _, test := range suite.Tests { + if test.ID == req.ID { + testCase = &test + break + } + } + if testCase == nil { + resp := runner.Response{ + ID: req.ID, + Error: &runner.ErrorObj{ + Code: "TEST_NOT_FOUND", + Message: fmt.Sprintf("Test case %s not found in file %s", req.ID, filename), + }, + } + return writeResponse(resp) + } + + // Verify method matches + if req.Method != testCase.Method { + resp := runner.Response{ + ID: req.ID, + Error: &runner.ErrorObj{ + Code: "METHOD_MISMATCH", + Message: fmt.Sprintf("Expected method %s, got %s", testCase.Method, req.Method), + }, + } + return writeResponse(resp) + } + + // Build response based on expected result + var resp runner.Response + resp.ID = req.ID + if testCase.Expected.Error != nil { + resp.Error = &runner.ErrorObj{ + Code: testCase.Expected.Error.Code, + Message: testCase.Expected.Error.Message, + } + } + if testCase.Expected.Success != nil { + resp.Result = *testCase.Expected.Success + } + return writeResponse(resp) +} + +// writeResponse writes a response to stdout as JSON +func writeResponse(resp runner.Response) error { + data, err := json.Marshal(resp) + if err != nil { + return fmt.Errorf("failed to marshal response: %w", err) + } + fmt.Println(string(data)) + return nil +} From 85efd3ffd4fe1dff5e344e0509f2607cccd984a9 Mon Sep 17 00:00:00 2001 From: stringintech Date: Mon, 17 Nov 2025 19:04:42 +0330 Subject: [PATCH 4/4] Add basic goreleaser configuration Configure goreleaser to build and release the runner binary for multiple platforms --- .github/workflows/release.yml | 32 +++++++++++++++++++++++++++ .goreleaser.yml | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d016d4f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..5d29f83 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,41 @@ +project_name: kernel-bindings-tests + +builds: + - id: runner + main: ./cmd/runner + binary: runner + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + ignore: + - goos: windows + goarch: arm64 + +archives: + - id: runner-archive + builds: + - runner + format: tar.gz + format_overrides: + - goos: windows + format: zip + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + +checksum: + disable: true + +changelog: + disable: true + +release: + github: + owner: stringintech + name: kernel-bindings-tests + draft: false + prerelease: auto