Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ mock-handler:

test: build
@echo "Running runner unit tests..."
go test -v ./runner/...
go test ./runner/...
@echo "Running conformance tests with mock handler..."
$(RUNNER_BIN) --handler $(MOCK_HANDLER_BIN) -vv
$(RUNNER_BIN) --handler $(MOCK_HANDLER_BIN)

suite-validate:
@echo "Validating testdata against the suite schema..."
Expand Down
67 changes: 55 additions & 12 deletions cmd/runner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,24 +66,35 @@ func main() {
totalPassed := 0
totalFailed := 0
totalTests := 0
loggedTimeout := false
failedTestLines := make([]string, 0)

for _, testFile := range testFiles {
fmt.Printf("\n=== Running test suite ===\n")

for suiteIdx, testFile := range testFiles {
// Load test suite from embedded FS
suite, err := runner.LoadTestSuiteFromFS(testdata.FS, testFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading test suite: %v\n", err)
continue
}
totalTests += len(suite.Tests)

// Check if context is already cancelled
if ctx.Err() != nil {
if !loggedTimeout {
fmt.Printf("Skipped remaining %d test suite(s) because total execution timeout (%v) was exceeded!\n",
len(testFiles)-suiteIdx, *timeout)
loggedTimeout = true
}
continue
}

// Run suite
result := testRunner.RunTestSuite(ctx, *suite, verbosity)
printResults(suite, result)
printResults(suite, result, verbosity)
failedTestLines = append(failedTestLines, collectFailedTestLines(suite, result)...)

totalPassed += result.PassedTests
totalFailed += result.FailedTests
totalTests += result.TotalTests

// Close handler after stateful suites to prevent state leaks.
// A new handler process will be spawned on-demand when the next request is sent.
Expand All @@ -92,29 +103,50 @@ func main() {
}
}

if len(failedTestLines) > 0 {
fmt.Printf("\n" + strings.Repeat("=", 60) + "\n")
fmt.Printf("FAILED TESTS\n")
fmt.Printf(strings.Repeat("=", 60) + "\n")
for _, line := range failedTestLines {
fmt.Printf("%s\n", line)
}
}

fmt.Printf("\n" + strings.Repeat("=", 60) + "\n")
fmt.Printf("TOTAL SUMMARY\n")
fmt.Printf(strings.Repeat("=", 60) + "\n")
fmt.Printf("Total Tests: %d\n", totalTests)
fmt.Printf("Passed: %d\n", totalPassed)
fmt.Printf("Failed: %d\n", totalFailed)
fmt.Printf("Skipped: %d\n", totalTests-(totalPassed+totalFailed))
fmt.Printf(strings.Repeat("=", 60) + "\n")

if totalFailed > 0 {
if totalTests > totalPassed {
os.Exit(1)
}
}

func printResults(suite *runner.TestSuite, result runner.TestResult) {
fmt.Printf("\nTest Suite: %s (%s)\n", result.SuiteTitle, result.SuiteFileName)
func printResults(suite *runner.TestSuite, result runner.TestResult, verbosity runner.VerbosityLevel) {
if verbosity < runner.VerbosityOnFailure && result.FailedTests == 0 {
return
}

fmt.Printf("=== %s (%s) ===\n", result.SuiteTitle, result.SuiteFileName)
if suite.Description != "" {
fmt.Printf("Description: %s\n", suite.Description)
fmt.Printf("%s\n", suite.Description)
}
fmt.Printf("Total: %d, Passed: %d, Failed: %d\n\n", result.TotalTests, result.PassedTests, result.FailedTests)
totalSkipped := result.TotalTests - (result.PassedTests + result.FailedTests)
fmt.Printf("Total: %d, Passed: %d, Failed: %d, Skipped: %d\n", result.TotalTests, result.PassedTests,
result.FailedTests, totalSkipped)

for i, tr := range result.TestResults {
status := "✓"
if !tr.Passed {
var status string
if tr.Passed {
if verbosity < runner.VerbosityAlways {
continue
}
status = "✓"
} else {
status = "✗"
}

Expand All @@ -131,3 +163,14 @@ func printResults(suite *runner.TestSuite, result runner.TestResult) {

fmt.Printf("\n")
}

func collectFailedTestLines(suite *runner.TestSuite, result runner.TestResult) []string {
lines := make([]string, 0, result.FailedTests)
for i, tr := range result.TestResults {
if tr.Passed {
continue
}
lines = append(lines, fmt.Sprintf("%s %s (%s)", result.SuiteFileName, tr.TestID, suite.Tests[i].Description))
}
return lines
}
65 changes: 63 additions & 2 deletions docs/handler-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Many operations return objects (contexts, blocks, chains, etc.) that must persis
// Request
{"id": "1", "method": "btck_context_create", "params": {...}, "ref": "$ctx1"}
// Response
{"id": "1", "result": {"ref": "$ctx1"}, "error": null}
{"id": "1", "result": {"ref": "$ctx1"}}
// Handler action: registry["$ctx1"] = created_context_ptr
```

Expand All @@ -91,12 +91,68 @@ Many operations return objects (contexts, blocks, chains, etc.) that must persis
// Request
{"id": "2", "method": "btck_chainstate_manager_create", "params": {"context": {"ref": "$ctx1"}}, "ref": "$csm1"}
// Response
{"id": "2", "result": {"ref": "$csm1"}, "error": null}
{"id": "2", "result": {"ref": "$csm1"}}
// Handler action: Extract ref from params.context, look up registry["$ctx1"], create manager, store as registry["$csm1"]
```

**Implementation**: Handlers must maintain a registry (map of reference names to object pointers) throughout their lifetime. Objects remain alive until explicitly destroyed or handler exit.

## Callback Interfaces

Some kernel operations trigger callbacks — notification events and validation interface events — that fire synchronously during the operation. The protocol requires handlers to implement two callback interfaces as queueing objects: each maintains an internal invocation queue, records every callback firing into it, and exposes that queue via a drain method. This gives the runner a way to assert which callbacks fired, in what order, and what values they carried.

### Creation and Wiring

Callback interface objects are binding-level objects — not direct C kernel API handles, but wired into the kernel at context creation time. The protocol introduces dedicated methods for creating them — `notification_callbacks_create` and `validation_interface_callbacks_create` — deliberately omitting the `btck_` prefix to make clear they have no direct C API counterpart. They follow the same ref/registry pattern as kernel objects. Interface refs are passed as optional params to `btck_context_create` to wire them in:

The `callbacks` param is required and must list at least one callback name. The interface only queues invocations for the listed callbacks; any unlisted callback fires at the C level but is silently discarded.

```json
// Request
{"id": "1", "method": "notification_callbacks_create", "params": {"callbacks": ["btck_NotifyBlockTip"]}, "ref": "$notif"}
// Response
{"id": "1", "result": {"ref": "$notif"}}

// Request
{"id": "2", "method": "btck_context_create", "params": {"chain_parameters": {...}, "notifications": {"ref": "$notif"}}, "ref": "$ctx"}
// Response
{"id": "2", "result": {"ref": "$ctx"}}
```

Neither interface has a matching destroy method. Both are cleaned up implicitly when the chainstate manager associated with the wired context is destroyed.

### Invocation Recording

When a callback fires, the interface implementation must, before the callback returns, append an invocation record to its queue, preserving firing order. The record identifies the callback that fired and carries its arguments, which may be primitives (e.g. an enum like `btck_SynchronizationState`, or a number like `verification_progress`) or objects (e.g. a `btck_BlockTreeEntry`). At this point object arguments sit on the queue and are not yet directly referenceable by the runner — they only become referenceable later, when [drain](#drain) registers them in the registry.

What the record carries for each object argument depends on how the kernel passes it. For arguments the kernel passes as owned copies, carry the copy directly. For arguments passed as views (pointers or references into kernel-owned memory), the handler must judge whether the underlying memory will remain valid long enough — at least until the last request that may reference this ref: if it will (e.g. a `btck_BlockTreeEntry` view that remains valid for the chainstate manager's lifetime), carry the view directly; if it will not — because the view points to a stack-local or other short-lived storage — copy the object before the callback returns and carry the copy instead. This decision is made at callback time; drain has no visibility into argument lifetimes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As documented here, handlers might need to copy callback args to extend their lifetime; but right now this is only needed for state in btck_ValidationInterfaceBlockChecked, a view into a stack-local. BlockChecked is also the only callback with mixed ownership: block arrives owned, state arrives as a short-lived view. A cleaner upstream contract might be to always pass object args as views and let consumers copy only when they need to outlive the callback - so block would become const btck_Block* too?


### Drain

Drain is what gives the runner access to the queued invocations: the firing order, each callback's primitive argument values, and a way to reference its object arguments in follow-up requests. The drain methods (`notification_callbacks_drain`, `validation_callbacks_drain`) are protocol-level methods with no C API counterpart. Each takes an interface ref, registers every queued object under a deterministic ref name, flushes the queue, and returns the invocation records in firing order with each object argument resolved to `{"ref": "<ref-name>"}`. Until drain is called, callback-produced refs are not in the registry, so the runner must call drain before referencing any callback-produced object in a follow-up request.

The registered ref name must follow this pattern, so the runner can predict it and use it in follow-up assertions (computing it at callback time is often more straightforward, but the spec only requires that the object end up registered under this name on drain):

```
$<interface_ref>_<n>_<callback_typedef>_<arg_name>
```

- `<interface_ref>`: ref name of the interface object, without the leading `$`
- `<n>`: ordinal position of this invocation in the queue since the last drain, counting across all callback types, starting at 1
- `<callback_typedef>`: exact C typedef name from `bitcoinkernel.h`
- `<arg_name>`: C parameter name of the object argument

Examples: `$notif_1_btck_NotifyBlockTip_entry`, `$vi_1_btck_ValidationInterfaceBlockChecked_block`, `$vi_2_btck_ValidationInterfaceBlockConnected_entry`.

Both callback families are synchronous — all callbacks triggered during a kernel operation complete before the operation returns — so a drain issued after a kernel operation (e.g. `btck_chainstate_manager_process_block`) will always see the complete set of records for that call. An empty array means no callbacks fired since the last drain.

```json
// Request
{"id": "3", "method": "notification_callbacks_drain", "params": {"interface": {"ref": "$notif"}}}
// Response
{"id": "3", "result": [{"callback": "btck_NotifyBlockTip", "state": "btck_SynchronizationState_POST_INIT", "entry": {"ref": "$notif_1_btck_NotifyBlockTip_entry"}, "verification_progress": 1.0}]}
```

## Test Suites Overview

The conformance tests are organized into suites, each testing a specific aspect of the Bitcoin Kernel bindings. Test files are located in [`../testdata/`](../testdata/).
Expand Down Expand Up @@ -209,6 +265,11 @@ Test cases where the verification operation fails to determine validity of the s

Sets up blocks, checks chain state, and verifies that the chain tip changes as expected after a reorg scenario.

### Callback Interfaces
**File:** [`callbacks.json`](../testdata/callbacks.json)

Registers both a notification callbacks interface and a validation interface, wires both into a context, drains init-time invocations after chainstate manager creation, processes a block, drains both interfaces, and asserts on notification entry height, validation mode, and validation entry height.

## Method Reference

Handler method definitions are maintained in the auto-generated [methods-spec.md](./methods-spec.md).
115 changes: 115 additions & 0 deletions docs/methods-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,45 @@ Gets the block hash from a block tree entry.

---

#### `btck_block_tree_entry_get_height`

Gets the height of a block tree entry.

**Parameters:**
- `block_tree_entry` (reference, required): Block tree entry reference

**Result:** Integer - Height of the block tree entry

**Error:** `null` (cannot return error)

---

#### `btck_block_validation_state_destroy`

Destroys a block validation state.

**Parameters:**
- `state` (reference, required): Block validation state reference to destroy

**Result:** `null` (void operation)

**Error:** `null` (cannot return error)

---

#### `btck_block_validation_state_get_validation_mode`

Gets the validation mode of a block validation state.

**Parameters:**
- `state` (reference, required): Block validation state reference

**Result:** Validation mode of the block validation state

**Error:** `null` (cannot return error)

---

#### `btck_chain_contains`

Checks whether a block tree entry is part of the active chain.
Expand Down Expand Up @@ -425,6 +464,8 @@ Creates a context with specified chain parameters.
- `btck_ChainType_TESTNET_4`
- `btck_ChainType_SIGNET`
- `btck_ChainType_REGTEST`
- `notifications` (reference, optional): Notification callbacks interface to wire into this context
- `validation_interface` (reference, optional): Validation interface to wire into this context

**Result:** Reference - Contains the created context (e.g., `{"ref": "$context"}`)

Expand Down Expand Up @@ -878,3 +919,77 @@ Returns the txid as raw bytes encoded as hex.
**Result:** String - Hex-encoded raw 32-byte txid

**Error:** `null` (cannot return error)

---

#### `notification_callbacks_create`

Creates a notification callbacks interface.

**Parameters:**
- `callbacks` (array of strings, required): Allowed values:
- `btck_NotifyBlockTip`
- `btck_NotifyHeaderTip`
- `btck_NotifyProgress`
- `btck_NotifyWarningSet`
- `btck_NotifyWarningUnset`
- `btck_NotifyFlushError`
- `btck_NotifyFatalError`

**Result:** Reference - Contains the created notification callbacks interface ref

**Error:** `null` (cannot return error)

---

#### `notification_callbacks_drain`

Drains all queued callback invocation records from a notification callbacks interface.

**Parameters:**
- `interface` (reference, required): Notification callbacks interface reference

**Result:** Array of invocation records, one per callback fired since the last drain. Each record is an object with a `callback` field identifying the type, plus type-specific fields:
- `btck_NotifyBlockTip`: `state` (SynchronizationState string), `entry` (reference to a BlockTreeEntry), `verification_progress` (number)
- `btck_NotifyHeaderTip`: `state` (SynchronizationState string), `height` (integer), `timestamp` (integer), `presync` (boolean)
- `btck_NotifyProgress`: `title` (string), `percent` (integer), `resumable` (boolean)
- `btck_NotifyWarningSet`: `warning` (Warning string), `message` (string)
- `btck_NotifyWarningUnset`: `warning` (Warning string)
- `btck_NotifyFlushError`: `message` (string)
- `btck_NotifyFatalError`: `message` (string)

**Error:** `null` (cannot return error)

---

#### `validation_callbacks_drain`

Drains all queued callback invocation records from a validation interface.

**Parameters:**
- `interface` (reference, required): Validation interface reference

**Result:** Array of invocation records, one per callback fired since the last drain. Each record is an object with a `callback` field identifying the type, plus type-specific fields:
- `btck_ValidationInterfaceBlockChecked`: `block` (reference to an owned Block copy), `state` (reference to an owned BlockValidationState copy)
- `btck_ValidationInterfacePoWValidBlock`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view)
- `btck_ValidationInterfaceBlockConnected`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view)
- `btck_ValidationInterfaceBlockDisconnected`: `block` (reference to an owned Block copy), `entry` (reference to a BlockTreeEntry view)

**Error:** `null` (cannot return error)

---

#### `validation_interface_callbacks_create`

Creates a validation interface.

**Parameters:**
- `callbacks` (array of strings, required): Allowed values:
- `btck_ValidationInterfaceBlockChecked`
- `btck_ValidationInterfacePoWValidBlock`
- `btck_ValidationInterfaceBlockConnected`
- `btck_ValidationInterfaceBlockDisconnected`

**Result:** Reference - Contains the created validation interface ref

**Error:** `null` (cannot return error)
49 changes: 49 additions & 0 deletions docs/schemas/btck_block_tree_entry_get_height.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Gets the height of a block tree entry",
"type": "object",
"additionalProperties": false,
"required": [
"description",
"request",
"expected_response"
],
"properties": {
"description": {
"$ref": "./shared.json#/$defs/NonEmptyText"
},
"request": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"method",
"params"
],
"properties": {
"id": {
"$ref": "./shared.json#/$defs/TestCaseID"
},
"method": {
"const": "btck_block_tree_entry_get_height"
},
"params": {
"type": "object",
"additionalProperties": false,
"required": [
"block_tree_entry"
],
"properties": {
"block_tree_entry": {
"$ref": "./shared.json#/$defs/RefObject",
"description": "Block tree entry reference"
}
}
}
}
},
"expected_response": {
"$ref": "./btck_block_tree_entry_get_height.response.json"
}
}
}
Loading
Loading