Skip to content

Commit e731255

Browse files
committed
conformance: add handler
This handler can be executed by the runner in https://github.com/stringintech/kernel-bindings-tests
1 parent 023bb02 commit e731255

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed

conformance/handler.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Simple Python handler for Bitcoin Kernel conformance tests.
4+
5+
This handler conforms to the handler-spec.md protocol:
6+
- Reads JSON requests from stdin (one per line)
7+
- Writes JSON responses to stdout (one per line)
8+
- Processes requests sequentially
9+
- Exits cleanly when stdin closes
10+
"""
11+
12+
from dataclasses import asdict, dataclass
13+
from enum import Enum
14+
from functools import reduce
15+
import json
16+
from operator import or_
17+
import sys
18+
from typing import Any, Dict, Optional
19+
20+
import pbk
21+
22+
23+
def parse_enum(value: str):
24+
prefix, type_str, member = value.split("_", maxsplit=2)
25+
assert prefix == "btck"
26+
return getattr(pbk, type_str)[member]
27+
28+
29+
@dataclass
30+
class Request:
31+
"""Represents an incoming request."""
32+
33+
id: str
34+
method: str
35+
params: Optional[Dict[str, Any]] = None
36+
37+
38+
@dataclass
39+
class Response:
40+
"""Represents an outgoing response."""
41+
42+
id: str
43+
result: Optional[Any] = None
44+
error: Optional[Dict[str, str]] = None
45+
46+
def write(self) -> None:
47+
"""Write a response to stdout as JSON."""
48+
print(json.dumps(asdict(self)), flush=True)
49+
50+
51+
@dataclass
52+
class CodeError:
53+
type: str
54+
member: str
55+
56+
@classmethod
57+
def from_enum(cls, enum_value: Enum) -> "CodeError":
58+
"""Create a CodeError from an enum value."""
59+
return cls(type=f"btck_{type(enum_value).__name__}", member=enum_value.name)
60+
61+
62+
class Handler:
63+
"""Main handler class for processing requests."""
64+
65+
def handle_request(self, request: Request) -> Response:
66+
"""
67+
Process a single request and return a response.
68+
"""
69+
if request.method == "btck_script_pubkey_verify":
70+
return self.handle_script_verify(request)
71+
else:
72+
return Response(id=request.id, error={"code": {"type": "UNKNOWN_METHOD"}})
73+
74+
def handle_script_verify(self, request: Request) -> Response:
75+
"""
76+
Handle btck_script_pubkey_verify requests.
77+
"""
78+
flags = reduce(or_, (parse_enum(f) for f in request.params["flags"]), 0)
79+
spk = pbk.ScriptPubkey(bytes.fromhex(request.params["script_pubkey_hex"]))
80+
try:
81+
is_valid = spk.verify(
82+
request.params["amount"],
83+
pbk.Transaction(bytes.fromhex(request.params["tx_hex"])),
84+
[
85+
pbk.TransactionOutput(
86+
pbk.ScriptPubkey(bytes.fromhex(so["script_pubkey_hex"])),
87+
so["value"],
88+
)
89+
for so in request.params["spent_outputs"]
90+
],
91+
request.params["input_index"],
92+
flags,
93+
)
94+
95+
return Response(id=request.id, result=bool(is_valid))
96+
97+
except pbk.ScriptVerifyException as e:
98+
return Response(
99+
id=request.id, error={"code": asdict(CodeError.from_enum(e.status))}
100+
)
101+
102+
103+
def main():
104+
"""Main entry point - read requests from stdin and process them."""
105+
handler = Handler()
106+
request_data = {}
107+
108+
for line in sys.stdin:
109+
line = line.strip()
110+
if not line:
111+
continue
112+
try:
113+
request_data = json.loads(line)
114+
request = Request(**request_data)
115+
response = handler.handle_request(request)
116+
response.write()
117+
except Exception as e:
118+
# Try to send a JSON error back if possible
119+
if req_id := request_data.get("id"):
120+
resp = Response(
121+
id=req_id,
122+
error={
123+
"code": {"type": f"HandlerError {(str(e))}", "member": str(e)}
124+
},
125+
)
126+
resp.write()
127+
128+
129+
if __name__ == "__main__":
130+
main()

0 commit comments

Comments
 (0)