From 6ea0d78fd4eb39a6aedcb869ff0e2b01063e75a4 Mon Sep 17 00:00:00 2001 From: stringintech Date: Wed, 19 Nov 2025 18:31:07 +0330 Subject: [PATCH 1/2] Implement conformance handler for v0.0.3 tests --- .github/workflows/ci.yml | 43 ++++ cmd/conformance-handler/.gitignore | 2 + cmd/conformance-handler/Makefile | 79 +++++++ cmd/conformance-handler/README.md | 33 +++ cmd/conformance-handler/block.go | 56 +++++ cmd/conformance-handler/chain.go | 78 +++++++ cmd/conformance-handler/chainstate_manager.go | 128 ++++++++++ cmd/conformance-handler/context.go | 67 ++++++ cmd/conformance-handler/handler.go | 73 ++++++ cmd/conformance-handler/main.go | 50 ++++ cmd/conformance-handler/protocol.go | 74 ++++++ cmd/conformance-handler/registry.go | 218 ++++++++++++++++++ cmd/conformance-handler/script_pubkey.go | 161 +++++++++++++ cmd/conformance-handler/transaction.go | 154 +++++++++++++ 14 files changed, 1216 insertions(+) create mode 100644 cmd/conformance-handler/.gitignore create mode 100644 cmd/conformance-handler/Makefile create mode 100644 cmd/conformance-handler/README.md create mode 100644 cmd/conformance-handler/block.go create mode 100644 cmd/conformance-handler/chain.go create mode 100644 cmd/conformance-handler/chainstate_manager.go create mode 100644 cmd/conformance-handler/context.go create mode 100644 cmd/conformance-handler/handler.go create mode 100644 cmd/conformance-handler/main.go create mode 100644 cmd/conformance-handler/protocol.go create mode 100644 cmd/conformance-handler/registry.go create mode 100644 cmd/conformance-handler/script_pubkey.go create mode 100644 cmd/conformance-handler/transaction.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8b674e6..d75f32f4 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 00000000..04266695 --- /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 00000000..c7388e0c --- /dev/null +++ b/cmd/conformance-handler/Makefile @@ -0,0 +1,79 @@ +# Conformance Handler Makefile + +# Test suite configuration +TEST_VERSION := 0.0.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 00000000..d80304a1 --- /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` +- 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 00000000..bbaa0455 --- /dev/null +++ b/cmd/conformance-handler/block.go @@ -0,0 +1,56 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleBlockCreate creates a block from raw hex data +func handleBlockCreate(registry *Registry, req Request) (Response, error) { + var params struct { + RawBlock string `json:"raw_block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + blockBytes, err := hex.DecodeString(params.RawBlock) + if err != nil { + return Response{}, fmt.Errorf("raw_block must be valid hex: %w", err) + } + + block, err := kernel.NewBlock(blockBytes) + if err != nil { + return NewEmptyErrorResponse(), nil + } + + registry.Store(req.Ref, block) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockTreeEntryGetBlockHash gets the block hash from a block tree entry +func handleBlockTreeEntryGetBlockHash(registry *Registry, req Request) (Response, error) { + var params struct { + BlockTreeEntry RefObject `json:"block_tree_entry"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + entry, err := registry.GetBlockTreeEntry(params.BlockTreeEntry.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(entry.Hash().String()), nil +} diff --git a/cmd/conformance-handler/chain.go b/cmd/conformance-handler/chain.go new file mode 100644 index 00000000..537a67dc --- /dev/null +++ b/cmd/conformance-handler/chain.go @@ -0,0 +1,78 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +// handleChainGetHeight gets the current height of the chain +func handleChainGetHeight(registry *Registry, req Request) (Response, error) { + var params struct { + Chain RefObject `json:"chain"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + chain, err := registry.GetChain(params.Chain.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(chain.GetHeight()), nil +} + +// handleChainGetByHeight gets a block tree entry at the specified height +func handleChainGetByHeight(registry *Registry, req Request) (Response, error) { + var params struct { + Chain RefObject `json:"chain"` + BlockHeight int32 `json:"block_height"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + chain, err := registry.GetChain(params.Chain.Ref) + if err != nil { + return Response{}, err + } + + entry := chain.GetByHeight(params.BlockHeight) + if entry == nil { + return NewEmptyErrorResponse(), nil + } + + registry.Store(req.Ref, entry) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleChainContains checks if a block tree entry is in the active chain +func handleChainContains(registry *Registry, req Request) (Response, error) { + var params struct { + Chain RefObject `json:"chain"` + BlockTreeEntry RefObject `json:"block_tree_entry"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + chain, err := registry.GetChain(params.Chain.Ref) + if err != nil { + return Response{}, err + } + + entry, err := registry.GetBlockTreeEntry(params.BlockTreeEntry.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(chain.Contains(entry)), nil +} diff --git a/cmd/conformance-handler/chainstate_manager.go b/cmd/conformance-handler/chainstate_manager.go new file mode 100644 index 00000000..2ca0ead7 --- /dev/null +++ b/cmd/conformance-handler/chainstate_manager.go @@ -0,0 +1,128 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleChainstateManagerCreate creates a chainstate manager from a context +func handleChainstateManagerCreate(registry *Registry, req Request) (Response, error) { + var params struct { + Context RefObject `json:"context"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + ctx, err := registry.GetContext(params.Context.Ref) + if err != nil { + return Response{}, err + } + + tempDir, err := os.MkdirTemp("", "btck_conformance_test_*") + if err != nil { + return NewEmptyErrorResponse(), nil + } + + dataDir := filepath.Join(tempDir, "data") + blocksDir := filepath.Join(tempDir, "blocks") + + manager, err := kernel.NewChainstateManager(ctx, dataDir, blocksDir) + if err != nil { + _ = os.RemoveAll(tempDir) + return NewEmptyErrorResponse(), nil + } + + registry.Store(req.Ref, &ChainstateManagerState{ + Manager: manager, + TempDir: tempDir, + }) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleChainstateManagerGetActiveChain gets the active chain from a chainstate manager +func handleChainstateManagerGetActiveChain(registry *Registry, req Request) (Response, error) { + var params struct { + ChainstateManager RefObject `json:"chainstate_manager"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + csm, err := registry.GetChainstateManager(params.ChainstateManager.Ref) + if err != nil { + return Response{}, err + } + + chain := csm.Manager.GetActiveChain() + + registry.Store(req.Ref, chain) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleChainstateManagerProcessBlock processes a block +func handleChainstateManagerProcessBlock(registry *Registry, req Request) (Response, error) { + var params struct { + ChainstateManager RefObject `json:"chainstate_manager"` + Block RefObject `json:"block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + csm, err := registry.GetChainstateManager(params.ChainstateManager.Ref) + if err != nil { + return Response{}, err + } + + block, err := registry.GetBlock(params.Block.Ref) + if err != nil { + return Response{}, err + } + + ok, newBlock := csm.Manager.ProcessBlock(block) + if !ok { + return NewEmptyErrorResponse(), nil + } + + result := struct { + NewBlock bool `json:"new_block"` + }{ + NewBlock: newBlock, + } + return NewSuccessResponse(result), nil +} + +// handleChainstateManagerDestroy destroys a chainstate manager +func handleChainstateManagerDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + ChainstateManager RefObject `json:"chainstate_manager"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.ChainstateManager.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} diff --git a/cmd/conformance-handler/context.go b/cmd/conformance-handler/context.go new file mode 100644 index 00000000..09c70461 --- /dev/null +++ b/cmd/conformance-handler/context.go @@ -0,0 +1,67 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleContextCreate creates a context with specified chain parameters +func handleContextCreate(registry *Registry, req Request) (Response, error) { + var params struct { + ChainParameters struct { + ChainType string `json:"chain_type"` + } `json:"chain_parameters"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + 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 Response{}, fmt.Errorf("unknown chain_type: %s", params.ChainParameters.ChainType) + } + + ctx, err := kernel.NewContext(kernel.WithChainType(chainType)) + if err != nil { + return NewEmptyErrorResponse(), nil + } + + registry.Store(req.Ref, ctx) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleContextDestroy destroys a context +func handleContextDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + Context RefObject `json:"context"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.Context.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} diff --git a/cmd/conformance-handler/handler.go b/cmd/conformance-handler/handler.go new file mode 100644 index 00000000..d57cd573 --- /dev/null +++ b/cmd/conformance-handler/handler.go @@ -0,0 +1,73 @@ +package main + +import "fmt" + +// handleRequest dispatches a request to the appropriate handler +func handleRequest(registry *Registry, req Request) (resp Response, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + + switch req.Method { + // Script pubkey operations + case "btck_script_pubkey_create": + return handleScriptPubkeyCreate(registry, req) + case "btck_script_pubkey_destroy": + return handleScriptPubkeyDestroy(registry, req) + case "btck_script_pubkey_verify": + return handleScriptPubkeyVerify(registry, req) + + // Transaction operations + case "btck_transaction_create": + return handleTransactionCreate(registry, req) + case "btck_transaction_destroy": + return handleTransactionDestroy(registry, req) + + // Transaction output operations + case "btck_transaction_output_create": + return handleTransactionOutputCreate(registry, req) + case "btck_transaction_output_destroy": + return handleTransactionOutputDestroy(registry, req) + + // Precomputed transaction data operations + case "btck_precomputed_transaction_data_create": + return handlePrecomputedTransactionDataCreate(registry, req) + case "btck_precomputed_transaction_data_destroy": + return handlePrecomputedTransactionDataDestroy(registry, 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 Response{}, fmt.Errorf("unknown method: %s", req.Method) + } +} diff --git a/cmd/conformance-handler/main.go b/cmd/conformance-handler/main.go new file mode 100644 index 00000000..59dcbe5c --- /dev/null +++ b/cmd/conformance-handler/main.go @@ -0,0 +1,50 @@ +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 { + fmt.Fprintf(os.Stderr, "failed to parse request: %v\n", err) + os.Exit(1) + } + + resp, err := handleRequest(registry, req) + if err != nil { + fmt.Fprintf(os.Stderr, "handler error (request %s, method %s): %v\n", req.ID, req.Method, err) + os.Exit(1) + } + 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 00000000..9f669e51 --- /dev/null +++ b/cmd/conformance-handler/protocol.go @@ -0,0 +1,74 @@ +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 { + Result json.RawMessage `json:"result"` + Error *Error `json:"error,omitempty"` +} + +type Error struct { + Code *ErrorCode `json:"code,omitempty"` +} + +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"). +func NewErrorResponse(codeType, codeMember string) Response { + return Response{ + Error: &Error{ + Code: &ErrorCode{ + Type: codeType, + Member: codeMember, + }, + }, + } +} + +// 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() Response { + return Response{Error: &Error{}} +} + +// NewSuccessResponse creates a success response with a result value. +// Use when an operation succeeds and returns data. +func NewSuccessResponse(result interface{}) Response { + resultJSON, err := json.Marshal(result) + if err != nil { + panic(fmt.Sprintf("Failed to marshal result for request: %v", err)) + } + return Response{ + 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(ref string) Response { + return NewSuccessResponse(RefObject{Ref: ref}) +} + +// NewEmptySuccessResponse creates a success response with no result. +// Use for void/nullptr operations that succeed but return no data. +func NewEmptySuccessResponse() Response { + return Response{} +} diff --git a/cmd/conformance-handler/registry.go b/cmd/conformance-handler/registry.go new file mode 100644 index 00000000..5faa60ec --- /dev/null +++ b/cmd/conformance-handler/registry.go @@ -0,0 +1,218 @@ +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 +} + +// GetScriptPubkey retrieves a script pubkey by reference name +func (r *Registry) GetScriptPubkey(ref string) (kernel.ScriptPubkeyLike, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + spk, ok := obj.(kernel.ScriptPubkeyLike) + if !ok { + return nil, fmt.Errorf("reference %s is not a ScriptPubkey (got %T)", ref, obj) + } + return spk, nil +} + +// GetTransaction retrieves a transaction by reference name +func (r *Registry) GetTransaction(ref string) (kernel.TransactionLike, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + tx, ok := obj.(kernel.TransactionLike) + if !ok { + return nil, fmt.Errorf("reference %s is not a Transaction (got %T)", ref, obj) + } + return tx, nil +} + +// GetTransactionOutput retrieves a transaction output by reference name +func (r *Registry) GetTransactionOutput(ref string) (kernel.TransactionOutputLike, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + txOut, ok := obj.(kernel.TransactionOutputLike) + if !ok { + return nil, fmt.Errorf("reference %s is not a TransactionOutput (got %T)", ref, obj) + } + return txOut, nil +} + +// GetPrecomputedTransactionData retrieves precomputed transaction data by reference name +func (r *Registry) GetPrecomputedTransactionData(ref string) (*kernel.PrecomputedTransactionData, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + ptd, ok := obj.(*kernel.PrecomputedTransactionData) + if !ok { + return nil, fmt.Errorf("reference %s is not a PrecomputedTransactionData (got %T)", ref, obj) + } + return ptd, 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 releases owned resources. View-like objects without Destroy are ignored. +func (r *Registry) destroyObject(obj interface{}) { + if v, ok := obj.(interface{ Destroy() }); ok { + v.Destroy() + } +} + +// ChainstateManagerState holds the chainstate manager and its dependencies +type ChainstateManagerState struct { + Manager *kernel.ChainstateManager + TempDir string +} + +// Destroy releases all resources held by the chainstate manager state +func (c *ChainstateManagerState) Destroy() { + 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 00000000..3877cf22 --- /dev/null +++ b/cmd/conformance-handler/script_pubkey.go @@ -0,0 +1,161 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleScriptPubkeyCreate creates a ScriptPubkey from hex and stores it in the registry +func handleScriptPubkeyCreate(registry *Registry, req Request) (Response, error) { + var params struct { + ScriptPubkeyHex string `json:"script_pubkey"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + scriptBytes, err := hex.DecodeString(params.ScriptPubkeyHex) + if err != nil { + return Response{}, fmt.Errorf("script_pubkey must be valid hex: %w", err) + } + + spk := kernel.NewScriptPubkey(scriptBytes) + registry.Store(req.Ref, spk) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleScriptPubkeyDestroy destroys a ScriptPubkey from the registry +func handleScriptPubkeyDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + ScriptPubkey RefObject `json:"script_pubkey"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.ScriptPubkey.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} + +// handleScriptPubkeyVerify verifies a script against a transaction +func handleScriptPubkeyVerify(registry *Registry, req Request) (Response, error) { + var params struct { + ScriptPubkey RefObject `json:"script_pubkey"` + Amount int64 `json:"amount"` + TxTo RefObject `json:"tx_to"` + InputIndex uint `json:"input_index"` + Flags json.RawMessage `json:"flags"` + PrecomputedTxDat *RefObject `json:"precomputed_txdata"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + scriptPubkey, err := registry.GetScriptPubkey(params.ScriptPubkey.Ref) + if err != nil { + return Response{}, err + } + + tx, err := registry.GetTransaction(params.TxTo.Ref) + if err != nil { + return Response{}, err + } + + flags, err := parseScriptFlags(params.Flags) + if err != nil { + return Response{}, fmt.Errorf("invalid flags: %w", err) + } + + var precomputedTxData *kernel.PrecomputedTransactionData + if params.PrecomputedTxDat != nil && params.PrecomputedTxDat.Ref != "" { + precomputedTxData, err = registry.GetPrecomputedTransactionData(params.PrecomputedTxDat.Ref) + if err != nil { + return Response{}, err + } + } + + valid, err := scriptPubkey.Verify(params.Amount, tx, precomputedTxData, params.InputIndex, flags) + if err != nil { + var scriptVerifyError *kernel.ScriptVerifyError + if errors.As(err, &scriptVerifyError) { + switch { + case errors.Is(err, kernel.ErrVerifyScriptVerifyInvalidFlagsCombination): + return NewErrorResponse("btck_ScriptVerifyStatus", "ERROR_INVALID_FLAGS_COMBINATION"), nil + case errors.Is(err, kernel.ErrVerifyScriptVerifySpentOutputsRequired): + return NewErrorResponse("btck_ScriptVerifyStatus", "ERROR_SPENT_OUTPUTS_REQUIRED"), nil + case errors.Is(err, kernel.ErrVerifyScriptVerifyTxInputIndex), errors.Is(err, kernel.ErrVerifyScriptVerifyInvalidFlags): + return NewEmptyErrorResponse(), nil + 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(valid), nil +} + +// 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) + } +} diff --git a/cmd/conformance-handler/transaction.go b/cmd/conformance-handler/transaction.go new file mode 100644 index 00000000..7d594e11 --- /dev/null +++ b/cmd/conformance-handler/transaction.go @@ -0,0 +1,154 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleTransactionCreate creates a Transaction from raw hex and stores it in the registry +func handleTransactionCreate(registry *Registry, req Request) (Response, error) { + var params struct { + RawTransaction string `json:"raw_transaction"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + txBytes, err := hex.DecodeString(params.RawTransaction) + if err != nil { + return Response{}, fmt.Errorf("raw_transaction must be valid hex: %w", err) + } + + tx, err := kernel.NewTransaction(txBytes) + if err != nil { + return NewEmptyErrorResponse(), nil + } + + registry.Store(req.Ref, tx) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionDestroy destroys a Transaction from the registry +func handleTransactionDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + Transaction RefObject `json:"transaction"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.Transaction.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} + +// handleTransactionOutputCreate creates a TransactionOutput from a ScriptPubkey ref and amount +func handleTransactionOutputCreate(registry *Registry, req Request) (Response, error) { + var params struct { + ScriptPubkey RefObject `json:"script_pubkey"` + Amount int64 `json:"amount"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + spk, err := registry.GetScriptPubkey(params.ScriptPubkey.Ref) + if err != nil { + return Response{}, err + } + + txOut := kernel.NewTransactionOutput(spk, params.Amount) + registry.Store(req.Ref, txOut) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionOutputDestroy destroys a TransactionOutput from the registry +func handleTransactionOutputDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionOutput RefObject `json:"transaction_output"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.TransactionOutput.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} + +// handlePrecomputedTransactionDataCreate creates PrecomputedTransactionData from a tx and spent outputs +func handlePrecomputedTransactionDataCreate(registry *Registry, req Request) (Response, error) { + var params struct { + TxTo RefObject `json:"tx_to"` + SpentOutputs []RefObject `json:"spent_outputs"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + tx, err := registry.GetTransaction(params.TxTo.Ref) + if err != nil { + return Response{}, err + } + + spentOutputs := make([]kernel.TransactionOutputLike, len(params.SpentOutputs)) + for i, soRef := range params.SpentOutputs { + so, err := registry.GetTransactionOutput(soRef.Ref) + if err != nil { + return Response{}, err + } + spentOutputs[i] = so + } + + ptd, err := kernel.NewPrecomputedTransactionData(tx, spentOutputs) + if err != nil { + return NewEmptyErrorResponse(), nil + } + + registry.Store(req.Ref, ptd) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handlePrecomputedTransactionDataDestroy destroys PrecomputedTransactionData from the registry +func handlePrecomputedTransactionDataDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + PrecomputedTxData RefObject `json:"precomputed_txdata"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.PrecomputedTxData.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} From a34c473d7bd7440896aee07d7d9185a4a071ac66 Mon Sep 17 00:00:00 2001 From: stringintech Date: Thu, 14 May 2026 13:48:49 +0330 Subject: [PATCH 2/2] Implement conformance handler for v0.0.4 tests --- cmd/conformance-handler/Makefile | 2 +- cmd/conformance-handler/README.md | 2 +- cmd/conformance-handler/block.go | 146 ++++++++++- cmd/conformance-handler/block_hash.go | 125 ++++++++++ cmd/conformance-handler/block_header.go | 202 +++++++++++++++ cmd/conformance-handler/block_tree_entry.go | 30 +++ cmd/conformance-handler/chain.go | 4 +- cmd/conformance-handler/handler.go | 96 ++++++- cmd/conformance-handler/registry.go | 78 ++++++ cmd/conformance-handler/script_pubkey.go | 48 ++++ cmd/conformance-handler/transaction.go | 249 +++++++++++++++++++ cmd/conformance-handler/transaction_input.go | 140 +++++++++++ cmd/conformance-handler/txid.go | 91 +++++++ kernel/block_tree_entry_test.go | 37 ++- kernel/chain.go | 18 +- kernel/chain_test.go | 15 +- kernel/chainstate_manager_test.go | 30 ++- 17 files changed, 1273 insertions(+), 40 deletions(-) create mode 100644 cmd/conformance-handler/block_hash.go create mode 100644 cmd/conformance-handler/block_header.go create mode 100644 cmd/conformance-handler/block_tree_entry.go create mode 100644 cmd/conformance-handler/transaction_input.go create mode 100644 cmd/conformance-handler/txid.go diff --git a/cmd/conformance-handler/Makefile b/cmd/conformance-handler/Makefile index c7388e0c..023fb9cf 100644 --- a/cmd/conformance-handler/Makefile +++ b/cmd/conformance-handler/Makefile @@ -1,7 +1,7 @@ # Conformance Handler Makefile # Test suite configuration -TEST_VERSION := 0.0.3 +TEST_VERSION := 0.0.4-alpha.3 TEST_REPO := stringintech/kernel-bindings-tests TEST_DIR := .conformance-tests diff --git a/cmd/conformance-handler/README.md b/cmd/conformance-handler/README.md index d80304a1..9f139b4e 100644 --- a/cmd/conformance-handler/README.md +++ b/cmd/conformance-handler/README.md @@ -29,5 +29,5 @@ The test suite is automatically downloaded for your platform (darwin_arm64, darw ## Pinned Test Version This handler is compatible with: -- Test Suite Version: `0.0.3` +- Test Suite Version: `0.0.4-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 index bbaa0455..be214681 100644 --- a/cmd/conformance-handler/block.go +++ b/cmd/conformance-handler/block.go @@ -37,20 +37,156 @@ func handleBlockCreate(registry *Registry, req Request) (Response, error) { return NewSuccessResponseWithRef(req.Ref), nil } -// handleBlockTreeEntryGetBlockHash gets the block hash from a block tree entry -func handleBlockTreeEntryGetBlockHash(registry *Registry, req Request) (Response, error) { +// handleBlockGetHash gets the hash of a block and stores it in the registry. +func handleBlockGetHash(registry *Registry, req Request) (Response, error) { var params struct { - BlockTreeEntry RefObject `json:"block_tree_entry"` + Block RefObject `json:"block"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { return Response{}, fmt.Errorf("failed to parse params: %w", err) } - entry, err := registry.GetBlockTreeEntry(params.BlockTreeEntry.Ref) + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + block, err := registry.GetBlock(params.Block.Ref) + if err != nil { + return Response{}, err + } + + hash := block.Hash() + registry.Store(req.Ref, hash) + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockGetHeader extracts the block header and stores it in the registry +func handleBlockGetHeader(registry *Registry, req Request) (Response, error) { + var params struct { + Block RefObject `json:"block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + block, err := registry.GetBlock(params.Block.Ref) + if err != nil { + return Response{}, err + } + + header := block.GetHeader() + registry.Store(req.Ref, header) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockCountTransactions returns the number of transactions in the block +func handleBlockCountTransactions(registry *Registry, req Request) (Response, error) { + var params struct { + Block RefObject `json:"block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + block, err := registry.GetBlock(params.Block.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(block.CountTransactions()), nil +} + +// handleBlockGetTransactionAt retrieves the transaction at the given index and stores it in the registry +func handleBlockGetTransactionAt(registry *Registry, req Request) (Response, error) { + var params struct { + Block RefObject `json:"block"` + TransactionIndex uint64 `json:"transaction_index"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + block, err := registry.GetBlock(params.Block.Ref) if err != nil { return Response{}, err } - return NewSuccessResponse(entry.Hash().String()), nil + txView, err := block.GetTransactionAt(params.TransactionIndex) + if err != nil { + return Response{}, err + } + + registry.Store(req.Ref, txView) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockToBytes returns the consensus-serialized block as a hex string +func handleBlockToBytes(registry *Registry, req Request) (Response, error) { + var params struct { + Block RefObject `json:"block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + block, err := registry.GetBlock(params.Block.Ref) + if err != nil { + return Response{}, err + } + + data, err := block.Bytes() + if err != nil { + return NewEmptyErrorResponse(), nil + } + + return NewSuccessResponse(hex.EncodeToString(data)), nil +} + +// handleBlockCopy copies a block and stores the copy in the registry +func handleBlockCopy(registry *Registry, req Request) (Response, error) { + var params struct { + Block RefObject `json:"block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + block, err := registry.GetBlock(params.Block.Ref) + if err != nil { + return Response{}, err + } + + blockCopy := block.Copy() + registry.Store(req.Ref, blockCopy) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockDestroy destroys a block from the registry +func handleBlockDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + Block RefObject `json:"block"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.Block.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil } diff --git a/cmd/conformance-handler/block_hash.go b/cmd/conformance-handler/block_hash.go new file mode 100644 index 00000000..63b3c148 --- /dev/null +++ b/cmd/conformance-handler/block_hash.go @@ -0,0 +1,125 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleBlockHashCreate creates a BlockHash from raw stored 32 bytes encoded as hex. +func handleBlockHashCreate(registry *Registry, req Request) (Response, error) { + var params struct { + BlockHash string `json:"block_hash"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + var hashBytes [32]byte + if len(params.BlockHash) != 64 { + return Response{}, fmt.Errorf("block_hash must be exactly 64 hex characters") + } + if _, err := hex.Decode(hashBytes[:], []byte(params.BlockHash)); err != nil { + return Response{}, fmt.Errorf("block_hash must be valid hex: %w", err) + } + + bh := kernel.NewBlockHash(hashBytes) + registry.Store(req.Ref, bh) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockHashToBytes returns the raw stored 32-byte block hash as a hex string. +func handleBlockHashToBytes(registry *Registry, req Request) (Response, error) { + var params struct { + BlockHash RefObject `json:"block_hash"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + bh, err := registry.GetBlockHash(params.BlockHash.Ref) + if err != nil { + return Response{}, err + } + + raw := bh.Bytes() + return NewSuccessResponse(fmt.Sprintf("%x", raw)), nil +} + +// handleBlockHashEquals checks if two block hashes are equal +func handleBlockHashEquals(registry *Registry, req Request) (Response, error) { + var params struct { + Hash1 RefObject `json:"hash1"` + Hash2 RefObject `json:"hash2"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + bh, err := registry.GetBlockHash(params.Hash1.Ref) + if err != nil { + return Response{}, err + } + + bh2, err := registry.GetBlockHash(params.Hash2.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(bh.Equals(bh2)), nil +} + +// handleBlockHashCopy copies a block hash and stores the copy in the registry +func handleBlockHashCopy(registry *Registry, req Request) (Response, error) { + var params struct { + BlockHash RefObject `json:"block_hash"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + bh, err := registry.GetBlockHash(params.BlockHash.Ref) + if err != nil { + return Response{}, err + } + + registry.Store(req.Ref, bh.Copy()) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockHashDestroy destroys a block hash from the registry +func handleBlockHashDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + BlockHash RefObject `json:"block_hash"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if _, err := registry.GetBlockHash(params.BlockHash.Ref); err != nil { + return Response{}, err + } + + if err := registry.Destroy(params.BlockHash.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} diff --git a/cmd/conformance-handler/block_header.go b/cmd/conformance-handler/block_header.go new file mode 100644 index 00000000..2a92e9bc --- /dev/null +++ b/cmd/conformance-handler/block_header.go @@ -0,0 +1,202 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// handleBlockHeaderCreate creates a block header from raw hex bytes +func handleBlockHeaderCreate(registry *Registry, req Request) (Response, error) { + var params struct { + RawBlockHeader string `json:"raw_block_header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + headerBytes, err := hex.DecodeString(params.RawBlockHeader) + if err != nil { + return Response{}, fmt.Errorf("raw_block_header must be valid hex: %w", err) + } + + header, err := kernel.NewBlockHeader(headerBytes) + if err != nil { + return NewEmptyErrorResponse(), nil + } + + registry.Store(req.Ref, header) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockHeaderGetHash gets the block hash from a block header and stores it in the registry +func handleBlockHeaderGetHash(registry *Registry, req Request) (Response, error) { + var params struct { + Header RefObject `json:"header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + header, err := registry.GetBlockHeader(params.Header.Ref) + if err != nil { + return Response{}, err + } + + hash := header.Hash() + registry.Store(req.Ref, hash) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockHeaderGetPrevHash gets the previous block hash view from a block header and stores it in the registry +func handleBlockHeaderGetPrevHash(registry *Registry, req Request) (Response, error) { + var params struct { + Header RefObject `json:"header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + header, err := registry.GetBlockHeader(params.Header.Ref) + if err != nil { + return Response{}, err + } + + prevHash := header.PrevHash() + registry.Store(req.Ref, prevHash) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockHeaderGetTimestamp returns the timestamp of a block header +func handleBlockHeaderGetTimestamp(registry *Registry, req Request) (Response, error) { + var params struct { + Header RefObject `json:"header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + header, err := registry.GetBlockHeader(params.Header.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(header.Timestamp()), nil +} + +// handleBlockHeaderGetBits returns the nBits difficulty target of a block header +func handleBlockHeaderGetBits(registry *Registry, req Request) (Response, error) { + var params struct { + Header RefObject `json:"header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + header, err := registry.GetBlockHeader(params.Header.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(header.Bits()), nil +} + +// handleBlockHeaderGetVersion returns the version of a block header +func handleBlockHeaderGetVersion(registry *Registry, req Request) (Response, error) { + var params struct { + Header RefObject `json:"header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + header, err := registry.GetBlockHeader(params.Header.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(header.Version()), nil +} + +// handleBlockHeaderGetNonce returns the nonce of a block header +func handleBlockHeaderGetNonce(registry *Registry, req Request) (Response, error) { + var params struct { + Header RefObject `json:"header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + header, err := registry.GetBlockHeader(params.Header.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(header.Nonce()), nil +} + +// handleBlockHeaderCopy copies a block header and stores the copy in the registry +func handleBlockHeaderCopy(registry *Registry, req Request) (Response, error) { + var params struct { + Header RefObject `json:"header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + header, err := registry.GetBlockHeader(params.Header.Ref) + if err != nil { + return Response{}, err + } + + headerCopy := header.Copy() + registry.Store(req.Ref, headerCopy) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleBlockHeaderDestroy destroys a block header from the registry +func handleBlockHeaderDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + Header RefObject `json:"header"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.Header.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} diff --git a/cmd/conformance-handler/block_tree_entry.go b/cmd/conformance-handler/block_tree_entry.go new file mode 100644 index 00000000..39a4d70f --- /dev/null +++ b/cmd/conformance-handler/block_tree_entry.go @@ -0,0 +1,30 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +// handleBlockTreeEntryGetBlockHash gets the block hash from a block tree entry. +func handleBlockTreeEntryGetBlockHash(registry *Registry, req Request) (Response, error) { + var params struct { + BlockTreeEntry RefObject `json:"block_tree_entry"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + entry, err := registry.GetBlockTreeEntry(params.BlockTreeEntry.Ref) + if err != nil { + return Response{}, err + } + + hash := entry.Hash() + registry.Store(req.Ref, hash) + return NewSuccessResponseWithRef(req.Ref), nil +} diff --git a/cmd/conformance-handler/chain.go b/cmd/conformance-handler/chain.go index 537a67dc..fc337768 100644 --- a/cmd/conformance-handler/chain.go +++ b/cmd/conformance-handler/chain.go @@ -43,8 +43,8 @@ func handleChainGetByHeight(registry *Registry, req Request) (Response, error) { return Response{}, err } - entry := chain.GetByHeight(params.BlockHeight) - if entry == nil { + entry, err := chain.GetByHeight(params.BlockHeight) + if err != nil { return NewEmptyErrorResponse(), nil } diff --git a/cmd/conformance-handler/handler.go b/cmd/conformance-handler/handler.go index d57cd573..f51f8255 100644 --- a/cmd/conformance-handler/handler.go +++ b/cmd/conformance-handler/handler.go @@ -16,6 +16,10 @@ func handleRequest(registry *Registry, req Request) (resp Response, err error) { return handleScriptPubkeyCreate(registry, req) case "btck_script_pubkey_destroy": return handleScriptPubkeyDestroy(registry, req) + case "btck_script_pubkey_to_bytes": + return handleScriptPubkeyToBytes(registry, req) + case "btck_script_pubkey_copy": + return handleScriptPubkeyCopy(registry, req) case "btck_script_pubkey_verify": return handleScriptPubkeyVerify(registry, req) @@ -24,12 +28,52 @@ func handleRequest(registry *Registry, req Request) (resp Response, err error) { return handleTransactionCreate(registry, req) case "btck_transaction_destroy": return handleTransactionDestroy(registry, req) - - // Transaction output operations + case "btck_transaction_get_txid": + return handleTransactionGetTxid(registry, req) + case "btck_txid_to_bytes": + return handleTxidToBytes(registry, req) + case "btck_txid_equals": + return handleTxidEquals(registry, req) + case "btck_txid_copy": + return handleTxidCopy(registry, req) + case "btck_txid_destroy": + return handleTxidDestroy(registry, req) + case "btck_transaction_count_inputs": + return handleTransactionCountInputs(registry, req) + case "btck_transaction_count_outputs": + return handleTransactionCountOutputs(registry, req) + case "btck_transaction_to_bytes": + return handleTransactionToBytes(registry, req) + case "btck_transaction_get_output_at": + return handleTransactionGetOutputAt(registry, req) + case "btck_transaction_get_input_at": + return handleTransactionGetInputAt(registry, req) + case "btck_transaction_copy": + return handleTransactionCopy(registry, req) + case "btck_transaction_input_destroy": + return handleTransactionInputDestroy(registry, req) + case "btck_transaction_input_get_out_point": + return handleTransactionInputGetOutPoint(registry, req) + case "btck_transaction_input_copy": + return handleTransactionInputCopy(registry, req) + case "btck_transaction_out_point_get_txid": + return handleTransactionOutPointGetTxid(registry, req) + case "btck_transaction_out_point_get_index": + return handleTransactionOutPointGetIndex(registry, req) + case "btck_transaction_out_point_copy": + return handleTransactionOutPointCopy(registry, req) + case "btck_transaction_out_point_destroy": + return handleTransactionOutPointDestroy(registry, req) case "btck_transaction_output_create": return handleTransactionOutputCreate(registry, req) case "btck_transaction_output_destroy": return handleTransactionOutputDestroy(registry, req) + case "btck_transaction_output_copy": + return handleTransactionOutputCopy(registry, req) + case "btck_transaction_output_get_amount": + return handleTransactionOutputGetAmount(registry, req) + case "btck_transaction_output_get_script_pubkey": + return handleTransactionOutputGetScriptPubkey(registry, req) // Precomputed transaction data operations case "btck_precomputed_transaction_data_create": @@ -62,11 +106,59 @@ func handleRequest(registry *Registry, req Request) (resp Response, err error) { return handleChainContains(registry, req) // Block operations + case "btck_block_destroy": + return handleBlockDestroy(registry, req) case "btck_block_create": return handleBlockCreate(registry, req) + case "btck_block_get_hash": + return handleBlockGetHash(registry, req) + case "btck_block_get_header": + return handleBlockGetHeader(registry, req) + case "btck_block_count_transactions": + return handleBlockCountTransactions(registry, req) + case "btck_block_get_transaction_at": + return handleBlockGetTransactionAt(registry, req) + case "btck_block_to_bytes": + return handleBlockToBytes(registry, req) + case "btck_block_copy": + return handleBlockCopy(registry, req) + + // BlockTreeEntry operations case "btck_block_tree_entry_get_block_hash": return handleBlockTreeEntryGetBlockHash(registry, req) + // Block hash operations + case "btck_block_hash_create": + return handleBlockHashCreate(registry, req) + case "btck_block_hash_to_bytes": + return handleBlockHashToBytes(registry, req) + case "btck_block_hash_equals": + return handleBlockHashEquals(registry, req) + case "btck_block_hash_copy": + return handleBlockHashCopy(registry, req) + case "btck_block_hash_destroy": + return handleBlockHashDestroy(registry, req) + + // Block header operations + case "btck_block_header_create": + return handleBlockHeaderCreate(registry, req) + case "btck_block_header_get_hash": + return handleBlockHeaderGetHash(registry, req) + case "btck_block_header_get_prev_hash": + return handleBlockHeaderGetPrevHash(registry, req) + case "btck_block_header_get_timestamp": + return handleBlockHeaderGetTimestamp(registry, req) + case "btck_block_header_get_bits": + return handleBlockHeaderGetBits(registry, req) + case "btck_block_header_get_version": + return handleBlockHeaderGetVersion(registry, req) + case "btck_block_header_get_nonce": + return handleBlockHeaderGetNonce(registry, req) + case "btck_block_header_copy": + return handleBlockHeaderCopy(registry, req) + case "btck_block_header_destroy": + return handleBlockHeaderDestroy(registry, req) + default: return Response{}, fmt.Errorf("unknown method: %s", req.Method) } diff --git a/cmd/conformance-handler/registry.go b/cmd/conformance-handler/registry.go index 5faa60ec..f165244e 100644 --- a/cmd/conformance-handler/registry.go +++ b/cmd/conformance-handler/registry.go @@ -137,6 +137,84 @@ func (r *Registry) GetPrecomputedTransactionData(ref string) (*kernel.Precompute return ptd, nil } +// GetTransactionOutPoint retrieves a transaction out point by reference name. +func (r *Registry) GetTransactionOutPoint(ref string) (kernel.TransactionOutPointLike, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + op, ok := obj.(kernel.TransactionOutPointLike) + if !ok { + return nil, fmt.Errorf("reference %s is not a TransactionOutPoint (got %T)", ref, obj) + } + return op, nil +} + +// GetTxid retrieves a txid by reference name. +func (r *Registry) GetTxid(ref string) (kernel.TxidLike, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + txid, ok := obj.(kernel.TxidLike) + if !ok { + return nil, fmt.Errorf("reference %s is not a Txid (got %T)", ref, obj) + } + return txid, nil +} + +// GetTransactionInput retrieves a transaction input by reference name. +func (r *Registry) GetTransactionInput(ref string) (kernel.TransactionInputLike, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + ti, ok := obj.(kernel.TransactionInputLike) + if !ok { + return nil, fmt.Errorf("reference %s is not a TransactionInput (got %T)", ref, obj) + } + return ti, nil +} + +// GetChainParameters retrieves chain parameters by reference name +func (r *Registry) GetChainParameters(ref string) (*kernel.ChainParameters, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + cp, ok := obj.(*kernel.ChainParameters) + if !ok { + return nil, fmt.Errorf("reference %s is not a ChainParameters (got %T)", ref, obj) + } + return cp, nil +} + +// GetBlockHash retrieves a block hash by reference name. +func (r *Registry) GetBlockHash(ref string) (kernel.BlockHashLike, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + bh, ok := obj.(kernel.BlockHashLike) + if !ok { + return nil, fmt.Errorf("reference %s is not a BlockHash (got %T)", ref, obj) + } + return bh, nil +} + +// GetBlockHeader retrieves a block header by reference name +func (r *Registry) GetBlockHeader(ref string) (*kernel.BlockHeader, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + bh, ok := obj.(*kernel.BlockHeader) + if !ok { + return nil, fmt.Errorf("reference %s is not a BlockHeader (got %T)", ref, obj) + } + return bh, nil +} + // GetBlockTreeEntry retrieves a block tree entry by reference name func (r *Registry) GetBlockTreeEntry(ref string) (*kernel.BlockTreeEntry, error) { obj, ok := r.objects[ref] diff --git a/cmd/conformance-handler/script_pubkey.go b/cmd/conformance-handler/script_pubkey.go index 3877cf22..b99fa4f6 100644 --- a/cmd/conformance-handler/script_pubkey.go +++ b/cmd/conformance-handler/script_pubkey.go @@ -51,6 +51,54 @@ func handleScriptPubkeyDestroy(registry *Registry, req Request) (Response, error return NewEmptySuccessResponse(), nil } +// handleScriptPubkeyToBytes returns the serialized script pubkey as a hex string +func handleScriptPubkeyToBytes(registry *Registry, req Request) (Response, error) { + var params struct { + ScriptPubkey RefObject `json:"script_pubkey"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + spk, err := registry.GetScriptPubkey(params.ScriptPubkey.Ref) + if err != nil { + return Response{}, err + } + + b, err := spk.Bytes() + if err != nil { + return NewEmptyErrorResponse(), nil + } + + return NewSuccessResponse(fmt.Sprintf("%x", b)), nil +} + +// handleScriptPubkeyCopy copies a script pubkey and stores the copy in the registry +func handleScriptPubkeyCopy(registry *Registry, req Request) (Response, error) { + var params struct { + ScriptPubkey RefObject `json:"script_pubkey"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + spk, err := registry.GetScriptPubkey(params.ScriptPubkey.Ref) + if err != nil { + return Response{}, err + } + + spkCopy := spk.Copy() + registry.Store(req.Ref, spkCopy) + + return NewSuccessResponseWithRef(req.Ref), nil +} + // handleScriptPubkeyVerify verifies a script against a transaction func handleScriptPubkeyVerify(registry *Registry, req Request) (Response, error) { var params struct { diff --git a/cmd/conformance-handler/transaction.go b/cmd/conformance-handler/transaction.go index 7d594e11..0674290a 100644 --- a/cmd/conformance-handler/transaction.go +++ b/cmd/conformance-handler/transaction.go @@ -97,6 +97,103 @@ func handleTransactionOutputDestroy(registry *Registry, req Request) (Response, return NewEmptySuccessResponse(), nil } +// handleTransactionGetInputAt retrieves the input at the given index and stores the view in the registry. +func handleTransactionGetInputAt(registry *Registry, req Request) (Response, error) { + var params struct { + Transaction RefObject `json:"transaction"` + InputIndex uint64 `json:"input_index"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + tx, err := registry.GetTransaction(params.Transaction.Ref) + if err != nil { + return Response{}, err + } + + inputView, err := tx.GetInput(params.InputIndex) + if err != nil { + return Response{}, err + } + + registry.Store(req.Ref, inputView) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionCopy copies a Transaction and stores the copy in the registry +func handleTransactionCopy(registry *Registry, req Request) (Response, error) { + var params struct { + Transaction RefObject `json:"transaction"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + tx, err := registry.GetTransaction(params.Transaction.Ref) + if err != nil { + return Response{}, err + } + + txCopy := tx.Copy() + registry.Store(req.Ref, txCopy) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionInputDestroy destroys a TransactionInput from the registry +func handleTransactionInputDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionInput RefObject `json:"transaction_input"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.TransactionInput.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} + +// handleTransactionOutputCopy copies a TransactionOutput and stores the copy in the registry +func handleTransactionOutputCopy(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionOutput RefObject `json:"transaction_output"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + txOut, err := registry.GetTransactionOutput(params.TransactionOutput.Ref) + if err != nil { + return Response{}, err + } + + txOutCopy := txOut.Copy() + registry.Store(req.Ref, txOutCopy) + + return NewSuccessResponseWithRef(req.Ref), nil +} + // handlePrecomputedTransactionDataCreate creates PrecomputedTransactionData from a tx and spent outputs func handlePrecomputedTransactionDataCreate(registry *Registry, req Request) (Response, error) { var params struct { @@ -136,6 +233,158 @@ func handlePrecomputedTransactionDataCreate(registry *Registry, req Request) (Re return NewSuccessResponseWithRef(req.Ref), nil } +// handleTransactionGetTxid returns the txid view of a transaction. +// The request must include ref so the handler can return the txid as a view ref. +func handleTransactionGetTxid(registry *Registry, req Request) (Response, error) { + var params struct { + Transaction RefObject `json:"transaction"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + tx, err := registry.GetTransaction(params.Transaction.Ref) + if err != nil { + return Response{}, err + } + + txidView := tx.GetTxid() + registry.Store(req.Ref, txidView) + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionCountInputs returns the number of inputs in a transaction +func handleTransactionCountInputs(registry *Registry, req Request) (Response, error) { + var params struct { + Transaction RefObject `json:"transaction"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + tx, err := registry.GetTransaction(params.Transaction.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(tx.CountInputs()), nil +} + +// handleTransactionCountOutputs returns the number of outputs in a transaction +func handleTransactionCountOutputs(registry *Registry, req Request) (Response, error) { + var params struct { + Transaction RefObject `json:"transaction"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + tx, err := registry.GetTransaction(params.Transaction.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(tx.CountOutputs()), nil +} + +// handleTransactionToBytes returns the consensus-serialized transaction as a hex string +func handleTransactionToBytes(registry *Registry, req Request) (Response, error) { + var params struct { + Transaction RefObject `json:"transaction"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + tx, err := registry.GetTransaction(params.Transaction.Ref) + if err != nil { + return Response{}, err + } + + data, err := tx.Bytes() + if err != nil { + return NewEmptyErrorResponse(), nil + } + + return NewSuccessResponse(hex.EncodeToString(data)), nil +} + +// handleTransactionGetOutputAt retrieves the output at the given index and stores the view in the registry. +func handleTransactionGetOutputAt(registry *Registry, req Request) (Response, error) { + var params struct { + Transaction RefObject `json:"transaction"` + OutputIndex uint64 `json:"output_index"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + tx, err := registry.GetTransaction(params.Transaction.Ref) + if err != nil { + return Response{}, err + } + + outputView, err := tx.GetOutput(params.OutputIndex) + if err != nil { + return NewEmptyErrorResponse(), nil + } + + registry.Store(req.Ref, outputView) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionOutputGetAmount returns the amount of a transaction output +func handleTransactionOutputGetAmount(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionOutput RefObject `json:"transaction_output"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + txOut, err := registry.GetTransactionOutput(params.TransactionOutput.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(txOut.Amount()), nil +} + +// handleTransactionOutputGetScriptPubkey returns the script pubkey view of a transaction output. +func handleTransactionOutputGetScriptPubkey(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionOutput RefObject `json:"transaction_output"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + txOut, err := registry.GetTransactionOutput(params.TransactionOutput.Ref) + if err != nil { + return Response{}, err + } + + spkView := txOut.ScriptPubkey() + registry.Store(req.Ref, spkView) + return NewSuccessResponseWithRef(req.Ref), nil +} + // handlePrecomputedTransactionDataDestroy destroys PrecomputedTransactionData from the registry func handlePrecomputedTransactionDataDestroy(registry *Registry, req Request) (Response, error) { var params struct { diff --git a/cmd/conformance-handler/transaction_input.go b/cmd/conformance-handler/transaction_input.go new file mode 100644 index 00000000..8722a4a5 --- /dev/null +++ b/cmd/conformance-handler/transaction_input.go @@ -0,0 +1,140 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +// handleTransactionInputGetOutPoint retrieves the out point view from a transaction input and stores it in the registry. +func handleTransactionInputGetOutPoint(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionInput RefObject `json:"transaction_input"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + ti, err := registry.GetTransactionInput(params.TransactionInput.Ref) + if err != nil { + return Response{}, err + } + + registry.Store(req.Ref, ti.GetOutPoint()) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionInputCopy copies a transaction input and stores it in the registry +func handleTransactionInputCopy(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionInput RefObject `json:"transaction_input"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + ti, err := registry.GetTransactionInput(params.TransactionInput.Ref) + if err != nil { + return Response{}, err + } + + tiCopy := ti.Copy() + registry.Store(req.Ref, tiCopy) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionOutPointGetTxid returns the txid view of an out point. +// The request must include ref so the handler can return the txid as a view ref. +func handleTransactionOutPointGetTxid(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionOutPoint RefObject `json:"transaction_out_point"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + op, err := registry.GetTransactionOutPoint(params.TransactionOutPoint.Ref) + if err != nil { + return Response{}, err + } + + txidView := op.GetTxid() + registry.Store(req.Ref, txidView) + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionOutPointGetIndex returns the output index of an out point +func handleTransactionOutPointGetIndex(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionOutPoint RefObject `json:"transaction_out_point"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + op, err := registry.GetTransactionOutPoint(params.TransactionOutPoint.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(op.GetIndex()), nil +} + +// handleTransactionOutPointCopy copies an out point and stores it in the registry +func handleTransactionOutPointCopy(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionOutPoint RefObject `json:"transaction_out_point"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + op, err := registry.GetTransactionOutPoint(params.TransactionOutPoint.Ref) + if err != nil { + return Response{}, err + } + + opCopy := op.Copy() + registry.Store(req.Ref, opCopy) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTransactionOutPointDestroy destroys an out point from the registry +func handleTransactionOutPointDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + TransactionOutPoint RefObject `json:"transaction_out_point"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.TransactionOutPoint.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} diff --git a/cmd/conformance-handler/txid.go b/cmd/conformance-handler/txid.go new file mode 100644 index 00000000..1456c4a2 --- /dev/null +++ b/cmd/conformance-handler/txid.go @@ -0,0 +1,91 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +// handleTxidToBytes returns the raw stored 32-byte txid as a hex string. +func handleTxidToBytes(registry *Registry, req Request) (Response, error) { + var params struct { + Txid RefObject `json:"txid"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + txid, err := registry.GetTxid(params.Txid.Ref) + if err != nil { + return Response{}, err + } + + raw := txid.Bytes() + return NewSuccessResponse(fmt.Sprintf("%x", raw)), nil +} + +// handleTxidEquals checks if two txids are equal +func handleTxidEquals(registry *Registry, req Request) (Response, error) { + var params struct { + Txid1 RefObject `json:"txid1"` + Txid2 RefObject `json:"txid2"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + txid, err := registry.GetTxid(params.Txid1.Ref) + if err != nil { + return Response{}, err + } + + txid2, err := registry.GetTxid(params.Txid2.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(txid.Bytes() == txid2.Bytes()), nil +} + +// handleTxidCopy copies a txid and stores the copy in the registry +func handleTxidCopy(registry *Registry, req Request) (Response, error) { + var params struct { + Txid RefObject `json:"txid"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + txid, err := registry.GetTxid(params.Txid.Ref) + if err != nil { + return Response{}, err + } + + txidCopy := txid.Copy() + registry.Store(req.Ref, txidCopy) + + return NewSuccessResponseWithRef(req.Ref), nil +} + +// handleTxidDestroy destroys a txid from the registry +func handleTxidDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + Txid RefObject `json:"txid"` + } + + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.Txid.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} diff --git a/kernel/block_tree_entry_test.go b/kernel/block_tree_entry_test.go index c76e8054..14a44382 100644 --- a/kernel/block_tree_entry_test.go +++ b/kernel/block_tree_entry_test.go @@ -14,7 +14,10 @@ func TestBlockTreeEntry(t *testing.T) { t.Run("Previous", func(t *testing.T) { // Get block at height 1 - entry := chain.GetByHeight(1) + entry, err := chain.GetByHeight(1) + if err != nil { + t.Fatalf("GetByHeight(1) error = %v", err) + } // Test getting previous block (should be genesis) prevEntry := entry.Previous() @@ -29,7 +32,10 @@ func TestBlockTreeEntry(t *testing.T) { } // Test genesis block has no previous - genesisEntry := chain.GetByHeight(0) + genesisEntry, err := chain.GetByHeight(0) + if err != nil { + t.Fatalf("GetByHeight(0) error = %v", err) + } // Genesis should have no previous block (should return nil) genesisPrevious := genesisEntry.Previous() @@ -40,19 +46,28 @@ func TestBlockTreeEntry(t *testing.T) { t.Run("Equals", func(t *testing.T) { // Same entry should equal itself - entry1 := chain.GetByHeight(1) + entry1, err := chain.GetByHeight(1) + if err != nil { + t.Fatalf("GetByHeight(1) error = %v", err) + } if !entry1.Equals(entry1) { t.Error("Entry should equal itself") } // Different retrievals of same height should be equal - entry1Again := chain.GetByHeight(1) + entry1Again, err := chain.GetByHeight(1) + if err != nil { + t.Fatalf("GetByHeight(1) error = %v", err) + } if !entry1.Equals(entry1Again) { t.Error("Same height entries should be equal") } // Different heights should not be equal - entry0 := chain.GetByHeight(0) + entry0, err := chain.GetByHeight(0) + if err != nil { + t.Fatalf("GetByHeight(0) error = %v", err) + } if entry1.Equals(entry0) { t.Error("Different height entries should not be equal") } @@ -64,9 +79,9 @@ func TestBlockTreeEntry(t *testing.T) { }) t.Run("Ancestor", func(t *testing.T) { - entry2 := chain.GetByHeight(2) - if entry2 == nil { - t.Fatal("Entry at height 2 is nil") + entry2, err := chain.GetByHeight(2) + if err != nil { + t.Fatalf("GetByHeight(2) error = %v", err) } for _, height := range []int32{0, 1, 2} { @@ -88,9 +103,9 @@ func TestBlockTreeEntry(t *testing.T) { t.Run("GetHeader", func(t *testing.T) { // Get genesis block entry - genesisEntry := chain.GetByHeight(0) - if genesisEntry == nil { - t.Fatal("Genesis block tree entry is nil") + genesisEntry, err := chain.GetByHeight(0) + if err != nil { + t.Fatalf("GetByHeight(0) error = %v", err) } // Get the header from the entry diff --git a/kernel/chain.go b/kernel/chain.go index edd3cc77..aa1c5251 100644 --- a/kernel/chain.go +++ b/kernel/chain.go @@ -18,22 +18,22 @@ type Chain struct { // GetByHeight retrieves a block tree entry by its height in the currently active chain. // -// Returns nil if the height is out of bounds. Once retrieved, there is no guarantee -// that it remains in the active chain if new blocks are processed. +// Returns ErrKernelIndexOutOfBounds if the height is out of bounds. Once retrieved, +// there is no guarantee that it remains in the active chain if new blocks are processed. // // Parameters: // - height: Block height to retrieve // // Example usage: // -// genesis := chain.GetByHeight(0) -// tip := chain.GetByHeight(chain.GetHeight()) -func (c *Chain) GetByHeight(height int32) *BlockTreeEntry { +// genesis, _ := chain.GetByHeight(0) +// tip, _ := chain.GetByHeight(chain.GetHeight()) +func (c *Chain) GetByHeight(height int32) (*BlockTreeEntry, error) { ptr := C.btck_chain_get_by_height(c.ptr, C.int(height)) if ptr == nil { - return nil + return nil, ErrKernelIndexOutOfBounds } - return &BlockTreeEntry{ptr} + return &BlockTreeEntry{ptr}, nil } // Contains checks whether the given block tree entry is part of this chain. @@ -107,8 +107,8 @@ func (c *Chain) EntriesFrom(from int32) iter.Seq[*BlockTreeEntry] { // iterEntries is a helper that iterates over block tree entries in [from, to). func (c *Chain) iterEntries(from, to int32, yield func(*BlockTreeEntry) bool) { for h := from; h < to; h++ { - entry := c.GetByHeight(h) - if entry == nil { // Height may become out of bounds due to a reorg + entry, err := c.GetByHeight(h) + if err != nil { // Height may become out of bounds due to a reorg return } if !yield(entry) { diff --git a/kernel/chain_test.go b/kernel/chain_test.go index 8e7cfc54..769e2f7e 100644 --- a/kernel/chain_test.go +++ b/kernel/chain_test.go @@ -23,19 +23,28 @@ func TestChain(t *testing.T) { }) t.Run("GetByHeight", func(t *testing.T) { - block1 := chain.GetByHeight(1) + block1, err := chain.GetByHeight(1) + if err != nil { + t.Fatalf("GetByHeight(1) error = %v", err) + } if block1.Height() != 1 { t.Errorf("Expected block height 1, got %d", block1.Height()) } }) t.Run("Contains", func(t *testing.T) { - genesis := chain.GetByHeight(0) + genesis, err := chain.GetByHeight(0) + if err != nil { + t.Fatalf("GetByHeight(0) error = %v", err) + } if !chain.Contains(genesis) { t.Error("Chain should contain genesis block") } - block1 := chain.GetByHeight(1) + block1, err := chain.GetByHeight(1) + if err != nil { + t.Fatalf("GetByHeight(1) error = %v", err) + } if !chain.Contains(block1) { t.Error("Chain should contain block at height 1") } diff --git a/kernel/chainstate_manager_test.go b/kernel/chainstate_manager_test.go index 6a3ef9eb..bc980d13 100644 --- a/kernel/chainstate_manager_test.go +++ b/kernel/chainstate_manager_test.go @@ -26,7 +26,10 @@ func TestChainstateManager(t *testing.T) { func (s *ChainstateManagerTestSuite) TestBlockSpentOutputs(t *testing.T) { chain := s.Manager.GetActiveChain() - blockIndex := chain.GetByHeight(202) + blockIndex, err := chain.GetByHeight(202) + if err != nil { + t.Fatalf("GetByHeight(202) error = %v", err) + } blockSpentOutputs, err := s.Manager.ReadBlockSpentOutputs(blockIndex) if err != nil { @@ -84,7 +87,10 @@ func (s *ChainstateManagerTestSuite) TestBlockSpentOutputs(t *testing.T) { func (s *ChainstateManagerTestSuite) TestTransactionSpentOutputs(t *testing.T) { chain := s.Manager.GetActiveChain() - blockIndex := chain.GetByHeight(202) + blockIndex, err := chain.GetByHeight(202) + if err != nil { + t.Fatalf("GetByHeight(202) error = %v", err) + } blockSpentOutputs, err := s.Manager.ReadBlockSpentOutputs(blockIndex) if err != nil { @@ -158,7 +164,10 @@ func (s *ChainstateManagerTestSuite) TestReadBlock(t *testing.T) { chain := s.Manager.GetActiveChain() // Test reading genesis block - genesis := chain.GetByHeight(0) + genesis, err := chain.GetByHeight(0) + if err != nil { + t.Fatalf("GetByHeight(0) error = %v", err) + } genesisBlock, err := s.Manager.ReadBlock(genesis) if err != nil { t.Fatalf("ChainstateManager.ReadBlock() for genesis error = %v", err) @@ -169,7 +178,10 @@ func (s *ChainstateManagerTestSuite) TestReadBlock(t *testing.T) { defer genesisBlock.Destroy() // Test reading tip block - tip := chain.GetByHeight(chain.GetHeight()) + tip, err := chain.GetByHeight(chain.GetHeight()) + if err != nil { + t.Fatalf("GetByHeight(%d) error = %v", chain.GetHeight(), err) + } tipBlock, err := s.Manager.ReadBlock(tip) if err != nil { t.Fatalf("ChainstateManager.ReadBlock() for tip error = %v", err) @@ -184,7 +196,10 @@ func (s *ChainstateManagerTestSuite) TestGetBlockTreeEntryByHash(t *testing.T) { chain := s.Manager.GetActiveChain() // Test getting genesis block by hash - genesis := chain.GetByHeight(0) + genesis, err := chain.GetByHeight(0) + if err != nil { + t.Fatalf("GetByHeight(0) error = %v", err) + } genesisHash := genesis.Hash() @@ -202,7 +217,10 @@ func (s *ChainstateManagerTestSuite) TestGetBlockTreeEntryByHash(t *testing.T) { } // Test getting tip block by hash - tipIndex := chain.GetByHeight(chain.GetHeight()) + tipIndex, err := chain.GetByHeight(chain.GetHeight()) + if err != nil { + t.Fatalf("GetByHeight(%d) error = %v", chain.GetHeight(), err) + } tipHash := tipIndex.Hash()