v0.0.45 - Display qrexec service name (qubes.NsignerRpc) in signer connection info; use human-readable timestamps in activity log; fix static release build by adding miner.c to Dockerfile.alpine-musl
This commit is contained in:
@@ -83,6 +83,7 @@ RUN ARCH="$(uname -m)"; \
|
|||||||
/build/src/key_store.c \
|
/build/src/key_store.c \
|
||||||
/build/src/socket_name.c \
|
/build/src/socket_name.c \
|
||||||
/build/src/auth_envelope.c \
|
/build/src/auth_envelope.c \
|
||||||
|
/build/src/miner.c \
|
||||||
/build/resources/tui_continuous/tui_continuous.c \
|
/build/resources/tui_continuous/tui_continuous.c \
|
||||||
"$NOSTR_LIB" \
|
"$NOSTR_LIB" \
|
||||||
-o /build/nsigner_static \
|
-o /build/nsigner_static \
|
||||||
|
|||||||
21
Makefile
21
Makefile
@@ -1,5 +1,5 @@
|
|||||||
CC := gcc
|
CC := gcc
|
||||||
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections -DNOSTR_ENABLE_NSIGNER_CLIENT=1 -Isrc -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson -Iresources/tui_continuous
|
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections -DNOSTR_ENABLE_NSIGNER_CLIENT=1 -D_GNU_SOURCE -Isrc -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson -Iresources/tui_continuous
|
||||||
LDFLAGS := -Wl,--gc-sections resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
LDFLAGS := -Wl,--gc-sections resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||||
|
|
||||||
SRC_DIR := src
|
SRC_DIR := src
|
||||||
@@ -24,6 +24,7 @@ SOURCES := \
|
|||||||
$(SRC_DIR)/key_store.c \
|
$(SRC_DIR)/key_store.c \
|
||||||
$(SRC_DIR)/socket_name.c \
|
$(SRC_DIR)/socket_name.c \
|
||||||
$(SRC_DIR)/auth_envelope.c \
|
$(SRC_DIR)/auth_envelope.c \
|
||||||
|
$(SRC_DIR)/miner.c \
|
||||||
resources/tui_continuous/tui_continuous.c
|
resources/tui_continuous/tui_continuous.c
|
||||||
|
|
||||||
HEADERS :=
|
HEADERS :=
|
||||||
@@ -40,18 +41,19 @@ TEST_SOCKET_NAME_TARGET := $(BUILD_DIR)/test_socket_name
|
|||||||
TEST_AUTH_ENVELOPE_TARGET := $(BUILD_DIR)/test_auth_envelope
|
TEST_AUTH_ENVELOPE_TARGET := $(BUILD_DIR)/test_auth_envelope
|
||||||
TEST_QREXEC_AUTH_TARGET := $(BUILD_DIR)/test_qrexec_auth
|
TEST_QREXEC_AUTH_TARGET := $(BUILD_DIR)/test_qrexec_auth
|
||||||
TEST_MNEMONIC_INPUT_TARGET := $(BUILD_DIR)/test_mnemonic_input
|
TEST_MNEMONIC_INPUT_TARGET := $(BUILD_DIR)/test_mnemonic_input
|
||||||
|
TEST_MINE_EVENT_TARGET := $(BUILD_DIR)/test_mine_event
|
||||||
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
|
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
|
||||||
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
|
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
|
||||||
EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp
|
EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp
|
||||||
EXAMPLE_GET_PUBKEY_QREXEC_TARGET := $(BUILD_DIR)/example_get_pubkey_qrexec
|
EXAMPLE_GET_PUBKEY_QREXEC_TARGET := $(BUILD_DIR)/example_get_pubkey_qrexec
|
||||||
DEMO_C99_TARGET := $(BUILD_DIR)/demo_c99
|
DEMO_C99_TARGET := $(BUILD_DIR)/demo_c99
|
||||||
|
|
||||||
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth examples test-client clean
|
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-mine-event examples test-client clean
|
||||||
|
|
||||||
all: dev
|
all: dev
|
||||||
|
|
||||||
lib:
|
lib:
|
||||||
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,19,44
|
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,13,19,44
|
||||||
|
|
||||||
dev: lib $(TARGET_DEV)
|
dev: lib $(TARGET_DEV)
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ static-arm64:
|
|||||||
firmware-feather:
|
firmware-feather:
|
||||||
cd firmware/feather_s3_tft && idf.py build
|
cd firmware/feather_s3_tft && idf.py build
|
||||||
|
|
||||||
test: lib test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-client
|
test: lib test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-mine-event test-client
|
||||||
|
|
||||||
test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
|
test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
|
||||||
./$(TEST_INTEGRATION_TARGET)
|
./$(TEST_INTEGRATION_TARGET)
|
||||||
@@ -109,6 +111,9 @@ test-auth-envelope: $(TEST_AUTH_ENVELOPE_TARGET)
|
|||||||
test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
|
test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
|
||||||
./$(TEST_QREXEC_AUTH_TARGET)
|
./$(TEST_QREXEC_AUTH_TARGET)
|
||||||
|
|
||||||
|
test-mine-event: $(TEST_MINE_EVENT_TARGET) $(TARGET_DEV)
|
||||||
|
./$(TEST_MINE_EVENT_TARGET)
|
||||||
|
|
||||||
test-client: examples
|
test-client: examples
|
||||||
|
|
||||||
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(DEMO_C99_TARGET)
|
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(DEMO_C99_TARGET)
|
||||||
@@ -133,9 +138,9 @@ $(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcemen
|
|||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
|
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
|
||||||
|
|
||||||
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
|
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
|
||||||
|
|
||||||
$(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
|
$(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
@@ -157,6 +162,10 @@ $(TEST_QREXEC_AUTH_TARGET): $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envel
|
|||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c -o $(TEST_QREXEC_AUTH_TARGET) $(LDFLAGS)
|
$(CC) $(CFLAGS) $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c -o $(TEST_QREXEC_AUTH_TARGET) $(LDFLAGS)
|
||||||
|
|
||||||
|
$(TEST_MINE_EVENT_TARGET): $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
$(CC) $(CFLAGS) $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MINE_EVENT_TARGET) $(LDFLAGS)
|
||||||
|
|
||||||
$(EXAMPLE_GET_PUBLIC_KEY_TARGET): $(EXAMPLES_DIR)/get_public_key_client.c
|
$(EXAMPLE_GET_PUBLIC_KEY_TARGET): $(EXAMPLES_DIR)/get_public_key_client.c
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_public_key_client.c -o $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(LDFLAGS)
|
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_public_key_client.c -o $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(LDFLAGS)
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -178,6 +178,7 @@ Request shape is JSON-RPC with NIP-46-style methods and optional trailing select
|
|||||||
{ "id": "2", "method": "sign_event", "params": ["<event_json>", { "role": "main" }] }
|
{ "id": "2", "method": "sign_event", "params": ["<event_json>", { "role": "main" }] }
|
||||||
{ "id": "3", "method": "sign_event", "params": ["<event_json>", { "nostr_index": 7 }] }
|
{ "id": "3", "method": "sign_event", "params": ["<event_json>", { "nostr_index": 7 }] }
|
||||||
{ "id": "4", "method": "sign_event", "params": ["<event_json>", { "role_path": "m/84'/0'/0'/0/5", "purpose": "bitcoin", "curve": "secp256k1" }] }
|
{ "id": "4", "method": "sign_event", "params": ["<event_json>", { "role_path": "m/84'/0'/0'/0/5", "purpose": "bitcoin", "curve": "secp256k1" }] }
|
||||||
|
{ "id": "5", "method": "mine_event", "params": ["<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 }] }
|
||||||
```
|
```
|
||||||
|
|
||||||
Implemented signer verbs in this build:
|
Implemented signer verbs in this build:
|
||||||
@@ -186,6 +187,42 @@ Implemented signer verbs in this build:
|
|||||||
- `sign_event`
|
- `sign_event`
|
||||||
- `nip04_encrypt` / `nip04_decrypt`
|
- `nip04_encrypt` / `nip04_decrypt`
|
||||||
- `nip44_encrypt` / `nip44_decrypt`
|
- `nip44_encrypt` / `nip44_decrypt`
|
||||||
|
- `mine_event` — add NIP-13 proof-of-work and sign (see below)
|
||||||
|
|
||||||
|
### `mine_event` — NIP-13 Proof-of-Work
|
||||||
|
|
||||||
|
Mines proof-of-work (adds a `nonce` tag per NIP-13) and signs the event in one step. The mining runs in a detached thread so the server stays responsive.
|
||||||
|
|
||||||
|
**Parameters (in options object):**
|
||||||
|
|
||||||
|
| Option | Required | Default | Description |
|
||||||
|
|--------|----------|---------|-------------|
|
||||||
|
| `difficulty` | One of difficulty/timeout | 0 (no target) | Target leading zero bits. Stops early if reached. |
|
||||||
|
| `timeout_sec` | One of difficulty/timeout | 600 (safety) | Time budget in seconds. Always returns best result found. |
|
||||||
|
| `threads` | No | 1 | Number of mining threads (max 32). |
|
||||||
|
|
||||||
|
At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when either condition is met. The response always includes the best event found — timeout is not an error.
|
||||||
|
|
||||||
|
**Response format:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"result": {
|
||||||
|
"event": "<signed event JSON with nonce tag>",
|
||||||
|
"achieved_difficulty": 18,
|
||||||
|
"target_difficulty": 20,
|
||||||
|
"target_reached": false,
|
||||||
|
"elapsed_sec": 30,
|
||||||
|
"attempts": 4523456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error codes:**
|
||||||
|
|
||||||
|
- `1007` — `no_termination_condition` (neither difficulty nor timeout_sec specified)
|
||||||
|
- `1008` — `mining_failed` (internal error)
|
||||||
|
|
||||||
Selector resolution order:
|
Selector resolution order:
|
||||||
|
|
||||||
@@ -215,6 +252,7 @@ Selector resolution chooses *which* role. Enforcement decides *whether the reque
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
- `sign_event` requires `purpose="nostr"` and `curve="secp256k1"`.
|
- `sign_event` requires `purpose="nostr"` and `curve="secp256k1"`.
|
||||||
|
- `mine_event` requires `purpose="nostr"` and `curve="secp256k1"` (same as `sign_event`).
|
||||||
- If caller selects a Bitcoin-role key for `sign_event`, request fails with `purpose_mismatch`.
|
- If caller selects a Bitcoin-role key for `sign_event`, request fails with `purpose_mismatch`.
|
||||||
|
|
||||||
This prevents cross-protocol misuse inside one mnemonic-rooted signer process.
|
This prevents cross-protocol misuse inside one mnemonic-rooted signer process.
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
* 2. sign_event — sign a Nostr event (kind 1 text note)
|
* 2. sign_event — sign a Nostr event (kind 1 text note)
|
||||||
* 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
* 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back)
|
||||||
*
|
*
|
||||||
|
* Note: mine_event (NIP-13 PoW) is also available via the JSON-RPC interface.
|
||||||
|
* See demo_javascript.js and demo_python.py for mine_event usage examples.
|
||||||
|
* The high-level nostr_signer API does not yet wrap mine_event.
|
||||||
|
*
|
||||||
* This uses the high-level nostr_signer API from nostr_core_lib:
|
* This uses the high-level nostr_signer API from nostr_core_lib:
|
||||||
* - nostr_signer_nsigner_qrexec() — qrexec transport (no network)
|
* - nostr_signer_nsigner_qrexec() — qrexec transport (no network)
|
||||||
* - nostr_signer_nsigner_set_nostr_index() — select key by NIP-06 index
|
* - nostr_signer_nsigner_set_nostr_index() — select key by NIP-06 index
|
||||||
|
|||||||
@@ -198,6 +198,48 @@ async function demoNip44(targetQube, nostrIndex, pubkeyHex) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function demoMineEvent(targetQube, nostrIndex) {
|
||||||
|
console.log("\n--- Demo 4: mine_event (NIP-13 Proof-of-Work) ---");
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
kind: 1,
|
||||||
|
content: "Hello Nostr with PoW!",
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(" Mining with difficulty=4, threads=4, timeout_sec=30...");
|
||||||
|
const response = await callNsigner(targetQube, {
|
||||||
|
id: "5",
|
||||||
|
method: "mine_event",
|
||||||
|
params: [JSON.stringify(event), {
|
||||||
|
difficulty: 4,
|
||||||
|
threads: 4,
|
||||||
|
timeout_sec: 30,
|
||||||
|
nostr_index: nostrIndex,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`mine_event failed: ${JSON.stringify(response.error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = JSON.parse(response.result);
|
||||||
|
console.log(` achieved_difficulty: ${result.achieved_difficulty}`);
|
||||||
|
console.log(` target_reached: ${result.target_reached}`);
|
||||||
|
console.log(` elapsed_sec: ${result.elapsed_sec}`);
|
||||||
|
console.log(` attempts: ${result.attempts}`);
|
||||||
|
|
||||||
|
const minedEvent = JSON.parse(result.event);
|
||||||
|
console.log(` event id: ${minedEvent.id}`);
|
||||||
|
console.log(` nonce tag: ${JSON.stringify(minedEvent.tags[0])}`);
|
||||||
|
|
||||||
|
if (result.target_reached) {
|
||||||
|
console.log(" ✓ Target difficulty reached!");
|
||||||
|
} else {
|
||||||
|
console.log(` (Target not reached, best effort: ${result.achieved_difficulty} bits)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const targetQube = process.argv[2] || "nostr_signer";
|
const targetQube = process.argv[2] || "nostr_signer";
|
||||||
const nostrIndex = parseInt(process.argv[3] || "0", 10);
|
const nostrIndex = parseInt(process.argv[3] || "0", 10);
|
||||||
@@ -212,6 +254,7 @@ async function main() {
|
|||||||
const pubkeyHex = await demoGetPublicKey(targetQube, nostrIndex);
|
const pubkeyHex = await demoGetPublicKey(targetQube, nostrIndex);
|
||||||
await demoSignEvent(targetQube, nostrIndex, pubkeyHex);
|
await demoSignEvent(targetQube, nostrIndex, pubkeyHex);
|
||||||
await demoNip44(targetQube, nostrIndex, pubkeyHex);
|
await demoNip44(targetQube, nostrIndex, pubkeyHex);
|
||||||
|
await demoMineEvent(targetQube, nostrIndex);
|
||||||
|
|
||||||
console.log("\n=== Summary ===");
|
console.log("\n=== Summary ===");
|
||||||
console.log("All demos completed successfully.");
|
console.log("All demos completed successfully.");
|
||||||
|
|||||||
@@ -227,6 +227,45 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex):
|
|||||||
raise RuntimeError("Round-trip FAILED: plaintext does not match decrypted")
|
raise RuntimeError("Round-trip FAILED: plaintext does not match decrypted")
|
||||||
|
|
||||||
|
|
||||||
|
def demo_mine_event(target_qube, nostr_index):
|
||||||
|
print("\n--- Demo 4: mine_event (NIP-13 Proof-of-Work) ---")
|
||||||
|
|
||||||
|
event = {"kind": 1, "content": "Hello Nostr with PoW!", "tags": []}
|
||||||
|
|
||||||
|
print(" Mining with difficulty=4, threads=4, timeout_sec=30...")
|
||||||
|
response = call_nsigner(
|
||||||
|
target_qube,
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"method": "mine_event",
|
||||||
|
"params": [json.dumps(event), {
|
||||||
|
"difficulty": 4,
|
||||||
|
"threads": 4,
|
||||||
|
"timeout_sec": 30,
|
||||||
|
"nostr_index": nostr_index,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if "error" in response:
|
||||||
|
raise RuntimeError(f"mine_event failed: {json.dumps(response['error'])}")
|
||||||
|
|
||||||
|
result = json.loads(response["result"])
|
||||||
|
print(f" achieved_difficulty: {result['achieved_difficulty']}")
|
||||||
|
print(f" target_reached: {result['target_reached']}")
|
||||||
|
print(f" elapsed_sec: {result['elapsed_sec']}")
|
||||||
|
print(f" attempts: {result['attempts']}")
|
||||||
|
|
||||||
|
mined_event = json.loads(result["event"])
|
||||||
|
print(f" event id: {mined_event['id']}")
|
||||||
|
print(f" nonce tag: {mined_event['tags'][0]}")
|
||||||
|
|
||||||
|
if result["target_reached"]:
|
||||||
|
print(" ✓ Target difficulty reached!")
|
||||||
|
else:
|
||||||
|
print(f" (Target not reached, best effort: {result['achieved_difficulty']} bits)")
|
||||||
|
|
||||||
|
|
||||||
# --- Main ---
|
# --- Main ---
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -243,6 +282,7 @@ def main():
|
|||||||
pubkey_hex = demo_get_public_key(target_qube, nostr_index)
|
pubkey_hex = demo_get_public_key(target_qube, nostr_index)
|
||||||
demo_sign_event(target_qube, nostr_index, pubkey_hex)
|
demo_sign_event(target_qube, nostr_index, pubkey_hex)
|
||||||
demo_nip44(target_qube, nostr_index, pubkey_hex)
|
demo_nip44(target_qube, nostr_index, pubkey_hex)
|
||||||
|
demo_mine_event(target_qube, nostr_index)
|
||||||
|
|
||||||
print("\n=== Summary ===")
|
print("\n=== Summary ===")
|
||||||
print("All demos completed successfully.")
|
print("All demos completed successfully.")
|
||||||
|
|||||||
430
plans/mine_event_pow.md
Normal file
430
plans/mine_event_pow.md
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
# Plan: Add `mine_event` Verb (NIP-13 Proof-of-Work) to n_signer
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**Recommendation: Do NOT import event_miner as a subrepo or copy its code.**
|
||||||
|
|
||||||
|
n_signer already has [`nip013.h`](resources/nostr_core_lib/nostr_core/nip013.h:1) and [`nip013.c`](resources/nostr_core_lib/nostr_core/nip013.c:1) in its `resources/nostr_core_lib/`. event_miner is a standalone CLI tool (~716 lines) whose useful logic is ~50 lines. The rest is CLI parsing, stdin/file I/O, signal handlers, and `exit()` calls that are inappropriate for a long-running server.
|
||||||
|
|
||||||
|
Instead, we add a new `mine_event` verb to n_signer's existing dispatcher that:
|
||||||
|
1. Mines PoW for a time budget (e.g., "mine for 30 seconds with 4 threads")
|
||||||
|
2. Returns the **best result found** within that time — regardless of whether the target difficulty was reached
|
||||||
|
3. Signs the event with the role's derived private key
|
||||||
|
4. Returns the mined + signed event JSON along with metadata about the achieved difficulty
|
||||||
|
|
||||||
|
## Why Not Subrepo or Copy?
|
||||||
|
|
||||||
|
| Factor | Subrepo | Copy | New verb (recommended) |
|
||||||
|
|--------|---------|------|----------------------|
|
||||||
|
| Key management | event_miner takes raw nsec CLI arg | same | Uses n_signer's role table + key_store (secure, mlock'd) |
|
||||||
|
| Architecture | CLI tool, calls exit() | same | Fits JSON-RPC dispatcher/enforcement/policy model |
|
||||||
|
| Threading | Global vars, signal handlers, exit() | same | Clean detached thread per request, no globals |
|
||||||
|
| Best-effort model | No — stops at target or max_attempts | same | Yes — returns best result within time budget |
|
||||||
|
| Code reuse | ~50 lines useful | same | Rewrite ~150 lines for server-safe best-effort model |
|
||||||
|
| Maintenance | Extra submodule to track | Drift risk | Single codebase, single nostr_core_lib |
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
- **Verb name**: `mine_event`
|
||||||
|
- **Params**: `[event_json, {difficulty: N, threads: N, timeout_sec: N, ...role_selector}]`
|
||||||
|
- **Dual termination model**: Both `timeout_sec` and `difficulty` are independent options. Either, both, or at least one must be specified:
|
||||||
|
- **Both set**: Mine until target reached OR timeout — return best result (with `target_reached` flag)
|
||||||
|
- **Only `timeout_sec`**: Mine for the full duration, return best result found
|
||||||
|
- **Only `difficulty`**: Mine until target reached, with a safety max timeout (e.g., 10 minutes) to prevent infinite mining
|
||||||
|
- **Neither**: Error — must specify at least one termination condition
|
||||||
|
- **Best-effort return**: The response always includes the best event found, plus metadata (`achieved_difficulty`, `target_difficulty`, `target_reached`, `elapsed_sec`, `attempts`)
|
||||||
|
- **Threading**: Spawn a detached pthread for mining. Server stays responsive. Mining thread writes result to client socket when done.
|
||||||
|
- **Key source**: Uses the role's derived private key from `key_store` (same as `sign_event`)
|
||||||
|
|
||||||
|
## Why Our Own Mining Loop (Not `nostr_add_proof_of_work()`)
|
||||||
|
|
||||||
|
The existing [`nostr_add_proof_of_work()`](resources/nostr_core_lib/nostr_core/nip013.c:121) has two problems for the best-effort model:
|
||||||
|
|
||||||
|
1. **Discards work on failure**: It returns `NOSTR_ERROR_CRYPTO_FAILED` if `max_attempts` is exceeded without reaching the target. The best nonce found is lost.
|
||||||
|
2. **No time-based termination**: It uses `max_attempts` (a count), not a time deadline. For a "mine for 30 seconds" model, we need time-based termination.
|
||||||
|
|
||||||
|
Instead, `miner.c` implements its own mining loop that:
|
||||||
|
- Iterates nonces across multiple threads
|
||||||
|
- Uses [`nostr_create_and_sign_event()`](resources/nostr_core_lib/nostr_core/nip001.h:1) from nip001 to sign each attempt
|
||||||
|
- Uses [`nostr_calculate_pow_difficulty()`](resources/nostr_core_lib/nostr_core/nip013.h:42) from nip013 to check difficulty
|
||||||
|
- Tracks the best event (highest difficulty) across all threads
|
||||||
|
- Stops when target reached OR timeout
|
||||||
|
- Returns the best event with metadata
|
||||||
|
|
||||||
|
This reuses the library's crypto/JSON primitives without modifying the shared library.
|
||||||
|
|
||||||
|
## Request/Response Format
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "req-1",
|
||||||
|
"method": "mine_event",
|
||||||
|
"params": [
|
||||||
|
"{\"kind\":1,\"content\":\"Hello Nostr!\",\"tags\":[],\"created_at\":1723666800}",
|
||||||
|
{
|
||||||
|
"difficulty": 20,
|
||||||
|
"threads": 4,
|
||||||
|
"timeout_sec": 30,
|
||||||
|
"role": "main"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `difficulty` (optional): Target leading zero bits. If reached, mining stops early. If not specified, mining runs until timeout.
|
||||||
|
- `threads` (optional, default: 1): Number of mining threads.
|
||||||
|
- `timeout_sec` (optional): How long to mine in seconds. If not specified, mining runs until target difficulty is reached (with a safety max of 10 minutes).
|
||||||
|
- **At least one of `difficulty` or `timeout_sec` must be specified.** If neither is provided, returns error `1007` (`mining_failed` / `no_termination_condition`).
|
||||||
|
- **If only `difficulty` is specified**: A safety max timeout of 600 seconds (10 min) is applied to prevent infinite mining.
|
||||||
|
- **If only `timeout_sec` is specified**: Mining runs the full duration and returns the best result found.
|
||||||
|
- **If both are specified**: Mining stops when either condition is met (target reached OR timeout elapsed).
|
||||||
|
|
||||||
|
### Success Response (best-effort, always returned if mining ran)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "req-1",
|
||||||
|
"result": {
|
||||||
|
"event": "{\"kind\":1,\"content\":\"Hello Nostr!\",\"pubkey\":\"...\",\"id\":\"00000...\",\"sig\":\"...\",\"tags\":[[\"nonce\",\"12345\",\"20\"]],\"created_at\":1755197090}",
|
||||||
|
"achieved_difficulty": 18,
|
||||||
|
"target_difficulty": 20,
|
||||||
|
"target_reached": false,
|
||||||
|
"elapsed_sec": 30,
|
||||||
|
"attempts": 4523456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `result` is now a JSON **object** (not a string like other verbs) containing the signed event plus mining metadata. The `event` field within it is the signed event JSON string. This is a departure from the other verbs that return a plain string result, but the metadata is essential for the client to know whether the target was met.
|
||||||
|
|
||||||
|
### Error Responses
|
||||||
|
- `1007` - `mining_failed` — internal error (invalid event, bad params, crypto failure)
|
||||||
|
- `1008` - `mining_busy` — another mining operation is already running (optional: reject or queue)
|
||||||
|
- Existing error codes (1001-1006, -326xx) apply as usual for policy/selector/enforcement errors
|
||||||
|
|
||||||
|
Note: Timeout is NOT an error — it's the normal termination condition. The best result is always returned.
|
||||||
|
|
||||||
|
## Architecture Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Client -->|JSON-RPC mine_event| ServerHandleOne
|
||||||
|
ServerHandleOne -->|policy_check and enforce_verb_role| PolicyEnforcement
|
||||||
|
PolicyEnforcement -->|ALLOWED| SpawnDetachedThread
|
||||||
|
ServerHandleOne -->|returns immediately| ServerLoop
|
||||||
|
SpawnDetachedThread --> MinerCoordinator
|
||||||
|
MinerCoordinator -->|spawn N worker threads| Worker1
|
||||||
|
MinerCoordinator -->|spawn N worker threads| Worker2
|
||||||
|
MinerCoordinator -->|spawn N worker threads| WorkerN
|
||||||
|
Worker1 -->|nostr_create_and_sign_event| NIP001Lib
|
||||||
|
Worker2 -->|nostr_create_and_sign_event| NIP001Lib
|
||||||
|
WorkerN -->|nostr_create_and_sign_event| NIP001Lib
|
||||||
|
NIP001Lib -->|signed event with nonce| CalcDifficulty
|
||||||
|
CalcDifficulty -->|nostr_calculate_pow_difficulty| NIP013Lib
|
||||||
|
NIP013Lib -->|difficulty bits| TrackBest
|
||||||
|
TrackBest -->|mutex protected| BestEvent
|
||||||
|
MinerCoordinator -->|timeout or target reached| JoinThreads
|
||||||
|
JoinThreads -->|best event + metadata| BuildResponse
|
||||||
|
BuildResponse -->|transport_send_framed| Client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Step 1: Add nip013 to nostr_core_lib build
|
||||||
|
|
||||||
|
The Makefile's `lib` target runs:
|
||||||
|
```
|
||||||
|
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,19,44
|
||||||
|
```
|
||||||
|
Add `13` to the `--nips` flag so nip013.c is compiled into the static library.
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`Makefile`](Makefile:54) — change `--nips=1,4,6,19,44` to `--nips=1,4,6,13,19,44`
|
||||||
|
|
||||||
|
### Step 2: Add VERB_MINE_EVENT to enforcement.c
|
||||||
|
|
||||||
|
Add `mine_event` to the known nostr verbs so it passes enforcement.
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`src/enforcement.c`](src/enforcement.c:200) — add `#define VERB_MINE_EVENT "mine_event"` and add it to [`is_nostr_verb()`](src/enforcement.c:453)
|
||||||
|
- [`src/dispatcher.c`](src/dispatcher.c:200) — add the same `#define VERB_MINE_EVENT "mine_event"` (the headerless decls pattern means defines are repeated per-file)
|
||||||
|
|
||||||
|
### Step 3: Create src/miner.c — Multithreaded Best-Effort Mining Coordinator
|
||||||
|
|
||||||
|
This is the core new file. It implements a server-safe, best-effort mining loop.
|
||||||
|
|
||||||
|
**Key design:**
|
||||||
|
- No global variables (all state in context structs)
|
||||||
|
- No `exit()` calls (return result codes)
|
||||||
|
- No signal handlers (server handles signals)
|
||||||
|
- Time-based termination (not attempt-count-based)
|
||||||
|
- Tracks best event across all threads
|
||||||
|
- Returns best event + metadata regardless of whether target was reached
|
||||||
|
|
||||||
|
**Key structures:**
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
cJSON *best_event; /* best event found so far (mutex-protected) */
|
||||||
|
int best_difficulty; /* difficulty of best_event */
|
||||||
|
uint64_t total_attempts; /* total attempts across all threads */
|
||||||
|
int target_difficulty; /* 0 = no target, mine full timeout */
|
||||||
|
int target_reached; /* 1 if target was reached */
|
||||||
|
pthread_mutex_t mutex; /* protects best_event, best_difficulty, total_attempts */
|
||||||
|
volatile int stop; /* set by coordinator when target reached or timeout */
|
||||||
|
} mine_shared_state_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
mine_shared_state_t *shared;
|
||||||
|
cJSON *event_template; /* this thread's copy of the event */
|
||||||
|
unsigned char private_key[32];
|
||||||
|
int thread_id;
|
||||||
|
uint64_t nonce_start; /* starting nonce for this thread (thread_id * stride) */
|
||||||
|
uint64_t nonce_stride; /* increment per iteration to avoid overlap */
|
||||||
|
time_t deadline; /* absolute time to stop */
|
||||||
|
uint64_t attempts; /* this thread's attempt count */
|
||||||
|
} miner_worker_ctx_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
cJSON *best_event; /* caller frees */
|
||||||
|
int achieved_difficulty;
|
||||||
|
int target_difficulty;
|
||||||
|
int target_reached;
|
||||||
|
int elapsed_sec;
|
||||||
|
uint64_t total_attempts;
|
||||||
|
} mine_result_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Main function:**
|
||||||
|
```c
|
||||||
|
/* Returns 0 on success (result populated with best event found), -1 on error */
|
||||||
|
int miner_run(cJSON *event, const unsigned char *private_key,
|
||||||
|
int target_difficulty, int thread_count, int timeout_sec,
|
||||||
|
mine_result_t *result);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mining loop (per thread):**
|
||||||
|
```c
|
||||||
|
while (!shared->stop && time(NULL) < deadline) {
|
||||||
|
/* Build event with current nonce */
|
||||||
|
cJSON *working_tags = cJSON_Duplicate(original_tags, 1);
|
||||||
|
update_nonce_tag(working_tags, nonce, target_difficulty);
|
||||||
|
|
||||||
|
cJSON *signed_event = nostr_create_and_sign_event(kind, content, working_tags,
|
||||||
|
private_key, timestamp);
|
||||||
|
cJSON_Delete(working_tags);
|
||||||
|
|
||||||
|
/* Check difficulty */
|
||||||
|
const char *id = cJSON_GetStringValue(cJSON_GetObjectItem(signed_event, "id"));
|
||||||
|
int difficulty = nostr_calculate_pow_difficulty(id);
|
||||||
|
|
||||||
|
/* Track best under mutex */
|
||||||
|
pthread_mutex_lock(&shared->mutex);
|
||||||
|
shared->total_attempts++;
|
||||||
|
if (difficulty > shared->best_difficulty) {
|
||||||
|
if (shared->best_event) cJSON_Delete(shared->best_event);
|
||||||
|
shared->best_event = cJSON_Duplicate(signed_event, 1);
|
||||||
|
shared->best_difficulty = difficulty;
|
||||||
|
}
|
||||||
|
if (target_difficulty > 0 && difficulty >= target_difficulty) {
|
||||||
|
shared->target_reached = 1;
|
||||||
|
shared->stop = 1;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&shared->mutex);
|
||||||
|
|
||||||
|
cJSON_Delete(signed_event);
|
||||||
|
nonce += nonce_stride;
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files to create:**
|
||||||
|
- `src/miner.c` — mining coordinator (~250 lines)
|
||||||
|
|
||||||
|
### Step 4: Add crypto_mine_event() to key_store.c
|
||||||
|
|
||||||
|
Add a function that:
|
||||||
|
1. Parses the event JSON
|
||||||
|
2. Gets the role's private key from key_store
|
||||||
|
3. Calls `miner_run()` from miner.c
|
||||||
|
4. Builds the response JSON object (event string + metadata)
|
||||||
|
5. Zeroizes the private key copy
|
||||||
|
6. Returns the response JSON string (caller frees)
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
```c
|
||||||
|
/* Returns newly-allocated JSON string with result object, or NULL on error.
|
||||||
|
* Caller frees. */
|
||||||
|
char *crypto_mine_event(const key_store_t *store, int role_index,
|
||||||
|
const char *event_json, int difficulty,
|
||||||
|
int threads, int timeout_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response building:**
|
||||||
|
```c
|
||||||
|
cJSON *result_obj = cJSON_CreateObject();
|
||||||
|
cJSON *event_item = cJSON_CreateString(signed_event_json);
|
||||||
|
cJSON_AddItemToObject(result_obj, "event", event_item);
|
||||||
|
cJSON_AddNumberToObject(result_obj, "achieved_difficulty", result.achieved_difficulty);
|
||||||
|
cJSON_AddNumberToObject(result_obj, "target_difficulty", result.target_difficulty);
|
||||||
|
cJSON_AddBoolToObject(result_obj, "target_reached", result.target_reached);
|
||||||
|
cJSON_AddNumberToObject(result_obj, "elapsed_sec", result.elapsed_sec);
|
||||||
|
cJSON_AddNumberToObject(result_obj, "attempts", (double)result.total_attempts);
|
||||||
|
char *out = cJSON_PrintUnformatted(result_obj);
|
||||||
|
cJSON_Delete(result_obj);
|
||||||
|
return out;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`src/key_store.c`](src/key_store.c:456) — add `#include <nostr_core/nip013.h>`, add `crypto_mine_event()` function, add forward declaration in the headerless decls section
|
||||||
|
|
||||||
|
### Step 5: Update Makefile
|
||||||
|
|
||||||
|
Add `miner.c` to SOURCES. pthread is already linked (`-lpthread` in LDFLAGS).
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`Makefile`](Makefile:13) — add `$(SRC_DIR)/miner.c \` to SOURCES list
|
||||||
|
|
||||||
|
### Step 6: Add dispatcher handler for mine_event
|
||||||
|
|
||||||
|
In [`dispatcher_handle_request()`](src/dispatcher.c:545), add a branch for `VERB_MINE_EVENT` that:
|
||||||
|
1. Extracts `event_json` from params[0]
|
||||||
|
2. Extracts `difficulty`, `threads`, `timeout_sec` from the options object (params[last])
|
||||||
|
3. Validates: at least one of `difficulty` or `timeout_sec` must be specified (else error `1007`); `threads` defaults to 1 (max 32); if only `difficulty` is set, safety timeout of 600 sec is applied
|
||||||
|
4. Calls `crypto_mine_event()`
|
||||||
|
5. Returns the result
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`src/dispatcher.c`](src/dispatcher.c:675) — add `else if (strcmp(method, VERB_MINE_EVENT) == 0)` branch after the `VERB_SIGN_EVENT` branch
|
||||||
|
|
||||||
|
### Step 7: Modify server.c for async mining
|
||||||
|
|
||||||
|
The current [`server_handle_one()`](src/server.c:1453) is synchronous. For `mine_event`, we need to:
|
||||||
|
|
||||||
|
1. Detect `mine_event` method after policy check passes
|
||||||
|
2. Spawn a detached thread that:
|
||||||
|
- Calls `dispatcher_handle_request()` (which calls `crypto_mine_event()`)
|
||||||
|
- Sends the response via `transport_send_framed()` on the client_fd
|
||||||
|
- Closes the client_fd
|
||||||
|
3. Return immediately from `server_handle_one()` without closing the fd (the thread owns it now)
|
||||||
|
|
||||||
|
**Thread function:**
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
int client_fd;
|
||||||
|
dispatcher_ctx_t *dispatcher;
|
||||||
|
char *request;
|
||||||
|
} mine_thread_arg_t;
|
||||||
|
|
||||||
|
static void *mine_event_thread(void *arg) {
|
||||||
|
mine_thread_arg_t *a = (mine_thread_arg_t *)arg;
|
||||||
|
char *response = dispatcher_handle_request(a->dispatcher, a->request);
|
||||||
|
if (response != NULL) {
|
||||||
|
(void)transport_send_framed(a->client_fd, response);
|
||||||
|
free(response);
|
||||||
|
}
|
||||||
|
free(a->request);
|
||||||
|
close(a->client_fd);
|
||||||
|
free(a);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**In server_handle_one**, after `pchk == POLICY_ALLOW` and before calling `dispatcher_handle_request()`:
|
||||||
|
```c
|
||||||
|
if (pchk == POLICY_ALLOW && strcmp(method, "mine_event") == 0) {
|
||||||
|
pthread_t tid;
|
||||||
|
mine_thread_arg_t *arg = malloc(sizeof(*arg));
|
||||||
|
if (arg == NULL) {
|
||||||
|
/* fall through to error path */
|
||||||
|
} else {
|
||||||
|
arg->client_fd = client_fd;
|
||||||
|
arg->dispatcher = ctx->dispatcher;
|
||||||
|
arg->request = request; /* transfer ownership */
|
||||||
|
request = NULL; /* prevent free in cleanup */
|
||||||
|
if (pthread_create(&tid, NULL, mine_event_thread, arg) == 0) {
|
||||||
|
pthread_detach(tid);
|
||||||
|
/* Skip synchronous response path — thread owns fd and request */
|
||||||
|
/* Log activity and return without closing fd */
|
||||||
|
verdict = "ALLOWED";
|
||||||
|
source_label = "async-mine";
|
||||||
|
/* ... log activity ... */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
/* pthread_create failed — fall through to error path */
|
||||||
|
free(arg->request);
|
||||||
|
free(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`src/server.c`](src/server.c:1755) — add mine_event async path before the synchronous `dispatcher_handle_request()` call
|
||||||
|
|
||||||
|
### Step 8: Add client demos
|
||||||
|
|
||||||
|
Add `mine_event` examples to the existing demo files.
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`client/demo_javascript.js`](client/demo_javascript.js:1) — add mine_event example
|
||||||
|
- [`client/demo_python.py`](client/demo_python.py:1) — add mine_event example
|
||||||
|
- [`client/demo_c99.c`](client/demo_c99.c:1) — add mine_event example
|
||||||
|
|
||||||
|
### Step 9: Add tests
|
||||||
|
|
||||||
|
Create a test that:
|
||||||
|
1. Starts nsigner with a test mnemonic
|
||||||
|
2. Sends a `mine_event` request with low difficulty (e.g., 2) and short timeout (e.g., 5 sec)
|
||||||
|
3. Verifies the response contains a signed event with a nonce tag
|
||||||
|
4. Verifies the `achieved_difficulty` >= 2 and `target_reached` is true
|
||||||
|
5. Sends a `mine_event` request with high difficulty (e.g., 30) and short timeout (e.g., 3 sec)
|
||||||
|
6. Verifies the response contains a signed event, `target_reached` is false, and `achieved_difficulty` < 30
|
||||||
|
|
||||||
|
**Files to create:**
|
||||||
|
- `tests/test_mine_event.c`
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`Makefile`](Makefile:37) — add `TEST_MINE_EVENT_TARGET` and `test-mine-event` target
|
||||||
|
|
||||||
|
### Step 10: Update documentation
|
||||||
|
|
||||||
|
**Files to modify:**
|
||||||
|
- [`README.md`](README.md:1) — add `mine_event` to the verbs table with params description
|
||||||
|
- [`client/README.md`](client/README.md:1) — add mine_event usage example
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Private key never leaves key_store**: The mining thread receives a copy of the 32-byte private key, which is zeroized after use (following the pattern in [`crypto_sign_event()`](src/key_store.c:456))
|
||||||
|
2. **Policy enforcement applies**: `mine_event` goes through the same `policy_check()` and `enforce_verb_role()` as all other verbs
|
||||||
|
3. **Thread safety**:
|
||||||
|
- Each mining thread works on its own cJSON copies
|
||||||
|
- The shared state (best_event, best_difficulty, total_attempts) is protected by a mutex
|
||||||
|
- secp256k1 context is thread-safe for signing
|
||||||
|
4. **Resource limits**:
|
||||||
|
- Max threads capped at 32 to prevent resource exhaustion
|
||||||
|
- `timeout_sec` is required (must be > 0) to prevent infinite mining
|
||||||
|
- Optional: limit to 1 concurrent mining operation (reject with `mining_busy` if already running)
|
||||||
|
5. **Client fd ownership**: The detached thread owns the client_fd and is responsible for closing it. The main server loop must not close it.
|
||||||
|
|
||||||
|
## Concurrency Considerations
|
||||||
|
|
||||||
|
- Each mining thread uses a unique nonce stride (thread_id) to avoid duplicate work
|
||||||
|
- cJSON operations are NOT thread-safe across different cJSON objects, but each thread works on its own copies
|
||||||
|
- The result mutex protects only the shared best-event tracking
|
||||||
|
- `nostr_create_and_sign_event()` creates a new secp256k1 context per call (or uses a thread-local one) — need to verify this is thread-safe. If not, each thread may need its own context.
|
||||||
|
|
||||||
|
## File Summary
|
||||||
|
|
||||||
|
| File | Action | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `Makefile` | Modify | Add nip013 to --nips, add miner.c to SOURCES, add test target |
|
||||||
|
| `src/enforcement.c` | Modify | Add VERB_MINE_EVENT to is_nostr_verb() |
|
||||||
|
| `src/dispatcher.c` | Modify | Add VERB_MINE_EVENT define + handler branch |
|
||||||
|
| `src/key_store.c` | Modify | Add crypto_mine_event() function + nip013 include |
|
||||||
|
| `src/miner.c` | Create | Multithreaded best-effort mining coordinator |
|
||||||
|
| `src/server.c` | Modify | Add async mine_event path with detached thread |
|
||||||
|
| `tests/test_mine_event.c` | Create | Integration test for mine_event |
|
||||||
|
| `client/demo_javascript.js` | Modify | Add mine_event example |
|
||||||
|
| `client/demo_python.py` | Modify | Add mine_event example |
|
||||||
|
| `client/demo_c99.c` | Modify | Add mine_event example |
|
||||||
|
| `README.md` | Modify | Document mine_event verb |
|
||||||
|
| `client/README.md` | Modify | Add mine_event usage |
|
||||||
@@ -203,6 +203,7 @@ const char *selector_strerror(int err);
|
|||||||
#define VERB_NIP44_DECRYPT "nip44_decrypt"
|
#define VERB_NIP44_DECRYPT "nip44_decrypt"
|
||||||
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
|
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
|
||||||
#define VERB_NIP04_DECRYPT "nip04_decrypt"
|
#define VERB_NIP04_DECRYPT "nip04_decrypt"
|
||||||
|
#define VERB_MINE_EVENT "mine_event"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check whether `verb` is allowed to execute against `role`.
|
* Check whether `verb` is allowed to execute against `role`.
|
||||||
@@ -329,6 +330,13 @@ const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
|
|||||||
* Caller must free() the returned string. */
|
* Caller must free() the returned string. */
|
||||||
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
|
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
|
||||||
|
|
||||||
|
/* Mine a Nostr event with NIP-13 proof-of-work, then sign it.
|
||||||
|
* Returns a newly-allocated JSON string with result object, or NULL on error.
|
||||||
|
* Caller must free() the returned string. */
|
||||||
|
char *crypto_mine_event(const key_store_t *store, int role_index,
|
||||||
|
const char *event_json, int difficulty,
|
||||||
|
int threads, int timeout_sec);
|
||||||
|
|
||||||
/* NIP-44 encrypt/decrypt */
|
/* NIP-44 encrypt/decrypt */
|
||||||
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
|
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
|
||||||
const char *recipient_pubkey_hex, const char *plaintext);
|
const char *recipient_pubkey_hex, const char *plaintext);
|
||||||
@@ -687,6 +695,60 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
|
|||||||
return make_error_response(id_str, -32602, "invalid_params");
|
return make_error_response(id_str, -32602, "invalid_params");
|
||||||
}
|
}
|
||||||
result = owned_result;
|
result = owned_result;
|
||||||
|
} else if (strcmp(method, VERB_MINE_EVENT) == 0) {
|
||||||
|
cJSON *event_item = cJSON_GetArrayItem(params_item, 0);
|
||||||
|
cJSON *diff_item = NULL;
|
||||||
|
cJSON *threads_item = NULL;
|
||||||
|
cJSON *timeout_item = NULL;
|
||||||
|
int difficulty = 0;
|
||||||
|
int threads = 1;
|
||||||
|
int timeout_sec = 0;
|
||||||
|
int has_difficulty = 0;
|
||||||
|
int has_timeout = 0;
|
||||||
|
|
||||||
|
if (!cJSON_IsString(event_item) || event_item->valuestring == NULL) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return make_error_response(id_str, -32602, "invalid_params");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract mining options from the options object (last param) */
|
||||||
|
if (cJSON_IsObject(options_item)) {
|
||||||
|
diff_item = cJSON_GetObjectItemCaseSensitive(options_item, "difficulty");
|
||||||
|
if (cJSON_IsNumber(diff_item)) {
|
||||||
|
difficulty = diff_item->valueint;
|
||||||
|
has_difficulty = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
threads_item = cJSON_GetObjectItemCaseSensitive(options_item, "threads");
|
||||||
|
if (cJSON_IsNumber(threads_item)) {
|
||||||
|
threads = threads_item->valueint;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout_item = cJSON_GetObjectItemCaseSensitive(options_item, "timeout_sec");
|
||||||
|
if (cJSON_IsNumber(timeout_item)) {
|
||||||
|
timeout_sec = timeout_item->valueint;
|
||||||
|
has_timeout = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* At least one of difficulty or timeout_sec must be specified */
|
||||||
|
if (!has_difficulty && !has_timeout) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return make_error_response(id_str, 1007, "no_termination_condition");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clamp threads to 1..32 */
|
||||||
|
if (threads < 1) threads = 1;
|
||||||
|
if (threads > 32) threads = 32;
|
||||||
|
|
||||||
|
owned_result = crypto_mine_event(ctx->key_store, role_index,
|
||||||
|
event_item->valuestring,
|
||||||
|
difficulty, threads, timeout_sec);
|
||||||
|
if (owned_result == NULL) {
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return make_error_response(id_str, 1008, "mining_failed");
|
||||||
|
}
|
||||||
|
result = owned_result;
|
||||||
} else if (strcmp(method, VERB_NIP44_ENCRYPT) == 0) {
|
} else if (strcmp(method, VERB_NIP44_ENCRYPT) == 0) {
|
||||||
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
|
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
|
||||||
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
|
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ const char *selector_strerror(int err);
|
|||||||
#define VERB_NIP44_DECRYPT "nip44_decrypt"
|
#define VERB_NIP44_DECRYPT "nip44_decrypt"
|
||||||
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
|
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
|
||||||
#define VERB_NIP04_DECRYPT "nip04_decrypt"
|
#define VERB_NIP04_DECRYPT "nip04_decrypt"
|
||||||
|
#define VERB_MINE_EVENT "mine_event"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check whether `verb` is allowed to execute against `role`.
|
* Check whether `verb` is allowed to execute against `role`.
|
||||||
@@ -460,7 +461,8 @@ static int is_nostr_verb(const char *verb) {
|
|||||||
(strcmp(verb, VERB_NIP44_ENCRYPT) == 0) ||
|
(strcmp(verb, VERB_NIP44_ENCRYPT) == 0) ||
|
||||||
(strcmp(verb, VERB_NIP44_DECRYPT) == 0) ||
|
(strcmp(verb, VERB_NIP44_DECRYPT) == 0) ||
|
||||||
(strcmp(verb, VERB_NIP04_ENCRYPT) == 0) ||
|
(strcmp(verb, VERB_NIP04_ENCRYPT) == 0) ||
|
||||||
(strcmp(verb, VERB_NIP04_DECRYPT) == 0);
|
(strcmp(verb, VERB_NIP04_DECRYPT) == 0) ||
|
||||||
|
(strcmp(verb, VERB_MINE_EVENT) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
int enforce_verb_role(const char *verb, const role_entry_t *role) {
|
int enforce_verb_role(const char *verb, const role_entry_t *role) {
|
||||||
|
|||||||
@@ -334,6 +334,19 @@ const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
|
|||||||
* Caller must free() the returned string. */
|
* Caller must free() the returned string. */
|
||||||
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
|
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
|
||||||
|
|
||||||
|
/* Mine a Nostr event with NIP-13 proof-of-work, then sign it.
|
||||||
|
* event_json is the unsigned event JSON string.
|
||||||
|
* difficulty: target leading zero bits (0 = no target, mine for full timeout)
|
||||||
|
* threads: number of mining threads (1..32)
|
||||||
|
* timeout_sec: time budget in seconds (0 = use 600s safety default)
|
||||||
|
* Returns a newly-allocated JSON string containing a result object:
|
||||||
|
* {"event":"<signed event json>","achieved_difficulty":N,"target_difficulty":N,
|
||||||
|
* "target_reached":bool,"elapsed_sec":N,"attempts":N}
|
||||||
|
* Returns NULL on error. Caller must free() the returned string. */
|
||||||
|
char *crypto_mine_event(const key_store_t *store, int role_index,
|
||||||
|
const char *event_json, int difficulty,
|
||||||
|
int threads, int timeout_sec);
|
||||||
|
|
||||||
/* Zeroize all derived keys in the store. */
|
/* Zeroize all derived keys in the store. */
|
||||||
void crypto_wipe(key_store_t *store);
|
void crypto_wipe(key_store_t *store);
|
||||||
|
|
||||||
@@ -457,6 +470,7 @@ int socket_name_random(char *out, size_t out_len);
|
|||||||
#include <nostr_core/nip001.h>
|
#include <nostr_core/nip001.h>
|
||||||
#include <nostr_core/nip004.h>
|
#include <nostr_core/nip004.h>
|
||||||
#include <nostr_core/nip006.h>
|
#include <nostr_core/nip006.h>
|
||||||
|
#include <nostr_core/nip013.h>
|
||||||
#include <nostr_core/nip019.h>
|
#include <nostr_core/nip019.h>
|
||||||
#include <nostr_core/nip044.h>
|
#include <nostr_core/nip044.h>
|
||||||
#include <nostr_core/utils.h>
|
#include <nostr_core/utils.h>
|
||||||
@@ -690,6 +704,88 @@ char *crypto_sign_event(const key_store_t *store, int role_index, const char *ev
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- mine_result_t and miner_run are defined in miner.c ---- */
|
||||||
|
/* Forward declaration of the mining coordinator */
|
||||||
|
typedef struct {
|
||||||
|
cJSON *best_event;
|
||||||
|
int achieved_difficulty;
|
||||||
|
int target_difficulty;
|
||||||
|
int target_reached;
|
||||||
|
int elapsed_sec;
|
||||||
|
uint64_t total_attempts;
|
||||||
|
} mine_result_t;
|
||||||
|
|
||||||
|
int miner_run(cJSON *event, const unsigned char *private_key,
|
||||||
|
int target_difficulty, int thread_count, int timeout_sec,
|
||||||
|
mine_result_t *result);
|
||||||
|
|
||||||
|
char *crypto_mine_event(const key_store_t *store, int role_index,
|
||||||
|
const char *event_json, int difficulty,
|
||||||
|
int threads, int timeout_sec) {
|
||||||
|
const unsigned char *private_key;
|
||||||
|
cJSON *parsed = NULL;
|
||||||
|
mine_result_t result;
|
||||||
|
char *event_str = NULL;
|
||||||
|
cJSON *result_obj = NULL;
|
||||||
|
char *out = NULL;
|
||||||
|
|
||||||
|
if (event_json == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private_key = crypto_get_private_key(store, role_index);
|
||||||
|
if (private_key == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = cJSON_Parse(event_json);
|
||||||
|
if (parsed == NULL || !cJSON_IsObject(parsed)) {
|
||||||
|
cJSON_Delete(parsed);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run the mining coordinator */
|
||||||
|
memset(&result, 0, sizeof(result));
|
||||||
|
if (miner_run(parsed, private_key, difficulty, threads, timeout_sec, &result) != 0) {
|
||||||
|
cJSON_Delete(parsed);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.best_event == NULL) {
|
||||||
|
/* No result found (shouldn't happen unless threads failed immediately) */
|
||||||
|
cJSON_Delete(parsed);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build the result JSON object */
|
||||||
|
event_str = cJSON_PrintUnformatted(result.best_event);
|
||||||
|
cJSON_Delete(result.best_event);
|
||||||
|
cJSON_Delete(parsed);
|
||||||
|
|
||||||
|
if (event_str == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
result_obj = cJSON_CreateObject();
|
||||||
|
if (result_obj == NULL) {
|
||||||
|
free(event_str);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToObject(result_obj, "event", cJSON_CreateString(event_str));
|
||||||
|
free(event_str);
|
||||||
|
|
||||||
|
cJSON_AddNumberToObject(result_obj, "achieved_difficulty", result.achieved_difficulty);
|
||||||
|
cJSON_AddNumberToObject(result_obj, "target_difficulty", result.target_difficulty);
|
||||||
|
cJSON_AddBoolToObject(result_obj, "target_reached", result.target_reached);
|
||||||
|
cJSON_AddNumberToObject(result_obj, "elapsed_sec", result.elapsed_sec);
|
||||||
|
cJSON_AddNumberToObject(result_obj, "attempts", (double)result.total_attempts);
|
||||||
|
|
||||||
|
out = cJSON_PrintUnformatted(result_obj);
|
||||||
|
cJSON_Delete(result_obj);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
static int parse_hex_pubkey32(const char *hex, unsigned char out[32]) {
|
static int parse_hex_pubkey32(const char *hex, unsigned char out[32]) {
|
||||||
if (hex == NULL || out == NULL || strlen(hex) != 64) {
|
if (hex == NULL || out == NULL || strlen(hex) != 64) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
21
src/main.c
21
src/main.c
@@ -503,8 +503,8 @@ int socket_name_random(char *out, size_t out_len);
|
|||||||
/* Version information (auto-updated by build/version tooling) */
|
/* Version information (auto-updated by build/version tooling) */
|
||||||
#define NSIGNER_VERSION_MAJOR 0
|
#define NSIGNER_VERSION_MAJOR 0
|
||||||
#define NSIGNER_VERSION_MINOR 0
|
#define NSIGNER_VERSION_MINOR 0
|
||||||
#define NSIGNER_VERSION_PATCH 44
|
#define NSIGNER_VERSION_PATCH 45
|
||||||
#define NSIGNER_VERSION "v0.0.44"
|
#define NSIGNER_VERSION "v0.0.45"
|
||||||
|
|
||||||
|
|
||||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||||
@@ -536,6 +536,7 @@ int transport_recv_framed(int fd, char **out_payload, size_t max_size);
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#define NSIGNER_DEFAULT_SOCKET_NAME "nsigner"
|
#define NSIGNER_DEFAULT_SOCKET_NAME "nsigner"
|
||||||
|
#define NSIGNER_QREXEC_SERVICE_NAME "qubes.NsignerRpc"
|
||||||
#define ACTIVITY_LOG_CAP 16
|
#define ACTIVITY_LOG_CAP 16
|
||||||
#define CONNECTION_INFO_CAP 8
|
#define CONNECTION_INFO_CAP 8
|
||||||
|
|
||||||
@@ -1100,7 +1101,7 @@ static void activity_log_cb(const char *message, void *user_data) {
|
|||||||
int idx;
|
int idx;
|
||||||
time_t now;
|
time_t now;
|
||||||
struct tm tm_now;
|
struct tm tm_now;
|
||||||
char ts[16];
|
char ts[32];
|
||||||
|
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
if (message == NULL) {
|
if (message == NULL) {
|
||||||
@@ -1109,9 +1110,9 @@ static void activity_log_cb(const char *message, void *user_data) {
|
|||||||
|
|
||||||
now = time(NULL);
|
now = time(NULL);
|
||||||
if (localtime_r(&now, &tm_now) != NULL) {
|
if (localtime_r(&now, &tm_now) != NULL) {
|
||||||
(void)strftime(ts, sizeof(ts), "%m%d-%H%M%S", &tm_now);
|
(void)strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm_now);
|
||||||
} else {
|
} else {
|
||||||
strncpy(ts, "0000-000000", sizeof(ts) - 1);
|
strncpy(ts, "0000-00-00 00:00:00", sizeof(ts) - 1);
|
||||||
ts[sizeof(ts) - 1] = '\0';
|
ts[sizeof(ts) - 1] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2190,6 +2191,11 @@ int main(int argc, char *argv[]) {
|
|||||||
connection_info_add("listen: unix @%s", socket_name);
|
connection_info_add("listen: unix @%s", socket_name);
|
||||||
}
|
}
|
||||||
connection_info_add("client: nsigner --socket-name %s client '<json>'", socket_name);
|
connection_info_add("client: nsigner --socket-name %s client '<json>'", socket_name);
|
||||||
|
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
|
||||||
|
connection_info_add("qrexec service: %s", NSIGNER_QREXEC_SERVICE_NAME);
|
||||||
|
connection_info_add("qrexec caller: qrexec-client-vm <target_qube> %s", NSIGNER_QREXEC_SERVICE_NAME);
|
||||||
|
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
|
||||||
|
}
|
||||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||||
}
|
}
|
||||||
if (transport_mask & TRANSPORT_TCP) {
|
if (transport_mask & TRANSPORT_TCP) {
|
||||||
@@ -2199,6 +2205,11 @@ int main(int argc, char *argv[]) {
|
|||||||
} else if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
} else if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||||
connection_info_add("listen: unix @%s", socket_name);
|
connection_info_add("listen: unix @%s", socket_name);
|
||||||
connection_info_add("client: nsigner --socket-name %s client '<json>'", socket_name);
|
connection_info_add("client: nsigner --socket-name %s client '<json>'", socket_name);
|
||||||
|
if (bridge_source_trusted) {
|
||||||
|
connection_info_add("qrexec service: %s", NSIGNER_QREXEC_SERVICE_NAME);
|
||||||
|
connection_info_add("qrexec caller: qrexec-client-vm <target_qube> %s", NSIGNER_QREXEC_SERVICE_NAME);
|
||||||
|
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
|
||||||
|
}
|
||||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||||
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
||||||
connection_info_add("listen: %s", listen_target);
|
connection_info_add("listen: %s", listen_target);
|
||||||
|
|||||||
368
src/miner.c
Normal file
368
src/miner.c
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
/*
|
||||||
|
* miner.c - Multithreaded best-effort NIP-13 Proof-of-Work mining coordinator
|
||||||
|
*
|
||||||
|
* This module implements a server-safe mining loop that:
|
||||||
|
* - Runs N worker threads, each trying nonces with a unique stride
|
||||||
|
* - Tracks the best event (highest difficulty) across all threads
|
||||||
|
* - Stops when target difficulty is reached OR timeout expires
|
||||||
|
* - Always returns the best result found (best-effort, not failure on timeout)
|
||||||
|
*
|
||||||
|
* Unlike the standalone event_miner tool, this code:
|
||||||
|
* - Has no global variables (all state in context structs)
|
||||||
|
* - Never calls exit() (returns result codes)
|
||||||
|
* - Has no signal handlers (the server handles signals)
|
||||||
|
* - Uses time-based termination as the primary budget
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _GNU_SOURCE
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
#include <cJSON.h>
|
||||||
|
#include <nostr_core/nip001.h>
|
||||||
|
#include <nostr_core/nip013.h>
|
||||||
|
#include <nostr_core/utils.h>
|
||||||
|
|
||||||
|
/* ---- Constants ---- */
|
||||||
|
|
||||||
|
#define MINER_MAX_THREADS 32
|
||||||
|
#define MINER_SAFETY_TIMEOUT 600 /* 10 minutes safety max when only difficulty is set */
|
||||||
|
#define MINER_NONCE_STRIDE 0 /* 0 = use thread_count as stride */
|
||||||
|
|
||||||
|
/* ---- Result structure ---- */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
cJSON *best_event; /* best event found (caller frees) */
|
||||||
|
int achieved_difficulty;
|
||||||
|
int target_difficulty;
|
||||||
|
int target_reached;
|
||||||
|
int elapsed_sec;
|
||||||
|
uint64_t total_attempts;
|
||||||
|
} mine_result_t;
|
||||||
|
|
||||||
|
/* ---- Shared state across worker threads ---- */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
cJSON *best_event; /* best event found so far (mutex-protected) */
|
||||||
|
int best_difficulty; /* difficulty of best_event */
|
||||||
|
uint64_t total_attempts; /* total attempts across all threads */
|
||||||
|
int target_difficulty;/* 0 = no target, mine full timeout */
|
||||||
|
int target_reached; /* 1 if target was reached */
|
||||||
|
pthread_mutex_t mutex; /* protects best_event, best_difficulty, total_attempts */
|
||||||
|
volatile int stop; /* set when target reached or timeout */
|
||||||
|
} mine_shared_state_t;
|
||||||
|
|
||||||
|
/* ---- Per-worker context ---- */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
mine_shared_state_t *shared;
|
||||||
|
int kind;
|
||||||
|
char content[65536]; /* copy of event content */
|
||||||
|
cJSON *tags_template; /* this thread's copy of original tags */
|
||||||
|
unsigned char private_key[32];
|
||||||
|
int thread_id;
|
||||||
|
uint64_t nonce_start; /* starting nonce for this thread */
|
||||||
|
uint64_t nonce_stride; /* increment to avoid overlap with other threads */
|
||||||
|
time_t deadline; /* absolute time to stop */
|
||||||
|
uint64_t attempts; /* this thread's attempt count */
|
||||||
|
} miner_worker_ctx_t;
|
||||||
|
|
||||||
|
/* ---- Helper: update nonce tag in a tags array ---- */
|
||||||
|
|
||||||
|
static int miner_update_nonce_tag(cJSON *tags, uint64_t nonce, int target_difficulty) {
|
||||||
|
cJSON *tag;
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
if (!tags) return -1;
|
||||||
|
|
||||||
|
/* Remove existing nonce tag if present */
|
||||||
|
cJSON_ArrayForEach(tag, tags) {
|
||||||
|
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||||
|
cJSON *tag_type = cJSON_GetArrayItem(tag, 0);
|
||||||
|
if (tag_type && cJSON_IsString(tag_type) &&
|
||||||
|
strcmp(cJSON_GetStringValue(tag_type), "nonce") == 0) {
|
||||||
|
cJSON_DetachItemFromArray(tags, index);
|
||||||
|
cJSON_Delete(tag);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add new nonce tag: ["nonce", "<nonce>", "<target>"] */
|
||||||
|
{
|
||||||
|
cJSON *nonce_tag = cJSON_CreateArray();
|
||||||
|
char nonce_str[32];
|
||||||
|
char difficulty_str[16];
|
||||||
|
|
||||||
|
if (!nonce_tag) return -1;
|
||||||
|
|
||||||
|
snprintf(nonce_str, sizeof(nonce_str), "%llu", (unsigned long long)nonce);
|
||||||
|
snprintf(difficulty_str, sizeof(difficulty_str), "%d", target_difficulty);
|
||||||
|
|
||||||
|
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("nonce"));
|
||||||
|
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(nonce_str));
|
||||||
|
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(difficulty_str));
|
||||||
|
|
||||||
|
cJSON_AddItemToArray(tags, nonce_tag);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Worker thread function ---- */
|
||||||
|
|
||||||
|
static void *miner_worker(void *arg) {
|
||||||
|
miner_worker_ctx_t *ctx = (miner_worker_ctx_t *)arg;
|
||||||
|
uint64_t nonce = ctx->nonce_start;
|
||||||
|
time_t current_ts = time(NULL);
|
||||||
|
|
||||||
|
while (!ctx->shared->stop && time(NULL) < ctx->deadline) {
|
||||||
|
cJSON *working_tags;
|
||||||
|
cJSON *signed_event;
|
||||||
|
const char *event_id;
|
||||||
|
int difficulty;
|
||||||
|
|
||||||
|
/* Build tags with current nonce */
|
||||||
|
working_tags = cJSON_Duplicate(ctx->tags_template, 1);
|
||||||
|
if (!working_tags) break;
|
||||||
|
|
||||||
|
if (miner_update_nonce_tag(working_tags, nonce, ctx->shared->target_difficulty) != 0) {
|
||||||
|
cJSON_Delete(working_tags);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update timestamp periodically (every 1000 attempts) to keep it fresh */
|
||||||
|
if (ctx->attempts % 1000 == 0) {
|
||||||
|
current_ts = time(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create and sign the event with this nonce */
|
||||||
|
signed_event = nostr_create_and_sign_event(ctx->kind, ctx->content,
|
||||||
|
working_tags, ctx->private_key,
|
||||||
|
current_ts);
|
||||||
|
cJSON_Delete(working_tags);
|
||||||
|
|
||||||
|
if (!signed_event) {
|
||||||
|
nonce += ctx->nonce_stride;
|
||||||
|
ctx->attempts++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check difficulty of the resulting event id */
|
||||||
|
{
|
||||||
|
cJSON *id_item = cJSON_GetObjectItem(signed_event, "id");
|
||||||
|
if (!id_item || !cJSON_IsString(id_item)) {
|
||||||
|
cJSON_Delete(signed_event);
|
||||||
|
nonce += ctx->nonce_stride;
|
||||||
|
ctx->attempts++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
event_id = cJSON_GetStringValue(id_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
difficulty = nostr_calculate_pow_difficulty(event_id);
|
||||||
|
|
||||||
|
/* Track best under mutex */
|
||||||
|
pthread_mutex_lock(&ctx->shared->mutex);
|
||||||
|
ctx->shared->total_attempts++;
|
||||||
|
if (difficulty > ctx->shared->best_difficulty) {
|
||||||
|
if (ctx->shared->best_event) {
|
||||||
|
cJSON_Delete(ctx->shared->best_event);
|
||||||
|
}
|
||||||
|
ctx->shared->best_event = cJSON_Duplicate(signed_event, 1);
|
||||||
|
ctx->shared->best_difficulty = difficulty;
|
||||||
|
}
|
||||||
|
if (ctx->shared->target_difficulty > 0 && difficulty >= ctx->shared->target_difficulty) {
|
||||||
|
ctx->shared->target_reached = 1;
|
||||||
|
ctx->shared->stop = 1;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&ctx->shared->mutex);
|
||||||
|
|
||||||
|
cJSON_Delete(signed_event);
|
||||||
|
nonce += ctx->nonce_stride;
|
||||||
|
ctx->attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Main mining coordinator ---- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run multithreaded best-effort mining on `event`.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* event - cJSON event to mine (must have kind, content, tags, created_at)
|
||||||
|
* private_key - 32-byte private key for signing
|
||||||
|
* target_difficulty - 0 = no target (mine full timeout); >0 = stop when reached
|
||||||
|
* thread_count - number of worker threads (1..MINER_MAX_THREADS)
|
||||||
|
* timeout_sec - time budget in seconds (0 = use MINER_SAFETY_TIMEOUT)
|
||||||
|
* result - output struct (caller frees result->best_event)
|
||||||
|
*
|
||||||
|
* Returns 0 on success (result populated), -1 on error.
|
||||||
|
*/
|
||||||
|
int miner_run(cJSON *event, const unsigned char *private_key,
|
||||||
|
int target_difficulty, int thread_count, int timeout_sec,
|
||||||
|
mine_result_t *result)
|
||||||
|
{
|
||||||
|
mine_shared_state_t shared;
|
||||||
|
miner_worker_ctx_t *workers = NULL;
|
||||||
|
pthread_t *threads = NULL;
|
||||||
|
cJSON *kind_item, *content_item, *tags_item, *created_at_item;
|
||||||
|
int kind;
|
||||||
|
const char *content;
|
||||||
|
time_t start_time;
|
||||||
|
int effective_timeout;
|
||||||
|
int i;
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
|
if (!event || !private_key || !result) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(result, 0, sizeof(mine_result_t));
|
||||||
|
|
||||||
|
/* Validate event has required fields */
|
||||||
|
kind_item = cJSON_GetObjectItem(event, "kind");
|
||||||
|
content_item = cJSON_GetObjectItem(event, "content");
|
||||||
|
tags_item = cJSON_GetObjectItem(event, "tags");
|
||||||
|
created_at_item = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
|
||||||
|
if (!kind_item || !content_item || !tags_item) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If created_at is missing, add it with current time */
|
||||||
|
if (!created_at_item) {
|
||||||
|
cJSON_AddNumberToObject(event, "created_at", (double)time(NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
kind = (int)cJSON_GetNumberValue(kind_item);
|
||||||
|
content = cJSON_GetStringValue(content_item);
|
||||||
|
if (!content) {
|
||||||
|
content = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clamp thread count */
|
||||||
|
if (thread_count < 1) thread_count = 1;
|
||||||
|
if (thread_count > MINER_MAX_THREADS) thread_count = MINER_MAX_THREADS;
|
||||||
|
|
||||||
|
/* Determine effective timeout */
|
||||||
|
if (timeout_sec <= 0) {
|
||||||
|
effective_timeout = MINER_SAFETY_TIMEOUT;
|
||||||
|
} else {
|
||||||
|
effective_timeout = timeout_sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Initialize shared state */
|
||||||
|
memset(&shared, 0, sizeof(shared));
|
||||||
|
shared.best_event = NULL;
|
||||||
|
shared.best_difficulty = -1;
|
||||||
|
shared.total_attempts = 0;
|
||||||
|
shared.target_difficulty = target_difficulty;
|
||||||
|
shared.target_reached = 0;
|
||||||
|
shared.stop = 0;
|
||||||
|
if (pthread_mutex_init(&shared.mutex, NULL) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allocate worker contexts and thread array */
|
||||||
|
workers = (miner_worker_ctx_t *)calloc(thread_count, sizeof(miner_worker_ctx_t));
|
||||||
|
threads = (pthread_t *)calloc(thread_count, sizeof(pthread_t));
|
||||||
|
if (!workers || !threads) {
|
||||||
|
free(workers);
|
||||||
|
free(threads);
|
||||||
|
pthread_mutex_destroy(&shared.mutex);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
start_time = time(NULL);
|
||||||
|
|
||||||
|
/* Set up each worker */
|
||||||
|
for (i = 0; i < thread_count; i++) {
|
||||||
|
workers[i].shared = &shared;
|
||||||
|
workers[i].kind = kind;
|
||||||
|
strncpy(workers[i].content, content, sizeof(workers[i].content) - 1);
|
||||||
|
workers[i].content[sizeof(workers[i].content) - 1] = '\0';
|
||||||
|
workers[i].tags_template = cJSON_Duplicate(tags_item, 1);
|
||||||
|
memcpy(workers[i].private_key, private_key, 32);
|
||||||
|
workers[i].thread_id = i;
|
||||||
|
workers[i].nonce_start = (uint64_t)i;
|
||||||
|
workers[i].nonce_stride = (uint64_t)thread_count;
|
||||||
|
workers[i].deadline = start_time + effective_timeout;
|
||||||
|
workers[i].attempts = 0;
|
||||||
|
|
||||||
|
if (!workers[i].tags_template) {
|
||||||
|
/* Allocation failed for this worker's tags — clean up what we have */
|
||||||
|
int j;
|
||||||
|
for (j = 0; j < i; j++) {
|
||||||
|
cJSON_Delete(workers[j].tags_template);
|
||||||
|
}
|
||||||
|
free(workers);
|
||||||
|
free(threads);
|
||||||
|
pthread_mutex_destroy(&shared.mutex);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Start threads */
|
||||||
|
for (i = 0; i < thread_count; i++) {
|
||||||
|
if (pthread_create(&threads[i], NULL, miner_worker, &workers[i]) != 0) {
|
||||||
|
/* Failed to create thread i — signal stop and join what we have */
|
||||||
|
shared.stop = 1;
|
||||||
|
{
|
||||||
|
int j;
|
||||||
|
for (j = 0; j < i; j++) {
|
||||||
|
pthread_join(threads[j], NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret = -1;
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wait for all threads to complete (they stop on target/timeout) */
|
||||||
|
for (i = 0; i < thread_count; i++) {
|
||||||
|
pthread_join(threads[i], NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
/* Sum up total attempts from all workers */
|
||||||
|
{
|
||||||
|
uint64_t total = 0;
|
||||||
|
for (i = 0; i < thread_count; i++) {
|
||||||
|
total += workers[i].attempts;
|
||||||
|
}
|
||||||
|
/* Use the shared counter (more accurate, includes mutex-protected increments) */
|
||||||
|
result->total_attempts = shared.total_attempts > 0 ? shared.total_attempts : total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Populate result */
|
||||||
|
result->best_event = shared.best_event; /* transfer ownership */
|
||||||
|
result->achieved_difficulty = shared.best_difficulty;
|
||||||
|
result->target_difficulty = target_difficulty;
|
||||||
|
result->target_reached = shared.target_reached;
|
||||||
|
result->elapsed_sec = (int)(time(NULL) - start_time);
|
||||||
|
|
||||||
|
/* Clean up worker contexts */
|
||||||
|
for (i = 0; i < thread_count; i++) {
|
||||||
|
if (workers[i].tags_template) {
|
||||||
|
cJSON_Delete(workers[i].tags_template);
|
||||||
|
}
|
||||||
|
/* Zeroize private key copy */
|
||||||
|
memset(workers[i].private_key, 0, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(workers);
|
||||||
|
free(threads);
|
||||||
|
pthread_mutex_destroy(&shared.mutex);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
69
src/server.c
69
src/server.c
@@ -528,6 +528,7 @@ int transport_recv_framed(int fd, char **out_payload, size_t max_size);
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -1450,6 +1451,33 @@ int server_get_caller(int fd, caller_identity_t *out) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Async mine_event support ---- */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int client_fd;
|
||||||
|
int is_stdio;
|
||||||
|
dispatcher_ctx_t *dispatcher;
|
||||||
|
char *request;
|
||||||
|
} mine_thread_arg_t;
|
||||||
|
|
||||||
|
static void *mine_event_thread(void *arg) {
|
||||||
|
mine_thread_arg_t *a = (mine_thread_arg_t *)arg;
|
||||||
|
char *response = NULL;
|
||||||
|
|
||||||
|
response = dispatcher_handle_request(a->dispatcher, a->request);
|
||||||
|
if (response != NULL) {
|
||||||
|
(void)transport_send_framed(a->is_stdio ? STDOUT_FILENO : a->client_fd, response);
|
||||||
|
free(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(a->request);
|
||||||
|
if (!a->is_stdio) {
|
||||||
|
close(a->client_fd);
|
||||||
|
}
|
||||||
|
free(a);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
||||||
int client_fd;
|
int client_fd;
|
||||||
caller_identity_t caller;
|
caller_identity_t caller;
|
||||||
@@ -1753,6 +1781,47 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pchk == POLICY_ALLOW && !derivation_error) {
|
if (pchk == POLICY_ALLOW && !derivation_error) {
|
||||||
|
/*
|
||||||
|
* mine_event is a long-running operation (potentially seconds to minutes).
|
||||||
|
* Spawn a detached thread so the server stays responsive. The thread
|
||||||
|
* owns the client_fd and request, and sends the response when mining
|
||||||
|
* completes. We skip the synchronous response path below.
|
||||||
|
*/
|
||||||
|
if (strcmp(method, "mine_event") == 0 && client_fd != STDIN_FILENO) {
|
||||||
|
mine_thread_arg_t *marg = malloc(sizeof(mine_thread_arg_t));
|
||||||
|
if (marg != NULL) {
|
||||||
|
pthread_t mine_tid;
|
||||||
|
|
||||||
|
marg->client_fd = client_fd;
|
||||||
|
marg->is_stdio = 0;
|
||||||
|
marg->dispatcher = ctx->dispatcher;
|
||||||
|
marg->request = request;
|
||||||
|
|
||||||
|
if (pthread_create(&mine_tid, NULL, mine_event_thread, marg) == 0) {
|
||||||
|
pthread_detach(mine_tid);
|
||||||
|
/* Thread owns client_fd and request — prevent cleanup below */
|
||||||
|
request = NULL;
|
||||||
|
response = NULL;
|
||||||
|
verdict = "ALLOWED";
|
||||||
|
source_label = "async-mine";
|
||||||
|
(void)snprintf(activity,
|
||||||
|
sizeof(activity),
|
||||||
|
"%s %s(%s) %s:%s",
|
||||||
|
caller.caller_id,
|
||||||
|
method,
|
||||||
|
role_name,
|
||||||
|
"ALLOWED",
|
||||||
|
"async-mine");
|
||||||
|
if (cb != NULL) {
|
||||||
|
cb(activity, cb_data);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
/* pthread_create failed — fall through to synchronous path */
|
||||||
|
free(marg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
verdict = "ALLOWED";
|
verdict = "ALLOWED";
|
||||||
response = dispatcher_handle_request(ctx->dispatcher, request);
|
response = dispatcher_handle_request(ctx->dispatcher, request);
|
||||||
if (response == NULL) {
|
if (response == NULL) {
|
||||||
|
|||||||
459
tests/test_mine_event.c
Normal file
459
tests/test_mine_event.c
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
/* Test for mine_event verb (NIP-13 Proof-of-Work) */
|
||||||
|
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <cJSON.h>
|
||||||
|
|
||||||
|
/* from secure_mem.h */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *data;
|
||||||
|
size_t size;
|
||||||
|
int locked;
|
||||||
|
} secure_buf_t;
|
||||||
|
|
||||||
|
int secure_buf_alloc(secure_buf_t *buf, size_t size);
|
||||||
|
void secure_buf_free(secure_buf_t *buf);
|
||||||
|
void secure_memzero(void *ptr, size_t len);
|
||||||
|
|
||||||
|
/* from mnemonic.h */
|
||||||
|
|
||||||
|
#define MNEMONIC_MAX_LEN 256
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
secure_buf_t buf;
|
||||||
|
int loaded;
|
||||||
|
int word_count;
|
||||||
|
} mnemonic_state_t;
|
||||||
|
|
||||||
|
void mnemonic_init(mnemonic_state_t *state);
|
||||||
|
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
|
||||||
|
void mnemonic_unload(mnemonic_state_t *state);
|
||||||
|
int mnemonic_is_loaded(const mnemonic_state_t *state);
|
||||||
|
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
|
||||||
|
int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||||
|
|
||||||
|
/* from role_table.h */
|
||||||
|
|
||||||
|
#define ROLE_NAME_MAX 64
|
||||||
|
#define ROLE_PATH_MAX 128
|
||||||
|
#define ROLE_PURPOSE_MAX 32
|
||||||
|
#define ROLE_CURVE_MAX 16
|
||||||
|
#define ROLE_PUBKEY_HEX_MAX 66
|
||||||
|
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PURPOSE_NOSTR = 0,
|
||||||
|
PURPOSE_BITCOIN,
|
||||||
|
PURPOSE_SSH,
|
||||||
|
PURPOSE_AGE,
|
||||||
|
PURPOSE_FIPS,
|
||||||
|
PURPOSE_UNKNOWN
|
||||||
|
} role_purpose_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CURVE_SECP256K1 = 0,
|
||||||
|
CURVE_ED25519,
|
||||||
|
CURVE_X25519,
|
||||||
|
CURVE_UNKNOWN
|
||||||
|
} role_curve_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SELECTOR_NOSTR_INDEX,
|
||||||
|
SELECTOR_ROLE_PATH
|
||||||
|
} role_selector_type_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char name[ROLE_NAME_MAX];
|
||||||
|
char purpose_str[ROLE_PURPOSE_MAX];
|
||||||
|
char curve_str[ROLE_CURVE_MAX];
|
||||||
|
role_purpose_t purpose;
|
||||||
|
role_curve_t curve;
|
||||||
|
role_selector_type_t selector_type;
|
||||||
|
int nostr_index;
|
||||||
|
char role_path[ROLE_PATH_MAX];
|
||||||
|
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
|
||||||
|
int derived;
|
||||||
|
} role_entry_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
|
||||||
|
int count;
|
||||||
|
} role_table_t;
|
||||||
|
|
||||||
|
void role_table_init(role_table_t *table);
|
||||||
|
int role_table_add(role_table_t *table, const role_entry_t *entry);
|
||||||
|
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
|
||||||
|
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
|
||||||
|
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
|
||||||
|
role_entry_t *role_table_get_default(role_table_t *table);
|
||||||
|
role_purpose_t role_purpose_from_str(const char *s);
|
||||||
|
role_curve_t role_curve_from_str(const char *s);
|
||||||
|
const char *role_purpose_to_str(role_purpose_t p);
|
||||||
|
const char *role_curve_to_str(role_curve_t c);
|
||||||
|
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
|
||||||
|
|
||||||
|
/* from selector.h */
|
||||||
|
|
||||||
|
#define SELECTOR_OK 0
|
||||||
|
#define SELECTOR_ERR_AMBIGUOUS -1
|
||||||
|
#define SELECTOR_ERR_NOT_FOUND -2
|
||||||
|
#define SELECTOR_ERR_NO_DEFAULT -3
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int has_role;
|
||||||
|
char role_name[ROLE_NAME_MAX];
|
||||||
|
int has_nostr_index;
|
||||||
|
int nostr_index;
|
||||||
|
int has_role_path;
|
||||||
|
char role_path[ROLE_PATH_MAX];
|
||||||
|
} selector_request_t;
|
||||||
|
|
||||||
|
void selector_request_init(selector_request_t *req);
|
||||||
|
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
|
||||||
|
const char *selector_strerror(int err);
|
||||||
|
|
||||||
|
/* from enforcement.h */
|
||||||
|
|
||||||
|
#define ENFORCE_OK 0
|
||||||
|
#define ENFORCE_ERR_PURPOSE -1
|
||||||
|
#define ENFORCE_ERR_CURVE -2
|
||||||
|
#define ENFORCE_ERR_UNKNOWN_VERB -3
|
||||||
|
|
||||||
|
#define VERB_SIGN_EVENT "sign_event"
|
||||||
|
#define VERB_GET_PUBLIC_KEY "get_public_key"
|
||||||
|
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
|
||||||
|
#define VERB_NIP44_DECRYPT "nip44_decrypt"
|
||||||
|
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
|
||||||
|
#define VERB_NIP04_DECRYPT "nip04_decrypt"
|
||||||
|
#define VERB_MINE_EVENT "mine_event"
|
||||||
|
|
||||||
|
int enforce_verb_role(const char *verb, const role_entry_t *role);
|
||||||
|
const char *enforce_strerror(int err);
|
||||||
|
|
||||||
|
/* from policy.h */
|
||||||
|
|
||||||
|
#define POLICY_MAX_ENTRIES 32
|
||||||
|
#define POLICY_MAX_VERBS 16
|
||||||
|
#define POLICY_MAX_ROLES 16
|
||||||
|
#define POLICY_MAX_PURPOSES 8
|
||||||
|
#define POLICY_VERB_MAX_LEN 32
|
||||||
|
#define POLICY_CALLER_MAX_LEN 160
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PROMPT_NEVER = 0,
|
||||||
|
PROMPT_FIRST_PER_BOOT,
|
||||||
|
PROMPT_EVERY_REQUEST,
|
||||||
|
PROMPT_DENY
|
||||||
|
} prompt_mode_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
POLICY_SOURCE_DEFAULT = 0,
|
||||||
|
POLICY_SOURCE_PREAPPROVE,
|
||||||
|
POLICY_SOURCE_SESSION_GRANT
|
||||||
|
} policy_source_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char caller[POLICY_CALLER_MAX_LEN];
|
||||||
|
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
|
||||||
|
int verb_count;
|
||||||
|
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
|
||||||
|
int role_count;
|
||||||
|
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||||
|
int purpose_count;
|
||||||
|
prompt_mode_t prompt;
|
||||||
|
policy_source_t source;
|
||||||
|
} policy_entry_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
policy_entry_t entries[POLICY_MAX_ENTRIES];
|
||||||
|
int count;
|
||||||
|
} policy_table_t;
|
||||||
|
|
||||||
|
#define POLICY_ALLOW 0
|
||||||
|
#define POLICY_DENY -1
|
||||||
|
#define POLICY_PROMPT -2
|
||||||
|
#define POLICY_NO_MATCH -3
|
||||||
|
|
||||||
|
void policy_table_init(policy_table_t *table);
|
||||||
|
void policy_init_default(policy_table_t *table, uid_t owner_uid);
|
||||||
|
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||||
|
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||||
|
const char *verb, const char *role_name, const char *purpose,
|
||||||
|
policy_source_t *out_source);
|
||||||
|
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||||
|
const char *prompt_mode_to_str(prompt_mode_t m);
|
||||||
|
|
||||||
|
/* from crypto.h */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
secure_buf_t private_key;
|
||||||
|
unsigned char public_key[32];
|
||||||
|
char pubkey_hex[65];
|
||||||
|
char npub[128];
|
||||||
|
int valid;
|
||||||
|
} derived_key_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
|
||||||
|
int count;
|
||||||
|
} key_store_t;
|
||||||
|
|
||||||
|
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
|
||||||
|
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
|
||||||
|
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
|
||||||
|
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
|
||||||
|
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
|
||||||
|
char *crypto_mine_event(const key_store_t *store, int role_index,
|
||||||
|
const char *event_json, int difficulty,
|
||||||
|
int threads, int timeout_sec);
|
||||||
|
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
|
||||||
|
const char *recipient_pubkey_hex, const char *plaintext);
|
||||||
|
char *crypto_nip44_decrypt(const key_store_t *store, int role_index,
|
||||||
|
const char *sender_pubkey_hex, const char *ciphertext);
|
||||||
|
char *crypto_nip04_encrypt(const key_store_t *store, int role_index,
|
||||||
|
const char *recipient_pubkey_hex, const char *plaintext);
|
||||||
|
char *crypto_nip04_decrypt(const key_store_t *store, int role_index,
|
||||||
|
const char *sender_pubkey_hex, const char *ciphertext);
|
||||||
|
void crypto_wipe(key_store_t *store);
|
||||||
|
|
||||||
|
/* from dispatcher.h */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
role_table_t *role_table;
|
||||||
|
mnemonic_state_t *mnemonic;
|
||||||
|
key_store_t *key_store;
|
||||||
|
} dispatcher_ctx_t;
|
||||||
|
|
||||||
|
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
|
||||||
|
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
|
||||||
|
|
||||||
|
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||||
|
|
||||||
|
#include <nostr_core/nostr_common.h>
|
||||||
|
#include <nostr_core/nip013.h>
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int g_passes = 0;
|
||||||
|
static int g_total = 0;
|
||||||
|
|
||||||
|
static void check_condition(const char *name, int condition) {
|
||||||
|
g_total++;
|
||||||
|
if (condition) {
|
||||||
|
printf("PASS: %s\n", name);
|
||||||
|
g_passes++;
|
||||||
|
} else {
|
||||||
|
printf("FAIL: %s\n", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int response_has(const char *response, const char *needle) {
|
||||||
|
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse a dispatcher response and extract the result object (for mine_event).
|
||||||
|
* Returns a newly-allocated cJSON result object, or NULL on failure.
|
||||||
|
* Caller must delete the returned object. */
|
||||||
|
static cJSON *extract_result_object(const char *response) {
|
||||||
|
cJSON *resp_json = NULL;
|
||||||
|
cJSON *result_str = NULL;
|
||||||
|
cJSON *result_obj = NULL;
|
||||||
|
|
||||||
|
if (response == NULL) return NULL;
|
||||||
|
|
||||||
|
resp_json = cJSON_Parse(response);
|
||||||
|
if (!resp_json) return NULL;
|
||||||
|
|
||||||
|
result_str = cJSON_GetObjectItemCaseSensitive(resp_json, "result");
|
||||||
|
if (!result_str || !cJSON_IsString(result_str)) {
|
||||||
|
cJSON_Delete(resp_json);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
result_obj = cJSON_Parse(result_str->valuestring);
|
||||||
|
cJSON_Delete(resp_json);
|
||||||
|
return result_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if target_reached in the result object matches expected */
|
||||||
|
static int check_target_reached(const char *response, int expected) {
|
||||||
|
cJSON *result_obj = extract_result_object(response);
|
||||||
|
cJSON *tr;
|
||||||
|
int val = -1;
|
||||||
|
|
||||||
|
if (!result_obj) return 0;
|
||||||
|
|
||||||
|
tr = cJSON_GetObjectItemCaseSensitive(result_obj, "target_reached");
|
||||||
|
if (tr && cJSON_IsBool(tr)) {
|
||||||
|
val = cJSON_IsTrue(tr) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(result_obj);
|
||||||
|
return (val == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
static role_entry_t make_nostr_entry(const char *name, int idx) {
|
||||||
|
role_entry_t e;
|
||||||
|
memset(&e, 0, sizeof(e));
|
||||||
|
strncpy(e.name, name, sizeof(e.name) - 1);
|
||||||
|
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
|
||||||
|
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
|
||||||
|
e.purpose = role_purpose_from_str(e.purpose_str);
|
||||||
|
e.curve = role_curve_from_str(e.curve_str);
|
||||||
|
e.selector_type = SELECTOR_NOSTR_INDEX;
|
||||||
|
e.nostr_index = idx;
|
||||||
|
e.derived = 0;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
role_table_t table;
|
||||||
|
role_entry_t main_role;
|
||||||
|
mnemonic_state_t mnemonic;
|
||||||
|
dispatcher_ctx_t dispatcher;
|
||||||
|
key_store_t key_store;
|
||||||
|
const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
||||||
|
char *resp;
|
||||||
|
int derived;
|
||||||
|
|
||||||
|
role_table_init(&table);
|
||||||
|
main_role = make_nostr_entry("main", 0);
|
||||||
|
role_table_add(&table, &main_role);
|
||||||
|
|
||||||
|
mnemonic_init(&mnemonic);
|
||||||
|
mnemonic_load(&mnemonic, valid_12);
|
||||||
|
|
||||||
|
memset(&key_store, 0, sizeof(key_store));
|
||||||
|
dispatcher_init(&dispatcher, &table, &mnemonic, &key_store);
|
||||||
|
|
||||||
|
derived = nostr_init();
|
||||||
|
check_condition("nostr_init succeeds", derived == 0);
|
||||||
|
|
||||||
|
derived = crypto_derive_all(&key_store, &table, &mnemonic);
|
||||||
|
check_condition("crypto_derive_all derives at least one key", derived >= 1);
|
||||||
|
|
||||||
|
/* 1. mine_event with low difficulty (2) and short timeout (5 sec) — should reach target */
|
||||||
|
resp = dispatcher_handle_request(&dispatcher,
|
||||||
|
"{\"id\":\"1\",\"method\":\"mine_event\",\"params\":["
|
||||||
|
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello pow\\\",\\\"tags\\\":[]}\","
|
||||||
|
"{\"difficulty\":2,\"threads\":2,\"timeout_sec\":5}]}");
|
||||||
|
check_condition("mine_event low difficulty returns result object",
|
||||||
|
response_has(resp, "\"id\":\"1\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
|
||||||
|
check_condition("mine_event low difficulty returns signed event with nonce tag",
|
||||||
|
response_has(resp, "nonce") && response_has(resp, "sig") && response_has(resp, "pubkey"));
|
||||||
|
check_condition("mine_event low difficulty target_reached is true",
|
||||||
|
check_target_reached(resp, 1));
|
||||||
|
free(resp);
|
||||||
|
|
||||||
|
/* 2. mine_event with high difficulty (30) and short timeout (2 sec) — should NOT reach target */
|
||||||
|
resp = dispatcher_handle_request(&dispatcher,
|
||||||
|
"{\"id\":\"2\",\"method\":\"mine_event\",\"params\":["
|
||||||
|
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"hard pow\\\",\\\"tags\\\":[]}\","
|
||||||
|
"{\"difficulty\":30,\"threads\":4,\"timeout_sec\":2}]}");
|
||||||
|
check_condition("mine_event high difficulty returns result object",
|
||||||
|
response_has(resp, "\"id\":\"2\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
|
||||||
|
check_condition("mine_event high difficulty target_reached is false",
|
||||||
|
check_target_reached(resp, 0));
|
||||||
|
check_condition("mine_event high difficulty still returns a signed event",
|
||||||
|
response_has(resp, "nonce") && response_has(resp, "sig"));
|
||||||
|
free(resp);
|
||||||
|
|
||||||
|
/* 3. mine_event with only timeout (no difficulty) — should mine for full duration */
|
||||||
|
resp = dispatcher_handle_request(&dispatcher,
|
||||||
|
"{\"id\":\"3\",\"method\":\"mine_event\",\"params\":["
|
||||||
|
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"timeout only\\\",\\\"tags\\\":[]}\","
|
||||||
|
"{\"threads\":2,\"timeout_sec\":2}]}");
|
||||||
|
check_condition("mine_event timeout-only returns result",
|
||||||
|
response_has(resp, "\"id\":\"3\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
|
||||||
|
check_condition("mine_event timeout-only target_reached is false (no target set)",
|
||||||
|
check_target_reached(resp, 0));
|
||||||
|
free(resp);
|
||||||
|
|
||||||
|
/* 4. mine_event with neither difficulty nor timeout — should error */
|
||||||
|
resp = dispatcher_handle_request(&dispatcher,
|
||||||
|
"{\"id\":\"4\",\"method\":\"mine_event\",\"params\":["
|
||||||
|
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"no params\\\",\\\"tags\\\":[]}\","
|
||||||
|
"{\"threads\":2}]}");
|
||||||
|
check_condition("mine_event with no termination condition returns 1007",
|
||||||
|
response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1007") && response_has(resp, "no_termination_condition"));
|
||||||
|
free(resp);
|
||||||
|
|
||||||
|
/* 5. mine_event with only difficulty (no timeout) — should use safety timeout and reach low target */
|
||||||
|
resp = dispatcher_handle_request(&dispatcher,
|
||||||
|
"{\"id\":\"5\",\"method\":\"mine_event\",\"params\":["
|
||||||
|
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"difficulty only\\\",\\\"tags\\\":[]}\","
|
||||||
|
"{\"difficulty\":1,\"threads\":2}]}");
|
||||||
|
check_condition("mine_event difficulty-only returns result and reaches target",
|
||||||
|
response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"result\"") && check_target_reached(resp, 1));
|
||||||
|
free(resp);
|
||||||
|
|
||||||
|
/* 6. mine_event with invalid event JSON */
|
||||||
|
resp = dispatcher_handle_request(&dispatcher,
|
||||||
|
"{\"id\":\"6\",\"method\":\"mine_event\",\"params\":["
|
||||||
|
"\"not valid json\","
|
||||||
|
"{\"difficulty\":1,\"timeout_sec\":2}]}");
|
||||||
|
check_condition("mine_event with invalid event returns error",
|
||||||
|
response_has(resp, "\"id\":\"6\"") && (response_has(resp, "\"code\":1008") || response_has(resp, "\"code\":-32602")));
|
||||||
|
free(resp);
|
||||||
|
|
||||||
|
/* 7. Verify the mined event's difficulty via nip013 validation */
|
||||||
|
{
|
||||||
|
cJSON *resp_json, *result_str, *result_obj, *event_str, *event_json, *id_item;
|
||||||
|
const char *id_hex;
|
||||||
|
int pow_diff;
|
||||||
|
|
||||||
|
resp = dispatcher_handle_request(&dispatcher,
|
||||||
|
"{\"id\":\"7\",\"method\":\"mine_event\",\"params\":["
|
||||||
|
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"verify pow\\\",\\\"tags\\\":[]}\","
|
||||||
|
"{\"difficulty\":4,\"threads\":4,\"timeout_sec\":5}]}");
|
||||||
|
|
||||||
|
resp_json = cJSON_Parse(resp);
|
||||||
|
if (resp_json) {
|
||||||
|
result_str = cJSON_GetObjectItemCaseSensitive(resp_json, "result");
|
||||||
|
if (result_str && cJSON_IsString(result_str)) {
|
||||||
|
result_obj = cJSON_Parse(result_str->valuestring);
|
||||||
|
if (result_obj) {
|
||||||
|
event_str = cJSON_GetObjectItemCaseSensitive(result_obj, "event");
|
||||||
|
if (event_str && cJSON_IsString(event_str)) {
|
||||||
|
event_json = cJSON_Parse(event_str->valuestring);
|
||||||
|
if (event_json) {
|
||||||
|
id_item = cJSON_GetObjectItemCaseSensitive(event_json, "id");
|
||||||
|
if (id_item && cJSON_IsString(id_item)) {
|
||||||
|
id_hex = cJSON_GetStringValue(id_item);
|
||||||
|
pow_diff = nostr_calculate_pow_difficulty(id_hex);
|
||||||
|
check_condition("mined event has difficulty >= 4",
|
||||||
|
pow_diff >= 4);
|
||||||
|
} else {
|
||||||
|
check_condition("mined event has difficulty >= 4", 0);
|
||||||
|
}
|
||||||
|
cJSON_Delete(event_json);
|
||||||
|
} else {
|
||||||
|
check_condition("mined event has difficulty >= 4", 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
check_condition("mined event has difficulty >= 4", 0);
|
||||||
|
}
|
||||||
|
cJSON_Delete(result_obj);
|
||||||
|
} else {
|
||||||
|
check_condition("mined event has difficulty >= 4", 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
check_condition("mined event has difficulty >= 4", 0);
|
||||||
|
}
|
||||||
|
cJSON_Delete(resp_json);
|
||||||
|
} else {
|
||||||
|
check_condition("mined event has difficulty >= 4", 0);
|
||||||
|
}
|
||||||
|
free(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto_wipe(&key_store);
|
||||||
|
nostr_cleanup();
|
||||||
|
|
||||||
|
printf("%d/%d tests passed\n", g_passes, g_total);
|
||||||
|
return (g_passes == g_total) ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user