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 7642894dbdb6c350e6cd9b351f1ae6ff8309d9c6 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 | 224 ++++++++++++++++ cmd/conformance-handler/block_tree_entry.go | 48 ++++ cmd/conformance-handler/callback_queue.go | 68 +++++ cmd/conformance-handler/chain.go | 4 +- cmd/conformance-handler/context.go | 22 +- cmd/conformance-handler/handler.go | 118 ++++++++- .../notification_callbacks.go | 168 ++++++++++++ cmd/conformance-handler/registry.go | 117 ++++++++ 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 +++++++ .../validation_callbacks.go | 161 +++++++++++ kernel/block_tree_entry_test.go | 37 ++- kernel/chain.go | 18 +- kernel/chain_test.go | 15 +- kernel/chainstate_manager_test.go | 34 ++- kernel/context_options.go | 4 +- kernel/notification_callbacks.go | 76 ++---- kernel/notification_callbacks_test.go | 58 ++-- kernel/validation_interface_callbacks.go | 36 +-- kernel/validation_interface_callbacks_test.go | 68 +++-- 26 files changed, 1897 insertions(+), 182 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/callback_queue.go create mode 100644 cmd/conformance-handler/notification_callbacks.go create mode 100644 cmd/conformance-handler/transaction_input.go create mode 100644 cmd/conformance-handler/txid.go create mode 100644 cmd/conformance-handler/validation_callbacks.go diff --git a/cmd/conformance-handler/Makefile b/cmd/conformance-handler/Makefile index c7388e0c..a19b2e09 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.pr19.1 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..63d61f65 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.pr19.1` - 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..786f97da --- /dev/null +++ b/cmd/conformance-handler/block_header.go @@ -0,0 +1,224 @@ +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 +} + +func handleBlockHeaderToBytes(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 + } + + data, err := header.Bytes() + if err != nil { + return NewEmptyErrorResponse(), nil + } + + return NewSuccessResponse(hex.EncodeToString(data[:])), 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..39d6c588 --- /dev/null +++ b/cmd/conformance-handler/block_tree_entry.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +// handleBlockTreeEntryGetHeight gets the height of a block tree entry. +func handleBlockTreeEntryGetHeight(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.Height()), 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) + } + + 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/callback_queue.go b/cmd/conformance-handler/callback_queue.go new file mode 100644 index 00000000..a3fdbbd4 --- /dev/null +++ b/cmd/conformance-handler/callback_queue.go @@ -0,0 +1,68 @@ +package main + +// callbackQueue accumulates invocation records from callback firings and flushes them on drain. +// Embed in a callback interface struct and use record/indexedRecord to build records. +// Object arguments are held internally until drain registers them in the shared registry. +// +// Typical usage: +// +// iface.indexedRecord(func(n int) map[string]any { +// return map[string]any{ +// "callback": "btck_SomeCallback", +// "entry": iface.ref(fmt.Sprintf("$%s_%d_btck_SomeCallback_entry", iface.ifaceRef, n), entry), +// "height": 42, +// } +// }) +type callbackQueue struct { + records []map[string]any + ifaceRef string + registry *Registry +} + +// objectRef marks an object argument to be registered in the shared registry on drain. +// Use callbackQueue.ref to construct one. drain stores obj into the registry as-is, so +// if the kernel passes a view into short-lived memory (e.g. a stack-local), copy it before +// the callback returns and pass the copy. Views into long-lived kernel-owned memory +// (e.g. a btck_BlockTreeEntry valid for the chainstate manager's lifetime) can be passed directly. +type objectRef struct { + ref string + obj any + registry *Registry +} + +// ref creates an objectRef for an object argument using the deterministic naming pattern: +// $___. +func (q *callbackQueue) ref(name string, obj any) objectRef { + return objectRef{ref: name, obj: obj, registry: q.registry} +} + +// record appends an invocation record with no object arguments. +func (q *callbackQueue) record(r map[string]any) { + q.records = append(q.records, r) +} + +// indexedRecord appends an invocation record that needs n to compute ref names. +// n is the 1-based position of this record in the current queue batch. +func (q *callbackQueue) indexedRecord(fn func(n int) map[string]any) { + q.records = append(q.records, fn(len(q.records)+1)) +} + +// drain stores each object argument into the shared registry, making it available to the +// runner for follow-up requests, and returns the resolved records in firing order. +func (q *callbackQueue) drain() []map[string]any { + result := make([]map[string]any, 0, len(q.records)) + for _, record := range q.records { + resolved := make(map[string]any, len(record)) + for k, v := range record { + if o, ok := v.(objectRef); ok { + o.registry.Store(o.ref, o.obj) + resolved[k] = map[string]any{"ref": o.ref} + } else { + resolved[k] = v + } + } + result = append(result, resolved) + } + q.records = nil + return result +} 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/context.go b/cmd/conformance-handler/context.go index 09c70461..9d4ae13a 100644 --- a/cmd/conformance-handler/context.go +++ b/cmd/conformance-handler/context.go @@ -13,6 +13,8 @@ func handleContextCreate(registry *Registry, req Request) (Response, error) { ChainParameters struct { ChainType string `json:"chain_type"` } `json:"chain_parameters"` + Notifications *RefObject `json:"notifications,omitempty"` + ValidationInterface *RefObject `json:"validation_interface,omitempty"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { @@ -39,7 +41,25 @@ func handleContextCreate(registry *Registry, req Request) (Response, error) { return Response{}, fmt.Errorf("unknown chain_type: %s", params.ChainParameters.ChainType) } - ctx, err := kernel.NewContext(kernel.WithChainType(chainType)) + opts := []kernel.ContextOption{kernel.WithChainType(chainType)} + + if params.Notifications != nil { + iface, err := registry.GetNotificationCallbacksInterface(params.Notifications.Ref) + if err != nil { + return Response{}, err + } + opts = append(opts, kernel.WithNotifications(iface)) + } + + if params.ValidationInterface != nil { + iface, err := registry.GetValidationCallbacksInterface(params.ValidationInterface.Ref) + if err != nil { + return Response{}, err + } + opts = append(opts, kernel.WithValidationInterface(iface)) + } + + ctx, err := kernel.NewContext(opts...) if err != nil { return NewEmptyErrorResponse(), nil } diff --git a/cmd/conformance-handler/handler.go b/cmd/conformance-handler/handler.go index d57cd573..734f97d7 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,10 +106,80 @@ 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) + case "btck_block_tree_entry_get_height": + return handleBlockTreeEntryGetHeight(registry, req) + + // Notification callbacks operations + case "notification_callbacks_create": + return handleNotificationCallbacksCreate(registry, req) + case "notification_callbacks_drain": + return handleNotificationCallbacksDrain(registry, req) + + // Validation interface callbacks operations + case "validation_interface_callbacks_create": + return handleValidationInterfaceCallbacksCreate(registry, req) + case "validation_callbacks_drain": + return handleValidationCallbacksDrain(registry, req) + + // Block validation state operations + case "btck_block_validation_state_get_validation_mode": + return handleBlockValidationStateGetValidationMode(registry, req) + case "btck_block_validation_state_destroy": + return handleBlockValidationStateDestroy(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_to_bytes": + return handleBlockHeaderToBytes(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/notification_callbacks.go b/cmd/conformance-handler/notification_callbacks.go new file mode 100644 index 00000000..c64ea18e --- /dev/null +++ b/cmd/conformance-handler/notification_callbacks.go @@ -0,0 +1,168 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// NotificationCallbacksInterface implements kernel.NotificationCallbacks and queues invocation records. +type NotificationCallbacksInterface struct { + callbackQueue + enabledCallbacks map[string]bool +} + +func (iface *NotificationCallbacksInterface) enabled(name string) bool { + return iface.enabledCallbacks[name] +} + +func warningString(w kernel.Warning) string { + switch w { + case kernel.WarningUnknownNewRulesActivated: + return "btck_Warning_UNKNOWN_NEW_RULES_ACTIVATED" + case kernel.WarningLargeWorkInvalidChain: + return "btck_Warning_LARGE_WORK_INVALID_CHAIN" + default: + return fmt.Sprintf("unknown(%d)", w) + } +} + +func syncStateString(s kernel.SynchronizationState) string { + switch s { + case kernel.SyncStatePostInit: + return "btck_SynchronizationState_POST_INIT" + case kernel.SyncStateInitDownload: + return "btck_SynchronizationState_INIT_DOWNLOAD" + case kernel.SyncStateInitReindex: + return "btck_SynchronizationState_INIT_REINDEX" + default: + return fmt.Sprintf("unknown(%d)", s) + } +} + +func (iface *NotificationCallbacksInterface) BlockTip(state kernel.SynchronizationState, entry *kernel.BlockTreeEntry, progress float64) { + if !iface.enabled("btck_NotifyBlockTip") { + return + } + iface.indexedRecord(func(n int) map[string]any { + return map[string]any{ + "callback": "btck_NotifyBlockTip", + "state": syncStateString(state), + "entry": iface.ref(fmt.Sprintf("$%s_%d_btck_NotifyBlockTip_entry", iface.ifaceRef, n), entry), + "verification_progress": progress, + } + }) +} + +func (iface *NotificationCallbacksInterface) HeaderTip(state kernel.SynchronizationState, height int64, timestamp int64, presync bool) { + if !iface.enabled("btck_NotifyHeaderTip") { + return + } + iface.record(map[string]any{ + "callback": "btck_NotifyHeaderTip", + "state": syncStateString(state), + "height": height, + "timestamp": timestamp, + "presync": presync, + }) +} + +func (iface *NotificationCallbacksInterface) Progress(title string, percent int, resumable bool) { + if !iface.enabled("btck_NotifyProgress") { + return + } + iface.record(map[string]any{ + "callback": "btck_NotifyProgress", + "title": title, + "percent": percent, + "resumable": resumable, + }) +} + +func (iface *NotificationCallbacksInterface) WarningSet(warning kernel.Warning, message string) { + if !iface.enabled("btck_NotifyWarningSet") { + return + } + iface.record(map[string]any{ + "callback": "btck_NotifyWarningSet", + "warning": warningString(warning), + "message": message, + }) +} + +func (iface *NotificationCallbacksInterface) WarningUnset(warning kernel.Warning) { + if !iface.enabled("btck_NotifyWarningUnset") { + return + } + iface.record(map[string]any{ + "callback": "btck_NotifyWarningUnset", + "warning": warningString(warning), + }) +} + +func (iface *NotificationCallbacksInterface) FlushError(message string) { + if !iface.enabled("btck_NotifyFlushError") { + return + } + iface.record(map[string]any{ + "callback": "btck_NotifyFlushError", + "message": message, + }) +} + +func (iface *NotificationCallbacksInterface) FatalError(message string) { + if !iface.enabled("btck_NotifyFatalError") { + return + } + iface.record(map[string]any{ + "callback": "btck_NotifyFatalError", + "message": message, + }) +} + +func handleNotificationCallbacksCreate(registry *Registry, req Request) (Response, error) { + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + var params struct { + Callbacks []string `json:"callbacks"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + if len(params.Callbacks) == 0 { + return Response{}, fmt.Errorf("callbacks is required and must not be empty") + } + + enabledCallbacks := make(map[string]bool, len(params.Callbacks)) + for _, name := range params.Callbacks { + enabledCallbacks[name] = true + } + + ifaceRef := strings.TrimPrefix(req.Ref, "$") + iface := &NotificationCallbacksInterface{ + callbackQueue: callbackQueue{ifaceRef: ifaceRef, registry: registry}, + enabledCallbacks: enabledCallbacks, + } + registry.Store(req.Ref, iface) + return NewSuccessResponseWithRef(req.Ref), nil +} + +func handleNotificationCallbacksDrain(registry *Registry, req Request) (Response, error) { + var params struct { + Interface RefObject `json:"interface"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + iface, err := registry.GetNotificationCallbacksInterface(params.Interface.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(iface.drain()), nil +} diff --git a/cmd/conformance-handler/registry.go b/cmd/conformance-handler/registry.go index 5faa60ec..b264cfb6 100644 --- a/cmd/conformance-handler/registry.go +++ b/cmd/conformance-handler/registry.go @@ -137,6 +137,123 @@ 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 +} + +// GetNotificationCallbacksInterface retrieves a notification callbacks interface by reference name +func (r *Registry) GetNotificationCallbacksInterface(ref string) (*NotificationCallbacksInterface, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + iface, ok := obj.(*NotificationCallbacksInterface) + if !ok { + return nil, fmt.Errorf("reference %s is not a NotificationCallbacksInterface (got %T)", ref, obj) + } + return iface, nil +} + +// GetBlockValidationState retrieves a block validation state by reference name +func (r *Registry) GetBlockValidationState(ref string) (*kernel.BlockValidationState, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + s, ok := obj.(*kernel.BlockValidationState) + if !ok { + return nil, fmt.Errorf("reference %s is not a BlockValidationState (got %T)", ref, obj) + } + return s, nil +} + +// GetValidationCallbacksInterface retrieves a validation callbacks interface by reference name +func (r *Registry) GetValidationCallbacksInterface(ref string) (*ValidationCallbacksInterface, error) { + obj, ok := r.objects[ref] + if !ok { + return nil, fmt.Errorf("reference not found: %s", ref) + } + iface, ok := obj.(*ValidationCallbacksInterface) + if !ok { + return nil, fmt.Errorf("reference %s is not a ValidationCallbacksInterface (got %T)", ref, obj) + } + return iface, 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/cmd/conformance-handler/validation_callbacks.go b/cmd/conformance-handler/validation_callbacks.go new file mode 100644 index 00000000..d77797b4 --- /dev/null +++ b/cmd/conformance-handler/validation_callbacks.go @@ -0,0 +1,161 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/stringintech/go-bitcoinkernel/kernel" +) + +// ValidationCallbacksInterface implements kernel.ValidationInterfaceCallbacks and queues invocation records. +type ValidationCallbacksInterface struct { + callbackQueue + enabledCallbacks map[string]bool +} + +func (iface *ValidationCallbacksInterface) enabled(name string) bool { + return iface.enabledCallbacks[name] +} + +func validationModeString(m kernel.ValidationMode) string { + switch m { + case kernel.ValidationStateValid: + return "btck_ValidationMode_VALID" + case kernel.ValidationStateInvalid: + return "btck_ValidationMode_INVALID" + case kernel.ValidationStateError: + return "btck_ValidationMode_INTERNAL_ERROR" + default: + return fmt.Sprintf("unknown(%d)", m) + } +} + +func (iface *ValidationCallbacksInterface) BlockChecked(block *kernel.Block, state *kernel.BlockValidationStateView) { + if !iface.enabled("btck_ValidationInterfaceBlockChecked") { + return + } + // state is a view into a stack-local; must copy before the triggering call returns + iface.indexedRecord(func(n int) map[string]any { + return map[string]any{ + "callback": "btck_ValidationInterfaceBlockChecked", + "block": iface.ref(fmt.Sprintf("$%s_%d_btck_ValidationInterfaceBlockChecked_block", iface.ifaceRef, n), block), + "state": iface.ref(fmt.Sprintf("$%s_%d_btck_ValidationInterfaceBlockChecked_state", iface.ifaceRef, n), state.Copy()), + } + }) +} + +func (iface *ValidationCallbacksInterface) PoWValidBlock(block *kernel.Block, entry *kernel.BlockTreeEntry) { + if !iface.enabled("btck_ValidationInterfacePoWValidBlock") { + return + } + iface.indexedRecord(func(n int) map[string]any { + return map[string]any{ + "callback": "btck_ValidationInterfacePoWValidBlock", + "block": iface.ref(fmt.Sprintf("$%s_%d_btck_ValidationInterfacePoWValidBlock_block", iface.ifaceRef, n), block), + "entry": iface.ref(fmt.Sprintf("$%s_%d_btck_ValidationInterfacePoWValidBlock_entry", iface.ifaceRef, n), entry), + } + }) +} + +func (iface *ValidationCallbacksInterface) BlockConnected(block *kernel.Block, entry *kernel.BlockTreeEntry) { + if !iface.enabled("btck_ValidationInterfaceBlockConnected") { + return + } + iface.indexedRecord(func(n int) map[string]any { + return map[string]any{ + "callback": "btck_ValidationInterfaceBlockConnected", + "block": iface.ref(fmt.Sprintf("$%s_%d_btck_ValidationInterfaceBlockConnected_block", iface.ifaceRef, n), block), + "entry": iface.ref(fmt.Sprintf("$%s_%d_btck_ValidationInterfaceBlockConnected_entry", iface.ifaceRef, n), entry), + } + }) +} + +func (iface *ValidationCallbacksInterface) BlockDisconnected(block *kernel.Block, entry *kernel.BlockTreeEntry) { + if !iface.enabled("btck_ValidationInterfaceBlockDisconnected") { + return + } + iface.indexedRecord(func(n int) map[string]any { + return map[string]any{ + "callback": "btck_ValidationInterfaceBlockDisconnected", + "block": iface.ref(fmt.Sprintf("$%s_%d_btck_ValidationInterfaceBlockDisconnected_block", iface.ifaceRef, n), block), + "entry": iface.ref(fmt.Sprintf("$%s_%d_btck_ValidationInterfaceBlockDisconnected_entry", iface.ifaceRef, n), entry), + } + }) +} + +func handleBlockValidationStateGetValidationMode(registry *Registry, req Request) (Response, error) { + var params struct { + State RefObject `json:"state"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + s, err := registry.GetBlockValidationState(params.State.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(validationModeString(s.ValidationMode())), nil +} + +func handleBlockValidationStateDestroy(registry *Registry, req Request) (Response, error) { + var params struct { + State RefObject `json:"state"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + if err := registry.Destroy(params.State.Ref); err != nil { + return Response{}, err + } + + return NewEmptySuccessResponse(), nil +} + +func handleValidationInterfaceCallbacksCreate(registry *Registry, req Request) (Response, error) { + if req.Ref == "" { + return Response{}, fmt.Errorf("ref field is required") + } + + var params struct { + Callbacks []string `json:"callbacks"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + if len(params.Callbacks) == 0 { + return Response{}, fmt.Errorf("callbacks is required and must not be empty") + } + + enabledCallbacks := make(map[string]bool, len(params.Callbacks)) + for _, name := range params.Callbacks { + enabledCallbacks[name] = true + } + + ifaceRef := strings.TrimPrefix(req.Ref, "$") + iface := &ValidationCallbacksInterface{ + callbackQueue: callbackQueue{ifaceRef: ifaceRef, registry: registry}, + enabledCallbacks: enabledCallbacks, + } + registry.Store(req.Ref, iface) + return NewSuccessResponseWithRef(req.Ref), nil +} + +func handleValidationCallbacksDrain(registry *Registry, req Request) (Response, error) { + var params struct { + Interface RefObject `json:"interface"` + } + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + return Response{}, fmt.Errorf("failed to parse params: %w", err) + } + + iface, err := registry.GetValidationCallbacksInterface(params.Interface.Ref) + if err != nil { + return Response{}, err + } + + return NewSuccessResponse(iface.drain()), 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..23869882 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() @@ -221,8 +239,8 @@ func (s *ChainstateManagerTestSuite) TestGetBlockTreeEntryByHash(t *testing.T) { type ChainstateManagerTestSuite struct { MaxBlockHeightToImport int32 // leave zero to load all blocks - NotificationCallbacks *NotificationCallbacks - ValidationCallbacks *ValidationInterfaceCallbacks + NotificationCallbacks NotificationCallbacks + ValidationCallbacks ValidationInterfaceCallbacks Manager *ChainstateManager ImportedBlocksCount int32 diff --git a/kernel/context_options.go b/kernel/context_options.go index e63c9260..fbabc24c 100644 --- a/kernel/context_options.go +++ b/kernel/context_options.go @@ -47,7 +47,7 @@ func WithChainType(chainType ChainType) ContextOption { // // Parameters: // - callbacks: Notification callbacks to set -func WithNotifications(callbacks *NotificationCallbacks) ContextOption { +func WithNotifications(callbacks NotificationCallbacks) ContextOption { return func(opts *C.btck_ContextOptions) error { notificationCallbacks := C.btck_NotificationInterfaceCallbacks{ user_data: newCgoHandlePointer(callbacks), @@ -71,7 +71,7 @@ func WithNotifications(callbacks *NotificationCallbacks) ContextOption { // // Parameters: // - callbacks: The callbacks used for passing validation information to the user -func WithValidationInterface(callbacks *ValidationInterfaceCallbacks) ContextOption { +func WithValidationInterface(callbacks ValidationInterfaceCallbacks) ContextOption { return func(opts *C.btck_ContextOptions) error { validationCallbacks := C.btck_ValidationInterfaceCallbacks{ user_data: newCgoHandlePointer(callbacks), diff --git a/kernel/notification_callbacks.go b/kernel/notification_callbacks.go index 48d8dd2a..f34a75ed 100644 --- a/kernel/notification_callbacks.go +++ b/kernel/notification_callbacks.go @@ -8,15 +8,15 @@ import ( "unsafe" ) -// NotificationCallbacks contains all the Go callback function types for notifications. -type NotificationCallbacks struct { - OnBlockTip func(state SynchronizationState, entry *BlockTreeEntry, progress float64) - OnHeaderTip func(state SynchronizationState, height int64, timestamp int64, presync bool) - OnProgress func(title string, percent int, resumable bool) - OnWarningSet func(warning Warning, message string) - OnWarningUnset func(warning Warning) - OnFlushError func(message string) - OnFatalError func(message string) +// NotificationCallbacks is implemented by types that want to receive kernel notification events. +type NotificationCallbacks interface { + BlockTip(state SynchronizationState, entry *BlockTreeEntry, progress float64) + HeaderTip(state SynchronizationState, height int64, timestamp int64, presync bool) + Progress(title string, percent int, resumable bool) + WarningSet(warning Warning, message string) + WarningUnset(warning Warning) + FlushError(message string) + FatalError(message string) } // SynchronizationState represents the current sync state passed to tip changed callbacks. @@ -38,72 +38,42 @@ const ( //export go_notify_block_tip_bridge func go_notify_block_tip_bridge(user_data unsafe.Pointer, state C.btck_SynchronizationState, entry *C.btck_BlockTreeEntry, verification_progress C.double) { - callbacks := cgoHandleFromPointer(user_data).Value().(*NotificationCallbacks) - - if callbacks.OnBlockTip != nil { - goState := SynchronizationState(state) - goEntry := &BlockTreeEntry{ptr: (*C.btck_BlockTreeEntry)(unsafe.Pointer(entry))} - callbacks.OnBlockTip(goState, goEntry, float64(verification_progress)) - } + callbacks := cgoHandleFromPointer(user_data).Value().(NotificationCallbacks) + callbacks.BlockTip(SynchronizationState(state), &BlockTreeEntry{ptr: (*C.btck_BlockTreeEntry)(unsafe.Pointer(entry))}, float64(verification_progress)) } //export go_notify_header_tip_bridge func go_notify_header_tip_bridge(user_data unsafe.Pointer, state C.btck_SynchronizationState, height C.int64_t, timestamp C.int64_t, presync C.int) { - callbacks := cgoHandleFromPointer(user_data).Value().(*NotificationCallbacks) - - if callbacks.OnHeaderTip != nil { - goState := SynchronizationState(state) - callbacks.OnHeaderTip(goState, int64(height), int64(timestamp), presync != 0) - } + callbacks := cgoHandleFromPointer(user_data).Value().(NotificationCallbacks) + callbacks.HeaderTip(SynchronizationState(state), int64(height), int64(timestamp), presync != 0) } //export go_notify_progress_bridge func go_notify_progress_bridge(user_data unsafe.Pointer, title *C.char, title_len C.size_t, progress_percent C.int, resume_possible C.int) { - callbacks := cgoHandleFromPointer(user_data).Value().(*NotificationCallbacks) - - if callbacks.OnProgress != nil { - goTitle := C.GoStringN(title, C.int(title_len)) - callbacks.OnProgress(goTitle, int(progress_percent), resume_possible != 0) - } + callbacks := cgoHandleFromPointer(user_data).Value().(NotificationCallbacks) + callbacks.Progress(C.GoStringN(title, C.int(title_len)), int(progress_percent), resume_possible != 0) } //export go_notify_warning_set_bridge func go_notify_warning_set_bridge(user_data unsafe.Pointer, warning C.btck_Warning, message *C.char, message_len C.size_t) { - callbacks := cgoHandleFromPointer(user_data).Value().(*NotificationCallbacks) - - if callbacks.OnWarningSet != nil { - goWarning := Warning(warning) - goMessage := C.GoStringN(message, C.int(message_len)) - callbacks.OnWarningSet(goWarning, goMessage) - } + callbacks := cgoHandleFromPointer(user_data).Value().(NotificationCallbacks) + callbacks.WarningSet(Warning(warning), C.GoStringN(message, C.int(message_len))) } //export go_notify_warning_unset_bridge func go_notify_warning_unset_bridge(user_data unsafe.Pointer, warning C.btck_Warning) { - callbacks := cgoHandleFromPointer(user_data).Value().(*NotificationCallbacks) - - if callbacks.OnWarningUnset != nil { - goWarning := Warning(warning) - callbacks.OnWarningUnset(goWarning) - } + callbacks := cgoHandleFromPointer(user_data).Value().(NotificationCallbacks) + callbacks.WarningUnset(Warning(warning)) } //export go_notify_flush_error_bridge func go_notify_flush_error_bridge(user_data unsafe.Pointer, message *C.char, message_len C.size_t) { - callbacks := cgoHandleFromPointer(user_data).Value().(*NotificationCallbacks) - - if callbacks.OnFlushError != nil { - goMessage := C.GoStringN(message, C.int(message_len)) - callbacks.OnFlushError(goMessage) - } + callbacks := cgoHandleFromPointer(user_data).Value().(NotificationCallbacks) + callbacks.FlushError(C.GoStringN(message, C.int(message_len))) } //export go_notify_fatal_error_bridge func go_notify_fatal_error_bridge(user_data unsafe.Pointer, message *C.char, message_len C.size_t) { - callbacks := cgoHandleFromPointer(user_data).Value().(*NotificationCallbacks) - - if callbacks.OnFatalError != nil { - goMessage := C.GoStringN(message, C.int(message_len)) - callbacks.OnFatalError(goMessage) - } + callbacks := cgoHandleFromPointer(user_data).Value().(NotificationCallbacks) + callbacks.FatalError(C.GoStringN(message, C.int(message_len))) } diff --git a/kernel/notification_callbacks_test.go b/kernel/notification_callbacks_test.go index 2a1330f1..ae58a380 100644 --- a/kernel/notification_callbacks_test.go +++ b/kernel/notification_callbacks_test.go @@ -4,39 +4,47 @@ import ( "testing" ) -func TestNotificationCallbacks(t *testing.T) { - var blockTipCalled bool - var headerTipCalled bool - var lastBlockHeight int64 - var lastHeaderHeight int64 +type notificationTestCallbacks struct { + lastBlockHeight int64 + lastHeaderHeight int64 + blockTipCalled bool + headerTipCalled bool +} - callbacks := &NotificationCallbacks{ - OnBlockTip: func(state SynchronizationState, index *BlockTreeEntry, _ float64) { - blockTipCalled = true - lastBlockHeight = int64(index.Height()) - }, - OnHeaderTip: func(state SynchronizationState, height int64, timestamp int64, presync bool) { - headerTipCalled = true - lastHeaderHeight = height - }, - } +func (c *notificationTestCallbacks) BlockTip(state SynchronizationState, entry *BlockTreeEntry, progress float64) { + c.blockTipCalled = true + c.lastBlockHeight = int64(entry.Height()) +} + +func (c *notificationTestCallbacks) HeaderTip(state SynchronizationState, height int64, timestamp int64, presync bool) { + c.headerTipCalled = true + c.lastHeaderHeight = height +} + +func (c *notificationTestCallbacks) Progress(title string, percent int, resumable bool) {} +func (c *notificationTestCallbacks) WarningSet(warning Warning, message string) {} +func (c *notificationTestCallbacks) WarningUnset(warning Warning) {} +func (c *notificationTestCallbacks) FlushError(message string) {} +func (c *notificationTestCallbacks) FatalError(message string) {} + +func TestNotificationCallbacks(t *testing.T) { + cb := ¬ificationTestCallbacks{} suite := ChainstateManagerTestSuite{ MaxBlockHeightToImport: 5, - NotificationCallbacks: callbacks, + NotificationCallbacks: cb, } suite.Setup(t) - if !blockTipCalled { - t.Error("OnBlockTip callback was not called") + if !cb.blockTipCalled { + t.Error("BlockTip callback was not called") } - if lastBlockHeight != 5 { - t.Errorf("Expected last block height 5, got %d", lastBlockHeight) + if cb.lastBlockHeight != 5 { + t.Errorf("Expected last block height 5, got %d", cb.lastBlockHeight) } - - if !headerTipCalled { - t.Error("OnHeaderTip callback was not called") + if !cb.headerTipCalled { + t.Error("HeaderTip callback was not called") } - if lastHeaderHeight != 5 { - t.Errorf("Expected last header height 5, got %d", lastHeaderHeight) + if cb.lastHeaderHeight != 5 { + t.Errorf("Expected last header height 5, got %d", cb.lastHeaderHeight) } } diff --git a/kernel/validation_interface_callbacks.go b/kernel/validation_interface_callbacks.go index 3f436246..873ed0b1 100644 --- a/kernel/validation_interface_callbacks.go +++ b/kernel/validation_interface_callbacks.go @@ -8,44 +8,36 @@ import ( "unsafe" ) -// ValidationInterfaceCallbacks holds the validation interface callbacks. +// ValidationInterfaceCallbacks is implemented by types that want to receive kernel validation events. // // Note that these callbacks block any further validation execution when they are called. -type ValidationInterfaceCallbacks struct { - OnBlockChecked func(block *Block, state *BlockValidationStateView) // Called when a new block has been fully validated. Contains the result of its validation. - OnPoWValidBlock func(block *Block, entry *BlockTreeEntry) // Called when a new block extends the header chain and has a valid transaction and segwit merkle root. - OnBlockConnected func(block *Block, entry *BlockTreeEntry) // Called when a block is valid and has now been connected to the best chain. - OnBlockDisconnected func(block *Block, entry *BlockTreeEntry) // Called during a re-org when a block has been removed from the best chain. +type ValidationInterfaceCallbacks interface { + BlockChecked(block *Block, state *BlockValidationStateView) + PoWValidBlock(block *Block, entry *BlockTreeEntry) + BlockConnected(block *Block, entry *BlockTreeEntry) + BlockDisconnected(block *Block, entry *BlockTreeEntry) } //export go_validation_interface_block_checked_bridge func go_validation_interface_block_checked_bridge(user_data unsafe.Pointer, block *C.btck_Block, state *C.btck_BlockValidationState) { - callbacks := cgoHandleFromPointer(user_data).Value().(*ValidationInterfaceCallbacks) - if callbacks.OnBlockChecked != nil { - callbacks.OnBlockChecked(newBlock(block, true), newBlockValidationStateView(state)) - } + callbacks := cgoHandleFromPointer(user_data).Value().(ValidationInterfaceCallbacks) + callbacks.BlockChecked(newBlock(block, true), newBlockValidationStateView(state)) } //export go_validation_interface_pow_valid_block_bridge func go_validation_interface_pow_valid_block_bridge(user_data unsafe.Pointer, block *C.btck_Block, entry *C.btck_BlockTreeEntry) { - callbacks := cgoHandleFromPointer(user_data).Value().(*ValidationInterfaceCallbacks) - if callbacks.OnPoWValidBlock != nil { - callbacks.OnPoWValidBlock(newBlock(block, true), &BlockTreeEntry{ptr: entry}) - } + callbacks := cgoHandleFromPointer(user_data).Value().(ValidationInterfaceCallbacks) + callbacks.PoWValidBlock(newBlock(block, true), &BlockTreeEntry{ptr: entry}) } //export go_validation_interface_block_connected_bridge func go_validation_interface_block_connected_bridge(user_data unsafe.Pointer, block *C.btck_Block, entry *C.btck_BlockTreeEntry) { - callbacks := cgoHandleFromPointer(user_data).Value().(*ValidationInterfaceCallbacks) - if callbacks.OnBlockConnected != nil { - callbacks.OnBlockConnected(newBlock(block, true), &BlockTreeEntry{ptr: entry}) - } + callbacks := cgoHandleFromPointer(user_data).Value().(ValidationInterfaceCallbacks) + callbacks.BlockConnected(newBlock(block, true), &BlockTreeEntry{ptr: entry}) } //export go_validation_interface_block_disconnected_bridge func go_validation_interface_block_disconnected_bridge(user_data unsafe.Pointer, block *C.btck_Block, entry *C.btck_BlockTreeEntry) { - callbacks := cgoHandleFromPointer(user_data).Value().(*ValidationInterfaceCallbacks) - if callbacks.OnBlockDisconnected != nil { - callbacks.OnBlockDisconnected(newBlock(block, true), &BlockTreeEntry{ptr: entry}) - } + callbacks := cgoHandleFromPointer(user_data).Value().(ValidationInterfaceCallbacks) + callbacks.BlockDisconnected(newBlock(block, true), &BlockTreeEntry{ptr: entry}) } diff --git a/kernel/validation_interface_callbacks_test.go b/kernel/validation_interface_callbacks_test.go index 6683094e..85118dbb 100644 --- a/kernel/validation_interface_callbacks_test.go +++ b/kernel/validation_interface_callbacks_test.go @@ -5,57 +5,51 @@ import ( "testing" ) -func TestValidationInterfaceCallbacks(t *testing.T) { +type validationTestCallbacks struct { + lastBlockCheckedData []byte + lastValidationMode ValidationMode + lastConnectedData []byte + lastConnectedHeight int32 +} - var lastBlockCheckedBlockData []byte - var lastValidationMode ValidationMode +func (c *validationTestCallbacks) BlockChecked(block *Block, state *BlockValidationStateView) { + c.lastValidationMode = state.ValidationMode() + c.lastBlockCheckedData, _ = block.Bytes() +} - var lastBlockConnectedBlockData []byte - var lastBlockConnectedBlockHeight int32 +func (c *validationTestCallbacks) BlockConnected(block *Block, entry *BlockTreeEntry) { + c.lastConnectedHeight = entry.Height() + c.lastConnectedData, _ = block.Bytes() +} +func (c *validationTestCallbacks) PoWValidBlock(_ *Block, _ *BlockTreeEntry) {} +func (c *validationTestCallbacks) BlockDisconnected(_ *Block, _ *BlockTreeEntry) {} + +func TestValidationInterfaceCallbacks(t *testing.T) { + cb := &validationTestCallbacks{} suite := ChainstateManagerTestSuite{ MaxBlockHeightToImport: 2, - ValidationCallbacks: &ValidationInterfaceCallbacks{ - OnBlockChecked: func(block *Block, state *BlockValidationStateView) { - lastValidationMode = state.ValidationMode() - var err error - lastBlockCheckedBlockData, err = block.Bytes() - if err != nil { - t.Fatal(err) - } - }, - OnBlockConnected: func(block *Block, entry *BlockTreeEntry) { - lastBlockConnectedBlockHeight = entry.Height() - var err error - lastBlockConnectedBlockData, err = block.Bytes() - if err != nil { - t.Fatal(err) - } - }, - }, + ValidationCallbacks: cb, } suite.Setup(t) - // OnBlockChecked - if lastBlockCheckedBlockData == nil { - t.Error("OnBlockChecked callback was not called") + if cb.lastBlockCheckedData == nil { + t.Error("BlockChecked callback was not called") } - if lastValidationMode != ValidationStateValid { - t.Errorf("Expected validation mode %d, got %d", ValidationStateValid, lastValidationMode) + if cb.lastValidationMode != ValidationStateValid { + t.Errorf("Expected validation mode %d, got %d", ValidationStateValid, cb.lastValidationMode) } - expectedLastBlockDataHex := "00000020a629da61ccd6c9de14dd22d4dcf06ac4b98828801fb58275af1ed2c89e361b79677daedb5fc7781c5907a88133cd461b4865e9a4881fecfb362304ad1806acf3a7242d66ffff7f200100000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff0200f2052a010000001600141409745405c4e8310a875bcd602db6b9b3dc0cf90000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000" - if hex.EncodeToString(lastBlockCheckedBlockData) != expectedLastBlockDataHex { + expectedHex := "00000020a629da61ccd6c9de14dd22d4dcf06ac4b98828801fb58275af1ed2c89e361b79677daedb5fc7781c5907a88133cd461b4865e9a4881fecfb362304ad1806acf3a7242d66ffff7f200100000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025200ffffffff0200f2052a010000001600141409745405c4e8310a875bcd602db6b9b3dc0cf90000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000" + if hex.EncodeToString(cb.lastBlockCheckedData) != expectedHex { t.Errorf("Unexpected block data for last block") } - - // OnBlockConnected - if lastBlockConnectedBlockData == nil { - t.Error("OnBlockConnected callback was not called") + if cb.lastConnectedData == nil { + t.Error("BlockConnected callback was not called") } - if lastBlockConnectedBlockHeight != 2 { - t.Errorf("Expected connected block height 2, got %d", lastBlockConnectedBlockHeight) + if cb.lastConnectedHeight != 2 { + t.Errorf("Expected connected block height 2, got %d", cb.lastConnectedHeight) } - if hex.EncodeToString(lastBlockConnectedBlockData) != expectedLastBlockDataHex { + if hex.EncodeToString(cb.lastConnectedData) != expectedHex { t.Errorf("Unexpected block data for connected block") } }