Skip to content

Commit ee5efa9

Browse files
committed
add: conformance testing handler
1 parent 919ed3c commit ee5efa9

File tree

10 files changed

+758
-0
lines changed

10 files changed

+758
-0
lines changed

conformance-handler.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
# Wrapper script to run the conformance test handler
3+
4+
set -e
5+
6+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
8+
JAR_PATH="$SCRIPT_DIR/build/libs/java-bitcoinkernel-conformance-handler-0.1.0.jar"
9+
LIB_PATH="$SCRIPT_DIR/bitcoinkernel/bitcoin/build/lib"
10+
11+
if [ ! -f "$JAR_PATH" ]; then
12+
echo "Error: Conformance handler JAR not found at $JAR_PATH" >&2
13+
echo "Please run: ./gradlew buildConformanceJar" >&2
14+
exit 1
15+
fi
16+
17+
if [ ! -f "$LIB_PATH/libbitcoinkernel.so" ]; then
18+
echo "Error: libbitcoinkernel.so not found at $LIB_PATH" >&2
19+
echo "Please build the shared library first" >&2
20+
exit 1
21+
fi
22+
23+
# Run the handler with library path
24+
export LD_LIBRARY_PATH="$LIB_PATH:$LD_LIBRARY_PATH"
25+
exec java --enable-native-access=ALL-UNNAMED \
26+
-jar "$JAR_PATH" \
27+
"$@"
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package org.bitcoinkernel.conformance;
2+
3+
import org.bitcoinkernel.conformance.handlers.MethodHandler;
4+
import org.bitcoinkernel.conformance.handlers.ScriptPubkeyVerifyHandler;
5+
import org.bitcoinkernel.conformance.protocol.TestRequest;
6+
import org.bitcoinkernel.conformance.protocol.TestResponse;
7+
import org.bitcoinkernel.conformance.protocol.ErrorType;
8+
import org.bitcoinkernel.conformance.utils.JsonProtocol;
9+
10+
import java.io.BufferedReader;
11+
import java.io.IOException;
12+
import java.io.InputStreamReader;
13+
import java.io.PrintWriter;
14+
import java.nio.charset.StandardCharsets;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
18+
// Main conformance test handler
19+
public class ConformanceTestHandler {
20+
21+
private static final Map<String, MethodHandler> METHOD_HANDLERS = new HashMap<>();
22+
23+
static {
24+
METHOD_HANDLERS.put("script_pubkey.verify", new ScriptPubkeyVerifyHandler());
25+
}
26+
27+
public static void main(String[] args) {
28+
// set up stdin/stdout with UTF-8 encoding
29+
try (BufferedReader reader = new BufferedReader(
30+
new InputStreamReader(System.in, StandardCharsets.UTF_8));
31+
PrintWriter writer = new PrintWriter(System.out, true, StandardCharsets.UTF_8)) {
32+
33+
String line;
34+
while ((line = reader.readLine()) != null) {
35+
if (line.trim().isEmpty()) {
36+
continue;
37+
}
38+
39+
TestResponse response = processRequest(line);
40+
String responseJson = JsonProtocol.serializeResponse(response);
41+
writer.println(responseJson);
42+
}
43+
44+
} catch (IOException e) {
45+
System.err.println("Fatal I/O error: " + e.getMessage());
46+
System.exit(1);
47+
}
48+
49+
// Normal exit when stdin closes
50+
System.exit(0);
51+
}
52+
53+
// Process a single request line
54+
private static TestResponse processRequest(String line) {
55+
TestRequest request;
56+
57+
try {
58+
request = JsonProtocol.parseRequest(line);
59+
} catch (Exception e) {
60+
return JsonProtocol.createProtocolError(null, ErrorType.MALFORMED_REQUEST);
61+
}
62+
63+
if (request.getId() == null || request.getMethod() == null) {
64+
return JsonProtocol.createProtocolError(
65+
request.getId(),
66+
ErrorType.MALFORMED_REQUEST
67+
);
68+
}
69+
70+
MethodHandler handler = METHOD_HANDLERS.get(request.getMethod());
71+
if (handler == null) {
72+
return JsonProtocol.createProtocolError(
73+
request.getId(),
74+
ErrorType.UNKNOWN_METHOD
75+
);
76+
}
77+
78+
try {
79+
return handler.handle(request);
80+
} catch (Exception e) {
81+
System.err.println("Error processing request: " + e.getMessage());
82+
e.printStackTrace(System.err);
83+
84+
return TestResponse.error(
85+
request.getId(),
86+
ErrorType.BINDING,
87+
ErrorType.BINDING_ERROR
88+
);
89+
}
90+
}
91+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.bitcoinkernel.conformance.handlers;
2+
3+
import org.bitcoinkernel.conformance.protocol.TestResponse;
4+
import org.bitcoinkernel.conformance.protocol.TestRequest;
5+
6+
@FunctionalInterface
7+
public interface MethodHandler {
8+
TestResponse handle(TestRequest request);
9+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package org.bitcoinkernel.conformance.handlers;
2+
3+
import org.bitcoinkernel.KernelData.ScriptPubkey;
4+
import org.bitcoinkernel.KernelTypes;
5+
import org.bitcoinkernel.Transactions.Transaction;
6+
import org.bitcoinkernel.Transactions.TransactionOutput;
7+
import org.bitcoinkernel.conformance.protocol.ErrorType;
8+
import org.bitcoinkernel.conformance.protocol.TestRequest;
9+
import org.bitcoinkernel.conformance.protocol.TestResponse;
10+
import org.bitcoinkernel.conformance.utils.HexUtils;
11+
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
public class ScriptPubkeyVerifyHandler implements MethodHandler {
16+
17+
@Override
18+
public TestResponse handle(TestRequest request) {
19+
try {
20+
String scriptPubkeyHex = request.getParamAsString("script_pubkey_hex");
21+
Long amount = request.getParamAsLong("amount");
22+
String txToHex = request.getParamAsString("tx_hex");
23+
List<Map<String, Object>> spentOutputs = request.getParamAsList("spent_outputs");
24+
Integer inputIndex = request.getParamAsInt("input_index");
25+
26+
// Parse flags - can be either a string name or integer
27+
Integer flags;
28+
Object flagsObj = request.getParams().get("flags");
29+
if (flagsObj instanceof String) {
30+
flags = parseFlagsFromString((String) flagsObj);
31+
} else if (flagsObj instanceof Number) {
32+
flags = ((Number) flagsObj).intValue();
33+
} else {
34+
flags = null;
35+
}
36+
37+
if (scriptPubkeyHex == null || amount == null || txToHex == null || inputIndex == null || flags == null) {
38+
return TestResponse.error(
39+
request.getId(),
40+
ErrorType.PROTOCOL,
41+
ErrorType.INVALID_PARAMS
42+
);
43+
}
44+
45+
// Validate flags - check if any invalid bits are set
46+
int validFlagsMask = KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_ALL;
47+
if ((flags & ~validFlagsMask) != 0) {
48+
return TestResponse.error(
49+
request.getId(),
50+
ErrorType.SCRIPT_VERIFY,
51+
ErrorType.INVALID_FLAGS
52+
);
53+
}
54+
55+
byte[] scriptPubkeyBytes = HexUtils.decode(scriptPubkeyHex);
56+
byte[] txToBytes = HexUtils.decode(txToHex);
57+
58+
Transaction txTo;
59+
try {
60+
txTo = new Transaction(txToBytes);
61+
} catch (Exception e) {
62+
return TestResponse.error(
63+
request.getId(),
64+
ErrorType.BINDING,
65+
ErrorType.BINDING_ERROR
66+
);
67+
}
68+
69+
if (inputIndex < 0 || inputIndex >= txTo.countInputs()) {
70+
txTo.close();
71+
return TestResponse.error(
72+
request.getId(),
73+
ErrorType.SCRIPT_VERIFY,
74+
ErrorType.TX_INPUT_INDEX
75+
);
76+
}
77+
78+
TransactionOutput[] spentOutputsArray = null;
79+
if (spentOutputs != null && !spentOutputs.isEmpty()) {
80+
if (spentOutputs.size() != txTo.countInputs()) {
81+
txTo.close();
82+
return TestResponse.error(
83+
request.getId(),
84+
ErrorType.SCRIPT_VERIFY,
85+
ErrorType.SPENT_OUTPUTS_MISMATCH
86+
);
87+
}
88+
89+
spentOutputsArray = new TransactionOutput[spentOutputs.size()];
90+
for (int i = 0; i < spentOutputs.size(); ++i) {
91+
Map<String, Object> output = spentOutputs.get(i);
92+
String scriptHex = (String) output.get("script_pubkey_hex");
93+
// Try both "value" and "amount" field names
94+
Object valueObj = output.get("value");
95+
if (valueObj == null) {
96+
valueObj = output.get("amount");
97+
}
98+
99+
long value;
100+
if (valueObj instanceof Number) {
101+
value = ((Number) valueObj).longValue();
102+
} else if (valueObj instanceof String) {
103+
value = Long.parseLong((String) valueObj);
104+
} else {
105+
txTo.close();
106+
return TestResponse.error(
107+
request.getId(),
108+
ErrorType.PROTOCOL,
109+
ErrorType.INVALID_PARAMS
110+
);
111+
}
112+
113+
byte[] scriptBytes = HexUtils.decode(scriptHex);
114+
ScriptPubkey spk = new ScriptPubkey(scriptBytes);
115+
spentOutputsArray[i] = new TransactionOutput(spk, value);
116+
}
117+
}
118+
119+
ScriptPubkey scriptPubkey;
120+
try {
121+
scriptPubkey = new ScriptPubkey(scriptPubkeyBytes);
122+
} catch (Exception e) {
123+
txTo.close();
124+
if (spentOutputsArray != null) {
125+
for (TransactionOutput output : spentOutputsArray) {
126+
output.close();
127+
}
128+
}
129+
return TestResponse.error(
130+
request.getId(),
131+
ErrorType.BINDING,
132+
ErrorType.BINDING_ERROR
133+
);
134+
}
135+
136+
// Verification
137+
try {
138+
scriptPubkey.verify(amount, txTo, spentOutputsArray, inputIndex, flags);
139+
140+
// Success - clean up and return
141+
scriptPubkey.close();
142+
txTo.close();
143+
if (spentOutputsArray != null) {
144+
for (TransactionOutput output : spentOutputsArray) {
145+
output.close();
146+
}
147+
}
148+
149+
return TestResponse.success(request.getId());
150+
} catch (KernelTypes.KernelException e) {
151+
String variant = mapScriptVerifyError(e);
152+
153+
scriptPubkey.close();
154+
if (spentOutputsArray != null) {
155+
for (TransactionOutput output : spentOutputsArray) {
156+
output.close();
157+
}
158+
}
159+
160+
return TestResponse.error(
161+
request.getId(),
162+
ErrorType.SCRIPT_VERIFY,
163+
variant
164+
);
165+
} catch (Exception e) {
166+
// Unexpected error
167+
scriptPubkey.close();
168+
txTo.close();
169+
if (spentOutputsArray != null) {
170+
for (TransactionOutput output : spentOutputsArray) {
171+
output.close();
172+
}
173+
}
174+
175+
return TestResponse.error(
176+
request.getId(),
177+
ErrorType.BINDING,
178+
ErrorType.BINDING_ERROR
179+
);
180+
}
181+
} catch (Exception e) {
182+
// Protocol level error
183+
return TestResponse.error(
184+
request.getId(),
185+
ErrorType.PROTOCOL,
186+
ErrorType.INVALID_PARAMS
187+
);
188+
}
189+
}
190+
191+
private String mapScriptVerifyError(KernelTypes.KernelException e) {
192+
KernelTypes.KernelException.ScriptVerifyError error = e.getScriptVerifyError();
193+
194+
if (error == null) {
195+
return ErrorType.INVALID;
196+
}
197+
198+
return switch (error) {
199+
case INVALID_FLAGS_COMBINATION -> ErrorType.INVALID_FLAGS_COMBINATION;
200+
case SPENT_OUTPUTS_REQUIRED -> ErrorType.SPENT_OUTPUTS_REQUIRED;
201+
case OK -> ErrorType.INVALID; // Should not happen, but handle it
202+
default -> ErrorType.INVALID;
203+
};
204+
}
205+
206+
/**
207+
* Parse verification flags from string name.
208+
*/
209+
private Integer parseFlagsFromString(String flagsStr) {
210+
if (flagsStr == null || flagsStr.isEmpty()) {
211+
return KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_NONE;
212+
}
213+
214+
return switch (flagsStr) {
215+
case "VERIFY_NONE" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_NONE;
216+
case "VERIFY_P2SH" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_P2SH;
217+
case "VERIFY_DERSIG" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_DERSIG;
218+
case "VERIFY_NULLDUMMY" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_NULLDUMMY;
219+
case "VERIFY_CHECKLOCKTIMEVERIFY" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY;
220+
case "VERIFY_CHECKSEQUENCEVERIFY" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_CHECKSEQUENCEVERIFY;
221+
case "VERIFY_WITNESS" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_WITNESS;
222+
case "VERIFY_TAPROOT" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_TAPROOT;
223+
case "VERIFY_ALL" -> KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_ALL;
224+
case "VERIFY_ALL_PRE_TAPROOT" ->
225+
KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_P2SH |
226+
KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_DERSIG |
227+
KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_NULLDUMMY |
228+
KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY |
229+
KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_CHECKSEQUENCEVERIFY |
230+
KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_WITNESS;
231+
default -> {
232+
// Try parsing as integer
233+
try {
234+
yield Integer.parseInt(flagsStr);
235+
} catch (NumberFormatException e) {
236+
yield KernelTypes.ScriptVerificationFlags.SCRIPT_VERIFY_NONE;
237+
}
238+
}
239+
};
240+
}
241+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.bitcoinkernel.conformance.protocol;
2+
3+
public class ErrorType {
4+
5+
public static final String SCRIPT_VERIFY = "ScriptVerify";
6+
public static final String PROTOCOL = "Protocol";
7+
public static final String BINDING = "Binding";
8+
public static final String KERNEL = "Kernel";
9+
public static final String TX_INPUT_INDEX = "TxInputIndex";
10+
public static final String INVALID_FLAGS = "InvalidFlags";
11+
public static final String INVALID_FLAGS_COMBINATION = "InvalidFlagsCombination";
12+
public static final String SPENT_OUTPUTS_MISMATCH = "SpentOutputsMismatch";
13+
public static final String SPENT_OUTPUTS_REQUIRED = "SpentOutputsRequired";
14+
public static final String INVALID = "Invalid";
15+
public static final String UNKNOWN_METHOD = "UnknownMethod";
16+
public static final String INVALID_PARAMS = "InvalidParams";
17+
public static final String MALFORMED_REQUEST = "MalformedRequest";
18+
public static final String BINDING_ERROR = "BindingError";
19+
public static final String RESOURCE_CLOSED = "ResourceClosed";
20+
public static final String KERNEL_ERROR = "KernelError";
21+
22+
private ErrorType() {
23+
24+
}
25+
}

0 commit comments

Comments
 (0)