diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8b674e..d75f32f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,3 +119,46 @@ jobs: - name: Run linter run: nix develop --command make lint + + conformance: + name: Conformance Tests + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + platform: darwin_arm64 + - os: macos-15-intel + platform: darwin_amd64 + - os: ubuntu-latest + platform: linux_amd64 + - os: ubuntu-24.04-arm + platform: linux_arm64 + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.12' + + - name: Install dependencies (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libboost-all-dev + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install boost + + - name: Build Kernel + run: make build-kernel + + - name: Run conformance tests + working-directory: cmd/conformance-handler + run: make test diff --git a/cmd/conformance-handler/.gitignore b/cmd/conformance-handler/.gitignore new file mode 100644 index 0000000..0426669 --- /dev/null +++ b/cmd/conformance-handler/.gitignore @@ -0,0 +1,2 @@ +.conformance-tests +handler \ No newline at end of file diff --git a/cmd/conformance-handler/Makefile b/cmd/conformance-handler/Makefile new file mode 100644 index 0000000..a3736b0 --- /dev/null +++ b/cmd/conformance-handler/Makefile @@ -0,0 +1,79 @@ +# Conformance Handler Makefile + +# Test suite configuration +TEST_VERSION := 0.0.3-alpha.3 +TEST_REPO := stringintech/kernel-bindings-tests +TEST_DIR := .conformance-tests + +# Platform detection +UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) + +ifeq ($(UNAME_S),Darwin) + ifeq ($(UNAME_M),arm64) + PLATFORM := darwin_arm64 + else + PLATFORM := darwin_amd64 + endif +else ifeq ($(UNAME_S),Linux) + ifeq ($(UNAME_M),x86_64) + PLATFORM := linux_amd64 + else ifeq ($(UNAME_M),aarch64) + PLATFORM := linux_arm64 + else + PLATFORM := linux_amd64 + endif +else + $(error Unsupported platform: $(UNAME_S) $(UNAME_M)) +endif + +# Binary names +TEST_RUNNER := $(TEST_DIR)/runner +HANDLER_BIN := handler + +.PHONY: all build download-tests test clean help + +all: build test + +help: + @echo "Conformance Handler Makefile" + @echo "" + @echo "Targets:" + @echo " build - Build the conformance handler binary" + @echo " download-tests - Download the test suite for your platform" + @echo " test - Run conformance tests against the handler" + @echo " clean - Remove built binaries and downloaded tests" + @echo " help - Show this help message" + @echo "" + @echo "Configuration:" + @echo " Test Version: $(TEST_VERSION)" + @echo " Platform: $(PLATFORM)" + +build: + @echo "Building conformance handler..." + go build -o $(HANDLER_BIN) . + +download-tests: + @echo "Downloading test suite $(TEST_VERSION) for $(PLATFORM)..." + @mkdir -p $(TEST_DIR) + $(eval DOWNLOAD_URL := https://github.com/$(TEST_REPO)/releases/download/v$(TEST_VERSION)/kernel-bindings-tests_$(TEST_VERSION)_$(PLATFORM).tar.gz) + @echo "URL: $(DOWNLOAD_URL)" + @curl -L -o $(TEST_DIR)/test-runner.tar.gz "$(DOWNLOAD_URL)" + @echo "Extracting test runner..." + @tar -xzf $(TEST_DIR)/test-runner.tar.gz -C $(TEST_DIR) + @chmod +x $(TEST_RUNNER) + @rm $(TEST_DIR)/test-runner.tar.gz + @echo "Test runner downloaded to $(TEST_RUNNER)" + +test: build + @if [ ! -f "$(TEST_RUNNER)" ]; then \ + echo "Test runner not found. Downloading..."; \ + $(MAKE) download-tests; \ + fi + @echo "Running conformance tests..." + $(TEST_RUNNER) --handler ./$(HANDLER_BIN) -vv + +clean: + @echo "Cleaning up..." + rm -f $(HANDLER_BIN) + rm -rf $(TEST_DIR) \ No newline at end of file diff --git a/cmd/conformance-handler/README.md b/cmd/conformance-handler/README.md new file mode 100644 index 0000000..d40a67e --- /dev/null +++ b/cmd/conformance-handler/README.md @@ -0,0 +1,33 @@ +# Conformance Handler + +This binary implements the JSON protocol required by the [kernel-bindings-spec](https://github.com/stringintech/kernel-bindings-spec) conformance testing framework. + +## Purpose + +The conformance handler acts as a bridge between the test runner and the Go Bitcoin Kernel bindings. It: + +- Reads test requests from stdin (JSON protocol) +- Executes operations using the Go binding API +- Returns responses to stdout (JSON protocol) + +## Testing + +This handler is designed to work with the conformance test suite. The easiest way to run tests is using the Makefile: + +```bash +# Run conformance tests (builds handler and downloads test runner automatically) +make test + +# Or manually build and run +make build +make download-tests +./.conformance-tests/runner --handler ./handler +``` + +The test suite is automatically downloaded for your platform (darwin_arm64, darwin_amd64, linux_amd64, or linux_arm64). + +## Pinned Test Version + +This handler is compatible with: +- Test Suite Version: `0.0.3-alpha.3` +- Test Repository: [stringintech/kernel-bindings-tests](https://github.com/stringintech/kernel-bindings-tests) \ No newline at end of file diff --git a/cmd/conformance-handler/block.go b/cmd/conformance-handler/block.go new file mode 100644 index 0000000..95e8b98 --- /dev/null +++ b/cmd/conformance-handler/block.go @@ -0,0 +1,63 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleBlockCreate creates a block from raw hex data +func handleBlockCreate(registry *Registry, req Request) Response { + var params struct { + RawBlock string `json:"raw_block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + if req.Ref == "" { + return NewInvalidParamsResponse(req.ID, "ref field is required") + } + + // Decode hex to bytes + blockBytes, err := hex.DecodeString(params.RawBlock) + if err != nil { + return NewInvalidParamsResponse(req.ID, "raw_block must be valid hex") + } + + // Create block + block, err := kernel.NewBlock(blockBytes) + if err != nil { + return NewEmptyErrorResponse(req.ID) + } + + registry.Store(req.Ref, block) + + return NewSuccessResponseWithRef(req.ID, req.Ref) +} + +// handleBlockTreeEntryGetBlockHash gets the block hash from a block tree entry +func handleBlockTreeEntryGetBlockHash(registry *Registry, req Request) Response { + var params struct { + BlockTreeEntry RefObject `json:"block_tree_entry"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + // Get block tree entry from registry + entry, err := registry.GetBlockTreeEntry(params.BlockTreeEntry.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Get block hash and convert to string (handles display order conversion) + hashView := entry.Hash() + hashString := hashView.String() + + // Return hash as string + return NewSuccessResponse(req.ID, hashString) +} diff --git a/cmd/conformance-handler/chain.go b/cmd/conformance-handler/chain.go new file mode 100644 index 0000000..77648c9 --- /dev/null +++ b/cmd/conformance-handler/chain.go @@ -0,0 +1,90 @@ +package main + +import ( + "encoding/json" +) + +// handleChainGetHeight gets the current height of the chain +func handleChainGetHeight(registry *Registry, req Request) Response { + var params struct { + Chain RefObject `json:"chain"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + // Get chain from registry + chain, err := registry.GetChain(params.Chain.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Get height + height := chain.GetHeight() + + // Return height as integer + return NewSuccessResponse(req.ID, height) +} + +// handleChainGetByHeight gets a block tree entry at the specified height +func handleChainGetByHeight(registry *Registry, req Request) Response { + var params struct { + Chain RefObject `json:"chain"` + BlockHeight int32 `json:"block_height"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + if req.Ref == "" { + return NewInvalidParamsResponse(req.ID, "ref field is required") + } + + // Get chain from registry + chain, err := registry.GetChain(params.Chain.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Get block tree entry at height + entry := chain.GetByHeight(params.BlockHeight) + if entry == nil { + return NewEmptyErrorResponse(req.ID) + } + + registry.Store(req.Ref, entry) + + return NewSuccessResponseWithRef(req.ID, req.Ref) +} + +// handleChainContains checks if a block tree entry is in the active chain +func handleChainContains(registry *Registry, req Request) Response { + var params struct { + Chain RefObject `json:"chain"` + BlockTreeEntry RefObject `json:"block_tree_entry"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + // Get chain from registry + chain, err := registry.GetChain(params.Chain.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Get block tree entry from registry + entry, err := registry.GetBlockTreeEntry(params.BlockTreeEntry.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Check if chain contains the entry + contains := chain.Contains(entry) + + // Return boolean result + return NewSuccessResponse(req.ID, contains) +} diff --git a/cmd/conformance-handler/chainstate_manager.go b/cmd/conformance-handler/chainstate_manager.go new file mode 100644 index 0000000..0f11a16 --- /dev/null +++ b/cmd/conformance-handler/chainstate_manager.go @@ -0,0 +1,137 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleChainstateManagerCreate creates a chainstate manager from a context +func handleChainstateManagerCreate(registry *Registry, req Request) Response { + var params struct { + Context RefObject `json:"context"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + if req.Ref == "" { + return NewInvalidParamsResponse(req.ID, "ref field is required") + } + + // Get context from registry + ctx, err := registry.GetContext(params.Context.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Create temp directory for chainstate data + tempDir, err := os.MkdirTemp("", "btck_conformance_test_*") + if err != nil { + return NewEmptyErrorResponse(req.ID) + } + + dataDir := filepath.Join(tempDir, "data") + blocksDir := filepath.Join(tempDir, "blocks") + + // Create chainstate manager + manager, err := kernel.NewChainstateManager(ctx, dataDir, blocksDir) + if err != nil { + _ = os.RemoveAll(tempDir) + return NewEmptyErrorResponse(req.ID) + } + + registry.Store(req.Ref, &ChainstateManagerState{ + Manager: manager, + TempDir: tempDir, + }) + + return NewSuccessResponseWithRef(req.ID, req.Ref) +} + +// handleChainstateManagerGetActiveChain gets the active chain from a chainstate manager +func handleChainstateManagerGetActiveChain(registry *Registry, req Request) Response { + var params struct { + ChainstateManager RefObject `json:"chainstate_manager"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + if req.Ref == "" { + return NewInvalidParamsResponse(req.ID, "ref field is required") + } + + // Get chainstate manager from registry + csm, err := registry.GetChainstateManager(params.ChainstateManager.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Get active chain + chain := csm.Manager.GetActiveChain() + + registry.Store(req.Ref, chain) + + return NewSuccessResponseWithRef(req.ID, req.Ref) +} + +// handleChainstateManagerProcessBlock processes a block +func handleChainstateManagerProcessBlock(registry *Registry, req Request) Response { + var params struct { + ChainstateManager RefObject `json:"chainstate_manager"` + Block RefObject `json:"block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + // Get chainstate manager from registry + csm, err := registry.GetChainstateManager(params.ChainstateManager.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Get block from registry + block, err := registry.GetBlock(params.Block.Ref) + if err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + // Process the block + ok, newBlock := csm.Manager.ProcessBlock(block) + if !ok { + return NewEmptyErrorResponse(req.ID) + } + + // Return result with new_block field + result := struct { + NewBlock bool `json:"new_block"` + }{ + NewBlock: newBlock, + } + return NewSuccessResponse(req.ID, result) +} + +// handleChainstateManagerDestroy destroys a chainstate manager +func handleChainstateManagerDestroy(registry *Registry, req Request) Response { + var params struct { + ChainstateManager RefObject `json:"chainstate_manager"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + // Destroy and remove from registry + if err := registry.Destroy(params.ChainstateManager.Ref); err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + return NewEmptySuccessResponse(req.ID) +} diff --git a/cmd/conformance-handler/context.go b/cmd/conformance-handler/context.go new file mode 100644 index 0000000..bd3c193 --- /dev/null +++ b/cmd/conformance-handler/context.go @@ -0,0 +1,69 @@ +package main + +import ( + "encoding/json" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleContextCreate creates a context with specified chain parameters +func handleContextCreate(registry *Registry, req Request) Response { + var params struct { + ChainParameters struct { + ChainType string `json:"chain_type"` + } `json:"chain_parameters"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + if req.Ref == "" { + return NewInvalidParamsResponse(req.ID, "ref field is required") + } + + // Parse chain type + var chainType kernel.ChainType + switch params.ChainParameters.ChainType { + case "btck_ChainType_MAINNET": + chainType = kernel.ChainTypeMainnet + case "btck_ChainType_TESTNET": + chainType = kernel.ChainTypeTestnet + case "btck_ChainType_TESTNET_4": + chainType = kernel.ChainTypeTestnet4 + case "btck_ChainType_SIGNET": + chainType = kernel.ChainTypeSignet + case "btck_ChainType_REGTEST": + chainType = kernel.ChainTypeRegtest + default: + return NewInvalidParamsResponse(req.ID, "unknown chain_type: "+params.ChainParameters.ChainType) + } + + // Create context + ctx, err := kernel.NewContext(kernel.WithChainType(chainType)) + if err != nil { + return NewEmptyErrorResponse(req.ID) + } + + registry.Store(req.Ref, ctx) + + return NewSuccessResponseWithRef(req.ID, req.Ref) +} + +// handleContextDestroy destroys a context +func handleContextDestroy(registry *Registry, req Request) Response { + var params struct { + Context RefObject `json:"context"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "failed to parse params") + } + + // Destroy and remove from registry + if err := registry.Destroy(params.Context.Ref); err != nil { + return NewInvalidParamsResponse(req.ID, err.Error()) + } + + return NewEmptySuccessResponse(req.ID) +} diff --git a/cmd/conformance-handler/handler.go b/cmd/conformance-handler/handler.go new file mode 100644 index 0000000..7ba4a83 --- /dev/null +++ b/cmd/conformance-handler/handler.go @@ -0,0 +1,51 @@ +package main + +import "fmt" + +// handleRequest dispatches a request to the appropriate handler +func handleRequest(registry *Registry, req Request) (resp Response) { + defer func() { + if r := recover(); r != nil { + resp = NewHandlerErrorResponse(req.ID, "INTERNAL_ERROR", fmt.Sprintf("%v", r)) + } + }() + + switch req.Method { + // Script verification + case "btck_script_pubkey_verify": + return handleScriptPubkeyVerify(req) + + // Context management + case "btck_context_create": + return handleContextCreate(registry, req) + case "btck_context_destroy": + return handleContextDestroy(registry, req) + + // Chainstate manager operations + case "btck_chainstate_manager_create": + return handleChainstateManagerCreate(registry, req) + case "btck_chainstate_manager_get_active_chain": + return handleChainstateManagerGetActiveChain(registry, req) + case "btck_chainstate_manager_process_block": + return handleChainstateManagerProcessBlock(registry, req) + case "btck_chainstate_manager_destroy": + return handleChainstateManagerDestroy(registry, req) + + // Chain operations + case "btck_chain_get_height": + return handleChainGetHeight(registry, req) + case "btck_chain_get_by_height": + return handleChainGetByHeight(registry, req) + case "btck_chain_contains": + return handleChainContains(registry, req) + + // Block operations + case "btck_block_create": + return handleBlockCreate(registry, req) + case "btck_block_tree_entry_get_block_hash": + return handleBlockTreeEntryGetBlockHash(registry, req) + + default: + return NewHandlerErrorResponse(req.ID, "METHOD_NOT_FOUND", "") + } +} diff --git a/cmd/conformance-handler/main.go b/cmd/conformance-handler/main.go new file mode 100644 index 0000000..a6cd63f --- /dev/null +++ b/cmd/conformance-handler/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "os" +) + +func main() { + // Initialize registry for object references + registry := NewRegistry() + defer registry.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 { + sendResponse(NewHandlerErrorResponse("", "INVALID_REQUEST", "")) + continue + } + + resp := handleRequest(registry, req) + 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/cmd/conformance-handler/protocol.go b/cmd/conformance-handler/protocol.go new file mode 100644 index 0000000..3948109 --- /dev/null +++ b/cmd/conformance-handler/protocol.go @@ -0,0 +1,95 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +type Request struct { + ID string `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + Ref string `json:"ref,omitempty"` +} + +type Response struct { + ID string `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *Error `json:"error,omitempty"` +} + +type Error struct { + Code ErrorCode `json:"code"` +} + +type ErrorCode struct { + Type string `json:"type"` + Member string `json:"member"` +} + +type RefObject struct { + Ref string `json:"ref"` +} + +// NewErrorResponse creates an error response with the given code type and member. +// Use directly for C API error codes (e.g., "btck_ScriptVerifyStatus"). +// For handler errors, use NewHandlerErrorResponse. +func NewErrorResponse(id, codeType, codeMember string) Response { + return Response{ + ID: id, + Error: &Error{ + Code: ErrorCode{ + Type: codeType, + Member: codeMember, + }, + }, + } +} + +// NewHandlerErrorResponse creates an error response for handler layer errors. +// Use for request validation, method routing, and parameter parsing errors. +// Optional detail parameter adds context to the error (e.g., "INVALID_PARAMS (missing field 'foo')"). +func NewHandlerErrorResponse(id, codeMember, detail string) Response { + member := codeMember + if detail != "" { + member += fmt.Sprintf(" (%s)", detail) + } + return NewErrorResponse(id, "Handler", member) +} + +// NewInvalidParamsResponse creates an INVALID_PARAMS error with optional detail. +// Use when request parameters are malformed or missing. Detail provides context about the issue. +func NewInvalidParamsResponse(id, detail string) Response { + return NewHandlerErrorResponse(id, "INVALID_PARAMS", detail) +} + +// NewEmptyErrorResponse creates an error response with an empty error object {}. +// Use when an operation fails but no specific error code applies (e.g., C API returned null). +func NewEmptyErrorResponse(id string) Response { + return Response{ID: id} +} + +// NewSuccessResponse creates a success response with a result value. +// Use when an operation succeeds and returns data. +func NewSuccessResponse(id string, result interface{}) Response { + resultJSON, err := json.Marshal(result) + if err != nil { + panic(fmt.Sprintf("Failed to marshal result for request %s: %v", id, err)) + } + return Response{ + ID: id, + Result: resultJSON, + } +} + +// NewSuccessResponseWithRef creates a success response returning a reference object. +// Use for methods that create objects and store them in the registry. +func NewSuccessResponseWithRef(id, ref string) Response { + return NewSuccessResponse(id, RefObject{Ref: ref}) +} + +// NewEmptySuccessResponse creates a success response with no result. +// Use for void/nullptr operations that succeed but return no data. +func NewEmptySuccessResponse(id string) Response { + return Response{ID: id} +} diff --git a/cmd/conformance-handler/registry.go b/cmd/conformance-handler/registry.go new file mode 100644 index 0000000..54918e8 --- /dev/null +++ b/cmd/conformance-handler/registry.go @@ -0,0 +1,178 @@ +package main + +import ( + "fmt" + "os" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// Registry stores named references to objects created during the test session. +// Objects remain alive throughout the handler's lifetime unless explicitly destroyed. +type Registry struct { + objects map[string]interface{} + order []string // Tracks insertion order for proper cleanup (newest to oldest) +} + +// NewRegistry creates a new empty registry +func NewRegistry() *Registry { + return &Registry{ + objects: make(map[string]interface{}), + order: make([]string, 0), + } +} + +// Store stores an object under the given reference name +func (r *Registry) Store(ref string, obj interface{}) { + // Check if object already exists + if _, ok := r.objects[ref]; ok { + // Cleanup the old object before replacing + _ = r.Destroy(ref) + } + r.order = append(r.order, ref) + r.objects[ref] = obj +} + +// GetContext retrieves a context by reference name +func (r *Registry) GetContext(ref string) (*kernel.Context, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + ctx, ok := obj.(*kernel.Context) + if !ok { + return nil, fmt.Errorf("reference %s is not a Context (got %T)", ref, obj) + } + return ctx, nil +} + +// GetChainstateManager retrieves a chainstate manager by reference name +func (r *Registry) GetChainstateManager(ref string) (*ChainstateManagerState, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + csm, ok := obj.(*ChainstateManagerState) + if !ok { + return nil, fmt.Errorf("reference %s is not a ChainstateManager (got %T)", ref, obj) + } + return csm, nil +} + +// GetChain retrieves a chain by reference name +func (r *Registry) GetChain(ref string) (*kernel.Chain, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + chain, ok := obj.(*kernel.Chain) + if !ok { + return nil, fmt.Errorf("reference %s is not a Chain (got %T)", ref, obj) + } + return chain, nil +} + +// GetBlock retrieves a block by reference name +func (r *Registry) GetBlock(ref string) (*kernel.Block, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + block, ok := obj.(*kernel.Block) + if !ok { + return nil, fmt.Errorf("reference %s is not a Block (got %T)", ref, obj) + } + return block, nil +} + +// GetBlockTreeEntry retrieves a block tree entry by reference name +func (r *Registry) GetBlockTreeEntry(ref string) (*kernel.BlockTreeEntry, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + entry, ok := obj.(*kernel.BlockTreeEntry) + if !ok { + return nil, fmt.Errorf("reference %s is not a BlockTreeEntry (got %T)", ref, obj) + } + return entry, nil +} + +// Destroy removes and destroys a single object from the registry by reference name +func (r *Registry) Destroy(ref string) error { + obj, ok := r.objects[ref] + if !ok { + return fmt.Errorf("reference not found: %s", ref) + } + + // Destroy the object + r.destroyObject(obj) + + // Remove from registry + delete(r.objects, ref) + + // Remove from order slice + for i, name := range r.order { + if name == ref { + r.order = append(r.order[:i], r.order[i+1:]...) + break + } + } + + return nil +} + +// Cleanup destroys all objects in the registry and clears all references +// Objects are destroyed in reverse order (newest to oldest) to handle dependencies +func (r *Registry) Cleanup() { + // Destroy objects in reverse order (newest to oldest) + for i := len(r.order) - 1; i >= 0; i-- { + ref := r.order[i] + if obj, ok := r.objects[ref]; ok { + r.destroyObject(obj) + } + } + + // Clear everything + r.objects = make(map[string]interface{}) + r.order = nil +} + +// destroyObject performs cleanup on a single object based on its type +func (r *Registry) destroyObject(obj interface{}) { + switch v := obj.(type) { + case *kernel.Context: + if v != nil { + v.Destroy() + } + case *ChainstateManagerState: + if v != nil { + v.Cleanup() + } + case *kernel.Block: + if v != nil { + v.Destroy() + } + // Chain and BlockTreeEntry don't need explicit cleanup + } +} + +// ChainstateManagerState holds the chainstate manager and its dependencies +type ChainstateManagerState struct { + Manager *kernel.ChainstateManager + TempDir string +} + +// Cleanup releases all resources held by the chainstate manager state +func (c *ChainstateManagerState) Cleanup() { + if c.Manager != nil { + c.Manager.Destroy() + c.Manager = nil + } + + // Remove temp directory if it exists + if c.TempDir != "" { + _ = os.RemoveAll(c.TempDir) + c.TempDir = "" + } +} diff --git a/cmd/conformance-handler/script_pubkey.go b/cmd/conformance-handler/script_pubkey.go new file mode 100644 index 0000000..28d1458 --- /dev/null +++ b/cmd/conformance-handler/script_pubkey.go @@ -0,0 +1,166 @@ +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"` + Amount int64 `json:"amount"` + TxToHex string `json:"tx_to"` + InputIndex uint `json:"input_index"` + Flags json.RawMessage `json:"flags"` + SpentOutputs []SpentOutput `json:"spent_outputs"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return NewInvalidParamsResponse(req.ID, "") + } + + // Decode script pubkey + var scriptBytes []byte + var err error + if params.ScriptPubkeyHex != "" { + scriptBytes, err = hex.DecodeString(params.ScriptPubkeyHex) + if err != nil { + return NewInvalidParamsResponse(req.ID, "script pubkey hex") + } + } + + // Decode transaction + txBytes, err := hex.DecodeString(params.TxToHex) + if err != nil { + return NewInvalidParamsResponse(req.ID, "transaction hex") + } + + // Parse flags + flags, err := parseScriptFlags(params.Flags) + if err != nil { + return NewInvalidParamsResponse(req.ID, "flags") + } + + // Parse spent outputs + spentOutputs, err := parseSpentOutputs(params.SpentOutputs) + if err != nil { + return NewInvalidParamsResponse(req.ID, "spent outputs") + } + defer func() { + for _, so := range spentOutputs { + so.Destroy() + } + }() + + // Create script pubkey and transaction + scriptPubkey := kernel.NewScriptPubkey(scriptBytes) + defer scriptPubkey.Destroy() + + tx, err := kernel.NewTransaction(txBytes) + if err != nil { + return NewInvalidParamsResponse(req.ID, "transaction decode") + } + defer tx.Destroy() + + // Verify script + valid, err := scriptPubkey.Verify(params.Amount, tx, spentOutputs, params.InputIndex, flags) + if err != nil { + var scriptVerifyError *kernel.ScriptVerifyError + if errors.As(err, &scriptVerifyError) { + switch { + case errors.Is(err, kernel.ErrVerifyScriptVerifyInvalidFlagsCombination): + return NewErrorResponse(req.ID, "btck_ScriptVerifyStatus", "ERROR_INVALID_FLAGS_COMBINATION") + case errors.Is(err, kernel.ErrVerifyScriptVerifySpentOutputsRequired): + return NewErrorResponse(req.ID, "btck_ScriptVerifyStatus", "ERROR_SPENT_OUTPUTS_REQUIRED") + default: + panic("scriptPubkey.Verify returned unhandled ScriptVerifyError (request ID: " + req.ID + "): " + err.Error()) + } + } + panic("scriptPubkey.Verify returned non-ScriptVerifyError (request ID: " + req.ID + "): " + err.Error()) + } + + return NewSuccessResponse(req.ID, valid) +} + +// parseScriptFlags parses flags from array or numeric format +func parseScriptFlags(flagsJSON json.RawMessage) (kernel.ScriptFlags, error) { + // Try array format first + var flagsArray []string + if err := json.Unmarshal(flagsJSON, &flagsArray); err == nil { + var result kernel.ScriptFlags + for _, flagStr := range flagsArray { + flag, err := parseSingleFlag(flagStr) + if err != nil { + return 0, err + } + result |= flag + } + return result, nil + } + + // Numeric flags + var numFlags uint32 + if err := json.Unmarshal(flagsJSON, &numFlags); err != nil { + return 0, errors.New("invalid flags format: must be array or number") + } + return kernel.ScriptFlags(numFlags), nil +} + +// parseSingleFlag maps a flag string to its kernel constant +func parseSingleFlag(flagStr string) (kernel.ScriptFlags, error) { + switch flagStr { + case "btck_ScriptVerificationFlags_NONE": + return kernel.ScriptFlagsVerifyNone, nil + case "btck_ScriptVerificationFlags_P2SH": + return kernel.ScriptFlagsVerifyP2SH, nil + case "btck_ScriptVerificationFlags_DERSIG": + return kernel.ScriptFlagsVerifyDERSig, nil + case "btck_ScriptVerificationFlags_NULLDUMMY": + return kernel.ScriptFlagsVerifyNullDummy, nil + case "btck_ScriptVerificationFlags_CHECKLOCKTIMEVERIFY": + return kernel.ScriptFlagsVerifyCheckLockTimeVerify, nil + case "btck_ScriptVerificationFlags_CHECKSEQUENCEVERIFY": + return kernel.ScriptFlagsVerifyCheckSequenceVerify, nil + case "btck_ScriptVerificationFlags_WITNESS": + return kernel.ScriptFlagsVerifyWitness, nil + case "btck_ScriptVerificationFlags_TAPROOT": + return kernel.ScriptFlagsVerifyTaproot, nil + case "btck_ScriptVerificationFlags_ALL": + return kernel.ScriptFlagsVerifyAll, nil + default: + return 0, errors.New("unknown flag: " + flagStr) + } +} + +type SpentOutput struct { + ScriptPubkeyHex string `json:"script_pubkey"` + Amount int64 `json:"amount"` +} + +// parseSpentOutputs parses spent outputs +func parseSpentOutputs(spentOutputParams []SpentOutput) (spentOutputs []*kernel.TransactionOutput, err error) { + defer func() { + // Clean up already created outputs on error + if err != nil { + for _, so := range spentOutputs { + if so != nil { + so.Destroy() + } + } + } + }() + for _, so := range spentOutputParams { + var scriptBytes []byte + scriptBytes, err = hex.DecodeString(so.ScriptPubkeyHex) + if err != nil { + return + } + scriptPubkeyOut := kernel.NewScriptPubkey(scriptBytes) + spentOutputs = append(spentOutputs, kernel.NewTransactionOutput(scriptPubkeyOut, so.Amount)) + } + return spentOutputs, nil +}